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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
398 pub parts: Vec<ContentPart>,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
403#[serde(rename_all = "lowercase")]
404pub enum MessageRole {
405 System,
407 User,
409 Assistant,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct UserMemory {
418 pub user_id: String,
420 pub preferences: Vec<Preference>,
422 pub facts: Vec<MemoryFact>,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct Preference {
429 pub category: String,
431 pub key: String,
433 pub value: String,
435 pub confidence: f32,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct MemoryFact {
442 pub id: String,
444 pub user_id: String,
446 pub category: String,
448 pub fact_key: String,
450 pub fact_value: String,
452 pub confidence: f32,
454 pub created_at: DateTime<Utc>,
456 pub updated_at: DateTime<Utc>,
458}
459
460#[derive(Debug, Serialize, Deserialize, Clone)]
464pub struct ToolDefinition {
465 pub name: String,
467 pub description: String,
469 pub parameters: serde_json::Value,
471}
472
473#[derive(Debug, Serialize, Deserialize, Clone)]
475pub struct ToolCall {
476 pub id: String,
478 pub name: String,
480 pub arguments: serde_json::Value,
482}
483
484#[derive(Debug, Serialize, Deserialize)]
486pub struct ToolResult {
487 pub tool_call_id: String,
489 pub result: serde_json::Value,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct Document {
498 pub id: String,
500 pub content: String,
502 pub metadata: DocumentMetadata,
504 pub embedding: Option<Vec<f32>>,
506}
507
508#[derive(Debug, Clone, Default, Serialize, Deserialize)]
510pub struct DocumentMetadata {
511 #[serde(default)]
513 pub title: String,
514 #[serde(default)]
516 pub source: String,
517 #[serde(default = "default_datetime")]
519 pub created_at: DateTime<Utc>,
520 #[serde(default)]
522 pub tags: Vec<String>,
523}
524
525#[derive(Debug, Clone)]
527pub struct SearchQuery {
528 pub query: String,
530 pub limit: usize,
532 pub threshold: f32,
534 pub filters: Option<Vec<SearchFilter>>,
536}
537
538#[derive(Debug, Clone)]
540pub struct SearchFilter {
541 pub field: String,
543 pub value: String,
545}
546
547#[derive(Debug, Clone)]
549pub struct SearchResult {
550 pub document: Document,
552 pub score: f32,
554}
555
556#[derive(Debug, Serialize, Deserialize)]
560pub struct LoginRequest {
561 pub email: String,
563 pub password: String,
565}
566
567#[derive(Debug, Serialize, Deserialize)]
569pub struct RegisterRequest {
570 pub email: String,
572 pub password: String,
574 pub name: String,
576}
577
578#[derive(Debug, Serialize, Deserialize)]
580pub struct TokenResponse {
581 pub access_token: String,
583 pub refresh_token: String,
585 pub expires_in: i64,
587}
588
589#[derive(Debug, Serialize, Deserialize, Clone)]
591pub struct Claims {
592 pub sub: String,
594 pub email: String,
596 pub exp: usize,
598 pub iat: usize,
600 #[serde(default, skip_serializing_if = "String::is_empty")]
602 pub jti: String,
603 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub tenant_id: Option<String>,
606}
607
608#[derive(Debug, Clone, Copy, Serialize)]
613#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
614pub enum ErrorCode {
615 DatabaseError,
617 LlmError,
619 AuthenticationFailed,
621 AuthorizationFailed,
623 NotFound,
625 InvalidInput,
627 ConfigurationError,
629 ExternalServiceError,
631 InternalError,
633}
634
635#[derive(Debug, thiserror::Error)]
637pub enum AppError {
638 #[error("Database error: {0}")]
640 Database(String),
641
642 #[error("LLM error: {0}")]
644 LLM(String),
645
646 #[error("Authentication error: {0}")]
648 Auth(String),
649
650 #[error("Not found: {0}")]
652 NotFound(String),
653
654 #[error("Invalid input: {0}")]
656 InvalidInput(String),
657
658 #[error("Configuration error: {0}")]
660 Configuration(String),
661
662 #[error("External service error: {0}")]
664 External(String),
665
666 #[error("Internal error: {0}")]
668 Internal(String),
669
670 #[error("Service unavailable: {0}")]
672 Unavailable(String),
673 #[error("Feature disabled: {0}")]
675 FeatureDisabled(String),
676
677 #[error("Rate limited: {0}")]
679 RateLimited(String),
680}
681
682impl AppError {
683 pub fn code(&self) -> ErrorCode {
685 match self {
686 AppError::Database(_) => ErrorCode::DatabaseError,
687 AppError::LLM(_) => ErrorCode::LlmError,
688 AppError::Auth(_) => ErrorCode::AuthenticationFailed,
689 AppError::NotFound(_) => ErrorCode::NotFound,
690 AppError::InvalidInput(_) => ErrorCode::InvalidInput,
691 AppError::Configuration(_) => ErrorCode::ConfigurationError,
692 AppError::External(_) => ErrorCode::ExternalServiceError,
693 AppError::Internal(_) => ErrorCode::InternalError,
694 AppError::Unavailable(_) => ErrorCode::InternalError,
695AppError::RateLimited(_) => ErrorCode::InternalError,
696AppError::FeatureDisabled(_) => ErrorCode::InternalError,
697 }
698 }
699
700 pub fn is_retryable(&self) -> bool {
703 matches!(
704 self,
705 AppError::External(_) | AppError::Unavailable(_) | AppError::RateLimited(_)
706 )
707 }
708
709 pub fn status_code(&self) -> u16 {
711 match self {
712 AppError::Database(_) => 500,
713 AppError::LLM(_) => 500,
714 AppError::Auth(_) => 401,
715 AppError::NotFound(_) => 404,
716 AppError::InvalidInput(_) => 400,
717 AppError::Configuration(_) => 500,
718 AppError::External(_) => 502,
719 AppError::Internal(_) => 500,
720 AppError::Unavailable(_) => 503,
721 AppError::RateLimited(_) => 429,
722 AppError::FeatureDisabled(_) => 400,
723 }
724 }
725}
726
727impl From<std::io::Error> for AppError {
730 fn from(err: std::io::Error) -> Self {
731 AppError::Internal(format!("IO error: {}", err))
732 }
733}
734
735impl From<serde_json::Error> for AppError {
736 fn from(err: serde_json::Error) -> Self {
737 AppError::InvalidInput(format!("JSON error: {}", err))
738 }
739}
740
741pub type Result<T> = std::result::Result<T, AppError>;
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747 use chrono::{TimeZone, Utc};
748
749 #[test]
750 fn test_agent_type_display_all_builtins() {
751 let cases = vec![
752 (AgentType::Router, "router"),
753 (AgentType::Orchestrator, "orchestrator"),
754 (AgentType::Product, "product"),
755 (AgentType::Invoice, "invoice"),
756 (AgentType::Sales, "sales"),
757 (AgentType::Finance, "finance"),
758 (AgentType::HR, "hr"),
759 ];
760 for (agent, expected) in cases {
761 assert_eq!(agent.to_string(), expected);
762 assert_eq!(format!("{}", agent), expected);
763 }
764 }
765
766 #[test]
767 fn test_agent_type_custom_display() {
768 let custom = AgentType::Custom("my-agent".into());
769 assert_eq!(custom.to_string(), "my-agent");
770 assert!(!custom.is_builtin());
771 }
772
773 #[test]
774 fn test_agent_type_from_string_roundtrip() {
775 for name in ["router", "finance", "hr"] {
776 let agent = AgentType::from_string(name);
777 assert_eq!(agent.as_str(), name);
778 }
779 let custom = AgentType::from_string("custom-bot");
780 assert_eq!(custom.as_str(), "custom-bot");
781 }
782
783 #[test]
784 fn test_message_role_serde_roundtrip() {
785 let role = MessageRole::Assistant;
786 let json = serde_json::to_string(&role).unwrap();
787 assert_eq!(json, "\"assistant\"");
788 let parsed: MessageRole = serde_json::from_str(&json).unwrap();
789 assert!(matches!(parsed, MessageRole::Assistant));
790 }
791
792 #[test]
793 fn test_chat_request_serde_roundtrip() {
794 let req = ChatRequest {
795 message: "hello".into(),
796 agent_type: Some(AgentType::Router),
797 context_id: Some("ctx-1".into()),
798 workspace_id: None,
799 model: None,
800 parts: None,
801 previous_response_id: None,
802 web_search: None,
803 };
804 let json = serde_json::to_string(&req).unwrap();
805 let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
806 assert_eq!(parsed.message, "hello");
807 assert_eq!(parsed.agent_type, Some(AgentType::Router));
808 }
809
810 #[test]
811 fn test_source_serde_roundtrip() {
812 let source = Source {
813 title: "Doc".into(),
814 url: Some("https://example.com".into()),
815 relevance_score: 0.9,
816 };
817 let parsed: Source = serde_json::from_str(&serde_json::to_string(&source).unwrap()).unwrap();
818 assert_eq!(parsed.title, "Doc");
819 assert_eq!(parsed.relevance_score, 0.9);
820 }
821
822 #[test]
823 fn test_document_metadata_default_datetime() {
824 let json = r#"{"title":"t","source":"s"}"#;
825 let meta: DocumentMetadata = serde_json::from_str(json).unwrap();
826 assert_eq!(meta.title, "t");
827 assert!(meta.created_at <= Utc::now());
828 }
829
830 #[test]
831 fn test_tool_call_serde_roundtrip() {
832 let call = ToolCall {
833 id: "c1".into(),
834 name: "search".into(),
835 arguments: serde_json::json!({"q": "ares"}),
836 };
837 let parsed: ToolCall = serde_json::from_str(&serde_json::to_string(&call).unwrap()).unwrap();
838 assert_eq!(parsed.name, "search");
839 }
840
841 #[test]
842 fn test_app_error_code_mapping() {
843 assert!(matches!(AppError::Database("x".into()).code(), ErrorCode::DatabaseError));
844 assert!(matches!(AppError::Auth("x".into()).code(), ErrorCode::AuthenticationFailed));
845 assert!(matches!(AppError::NotFound("x".into()).code(), ErrorCode::NotFound));
846 assert!(matches!(AppError::RateLimited("x".into()).code(), ErrorCode::InternalError));
847 }
848
849 #[test]
850 fn test_app_error_from_io() {
851 let err: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "missing").into();
852 assert!(matches!(err, AppError::Internal(_)));
853 assert!(err.to_string().contains("IO error"));
854 }
855
856 #[test]
857 fn test_app_error_from_serde_json() {
858 let bad = "{not json";
859 let err: AppError = serde_json::from_str::<serde_json::Value>(bad).unwrap_err().into();
860 assert!(matches!(err, AppError::InvalidInput(_)));
861 }
862
863 #[test]
864 fn test_search_filter_application() {
865 let doc = Document {
866 id: "1".into(),
867 content: "body".into(),
868 metadata: DocumentMetadata {
869 title: "Guide".into(),
870 source: "docs/rust".into(),
871 tags: vec!["rust".into(), "rag".into()],
872 ..Default::default()
873 },
874 embedding: None,
875 };
876 let filters = [SearchFilter { field: "tags".into(), value: "rust".into() },
877 SearchFilter { field: "source".into(), value: "docs/rust".into() }];
878 let matches = filters.iter().all(|f| match f.field.as_str() {
879 "tags" => doc.metadata.tags.iter().any(|t| t == &f.value),
880 "source" => doc.metadata.source == f.value,
881 _ => false,
882 });
883 assert!(matches);
884 }
885
886 #[test]
887 fn test_rag_search_request_defaults() {
888 let json = r#"{"collection":"c","query":"q"}"#;
889 let req: RagSearchRequest = serde_json::from_str(json).unwrap();
890 assert_eq!(req.limit, 10);
891 assert!((req.threshold - 0.0).abs() < f32::EPSILON);
892 assert!(!req.rerank);
893 }
894
895 #[test]
896 fn test_chat_response_serde_roundtrip() {
897 let resp = ChatResponse {
898 response: "こんにちは 🌍".into(),
899 agent: "router".into(),
900 context_id: String::new(),
901 sources: Some(vec![]),
902 };
903 let parsed: ChatResponse =
904 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
905 assert_eq!(parsed.response, "こんにちは 🌍");
906 assert_eq!(parsed.context_id, "");
907 assert!(parsed.sources.as_ref().unwrap().is_empty());
908 }
909
910 #[test]
911 fn test_research_request_serde_optional_fields() {
912 let json = r#"{"query":"quantum computing"}"#;
913 let req: ResearchRequest = serde_json::from_str(json).unwrap();
914 assert_eq!(req.query, "quantum computing");
915 assert!(req.depth.is_none());
916 assert!(req.max_iterations.is_none());
917
918 let full = ResearchRequest {
919 query: String::new(),
920 depth: Some(0),
921 max_iterations: Some(u8::MAX),
922 };
923 let parsed: ResearchRequest =
924 serde_json::from_str(&serde_json::to_string(&full).unwrap()).unwrap();
925 assert_eq!(parsed.query, "");
926 assert_eq!(parsed.depth, Some(0));
927 assert_eq!(parsed.max_iterations, Some(u8::MAX));
928 }
929
930 #[test]
931 fn test_research_response_serde_empty_sources() {
932 let resp = ResearchResponse {
933 findings: String::new(),
934 sources: vec![],
935 duration_ms: 0,
936 };
937 let parsed: ResearchResponse =
938 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
939 assert!(parsed.findings.is_empty());
940 assert!(parsed.sources.is_empty());
941 assert_eq!(parsed.duration_ms, 0);
942 }
943
944 #[test]
945 fn test_rag_ingest_request_defaults_and_unicode() {
946 let json = r#"{"collection":"docs","content":"café ☕"}"#;
947 let req: RagIngestRequest = serde_json::from_str(json).unwrap();
948 assert_eq!(req.content, "café ☕");
949 assert!(req.title.is_none());
950 assert!(req.source.is_none());
951 assert!(req.tags.is_empty());
952 assert!(req.chunking_strategy.is_none());
953 }
954
955 #[test]
956 fn test_rag_ingest_response_serde_roundtrip() {
957 let resp = RagIngestResponse {
958 chunks_created: 0,
959 document_ids: vec![],
960 collection: "empty".into(),
961 };
962 let parsed: RagIngestResponse =
963 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
964 assert_eq!(parsed.chunks_created, 0);
965 assert!(parsed.document_ids.is_empty());
966 }
967
968 #[test]
969 fn test_rag_search_response_serde_roundtrip() {
970 let resp = RagSearchResponse {
971 results: vec![RagSearchResult {
972 id: "d1".into(),
973 content: "match".into(),
974 score: 1.0,
975 metadata: DocumentMetadata::default(),
976 }],
977 total: 1,
978 strategy: "hybrid".into(),
979 reranked: false,
980 duration_ms: u64::MAX,
981 };
982 let parsed: RagSearchResponse =
983 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
984 assert_eq!(parsed.total, 1);
985 assert_eq!(parsed.duration_ms, u64::MAX);
986 }
987
988 #[test]
989 fn test_rag_delete_collection_serde_roundtrip() {
990 let req = RagDeleteCollectionRequest {
991 collection: "to-delete".into(),
992 };
993 let parsed: RagDeleteCollectionRequest =
994 serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
995 assert_eq!(parsed.collection, "to-delete");
996
997 let resp = RagDeleteCollectionResponse {
998 success: true,
999 collection: "to-delete".into(),
1000 documents_deleted: 0,
1001 };
1002 let parsed_resp: RagDeleteCollectionResponse =
1003 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1004 assert!(parsed_resp.success);
1005 assert_eq!(parsed_resp.documents_deleted, 0);
1006 }
1007
1008 #[test]
1009 fn test_semantic_search_request_defaults() {
1010 let json = r#"{"collection":"c","query":"q"}"#;
1011 let req: SemanticSearchRequest = serde_json::from_str(json).unwrap();
1012 assert_eq!(req.limit, 10);
1013 assert!((req.threshold - 0.0).abs() < f32::EPSILON);
1014 }
1015
1016 #[test]
1017 fn test_semantic_search_response_serde_roundtrip() {
1018 let resp = SemanticSearchResponse {
1019 results: vec![],
1020 total: 0,
1021 duration_ms: 0,
1022 };
1023 let parsed: SemanticSearchResponse =
1024 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1025 assert!(parsed.results.is_empty());
1026 assert_eq!(parsed.total, 0);
1027 }
1028
1029 #[test]
1030 fn test_workflow_request_empty_context_default() {
1031 let json = r#"{"query":"run workflow"}"#;
1032 let req: WorkflowRequest = serde_json::from_str(json).unwrap();
1033 assert_eq!(req.query, "run workflow");
1034 assert!(req.context.is_empty());
1035
1036 let with_ctx = WorkflowRequest {
1037 query: "q".into(),
1038 context: [("key".into(), serde_json::json!(null))]
1039 .into_iter()
1040 .collect(),
1041 };
1042 let parsed: WorkflowRequest =
1043 serde_json::from_str(&serde_json::to_string(&with_ctx).unwrap()).unwrap();
1044 assert!(parsed.context.contains_key("key"));
1045 }
1046
1047 #[test]
1048 fn test_agent_type_serde_builtin_and_custom_unicode() {
1049 for (agent, expected) in [
1050 (AgentType::Router, "\"router\""),
1051 (AgentType::HR, "\"hr\""),
1052 ] {
1053 let json = serde_json::to_string(&agent).unwrap();
1054 assert_eq!(json, expected);
1055 let parsed: AgentType = serde_json::from_str(&json).unwrap();
1056 assert_eq!(parsed, agent);
1057 }
1058 let custom = AgentType::Custom("代理-🤖".into());
1059 let json = serde_json::to_string(&custom).unwrap();
1060 let parsed: AgentType = serde_json::from_str(&json).unwrap();
1061 assert_eq!(parsed.as_str(), "代理-🤖");
1062 }
1063
1064 #[test]
1065 fn test_agent_type_partial_eq_and_clone() {
1066 let a = AgentType::Finance;
1067 let b = a.clone();
1068 assert_eq!(a, b);
1069 assert_ne!(a, AgentType::Sales);
1070 assert!(a.is_builtin());
1071 }
1072
1073 #[test]
1074 fn test_message_role_all_variants_serde() {
1075 for (role, expected) in [
1076 (MessageRole::System, "\"system\""),
1077 (MessageRole::User, "\"user\""),
1078 (MessageRole::Assistant, "\"assistant\""),
1079 ] {
1080 let json = serde_json::to_string(&role).unwrap();
1081 assert_eq!(json, expected);
1082 let parsed: MessageRole = serde_json::from_str(&json).unwrap();
1083 assert_eq!(format!("{:?}", parsed), format!("{:?}", role));
1084 }
1085 }
1086
1087 #[test]
1088 fn test_message_serde_roundtrip_unicode() {
1089 let msg = Message {
1090 role: MessageRole::User,
1091 content: "emoji 🚀 & unicode ñ".into(),
1092 timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1093 parts: vec![],
1094 };
1095 let parsed: Message =
1096 serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1097 assert_eq!(parsed.content, "emoji 🚀 & unicode ñ");
1098 assert!(matches!(parsed.role, MessageRole::User));
1099 }
1100
1101 #[test]
1102 fn test_message_parts_serde_default_and_roundtrip() {
1103 let parsed: Message = serde_json::from_str(
1104 r#"{"role":"user","content":"hi","timestamp":"2024-01-01T00:00:00Z"}"#,
1105 )
1106 .unwrap();
1107 assert!(parsed.parts.is_empty());
1108
1109 let msg = Message {
1110 role: MessageRole::User,
1111 content: "hi".into(),
1112 timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1113 parts: vec![ContentPart::Text {
1114 text: "photo".into(),
1115 }],
1116 };
1117 let parsed: Message =
1118 serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1119 assert_eq!(
1120 parsed.parts,
1121 vec![ContentPart::Text {
1122 text: "photo".into(),
1123 }]
1124 );
1125 }
1126
1127 #[test]
1128 fn test_user_memory_empty_collections_serde() {
1129 let mem = UserMemory {
1130 user_id: "u0".into(),
1131 preferences: vec![],
1132 facts: vec![],
1133 };
1134 let parsed: UserMemory =
1135 serde_json::from_str(&serde_json::to_string(&mem).unwrap()).unwrap();
1136 assert!(parsed.preferences.is_empty());
1137 assert!(parsed.facts.is_empty());
1138 }
1139
1140 #[test]
1141 fn test_preference_and_memory_fact_boundary_confidence() {
1142 let pref = Preference {
1143 category: String::new(),
1144 key: "lang".into(),
1145 value: "rust".into(),
1146 confidence: 0.0,
1147 };
1148 let parsed: Preference =
1149 serde_json::from_str(&serde_json::to_string(&pref).unwrap()).unwrap();
1150 assert!((parsed.confidence - 0.0).abs() < f32::EPSILON);
1151
1152 let now = Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap();
1153 let fact = MemoryFact {
1154 id: "f1".into(),
1155 user_id: "u1".into(),
1156 category: "work".into(),
1157 fact_key: "role".into(),
1158 fact_value: "engineer".into(),
1159 confidence: 1.0,
1160 created_at: now,
1161 updated_at: now,
1162 };
1163 let parsed_fact: MemoryFact =
1164 serde_json::from_str(&serde_json::to_string(&fact).unwrap()).unwrap();
1165 assert!((parsed_fact.confidence - 1.0).abs() < f32::EPSILON);
1166 }
1167
1168 #[test]
1169 fn test_tool_definition_and_result_serde_roundtrip() {
1170 let def = ToolDefinition {
1171 name: "calc".into(),
1172 description: String::new(),
1173 parameters: serde_json::json!({}),
1174 };
1175 let parsed_def: ToolDefinition =
1176 serde_json::from_str(&serde_json::to_string(&def).unwrap()).unwrap();
1177 assert_eq!(parsed_def.name, "calc");
1178 assert!(parsed_def.description.is_empty());
1179
1180 let result = ToolResult {
1181 tool_call_id: "c1".into(),
1182 result: serde_json::Value::Null,
1183 };
1184 let parsed_result: ToolResult =
1185 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1186 assert!(parsed_result.result.is_null());
1187 }
1188
1189 #[test]
1190 fn test_document_serde_none_embedding_and_metadata_default() {
1191 let doc = Document {
1192 id: "doc-1".into(),
1193 content: String::new(),
1194 metadata: DocumentMetadata::default(),
1195 embedding: None,
1196 };
1197 let parsed: Document =
1198 serde_json::from_str(&serde_json::to_string(&doc).unwrap()).unwrap();
1199 assert!(parsed.content.is_empty());
1200 assert!(parsed.embedding.is_none());
1201 assert!(parsed.metadata.title.is_empty());
1202
1203 let default_meta = DocumentMetadata::default();
1204 assert!(default_meta.tags.is_empty());
1205 assert!(default_meta.source.is_empty());
1206 }
1207
1208 #[test]
1209 fn test_search_query_and_result_clone_debug() {
1210 let query = SearchQuery {
1211 query: "find".into(),
1212 limit: 0,
1213 threshold: 1.0,
1214 filters: None,
1215 };
1216 let cloned = query.clone();
1217 assert_eq!(cloned.limit, 0);
1218 assert!(cloned.filters.is_none());
1219 assert!(format!("{:?}", cloned).contains("find"));
1220
1221 let result = SearchResult {
1222 document: Document {
1223 id: "1".into(),
1224 content: "x".into(),
1225 metadata: DocumentMetadata::default(),
1226 embedding: Some(vec![]),
1227 },
1228 score: 0.0,
1229 };
1230 let cloned_result = result.clone();
1231 assert!((cloned_result.score - 0.0).abs() < f32::EPSILON);
1232 assert!(cloned_result.document.embedding.as_ref().unwrap().is_empty());
1233 }
1234
1235 #[test]
1236 fn test_agent_context_clone_debug() {
1237 let ctx = AgentContext {
1238 user_id: "u1".into(),
1239 session_id: "s1".into(),
1240 conversation_history: vec![],
1241 user_memory: None,
1242 };
1243 let cloned = ctx.clone();
1244 assert_eq!(cloned.user_id, "u1");
1245 assert!(cloned.user_memory.is_none());
1246 assert!(format!("{:?}", cloned).contains("AgentContext"));
1247 }
1248
1249 #[test]
1250 fn test_login_register_token_claims_serde_roundtrip() {
1251 let login = LoginRequest {
1252 email: "user@example.com".into(),
1253 password: String::new(),
1254 };
1255 let parsed_login: LoginRequest =
1256 serde_json::from_str(&serde_json::to_string(&login).unwrap()).unwrap();
1257 assert!(parsed_login.password.is_empty());
1258
1259 let register = RegisterRequest {
1260 email: "new@example.com".into(),
1261 password: "secret".into(),
1262 name: "新規ユーザー".into(),
1263 };
1264 let parsed_register: RegisterRequest =
1265 serde_json::from_str(&serde_json::to_string(®ister).unwrap()).unwrap();
1266 assert_eq!(parsed_register.name, "新規ユーザー");
1267
1268 let token = TokenResponse {
1269 access_token: "access".into(),
1270 refresh_token: "refresh".into(),
1271 expires_in: 0,
1272 };
1273 let parsed_token: TokenResponse =
1274 serde_json::from_str(&serde_json::to_string(&token).unwrap()).unwrap();
1275 assert_eq!(parsed_token.expires_in, 0);
1276
1277 let claims = Claims {
1278 sub: "user-1".into(),
1279 email: "user@example.com".into(),
1280 exp: usize::MAX,
1281 iat: 0,
1282 jti: String::new(),
1283 tenant_id: None,
1284 };
1285 let json = serde_json::to_string(&claims).unwrap();
1286 assert!(!json.contains("jti"));
1287 let parsed_claims: Claims = serde_json::from_str(&json).unwrap();
1288 assert_eq!(parsed_claims.jti, "");
1289 }
1290
1291 #[test]
1292 fn test_error_code_serialize_all_variants() {
1293 let codes = [
1294 (ErrorCode::DatabaseError, "DATABASE_ERROR"),
1295 (ErrorCode::LlmError, "LLM_ERROR"),
1296 (ErrorCode::AuthenticationFailed, "AUTHENTICATION_FAILED"),
1297 (ErrorCode::AuthorizationFailed, "AUTHORIZATION_FAILED"),
1298 (ErrorCode::NotFound, "NOT_FOUND"),
1299 (ErrorCode::InvalidInput, "INVALID_INPUT"),
1300 (ErrorCode::ConfigurationError, "CONFIGURATION_ERROR"),
1301 (ErrorCode::ExternalServiceError, "EXTERNAL_SERVICE_ERROR"),
1302 (ErrorCode::InternalError, "INTERNAL_ERROR"),
1303 ];
1304 for (code, expected) in codes {
1305 let json = serde_json::to_string(&code).unwrap();
1306 assert_eq!(json, format!("\"{}\"", expected));
1307 }
1308 }
1309
1310 #[test]
1311 fn test_app_error_remaining_code_mappings() {
1312 assert!(matches!(
1313 AppError::LLM("x".into()).code(),
1314 ErrorCode::LlmError
1315 ));
1316 assert!(matches!(
1317 AppError::Configuration("x".into()).code(),
1318 ErrorCode::ConfigurationError
1319 ));
1320 assert!(matches!(
1321 AppError::External("x".into()).code(),
1322 ErrorCode::ExternalServiceError
1323 ));
1324 assert!(matches!(
1325 AppError::Unavailable("x".into()).code(),
1326 ErrorCode::InternalError
1327 ));
1328 assert!(matches!(
1329 AppError::FeatureDisabled("x".into()).code(),
1330 ErrorCode::InternalError
1331 ));
1332 assert!(matches!(
1333 AppError::Internal("x".into()).code(),
1334 ErrorCode::InternalError
1335 ));
1336 }
1337
1338 #[test]
1339 fn test_source_clone_and_boundary_scores() {
1340 let source = Source {
1341 title: "t".into(),
1342 url: None,
1343 relevance_score: 0.0,
1344 };
1345 let cloned = source.clone();
1346 assert!(cloned.url.is_none());
1347 assert!((cloned.relevance_score - 0.0).abs() < f32::EPSILON);
1348
1349 let max = Source {
1350 title: "max".into(),
1351 url: Some("https://example.com?q=100%".into()),
1352 relevance_score: 1.0,
1353 };
1354 let parsed: Source =
1355 serde_json::from_str(&serde_json::to_string(&max).unwrap()).unwrap();
1356 assert!((parsed.relevance_score - 1.0).abs() < f32::EPSILON);
1357 }
1358
1359 #[test]
1360 fn test_chat_request_workspace_id_serde_roundtrip() {
1361 let req = ChatRequest {
1362 message: "ping".into(),
1363 agent_type: None,
1364 context_id: None,
1365 workspace_id: Some("ws-éruka-42".into()),
1366 model: None,
1367 parts: None,
1368 previous_response_id: None,
1369 web_search: None,
1370 };
1371 let json = serde_json::to_string(&req).unwrap();
1372 assert!(json.contains("workspace_id"));
1373 let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
1374 assert_eq!(parsed.workspace_id.as_deref(), Some("ws-éruka-42"));
1375 }
1376
1377 #[test]
1378 fn test_rag_search_result_serde_roundtrip() {
1379 let result = RagSearchResult {
1380 id: "chunk-1".into(),
1381 content: "snippet".into(),
1382 score: 0.75,
1383 metadata: DocumentMetadata {
1384 title: "Guide".into(),
1385 source: "docs/guide.md".into(),
1386 tags: vec!["rag".into()],
1387 ..Default::default()
1388 },
1389 };
1390 let parsed: RagSearchResult =
1391 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1392 assert_eq!(parsed.id, "chunk-1");
1393 assert!((parsed.score - 0.75).abs() < f32::EPSILON);
1394 assert_eq!(parsed.metadata.tags, vec!["rag"]);
1395 }
1396
1397 #[test]
1398 fn test_semantic_search_result_serde_roundtrip() {
1399 let result = SemanticSearchResult {
1400 id: "doc-9".into(),
1401 content: "semantic hit".into(),
1402 similarity: 0.91,
1403 metadata: DocumentMetadata::default(),
1404 };
1405 let parsed: SemanticSearchResult =
1406 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1407 assert_eq!(parsed.content, "semantic hit");
1408 assert!((parsed.similarity - 0.91).abs() < f32::EPSILON);
1409 }
1410
1411 #[test]
1412 fn test_app_error_into_response_status_codes() {
1413 let cases = [
1414 (AppError::Auth("denied".into()), 401u16),
1415 (AppError::NotFound("gone".into()), 404),
1416 (AppError::InvalidInput("bad".into()), 400),
1417 (AppError::External("upstream".into()), 502),
1418 (AppError::Unavailable("maintenance".into()), 503),
1419 (AppError::RateLimited("slow".into()), 429),
1420 (AppError::FeatureDisabled("off".into()), 400),
1421 (AppError::Database("db".into()), 500),
1422 ];
1423 for (err, expected) in cases {
1424 assert_eq!(err.status_code(), expected);
1425 }
1426 }
1427
1428 #[test]
1429 fn test_agent_type_from_string_is_case_insensitive() {
1430 assert_eq!(AgentType::from_string("ROUTER"), AgentType::Router);
1431 assert_eq!(AgentType::from_string("Hr"), AgentType::HR);
1432 assert_eq!(
1433 AgentType::from_string("MyCustom"),
1434 AgentType::Custom("MyCustom".into())
1435 );
1436 }
1437}