use serde_json::Value as JsonValue;
use toolkit_macros::api_dto;
use uuid::Uuid;
use chat_engine_sdk::models::{
CapabilityValue, LifecycleState, Message as SdkMessage, MessagePart as SdkMessagePart,
MessagePartInput as SdkMessagePartInput, MessagePartType, MessageRole, Session as SdkSession,
SessionType as SdkSessionType, VariantInfo,
};
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct SessionDto {
pub session_id: Uuid,
pub tenant_id: String,
pub user_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_type_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled_capabilities: Option<JsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<JsonValue>,
pub lifecycle_state: String,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub updated_at: time::OffsetDateTime,
}
impl From<SdkSession> for SessionDto {
fn from(s: SdkSession) -> Self {
Self {
session_id: s.session_id,
tenant_id: s.tenant_id.to_string(),
user_id: s.user_id.to_string(),
client_id: s.client_id,
session_type_id: s.session_type_id,
enabled_capabilities: s.enabled_capabilities,
metadata: s.metadata,
lifecycle_state: s.lifecycle_state.as_str().to_owned(),
created_at: s.created_at,
updated_at: s.updated_at,
}
}
}
#[api_dto(request)]
#[derive(Debug, Clone)]
pub struct CreateSessionRequestDto {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_type_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<JsonValue>,
}
#[api_dto(request)]
#[derive(Debug, Clone)]
pub struct SwitchSessionTypeRequestDto {
pub session_type_id: Uuid,
}
#[api_dto(response)]
#[derive(Debug, Clone)]
pub struct SessionListDto {
pub items: Vec<SessionDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
pub has_more: bool,
}
#[api_dto(request)]
#[derive(Debug, Clone)]
pub struct RegisterSessionTypeRequestDto {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plugin_instance_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plugin_config: Option<JsonValue>,
}
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct SessionTypeDto {
pub session_type_id: Uuid,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub plugin_instance_id: Option<String>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub updated_at: time::OffsetDateTime,
}
impl From<SdkSessionType> for SessionTypeDto {
fn from(t: SdkSessionType) -> Self {
Self {
session_type_id: t.session_type_id,
name: t.name,
plugin_instance_id: t.plugin_instance_id,
created_at: t.created_at,
updated_at: t.updated_at,
}
}
}
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct MessagePartDto {
pub id: Uuid,
#[serde(rename = "type")]
pub part_type: String,
pub content: JsonValue,
pub number: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub file_citations: Vec<JsonValue>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub link_citations: Vec<JsonValue>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub references: Vec<JsonValue>,
}
impl From<SdkMessagePart> for MessagePartDto {
fn from(p: SdkMessagePart) -> Self {
Self {
id: p.id,
part_type: part_type_to_wire(p.part_type).to_owned(),
content: p.content,
number: p.number,
file_citations: p
.file_citations
.iter()
.filter_map(|c| serde_json::to_value(c).ok())
.collect(),
link_citations: p
.link_citations
.iter()
.filter_map(|c| serde_json::to_value(c).ok())
.collect(),
references: p
.references
.iter()
.filter_map(|r| serde_json::to_value(r).ok())
.collect(),
}
}
}
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct MessagePartInputDto {
#[serde(rename = "type")]
pub part_type: String,
pub content: JsonValue,
}
impl From<MessagePartInputDto> for SdkMessagePartInput {
fn from(d: MessagePartInputDto) -> Self {
let part_type = part_type_from_wire(&d.part_type);
Self {
content: normalize_part_content(part_type, d.content),
part_type,
file_citations: Vec::new(),
link_citations: Vec::new(),
references: Vec::new(),
}
}
}
fn normalize_part_content(part_type: MessagePartType, content: JsonValue) -> JsonValue {
match (part_type, content) {
(MessagePartType::Text, JsonValue::String(text)) => {
serde_json::json!({ "text": text })
}
(_, content) => content,
}
}
fn part_type_to_wire(t: MessagePartType) -> &'static str {
match t {
MessagePartType::Text => "text",
MessagePartType::Code => "code",
MessagePartType::Images => "images",
MessagePartType::Videos => "videos",
MessagePartType::Links => "links",
MessagePartType::Statuses => "statuses",
}
}
fn part_type_from_wire(s: &str) -> MessagePartType {
match s {
"code" => MessagePartType::Code,
"images" => MessagePartType::Images,
"videos" => MessagePartType::Videos,
"links" => MessagePartType::Links,
"statuses" => MessagePartType::Statuses,
_ => MessagePartType::Text,
}
}
#[must_use]
pub fn parts_into_sdk(parts: Vec<MessagePartInputDto>) -> Vec<SdkMessagePartInput> {
parts.into_iter().map(SdkMessagePartInput::from).collect()
}
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct MessageDto {
pub message_id: Uuid,
pub session_id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_message_id: Option<Uuid>,
pub variant_index: u32,
pub is_active: bool,
pub role: String,
#[serde(default)]
pub parts: Vec<MessagePartDto>,
#[serde(default)]
pub file_ids: Vec<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<JsonValue>,
pub is_complete: bool,
pub is_hidden_from_user: bool,
pub is_hidden_from_backend: bool,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub updated_at: time::OffsetDateTime,
}
impl From<SdkMessage> for MessageDto {
fn from(m: SdkMessage) -> Self {
Self {
message_id: m.message_id,
session_id: m.session_id,
parent_message_id: m.parent_message_id,
variant_index: m.variant_index,
is_active: m.is_active,
role: role_to_wire(&m.role).to_owned(),
parts: m.parts.into_iter().map(MessagePartDto::from).collect(),
file_ids: m.file_ids,
metadata: m.metadata,
is_complete: m.is_complete,
is_hidden_from_user: m.is_hidden_from_user,
is_hidden_from_backend: m.is_hidden_from_backend,
created_at: m.created_at,
updated_at: m.updated_at,
}
}
}
fn role_to_wire(role: &MessageRole) -> &'static str {
match role {
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
MessageRole::System => "system",
}
}
#[api_dto(request)]
#[derive(Debug, Clone)]
pub struct SendMessageRequestDto {
#[serde(default)]
pub parts: Vec<MessagePartInputDto>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file_ids: Option<Vec<Uuid>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_message_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<Vec<CapabilityValueDto>>,
}
#[api_dto(request)]
#[derive(Debug, Clone, Default)]
pub struct RecreateMessageRequestDto {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled_capabilities: Option<Vec<CapabilityValueDto>>,
}
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct CapabilityValueDto {
pub name: String,
pub value: JsonValue,
}
impl From<CapabilityValueDto> for CapabilityValue {
fn from(v: CapabilityValueDto) -> Self {
Self {
name: v.name,
value: v.value,
}
}
}
impl From<CapabilityValue> for CapabilityValueDto {
fn from(v: CapabilityValue) -> Self {
Self {
name: v.name,
value: v.value,
}
}
}
#[must_use]
pub fn capabilities_into_sdk(values: Vec<CapabilityValueDto>) -> Vec<CapabilityValue> {
values.into_iter().map(CapabilityValue::from).collect()
}
#[api_dto(response)]
#[derive(Debug, Clone)]
pub struct MessageListDto {
pub items: Vec<MessageDto>,
}
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct VariantInfoDto {
pub message_id: Uuid,
pub variant_index: u32,
pub total_variants: u32,
pub is_active: bool,
}
impl From<VariantInfo> for VariantInfoDto {
fn from(v: VariantInfo) -> Self {
Self {
message_id: v.message_id,
variant_index: v.variant_index,
total_variants: v.total_variants,
is_active: v.is_active,
}
}
}
#[api_dto(response)]
#[derive(Debug, Clone)]
pub struct VariantListDto {
pub variants: Vec<VariantInfoDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_index: Option<u32>,
}
#[api_dto(request)]
#[derive(Debug, Clone)]
pub struct SearchRequestDto {
pub query: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
}
#[api_dto(response)]
#[derive(Debug, Clone)]
pub struct SearchResultsDto {
pub results: Vec<JsonValue>,
}
#[api_dto(request)]
#[derive(Debug, Clone)]
pub struct ReactionRequestDto {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<JsonValue>,
}
#[api_dto(response)]
#[derive(Debug, Clone)]
pub struct ReactionDto {
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<JsonValue>,
pub user_id: String,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
}
#[api_dto(response)]
#[derive(Debug, Clone)]
pub struct ReactionListDto {
pub reactions: Vec<ReactionDto>,
}
#[api_dto(request)]
#[derive(Debug, Clone, Default)]
pub struct ExportRequestDto {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_plugin_metadata: Option<bool>,
}
#[api_dto(response)]
#[derive(Debug, Clone)]
pub struct ExportAcceptedDto {
pub session_id: Uuid,
pub format: String,
pub download_url: String,
pub message_count: u32,
#[serde(with = "time::serde::rfc3339")]
pub expires_at: time::OffsetDateTime,
}
#[api_dto(request)]
#[derive(Debug, Clone, Default)]
pub struct ShareRequestDto {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_in_hours: Option<u32>,
}
#[api_dto(response)]
#[derive(Debug, Clone)]
pub struct ShareResponseDto {
pub session_id: Uuid,
pub share_token: String,
pub share_url: String,
#[serde(default, skip_serializing_if = "Option::is_none", with = "rfc3339_opt")]
pub expires_at: Option<time::OffsetDateTime>,
}
#[api_dto(response)]
#[derive(Debug, Clone)]
pub struct SharedSessionDto {
pub session_id: Uuid,
pub messages: Vec<MessageDto>,
pub read_only: bool,
}
#[api_dto(response)]
#[derive(Debug, Clone)]
pub struct SummarizeAcceptedDto {
pub session_id: Uuid,
pub status_url: String,
}
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct StreamingStartDto {
pub message_id: Uuid,
}
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct StreamingChunkDto {
pub message_id: Uuid,
pub chunk: String,
}
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct StreamingCompleteDto {
pub message_id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<JsonValue>,
}
#[api_dto(request, response)]
#[derive(Debug, Clone)]
pub struct StreamingErrorDto {
pub message_id: Uuid,
pub error: String,
}
#[api_dto(request, response)]
#[serde(tag = "type")]
pub enum StreamingEventDto {
Start(StreamingStartDto),
Chunk(StreamingChunkDto),
Complete(StreamingCompleteDto),
Error(StreamingErrorDto),
}
use crate::domain::message::{
StreamingChunkEvent, StreamingCompleteEvent, StreamingErrorEvent, StreamingStartEvent,
};
impl From<StreamingStartEvent> for StreamingStartDto {
fn from(e: StreamingStartEvent) -> Self {
Self {
message_id: e.message_id,
}
}
}
impl From<StreamingChunkEvent> for StreamingChunkDto {
fn from(e: StreamingChunkEvent) -> Self {
Self {
message_id: e.message_id,
chunk: e.chunk,
}
}
}
impl From<StreamingCompleteEvent> for StreamingCompleteDto {
fn from(e: StreamingCompleteEvent) -> Self {
Self {
message_id: e.message_id,
metadata: e.metadata,
}
}
}
impl From<StreamingErrorEvent> for StreamingErrorDto {
fn from(e: StreamingErrorEvent) -> Self {
Self {
message_id: e.message_id,
error: e.error,
}
}
}
#[must_use]
pub fn lifecycle_to_wire(state: LifecycleState) -> &'static str {
state.as_str()
}
mod rfc3339_opt {
use serde::Serializer;
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
#[allow(clippy::ref_option)]
pub fn serialize<S>(value: &Option<OffsetDateTime>, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(dt) => {
let s = dt.format(&Rfc3339).map_err(serde::ser::Error::custom)?;
ser.serialize_str(&s)
}
None => ser.serialize_none(),
}
}
}
#[cfg(test)]
#[path = "dto_tests.rs"]
mod dto_tests;