1use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14
15fn default_datetime() -> DateTime<Utc> {
17 Utc::now()
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24#[serde(tag = "type", rename_all = "snake_case")]
25pub enum ContentPart {
26 Text {
28 text: String,
30 },
31 ImageUrl {
33 url: String,
35 },
36 ImageBase64 {
38 mime: String,
40 data: String,
42 },
43 FileUrl {
45 url: String,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 mime: Option<String>,
50 },
51 FileBase64 {
53 mime: String,
55 data: String,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 name: Option<String>,
60 },
61}
62
63#[derive(Debug, Serialize, Deserialize)]
65pub struct ChatRequest {
66 pub message: String,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub agent_type: Option<AgentType>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub context_id: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
77 pub workspace_id: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub model: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub parts: Option<Vec<ContentPart>>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub previous_response_id: Option<String>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub web_search: Option<bool>,
90}
91
92#[derive(Debug, Serialize, Deserialize)]
94pub struct ChatResponse {
95 pub response: String,
97 pub agent: String,
99 pub context_id: String,
101 pub sources: Option<Vec<Source>>,
103}
104
105#[derive(Debug, Serialize, Deserialize, Clone)]
107pub struct Source {
108 pub title: String,
110 pub url: Option<String>,
112 pub relevance_score: f32,
114}
115
116#[derive(Debug, Serialize, Deserialize)]
118pub struct ResearchRequest {
119 pub query: String,
121 pub depth: Option<u8>,
123 pub max_iterations: Option<u8>,
125}
126
127#[derive(Debug, Serialize, Deserialize)]
129pub struct ResearchResponse {
130 pub findings: String,
132 pub sources: Vec<Source>,
134 pub duration_ms: u64,
136}
137
138#[derive(Debug, Serialize, Deserialize)]
142pub struct RagIngestRequest {
143 pub collection: String,
145 pub content: String,
147 pub title: Option<String>,
149 pub source: Option<String>,
151 #[serde(default)]
153 pub tags: Vec<String>,
154 #[serde(default)]
156 pub chunking_strategy: Option<String>,
157}
158
159#[derive(Debug, Serialize, Deserialize)]
161pub struct RagIngestResponse {
162 pub chunks_created: usize,
164 pub document_ids: Vec<String>,
166 pub collection: String,
168}
169
170#[derive(Debug, Serialize, Deserialize)]
172pub struct RagSearchRequest {
173 pub collection: String,
175 pub query: String,
177 #[serde(default = "default_search_limit")]
179 pub limit: usize,
180 #[serde(default)]
182 pub strategy: Option<String>,
183 #[serde(default = "default_search_threshold")]
185 pub threshold: f32,
186 #[serde(default)]
188 pub rerank: bool,
189 #[serde(default)]
191 pub reranker_model: Option<String>,
192}
193
194fn default_search_limit() -> usize {
195 10
196}
197
198fn default_search_threshold() -> f32 {
199 0.0
200}
201
202#[derive(Debug, Serialize, Deserialize)]
204pub struct RagSearchResult {
205 pub id: String,
207 pub content: String,
209 pub score: f32,
211 pub metadata: DocumentMetadata,
213}
214
215#[derive(Debug, Serialize, Deserialize)]
217pub struct RagSearchResponse {
218 pub results: Vec<RagSearchResult>,
220 pub total: usize,
222 pub strategy: String,
224 pub reranked: bool,
226 pub duration_ms: u64,
228}
229
230#[derive(Debug, Serialize, Deserialize)]
232pub struct RagDeleteCollectionRequest {
233 pub collection: String,
235}
236
237#[derive(Debug, Serialize, Deserialize)]
239pub struct RagDeleteCollectionResponse {
240 pub success: bool,
242 pub collection: String,
244 pub documents_deleted: usize,
246}
247
248#[derive(Debug, Serialize, Deserialize)]
255pub struct SemanticSearchRequest {
256 pub collection: String,
258 pub query: String,
260 #[serde(default = "default_search_limit")]
262 pub limit: usize,
263 #[serde(default = "default_search_threshold")]
265 pub threshold: f32,
266}
267
268#[derive(Debug, Serialize, Deserialize)]
270pub struct SemanticSearchResult {
271 pub id: String,
273 pub content: String,
275 pub similarity: f32,
277 pub metadata: DocumentMetadata,
279}
280
281#[derive(Debug, Serialize, Deserialize)]
283pub struct SemanticSearchResponse {
284 pub results: Vec<SemanticSearchResult>,
286 pub total: usize,
288 pub duration_ms: u64,
290}
291
292#[derive(Debug, Serialize, Deserialize)]
294pub struct WorkflowRequest {
295 pub query: String,
297 #[serde(default)]
299 pub context: std::collections::HashMap<String, serde_json::Value>,
300}
301
302#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
309#[serde(rename_all = "lowercase")]
310#[non_exhaustive]
311pub enum AgentType {
312 Router,
314 Orchestrator,
316 Product,
318 Invoice,
320 Sales,
322 Finance,
324 #[serde(rename = "hr")]
326 HR,
327 #[serde(untagged)]
330 Custom(String),
331}
332
333impl AgentType {
334 pub fn as_str(&self) -> &str {
336 match self {
337 AgentType::Router => "router",
338 AgentType::Orchestrator => "orchestrator",
339 AgentType::Product => "product",
340 AgentType::Invoice => "invoice",
341 AgentType::Sales => "sales",
342 AgentType::Finance => "finance",
343 AgentType::HR => "hr",
344 AgentType::Custom(name) => name,
345 }
346 }
347
348 pub fn from_string(s: &str) -> Self {
350 match s.to_lowercase().as_str() {
351 "router" => AgentType::Router,
352 "orchestrator" => AgentType::Orchestrator,
353 "product" => AgentType::Product,
354 "invoice" => AgentType::Invoice,
355 "sales" => AgentType::Sales,
356 "finance" => AgentType::Finance,
357 "hr" => AgentType::HR,
358 _ => AgentType::Custom(s.to_string()),
359 }
360 }
361
362 pub fn is_builtin(&self) -> bool {
364 !matches!(self, AgentType::Custom(_))
365 }
366}
367
368impl std::fmt::Display for AgentType {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 write!(f, "{}", self.as_str())
371 }
372}
373
374#[derive(Debug, Clone)]
376pub struct AgentContext {
377 pub user_id: String,
379 pub session_id: String,
381 pub conversation_history: Vec<Message>,
383 pub user_memory: Option<UserMemory>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct Message {
390 pub role: MessageRole,
392 pub content: String,
394 pub timestamp: DateTime<Utc>,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400#[serde(rename_all = "lowercase")]
401pub enum MessageRole {
402 System,
404 User,
406 Assistant,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct UserMemory {
415 pub user_id: String,
417 pub preferences: Vec<Preference>,
419 pub facts: Vec<MemoryFact>,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct Preference {
426 pub category: String,
428 pub key: String,
430 pub value: String,
432 pub confidence: f32,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct MemoryFact {
439 pub id: String,
441 pub user_id: String,
443 pub category: String,
445 pub fact_key: String,
447 pub fact_value: String,
449 pub confidence: f32,
451 pub created_at: DateTime<Utc>,
453 pub updated_at: DateTime<Utc>,
455}
456
457#[derive(Debug, Serialize, Deserialize, Clone)]
461pub struct ToolDefinition {
462 pub name: String,
464 pub description: String,
466 pub parameters: serde_json::Value,
468}
469
470#[derive(Debug, Serialize, Deserialize, Clone)]
472pub struct ToolCall {
473 pub id: String,
475 pub name: String,
477 pub arguments: serde_json::Value,
479}
480
481#[derive(Debug, Serialize, Deserialize)]
483pub struct ToolResult {
484 pub tool_call_id: String,
486 pub result: serde_json::Value,
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct Document {
495 pub id: String,
497 pub content: String,
499 pub metadata: DocumentMetadata,
501 pub embedding: Option<Vec<f32>>,
503}
504
505#[derive(Debug, Clone, Default, Serialize, Deserialize)]
507pub struct DocumentMetadata {
508 #[serde(default)]
510 pub title: String,
511 #[serde(default)]
513 pub source: String,
514 #[serde(default = "default_datetime")]
516 pub created_at: DateTime<Utc>,
517 #[serde(default)]
519 pub tags: Vec<String>,
520}
521
522#[derive(Debug, Clone)]
524pub struct SearchQuery {
525 pub query: String,
527 pub limit: usize,
529 pub threshold: f32,
531 pub filters: Option<Vec<SearchFilter>>,
533}
534
535#[derive(Debug, Clone)]
537pub struct SearchFilter {
538 pub field: String,
540 pub value: String,
542}
543
544#[derive(Debug, Clone)]
546pub struct SearchResult {
547 pub document: Document,
549 pub score: f32,
551}
552
553#[derive(Debug, Serialize, Deserialize)]
557pub struct LoginRequest {
558 pub email: String,
560 pub password: String,
562}
563
564#[derive(Debug, Serialize, Deserialize)]
566pub struct RegisterRequest {
567 pub email: String,
569 pub password: String,
571 pub name: String,
573}
574
575#[derive(Debug, Serialize, Deserialize)]
577pub struct TokenResponse {
578 pub access_token: String,
580 pub refresh_token: String,
582 pub expires_in: i64,
584}
585
586#[derive(Debug, Serialize, Deserialize, Clone)]
588pub struct Claims {
589 pub sub: String,
591 pub email: String,
593 pub exp: usize,
595 pub iat: usize,
597 #[serde(default, skip_serializing_if = "String::is_empty")]
599 pub jti: String,
600 #[serde(default, skip_serializing_if = "Option::is_none")]
602 pub tenant_id: Option<String>,
603}
604
605#[derive(Debug, Clone, Copy, Serialize)]
610#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
611pub enum ErrorCode {
612 DatabaseError,
614 LlmError,
616 AuthenticationFailed,
618 AuthorizationFailed,
620 NotFound,
622 InvalidInput,
624 ConfigurationError,
626 ExternalServiceError,
628 InternalError,
630}
631
632#[derive(Debug, thiserror::Error)]
634pub enum AppError {
635 #[error("Database error: {0}")]
637 Database(String),
638
639 #[error("LLM error: {0}")]
641 LLM(String),
642
643 #[error("Authentication error: {0}")]
645 Auth(String),
646
647 #[error("Not found: {0}")]
649 NotFound(String),
650
651 #[error("Invalid input: {0}")]
653 InvalidInput(String),
654
655 #[error("Configuration error: {0}")]
657 Configuration(String),
658
659 #[error("External service error: {0}")]
661 External(String),
662
663 #[error("Internal error: {0}")]
665 Internal(String),
666
667 #[error("Service unavailable: {0}")]
669 Unavailable(String),
670 #[error("Feature disabled: {0}")]
672 FeatureDisabled(String),
673
674 #[error("Rate limited: {0}")]
676 RateLimited(String),
677}
678
679impl AppError {
680 pub fn code(&self) -> ErrorCode {
682 match self {
683 AppError::Database(_) => ErrorCode::DatabaseError,
684 AppError::LLM(_) => ErrorCode::LlmError,
685 AppError::Auth(_) => ErrorCode::AuthenticationFailed,
686 AppError::NotFound(_) => ErrorCode::NotFound,
687 AppError::InvalidInput(_) => ErrorCode::InvalidInput,
688 AppError::Configuration(_) => ErrorCode::ConfigurationError,
689 AppError::External(_) => ErrorCode::ExternalServiceError,
690 AppError::Internal(_) => ErrorCode::InternalError,
691 AppError::Unavailable(_) => ErrorCode::InternalError,
692AppError::RateLimited(_) => ErrorCode::InternalError,
693AppError::FeatureDisabled(_) => ErrorCode::InternalError,
694 }
695 }
696
697 pub fn is_retryable(&self) -> bool {
700 matches!(
701 self,
702 AppError::External(_) | AppError::Unavailable(_) | AppError::RateLimited(_)
703 )
704 }
705
706 pub fn status_code(&self) -> u16 {
708 match self {
709 AppError::Database(_) => 500,
710 AppError::LLM(_) => 500,
711 AppError::Auth(_) => 401,
712 AppError::NotFound(_) => 404,
713 AppError::InvalidInput(_) => 400,
714 AppError::Configuration(_) => 500,
715 AppError::External(_) => 502,
716 AppError::Internal(_) => 500,
717 AppError::Unavailable(_) => 503,
718 AppError::RateLimited(_) => 429,
719 AppError::FeatureDisabled(_) => 400,
720 }
721 }
722}
723
724impl From<std::io::Error> for AppError {
727 fn from(err: std::io::Error) -> Self {
728 AppError::Internal(format!("IO error: {}", err))
729 }
730}
731
732impl From<serde_json::Error> for AppError {
733 fn from(err: serde_json::Error) -> Self {
734 AppError::InvalidInput(format!("JSON error: {}", err))
735 }
736}
737
738pub type Result<T> = std::result::Result<T, AppError>;
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744 use chrono::{TimeZone, Utc};
745
746 #[test]
747 fn test_agent_type_display_all_builtins() {
748 let cases = vec![
749 (AgentType::Router, "router"),
750 (AgentType::Orchestrator, "orchestrator"),
751 (AgentType::Product, "product"),
752 (AgentType::Invoice, "invoice"),
753 (AgentType::Sales, "sales"),
754 (AgentType::Finance, "finance"),
755 (AgentType::HR, "hr"),
756 ];
757 for (agent, expected) in cases {
758 assert_eq!(agent.to_string(), expected);
759 assert_eq!(format!("{}", agent), expected);
760 }
761 }
762
763 #[test]
764 fn test_agent_type_custom_display() {
765 let custom = AgentType::Custom("my-agent".into());
766 assert_eq!(custom.to_string(), "my-agent");
767 assert!(!custom.is_builtin());
768 }
769
770 #[test]
771 fn test_agent_type_from_string_roundtrip() {
772 for name in ["router", "finance", "hr"] {
773 let agent = AgentType::from_string(name);
774 assert_eq!(agent.as_str(), name);
775 }
776 let custom = AgentType::from_string("custom-bot");
777 assert_eq!(custom.as_str(), "custom-bot");
778 }
779
780 #[test]
781 fn test_message_role_serde_roundtrip() {
782 let role = MessageRole::Assistant;
783 let json = serde_json::to_string(&role).unwrap();
784 assert_eq!(json, "\"assistant\"");
785 let parsed: MessageRole = serde_json::from_str(&json).unwrap();
786 assert!(matches!(parsed, MessageRole::Assistant));
787 }
788
789 #[test]
790 fn test_chat_request_serde_roundtrip() {
791 let req = ChatRequest {
792 message: "hello".into(),
793 agent_type: Some(AgentType::Router),
794 context_id: Some("ctx-1".into()),
795 workspace_id: None,
796 model: None,
797 parts: None,
798 previous_response_id: None,
799 web_search: None,
800 };
801 let json = serde_json::to_string(&req).unwrap();
802 let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
803 assert_eq!(parsed.message, "hello");
804 assert_eq!(parsed.agent_type, Some(AgentType::Router));
805 }
806
807 #[test]
808 fn test_source_serde_roundtrip() {
809 let source = Source {
810 title: "Doc".into(),
811 url: Some("https://example.com".into()),
812 relevance_score: 0.9,
813 };
814 let parsed: Source = serde_json::from_str(&serde_json::to_string(&source).unwrap()).unwrap();
815 assert_eq!(parsed.title, "Doc");
816 assert_eq!(parsed.relevance_score, 0.9);
817 }
818
819 #[test]
820 fn test_document_metadata_default_datetime() {
821 let json = r#"{"title":"t","source":"s"}"#;
822 let meta: DocumentMetadata = serde_json::from_str(json).unwrap();
823 assert_eq!(meta.title, "t");
824 assert!(meta.created_at <= Utc::now());
825 }
826
827 #[test]
828 fn test_tool_call_serde_roundtrip() {
829 let call = ToolCall {
830 id: "c1".into(),
831 name: "search".into(),
832 arguments: serde_json::json!({"q": "ares"}),
833 };
834 let parsed: ToolCall = serde_json::from_str(&serde_json::to_string(&call).unwrap()).unwrap();
835 assert_eq!(parsed.name, "search");
836 }
837
838 #[test]
839 fn test_app_error_code_mapping() {
840 assert!(matches!(AppError::Database("x".into()).code(), ErrorCode::DatabaseError));
841 assert!(matches!(AppError::Auth("x".into()).code(), ErrorCode::AuthenticationFailed));
842 assert!(matches!(AppError::NotFound("x".into()).code(), ErrorCode::NotFound));
843 assert!(matches!(AppError::RateLimited("x".into()).code(), ErrorCode::InternalError));
844 }
845
846 #[test]
847 fn test_app_error_from_io() {
848 let err: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "missing").into();
849 assert!(matches!(err, AppError::Internal(_)));
850 assert!(err.to_string().contains("IO error"));
851 }
852
853 #[test]
854 fn test_app_error_from_serde_json() {
855 let bad = "{not json";
856 let err: AppError = serde_json::from_str::<serde_json::Value>(bad).unwrap_err().into();
857 assert!(matches!(err, AppError::InvalidInput(_)));
858 }
859
860 #[test]
861 fn test_search_filter_application() {
862 let doc = Document {
863 id: "1".into(),
864 content: "body".into(),
865 metadata: DocumentMetadata {
866 title: "Guide".into(),
867 source: "docs/rust".into(),
868 tags: vec!["rust".into(), "rag".into()],
869 ..Default::default()
870 },
871 embedding: None,
872 };
873 let filters = [SearchFilter { field: "tags".into(), value: "rust".into() },
874 SearchFilter { field: "source".into(), value: "docs/rust".into() }];
875 let matches = filters.iter().all(|f| match f.field.as_str() {
876 "tags" => doc.metadata.tags.iter().any(|t| t == &f.value),
877 "source" => doc.metadata.source == f.value,
878 _ => false,
879 });
880 assert!(matches);
881 }
882
883 #[test]
884 fn test_rag_search_request_defaults() {
885 let json = r#"{"collection":"c","query":"q"}"#;
886 let req: RagSearchRequest = serde_json::from_str(json).unwrap();
887 assert_eq!(req.limit, 10);
888 assert!((req.threshold - 0.0).abs() < f32::EPSILON);
889 assert!(!req.rerank);
890 }
891
892 #[test]
893 fn test_chat_response_serde_roundtrip() {
894 let resp = ChatResponse {
895 response: "こんにちは 🌍".into(),
896 agent: "router".into(),
897 context_id: String::new(),
898 sources: Some(vec![]),
899 };
900 let parsed: ChatResponse =
901 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
902 assert_eq!(parsed.response, "こんにちは 🌍");
903 assert_eq!(parsed.context_id, "");
904 assert!(parsed.sources.as_ref().unwrap().is_empty());
905 }
906
907 #[test]
908 fn test_research_request_serde_optional_fields() {
909 let json = r#"{"query":"quantum computing"}"#;
910 let req: ResearchRequest = serde_json::from_str(json).unwrap();
911 assert_eq!(req.query, "quantum computing");
912 assert!(req.depth.is_none());
913 assert!(req.max_iterations.is_none());
914
915 let full = ResearchRequest {
916 query: String::new(),
917 depth: Some(0),
918 max_iterations: Some(u8::MAX),
919 };
920 let parsed: ResearchRequest =
921 serde_json::from_str(&serde_json::to_string(&full).unwrap()).unwrap();
922 assert_eq!(parsed.query, "");
923 assert_eq!(parsed.depth, Some(0));
924 assert_eq!(parsed.max_iterations, Some(u8::MAX));
925 }
926
927 #[test]
928 fn test_research_response_serde_empty_sources() {
929 let resp = ResearchResponse {
930 findings: String::new(),
931 sources: vec![],
932 duration_ms: 0,
933 };
934 let parsed: ResearchResponse =
935 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
936 assert!(parsed.findings.is_empty());
937 assert!(parsed.sources.is_empty());
938 assert_eq!(parsed.duration_ms, 0);
939 }
940
941 #[test]
942 fn test_rag_ingest_request_defaults_and_unicode() {
943 let json = r#"{"collection":"docs","content":"café ☕"}"#;
944 let req: RagIngestRequest = serde_json::from_str(json).unwrap();
945 assert_eq!(req.content, "café ☕");
946 assert!(req.title.is_none());
947 assert!(req.source.is_none());
948 assert!(req.tags.is_empty());
949 assert!(req.chunking_strategy.is_none());
950 }
951
952 #[test]
953 fn test_rag_ingest_response_serde_roundtrip() {
954 let resp = RagIngestResponse {
955 chunks_created: 0,
956 document_ids: vec![],
957 collection: "empty".into(),
958 };
959 let parsed: RagIngestResponse =
960 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
961 assert_eq!(parsed.chunks_created, 0);
962 assert!(parsed.document_ids.is_empty());
963 }
964
965 #[test]
966 fn test_rag_search_response_serde_roundtrip() {
967 let resp = RagSearchResponse {
968 results: vec![RagSearchResult {
969 id: "d1".into(),
970 content: "match".into(),
971 score: 1.0,
972 metadata: DocumentMetadata::default(),
973 }],
974 total: 1,
975 strategy: "hybrid".into(),
976 reranked: false,
977 duration_ms: u64::MAX,
978 };
979 let parsed: RagSearchResponse =
980 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
981 assert_eq!(parsed.total, 1);
982 assert_eq!(parsed.duration_ms, u64::MAX);
983 }
984
985 #[test]
986 fn test_rag_delete_collection_serde_roundtrip() {
987 let req = RagDeleteCollectionRequest {
988 collection: "to-delete".into(),
989 };
990 let parsed: RagDeleteCollectionRequest =
991 serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
992 assert_eq!(parsed.collection, "to-delete");
993
994 let resp = RagDeleteCollectionResponse {
995 success: true,
996 collection: "to-delete".into(),
997 documents_deleted: 0,
998 };
999 let parsed_resp: RagDeleteCollectionResponse =
1000 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1001 assert!(parsed_resp.success);
1002 assert_eq!(parsed_resp.documents_deleted, 0);
1003 }
1004
1005 #[test]
1006 fn test_semantic_search_request_defaults() {
1007 let json = r#"{"collection":"c","query":"q"}"#;
1008 let req: SemanticSearchRequest = serde_json::from_str(json).unwrap();
1009 assert_eq!(req.limit, 10);
1010 assert!((req.threshold - 0.0).abs() < f32::EPSILON);
1011 }
1012
1013 #[test]
1014 fn test_semantic_search_response_serde_roundtrip() {
1015 let resp = SemanticSearchResponse {
1016 results: vec![],
1017 total: 0,
1018 duration_ms: 0,
1019 };
1020 let parsed: SemanticSearchResponse =
1021 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1022 assert!(parsed.results.is_empty());
1023 assert_eq!(parsed.total, 0);
1024 }
1025
1026 #[test]
1027 fn test_workflow_request_empty_context_default() {
1028 let json = r#"{"query":"run workflow"}"#;
1029 let req: WorkflowRequest = serde_json::from_str(json).unwrap();
1030 assert_eq!(req.query, "run workflow");
1031 assert!(req.context.is_empty());
1032
1033 let with_ctx = WorkflowRequest {
1034 query: "q".into(),
1035 context: [("key".into(), serde_json::json!(null))]
1036 .into_iter()
1037 .collect(),
1038 };
1039 let parsed: WorkflowRequest =
1040 serde_json::from_str(&serde_json::to_string(&with_ctx).unwrap()).unwrap();
1041 assert!(parsed.context.contains_key("key"));
1042 }
1043
1044 #[test]
1045 fn test_agent_type_serde_builtin_and_custom_unicode() {
1046 for (agent, expected) in [
1047 (AgentType::Router, "\"router\""),
1048 (AgentType::HR, "\"hr\""),
1049 ] {
1050 let json = serde_json::to_string(&agent).unwrap();
1051 assert_eq!(json, expected);
1052 let parsed: AgentType = serde_json::from_str(&json).unwrap();
1053 assert_eq!(parsed, agent);
1054 }
1055 let custom = AgentType::Custom("代理-🤖".into());
1056 let json = serde_json::to_string(&custom).unwrap();
1057 let parsed: AgentType = serde_json::from_str(&json).unwrap();
1058 assert_eq!(parsed.as_str(), "代理-🤖");
1059 }
1060
1061 #[test]
1062 fn test_agent_type_partial_eq_and_clone() {
1063 let a = AgentType::Finance;
1064 let b = a.clone();
1065 assert_eq!(a, b);
1066 assert_ne!(a, AgentType::Sales);
1067 assert!(a.is_builtin());
1068 }
1069
1070 #[test]
1071 fn test_message_role_all_variants_serde() {
1072 for (role, expected) in [
1073 (MessageRole::System, "\"system\""),
1074 (MessageRole::User, "\"user\""),
1075 (MessageRole::Assistant, "\"assistant\""),
1076 ] {
1077 let json = serde_json::to_string(&role).unwrap();
1078 assert_eq!(json, expected);
1079 let parsed: MessageRole = serde_json::from_str(&json).unwrap();
1080 assert_eq!(format!("{:?}", parsed), format!("{:?}", role));
1081 }
1082 }
1083
1084 #[test]
1085 fn test_message_serde_roundtrip_unicode() {
1086 let msg = Message {
1087 role: MessageRole::User,
1088 content: "emoji 🚀 & unicode ñ".into(),
1089 timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1090 };
1091 let parsed: Message =
1092 serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1093 assert_eq!(parsed.content, "emoji 🚀 & unicode ñ");
1094 assert!(matches!(parsed.role, MessageRole::User));
1095 }
1096
1097 #[test]
1098 fn test_user_memory_empty_collections_serde() {
1099 let mem = UserMemory {
1100 user_id: "u0".into(),
1101 preferences: vec![],
1102 facts: vec![],
1103 };
1104 let parsed: UserMemory =
1105 serde_json::from_str(&serde_json::to_string(&mem).unwrap()).unwrap();
1106 assert!(parsed.preferences.is_empty());
1107 assert!(parsed.facts.is_empty());
1108 }
1109
1110 #[test]
1111 fn test_preference_and_memory_fact_boundary_confidence() {
1112 let pref = Preference {
1113 category: String::new(),
1114 key: "lang".into(),
1115 value: "rust".into(),
1116 confidence: 0.0,
1117 };
1118 let parsed: Preference =
1119 serde_json::from_str(&serde_json::to_string(&pref).unwrap()).unwrap();
1120 assert!((parsed.confidence - 0.0).abs() < f32::EPSILON);
1121
1122 let now = Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap();
1123 let fact = MemoryFact {
1124 id: "f1".into(),
1125 user_id: "u1".into(),
1126 category: "work".into(),
1127 fact_key: "role".into(),
1128 fact_value: "engineer".into(),
1129 confidence: 1.0,
1130 created_at: now,
1131 updated_at: now,
1132 };
1133 let parsed_fact: MemoryFact =
1134 serde_json::from_str(&serde_json::to_string(&fact).unwrap()).unwrap();
1135 assert!((parsed_fact.confidence - 1.0).abs() < f32::EPSILON);
1136 }
1137
1138 #[test]
1139 fn test_tool_definition_and_result_serde_roundtrip() {
1140 let def = ToolDefinition {
1141 name: "calc".into(),
1142 description: String::new(),
1143 parameters: serde_json::json!({}),
1144 };
1145 let parsed_def: ToolDefinition =
1146 serde_json::from_str(&serde_json::to_string(&def).unwrap()).unwrap();
1147 assert_eq!(parsed_def.name, "calc");
1148 assert!(parsed_def.description.is_empty());
1149
1150 let result = ToolResult {
1151 tool_call_id: "c1".into(),
1152 result: serde_json::Value::Null,
1153 };
1154 let parsed_result: ToolResult =
1155 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1156 assert!(parsed_result.result.is_null());
1157 }
1158
1159 #[test]
1160 fn test_document_serde_none_embedding_and_metadata_default() {
1161 let doc = Document {
1162 id: "doc-1".into(),
1163 content: String::new(),
1164 metadata: DocumentMetadata::default(),
1165 embedding: None,
1166 };
1167 let parsed: Document =
1168 serde_json::from_str(&serde_json::to_string(&doc).unwrap()).unwrap();
1169 assert!(parsed.content.is_empty());
1170 assert!(parsed.embedding.is_none());
1171 assert!(parsed.metadata.title.is_empty());
1172
1173 let default_meta = DocumentMetadata::default();
1174 assert!(default_meta.tags.is_empty());
1175 assert!(default_meta.source.is_empty());
1176 }
1177
1178 #[test]
1179 fn test_search_query_and_result_clone_debug() {
1180 let query = SearchQuery {
1181 query: "find".into(),
1182 limit: 0,
1183 threshold: 1.0,
1184 filters: None,
1185 };
1186 let cloned = query.clone();
1187 assert_eq!(cloned.limit, 0);
1188 assert!(cloned.filters.is_none());
1189 assert!(format!("{:?}", cloned).contains("find"));
1190
1191 let result = SearchResult {
1192 document: Document {
1193 id: "1".into(),
1194 content: "x".into(),
1195 metadata: DocumentMetadata::default(),
1196 embedding: Some(vec![]),
1197 },
1198 score: 0.0,
1199 };
1200 let cloned_result = result.clone();
1201 assert!((cloned_result.score - 0.0).abs() < f32::EPSILON);
1202 assert!(cloned_result.document.embedding.as_ref().unwrap().is_empty());
1203 }
1204
1205 #[test]
1206 fn test_agent_context_clone_debug() {
1207 let ctx = AgentContext {
1208 user_id: "u1".into(),
1209 session_id: "s1".into(),
1210 conversation_history: vec![],
1211 user_memory: None,
1212 };
1213 let cloned = ctx.clone();
1214 assert_eq!(cloned.user_id, "u1");
1215 assert!(cloned.user_memory.is_none());
1216 assert!(format!("{:?}", cloned).contains("AgentContext"));
1217 }
1218
1219 #[test]
1220 fn test_login_register_token_claims_serde_roundtrip() {
1221 let login = LoginRequest {
1222 email: "user@example.com".into(),
1223 password: String::new(),
1224 };
1225 let parsed_login: LoginRequest =
1226 serde_json::from_str(&serde_json::to_string(&login).unwrap()).unwrap();
1227 assert!(parsed_login.password.is_empty());
1228
1229 let register = RegisterRequest {
1230 email: "new@example.com".into(),
1231 password: "secret".into(),
1232 name: "新規ユーザー".into(),
1233 };
1234 let parsed_register: RegisterRequest =
1235 serde_json::from_str(&serde_json::to_string(®ister).unwrap()).unwrap();
1236 assert_eq!(parsed_register.name, "新規ユーザー");
1237
1238 let token = TokenResponse {
1239 access_token: "access".into(),
1240 refresh_token: "refresh".into(),
1241 expires_in: 0,
1242 };
1243 let parsed_token: TokenResponse =
1244 serde_json::from_str(&serde_json::to_string(&token).unwrap()).unwrap();
1245 assert_eq!(parsed_token.expires_in, 0);
1246
1247 let claims = Claims {
1248 sub: "user-1".into(),
1249 email: "user@example.com".into(),
1250 exp: usize::MAX,
1251 iat: 0,
1252 jti: String::new(),
1253 tenant_id: None,
1254 };
1255 let json = serde_json::to_string(&claims).unwrap();
1256 assert!(!json.contains("jti"));
1257 let parsed_claims: Claims = serde_json::from_str(&json).unwrap();
1258 assert_eq!(parsed_claims.jti, "");
1259 }
1260
1261 #[test]
1262 fn test_error_code_serialize_all_variants() {
1263 let codes = [
1264 (ErrorCode::DatabaseError, "DATABASE_ERROR"),
1265 (ErrorCode::LlmError, "LLM_ERROR"),
1266 (ErrorCode::AuthenticationFailed, "AUTHENTICATION_FAILED"),
1267 (ErrorCode::AuthorizationFailed, "AUTHORIZATION_FAILED"),
1268 (ErrorCode::NotFound, "NOT_FOUND"),
1269 (ErrorCode::InvalidInput, "INVALID_INPUT"),
1270 (ErrorCode::ConfigurationError, "CONFIGURATION_ERROR"),
1271 (ErrorCode::ExternalServiceError, "EXTERNAL_SERVICE_ERROR"),
1272 (ErrorCode::InternalError, "INTERNAL_ERROR"),
1273 ];
1274 for (code, expected) in codes {
1275 let json = serde_json::to_string(&code).unwrap();
1276 assert_eq!(json, format!("\"{}\"", expected));
1277 }
1278 }
1279
1280 #[test]
1281 fn test_app_error_remaining_code_mappings() {
1282 assert!(matches!(
1283 AppError::LLM("x".into()).code(),
1284 ErrorCode::LlmError
1285 ));
1286 assert!(matches!(
1287 AppError::Configuration("x".into()).code(),
1288 ErrorCode::ConfigurationError
1289 ));
1290 assert!(matches!(
1291 AppError::External("x".into()).code(),
1292 ErrorCode::ExternalServiceError
1293 ));
1294 assert!(matches!(
1295 AppError::Unavailable("x".into()).code(),
1296 ErrorCode::InternalError
1297 ));
1298 assert!(matches!(
1299 AppError::FeatureDisabled("x".into()).code(),
1300 ErrorCode::InternalError
1301 ));
1302 assert!(matches!(
1303 AppError::Internal("x".into()).code(),
1304 ErrorCode::InternalError
1305 ));
1306 }
1307
1308 #[test]
1309 fn test_source_clone_and_boundary_scores() {
1310 let source = Source {
1311 title: "t".into(),
1312 url: None,
1313 relevance_score: 0.0,
1314 };
1315 let cloned = source.clone();
1316 assert!(cloned.url.is_none());
1317 assert!((cloned.relevance_score - 0.0).abs() < f32::EPSILON);
1318
1319 let max = Source {
1320 title: "max".into(),
1321 url: Some("https://example.com?q=100%".into()),
1322 relevance_score: 1.0,
1323 };
1324 let parsed: Source =
1325 serde_json::from_str(&serde_json::to_string(&max).unwrap()).unwrap();
1326 assert!((parsed.relevance_score - 1.0).abs() < f32::EPSILON);
1327 }
1328
1329 #[test]
1330 fn test_chat_request_workspace_id_serde_roundtrip() {
1331 let req = ChatRequest {
1332 message: "ping".into(),
1333 agent_type: None,
1334 context_id: None,
1335 workspace_id: Some("ws-éruka-42".into()),
1336 model: None,
1337 parts: None,
1338 previous_response_id: None,
1339 web_search: None,
1340 };
1341 let json = serde_json::to_string(&req).unwrap();
1342 assert!(json.contains("workspace_id"));
1343 let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
1344 assert_eq!(parsed.workspace_id.as_deref(), Some("ws-éruka-42"));
1345 }
1346
1347 #[test]
1348 fn test_rag_search_result_serde_roundtrip() {
1349 let result = RagSearchResult {
1350 id: "chunk-1".into(),
1351 content: "snippet".into(),
1352 score: 0.75,
1353 metadata: DocumentMetadata {
1354 title: "Guide".into(),
1355 source: "docs/guide.md".into(),
1356 tags: vec!["rag".into()],
1357 ..Default::default()
1358 },
1359 };
1360 let parsed: RagSearchResult =
1361 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1362 assert_eq!(parsed.id, "chunk-1");
1363 assert!((parsed.score - 0.75).abs() < f32::EPSILON);
1364 assert_eq!(parsed.metadata.tags, vec!["rag"]);
1365 }
1366
1367 #[test]
1368 fn test_semantic_search_result_serde_roundtrip() {
1369 let result = SemanticSearchResult {
1370 id: "doc-9".into(),
1371 content: "semantic hit".into(),
1372 similarity: 0.91,
1373 metadata: DocumentMetadata::default(),
1374 };
1375 let parsed: SemanticSearchResult =
1376 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1377 assert_eq!(parsed.content, "semantic hit");
1378 assert!((parsed.similarity - 0.91).abs() < f32::EPSILON);
1379 }
1380
1381 #[test]
1382 fn test_app_error_into_response_status_codes() {
1383 let cases = [
1384 (AppError::Auth("denied".into()), 401u16),
1385 (AppError::NotFound("gone".into()), 404),
1386 (AppError::InvalidInput("bad".into()), 400),
1387 (AppError::External("upstream".into()), 502),
1388 (AppError::Unavailable("maintenance".into()), 503),
1389 (AppError::RateLimited("slow".into()), 429),
1390 (AppError::FeatureDisabled("off".into()), 400),
1391 (AppError::Database("db".into()), 500),
1392 ];
1393 for (err, expected) in cases {
1394 assert_eq!(err.status_code(), expected);
1395 }
1396 }
1397
1398 #[test]
1399 fn test_agent_type_from_string_is_case_insensitive() {
1400 assert_eq!(AgentType::from_string("ROUTER"), AgentType::Router);
1401 assert_eq!(AgentType::from_string("Hr"), AgentType::HR);
1402 assert_eq!(
1403 AgentType::from_string("MyCustom"),
1404 AgentType::Custom("MyCustom".into())
1405 );
1406 }
1407}