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,
695 AppError::RateLimited(_) => ErrorCode::InternalError,
696 AppError::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 =
818 serde_json::from_str(&serde_json::to_string(&source).unwrap()).unwrap();
819 assert_eq!(parsed.title, "Doc");
820 assert_eq!(parsed.relevance_score, 0.9);
821 }
822
823 #[test]
824 fn test_document_metadata_default_datetime() {
825 let json = r#"{"title":"t","source":"s"}"#;
826 let meta: DocumentMetadata = serde_json::from_str(json).unwrap();
827 assert_eq!(meta.title, "t");
828 assert!(meta.created_at <= Utc::now());
829 }
830
831 #[test]
832 fn test_tool_call_serde_roundtrip() {
833 let call = ToolCall {
834 id: "c1".into(),
835 name: "search".into(),
836 arguments: serde_json::json!({"q": "ares"}),
837 };
838 let parsed: ToolCall =
839 serde_json::from_str(&serde_json::to_string(&call).unwrap()).unwrap();
840 assert_eq!(parsed.name, "search");
841 }
842
843 #[test]
844 fn test_app_error_code_mapping() {
845 assert!(matches!(
846 AppError::Database("x".into()).code(),
847 ErrorCode::DatabaseError
848 ));
849 assert!(matches!(
850 AppError::Auth("x".into()).code(),
851 ErrorCode::AuthenticationFailed
852 ));
853 assert!(matches!(
854 AppError::NotFound("x".into()).code(),
855 ErrorCode::NotFound
856 ));
857 assert!(matches!(
858 AppError::RateLimited("x".into()).code(),
859 ErrorCode::InternalError
860 ));
861 }
862
863 #[test]
864 fn test_app_error_from_io() {
865 let err: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "missing").into();
866 assert!(matches!(err, AppError::Internal(_)));
867 assert!(err.to_string().contains("IO error"));
868 }
869
870 #[test]
871 fn test_app_error_from_serde_json() {
872 let bad = "{not json";
873 let err: AppError = serde_json::from_str::<serde_json::Value>(bad)
874 .unwrap_err()
875 .into();
876 assert!(matches!(err, AppError::InvalidInput(_)));
877 }
878
879 #[test]
880 fn test_search_filter_application() {
881 let doc = Document {
882 id: "1".into(),
883 content: "body".into(),
884 metadata: DocumentMetadata {
885 title: "Guide".into(),
886 source: "docs/rust".into(),
887 tags: vec!["rust".into(), "rag".into()],
888 ..Default::default()
889 },
890 embedding: None,
891 };
892 let filters = [
893 SearchFilter {
894 field: "tags".into(),
895 value: "rust".into(),
896 },
897 SearchFilter {
898 field: "source".into(),
899 value: "docs/rust".into(),
900 },
901 ];
902 let matches = filters.iter().all(|f| match f.field.as_str() {
903 "tags" => doc.metadata.tags.iter().any(|t| t == &f.value),
904 "source" => doc.metadata.source == f.value,
905 _ => false,
906 });
907 assert!(matches);
908 }
909
910 #[test]
911 fn test_rag_search_request_defaults() {
912 let json = r#"{"collection":"c","query":"q"}"#;
913 let req: RagSearchRequest = serde_json::from_str(json).unwrap();
914 assert_eq!(req.limit, 10);
915 assert!((req.threshold - 0.0).abs() < f32::EPSILON);
916 assert!(!req.rerank);
917 }
918
919 #[test]
920 fn test_chat_response_serde_roundtrip() {
921 let resp = ChatResponse {
922 response: "こんにちは 🌍".into(),
923 agent: "router".into(),
924 context_id: String::new(),
925 sources: Some(vec![]),
926 };
927 let parsed: ChatResponse =
928 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
929 assert_eq!(parsed.response, "こんにちは 🌍");
930 assert_eq!(parsed.context_id, "");
931 assert!(parsed.sources.as_ref().unwrap().is_empty());
932 }
933
934 #[test]
935 fn test_research_request_serde_optional_fields() {
936 let json = r#"{"query":"quantum computing"}"#;
937 let req: ResearchRequest = serde_json::from_str(json).unwrap();
938 assert_eq!(req.query, "quantum computing");
939 assert!(req.depth.is_none());
940 assert!(req.max_iterations.is_none());
941
942 let full = ResearchRequest {
943 query: String::new(),
944 depth: Some(0),
945 max_iterations: Some(u8::MAX),
946 };
947 let parsed: ResearchRequest =
948 serde_json::from_str(&serde_json::to_string(&full).unwrap()).unwrap();
949 assert_eq!(parsed.query, "");
950 assert_eq!(parsed.depth, Some(0));
951 assert_eq!(parsed.max_iterations, Some(u8::MAX));
952 }
953
954 #[test]
955 fn test_research_response_serde_empty_sources() {
956 let resp = ResearchResponse {
957 findings: String::new(),
958 sources: vec![],
959 duration_ms: 0,
960 };
961 let parsed: ResearchResponse =
962 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
963 assert!(parsed.findings.is_empty());
964 assert!(parsed.sources.is_empty());
965 assert_eq!(parsed.duration_ms, 0);
966 }
967
968 #[test]
969 fn test_rag_ingest_request_defaults_and_unicode() {
970 let json = r#"{"collection":"docs","content":"café ☕"}"#;
971 let req: RagIngestRequest = serde_json::from_str(json).unwrap();
972 assert_eq!(req.content, "café ☕");
973 assert!(req.title.is_none());
974 assert!(req.source.is_none());
975 assert!(req.tags.is_empty());
976 assert!(req.chunking_strategy.is_none());
977 }
978
979 #[test]
980 fn test_rag_ingest_response_serde_roundtrip() {
981 let resp = RagIngestResponse {
982 chunks_created: 0,
983 document_ids: vec![],
984 collection: "empty".into(),
985 };
986 let parsed: RagIngestResponse =
987 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
988 assert_eq!(parsed.chunks_created, 0);
989 assert!(parsed.document_ids.is_empty());
990 }
991
992 #[test]
993 fn test_rag_search_response_serde_roundtrip() {
994 let resp = RagSearchResponse {
995 results: vec![RagSearchResult {
996 id: "d1".into(),
997 content: "match".into(),
998 score: 1.0,
999 metadata: DocumentMetadata::default(),
1000 }],
1001 total: 1,
1002 strategy: "hybrid".into(),
1003 reranked: false,
1004 duration_ms: u64::MAX,
1005 };
1006 let parsed: RagSearchResponse =
1007 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1008 assert_eq!(parsed.total, 1);
1009 assert_eq!(parsed.duration_ms, u64::MAX);
1010 }
1011
1012 #[test]
1013 fn test_rag_delete_collection_serde_roundtrip() {
1014 let req = RagDeleteCollectionRequest {
1015 collection: "to-delete".into(),
1016 };
1017 let parsed: RagDeleteCollectionRequest =
1018 serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
1019 assert_eq!(parsed.collection, "to-delete");
1020
1021 let resp = RagDeleteCollectionResponse {
1022 success: true,
1023 collection: "to-delete".into(),
1024 documents_deleted: 0,
1025 };
1026 let parsed_resp: RagDeleteCollectionResponse =
1027 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1028 assert!(parsed_resp.success);
1029 assert_eq!(parsed_resp.documents_deleted, 0);
1030 }
1031
1032 #[test]
1033 fn test_semantic_search_request_defaults() {
1034 let json = r#"{"collection":"c","query":"q"}"#;
1035 let req: SemanticSearchRequest = serde_json::from_str(json).unwrap();
1036 assert_eq!(req.limit, 10);
1037 assert!((req.threshold - 0.0).abs() < f32::EPSILON);
1038 }
1039
1040 #[test]
1041 fn test_semantic_search_response_serde_roundtrip() {
1042 let resp = SemanticSearchResponse {
1043 results: vec![],
1044 total: 0,
1045 duration_ms: 0,
1046 };
1047 let parsed: SemanticSearchResponse =
1048 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1049 assert!(parsed.results.is_empty());
1050 assert_eq!(parsed.total, 0);
1051 }
1052
1053 #[test]
1054 fn test_workflow_request_empty_context_default() {
1055 let json = r#"{"query":"run workflow"}"#;
1056 let req: WorkflowRequest = serde_json::from_str(json).unwrap();
1057 assert_eq!(req.query, "run workflow");
1058 assert!(req.context.is_empty());
1059
1060 let with_ctx = WorkflowRequest {
1061 query: "q".into(),
1062 context: [("key".into(), serde_json::json!(null))]
1063 .into_iter()
1064 .collect(),
1065 };
1066 let parsed: WorkflowRequest =
1067 serde_json::from_str(&serde_json::to_string(&with_ctx).unwrap()).unwrap();
1068 assert!(parsed.context.contains_key("key"));
1069 }
1070
1071 #[test]
1072 fn test_agent_type_serde_builtin_and_custom_unicode() {
1073 for (agent, expected) in [(AgentType::Router, "\"router\""), (AgentType::HR, "\"hr\"")] {
1074 let json = serde_json::to_string(&agent).unwrap();
1075 assert_eq!(json, expected);
1076 let parsed: AgentType = serde_json::from_str(&json).unwrap();
1077 assert_eq!(parsed, agent);
1078 }
1079 let custom = AgentType::Custom("代理-🤖".into());
1080 let json = serde_json::to_string(&custom).unwrap();
1081 let parsed: AgentType = serde_json::from_str(&json).unwrap();
1082 assert_eq!(parsed.as_str(), "代理-🤖");
1083 }
1084
1085 #[test]
1086 fn test_agent_type_partial_eq_and_clone() {
1087 let a = AgentType::Finance;
1088 let b = a.clone();
1089 assert_eq!(a, b);
1090 assert_ne!(a, AgentType::Sales);
1091 assert!(a.is_builtin());
1092 }
1093
1094 #[test]
1095 fn test_message_role_all_variants_serde() {
1096 for (role, expected) in [
1097 (MessageRole::System, "\"system\""),
1098 (MessageRole::User, "\"user\""),
1099 (MessageRole::Assistant, "\"assistant\""),
1100 ] {
1101 let json = serde_json::to_string(&role).unwrap();
1102 assert_eq!(json, expected);
1103 let parsed: MessageRole = serde_json::from_str(&json).unwrap();
1104 assert_eq!(format!("{:?}", parsed), format!("{:?}", role));
1105 }
1106 }
1107
1108 #[test]
1109 fn test_message_serde_roundtrip_unicode() {
1110 let msg = Message {
1111 role: MessageRole::User,
1112 content: "emoji 🚀 & unicode ñ".into(),
1113 timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1114 parts: vec![],
1115 };
1116 let parsed: Message = serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1117 assert_eq!(parsed.content, "emoji 🚀 & unicode ñ");
1118 assert!(matches!(parsed.role, MessageRole::User));
1119 }
1120
1121 #[test]
1122 fn test_message_parts_serde_default_and_roundtrip() {
1123 let parsed: Message = serde_json::from_str(
1124 r#"{"role":"user","content":"hi","timestamp":"2024-01-01T00:00:00Z"}"#,
1125 )
1126 .unwrap();
1127 assert!(parsed.parts.is_empty());
1128
1129 let msg = Message {
1130 role: MessageRole::User,
1131 content: "hi".into(),
1132 timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1133 parts: vec![ContentPart::Text {
1134 text: "photo".into(),
1135 }],
1136 };
1137 let parsed: Message = serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1138 assert_eq!(
1139 parsed.parts,
1140 vec![ContentPart::Text {
1141 text: "photo".into(),
1142 }]
1143 );
1144 }
1145
1146 #[test]
1147 fn test_user_memory_empty_collections_serde() {
1148 let mem = UserMemory {
1149 user_id: "u0".into(),
1150 preferences: vec![],
1151 facts: vec![],
1152 };
1153 let parsed: UserMemory =
1154 serde_json::from_str(&serde_json::to_string(&mem).unwrap()).unwrap();
1155 assert!(parsed.preferences.is_empty());
1156 assert!(parsed.facts.is_empty());
1157 }
1158
1159 #[test]
1160 fn test_preference_and_memory_fact_boundary_confidence() {
1161 let pref = Preference {
1162 category: String::new(),
1163 key: "lang".into(),
1164 value: "rust".into(),
1165 confidence: 0.0,
1166 };
1167 let parsed: Preference =
1168 serde_json::from_str(&serde_json::to_string(&pref).unwrap()).unwrap();
1169 assert!((parsed.confidence - 0.0).abs() < f32::EPSILON);
1170
1171 let now = Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap();
1172 let fact = MemoryFact {
1173 id: "f1".into(),
1174 user_id: "u1".into(),
1175 category: "work".into(),
1176 fact_key: "role".into(),
1177 fact_value: "engineer".into(),
1178 confidence: 1.0,
1179 created_at: now,
1180 updated_at: now,
1181 };
1182 let parsed_fact: MemoryFact =
1183 serde_json::from_str(&serde_json::to_string(&fact).unwrap()).unwrap();
1184 assert!((parsed_fact.confidence - 1.0).abs() < f32::EPSILON);
1185 }
1186
1187 #[test]
1188 fn test_tool_definition_and_result_serde_roundtrip() {
1189 let def = ToolDefinition {
1190 name: "calc".into(),
1191 description: String::new(),
1192 parameters: serde_json::json!({}),
1193 };
1194 let parsed_def: ToolDefinition =
1195 serde_json::from_str(&serde_json::to_string(&def).unwrap()).unwrap();
1196 assert_eq!(parsed_def.name, "calc");
1197 assert!(parsed_def.description.is_empty());
1198
1199 let result = ToolResult {
1200 tool_call_id: "c1".into(),
1201 result: serde_json::Value::Null,
1202 };
1203 let parsed_result: ToolResult =
1204 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1205 assert!(parsed_result.result.is_null());
1206 }
1207
1208 #[test]
1209 fn test_document_serde_none_embedding_and_metadata_default() {
1210 let doc = Document {
1211 id: "doc-1".into(),
1212 content: String::new(),
1213 metadata: DocumentMetadata::default(),
1214 embedding: None,
1215 };
1216 let parsed: Document = serde_json::from_str(&serde_json::to_string(&doc).unwrap()).unwrap();
1217 assert!(parsed.content.is_empty());
1218 assert!(parsed.embedding.is_none());
1219 assert!(parsed.metadata.title.is_empty());
1220
1221 let default_meta = DocumentMetadata::default();
1222 assert!(default_meta.tags.is_empty());
1223 assert!(default_meta.source.is_empty());
1224 }
1225
1226 #[test]
1227 fn test_search_query_and_result_clone_debug() {
1228 let query = SearchQuery {
1229 query: "find".into(),
1230 limit: 0,
1231 threshold: 1.0,
1232 filters: None,
1233 };
1234 let cloned = query.clone();
1235 assert_eq!(cloned.limit, 0);
1236 assert!(cloned.filters.is_none());
1237 assert!(format!("{:?}", cloned).contains("find"));
1238
1239 let result = SearchResult {
1240 document: Document {
1241 id: "1".into(),
1242 content: "x".into(),
1243 metadata: DocumentMetadata::default(),
1244 embedding: Some(vec![]),
1245 },
1246 score: 0.0,
1247 };
1248 let cloned_result = result.clone();
1249 assert!((cloned_result.score - 0.0).abs() < f32::EPSILON);
1250 assert!(cloned_result
1251 .document
1252 .embedding
1253 .as_ref()
1254 .unwrap()
1255 .is_empty());
1256 }
1257
1258 #[test]
1259 fn test_agent_context_clone_debug() {
1260 let ctx = AgentContext {
1261 user_id: "u1".into(),
1262 session_id: "s1".into(),
1263 conversation_history: vec![],
1264 user_memory: None,
1265 };
1266 let cloned = ctx.clone();
1267 assert_eq!(cloned.user_id, "u1");
1268 assert!(cloned.user_memory.is_none());
1269 assert!(format!("{:?}", cloned).contains("AgentContext"));
1270 }
1271
1272 #[test]
1273 fn test_login_register_token_claims_serde_roundtrip() {
1274 let login = LoginRequest {
1275 email: "user@example.com".into(),
1276 password: String::new(),
1277 };
1278 let parsed_login: LoginRequest =
1279 serde_json::from_str(&serde_json::to_string(&login).unwrap()).unwrap();
1280 assert!(parsed_login.password.is_empty());
1281
1282 let register = RegisterRequest {
1283 email: "new@example.com".into(),
1284 password: "secret".into(),
1285 name: "新規ユーザー".into(),
1286 };
1287 let parsed_register: RegisterRequest =
1288 serde_json::from_str(&serde_json::to_string(®ister).unwrap()).unwrap();
1289 assert_eq!(parsed_register.name, "新規ユーザー");
1290
1291 let token = TokenResponse {
1292 access_token: "access".into(),
1293 refresh_token: "refresh".into(),
1294 expires_in: 0,
1295 };
1296 let parsed_token: TokenResponse =
1297 serde_json::from_str(&serde_json::to_string(&token).unwrap()).unwrap();
1298 assert_eq!(parsed_token.expires_in, 0);
1299
1300 let claims = Claims {
1301 sub: "user-1".into(),
1302 email: "user@example.com".into(),
1303 exp: usize::MAX,
1304 iat: 0,
1305 jti: String::new(),
1306 tenant_id: None,
1307 };
1308 let json = serde_json::to_string(&claims).unwrap();
1309 assert!(!json.contains("jti"));
1310 let parsed_claims: Claims = serde_json::from_str(&json).unwrap();
1311 assert_eq!(parsed_claims.jti, "");
1312 }
1313
1314 #[test]
1315 fn test_error_code_serialize_all_variants() {
1316 let codes = [
1317 (ErrorCode::DatabaseError, "DATABASE_ERROR"),
1318 (ErrorCode::LlmError, "LLM_ERROR"),
1319 (ErrorCode::AuthenticationFailed, "AUTHENTICATION_FAILED"),
1320 (ErrorCode::AuthorizationFailed, "AUTHORIZATION_FAILED"),
1321 (ErrorCode::NotFound, "NOT_FOUND"),
1322 (ErrorCode::InvalidInput, "INVALID_INPUT"),
1323 (ErrorCode::ConfigurationError, "CONFIGURATION_ERROR"),
1324 (ErrorCode::ExternalServiceError, "EXTERNAL_SERVICE_ERROR"),
1325 (ErrorCode::InternalError, "INTERNAL_ERROR"),
1326 ];
1327 for (code, expected) in codes {
1328 let json = serde_json::to_string(&code).unwrap();
1329 assert_eq!(json, format!("\"{}\"", expected));
1330 }
1331 }
1332
1333 #[test]
1334 fn test_app_error_remaining_code_mappings() {
1335 assert!(matches!(
1336 AppError::LLM("x".into()).code(),
1337 ErrorCode::LlmError
1338 ));
1339 assert!(matches!(
1340 AppError::Configuration("x".into()).code(),
1341 ErrorCode::ConfigurationError
1342 ));
1343 assert!(matches!(
1344 AppError::External("x".into()).code(),
1345 ErrorCode::ExternalServiceError
1346 ));
1347 assert!(matches!(
1348 AppError::Unavailable("x".into()).code(),
1349 ErrorCode::InternalError
1350 ));
1351 assert!(matches!(
1352 AppError::FeatureDisabled("x".into()).code(),
1353 ErrorCode::InternalError
1354 ));
1355 assert!(matches!(
1356 AppError::Internal("x".into()).code(),
1357 ErrorCode::InternalError
1358 ));
1359 }
1360
1361 #[test]
1362 fn test_source_clone_and_boundary_scores() {
1363 let source = Source {
1364 title: "t".into(),
1365 url: None,
1366 relevance_score: 0.0,
1367 };
1368 let cloned = source.clone();
1369 assert!(cloned.url.is_none());
1370 assert!((cloned.relevance_score - 0.0).abs() < f32::EPSILON);
1371
1372 let max = Source {
1373 title: "max".into(),
1374 url: Some("https://example.com?q=100%".into()),
1375 relevance_score: 1.0,
1376 };
1377 let parsed: Source = serde_json::from_str(&serde_json::to_string(&max).unwrap()).unwrap();
1378 assert!((parsed.relevance_score - 1.0).abs() < f32::EPSILON);
1379 }
1380
1381 #[test]
1382 fn test_chat_request_workspace_id_serde_roundtrip() {
1383 let req = ChatRequest {
1384 message: "ping".into(),
1385 agent_type: None,
1386 context_id: None,
1387 workspace_id: Some("ws-éruka-42".into()),
1388 model: None,
1389 parts: None,
1390 previous_response_id: None,
1391 web_search: None,
1392 };
1393 let json = serde_json::to_string(&req).unwrap();
1394 assert!(json.contains("workspace_id"));
1395 let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
1396 assert_eq!(parsed.workspace_id.as_deref(), Some("ws-éruka-42"));
1397 }
1398
1399 #[test]
1400 fn test_rag_search_result_serde_roundtrip() {
1401 let result = RagSearchResult {
1402 id: "chunk-1".into(),
1403 content: "snippet".into(),
1404 score: 0.75,
1405 metadata: DocumentMetadata {
1406 title: "Guide".into(),
1407 source: "docs/guide.md".into(),
1408 tags: vec!["rag".into()],
1409 ..Default::default()
1410 },
1411 };
1412 let parsed: RagSearchResult =
1413 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1414 assert_eq!(parsed.id, "chunk-1");
1415 assert!((parsed.score - 0.75).abs() < f32::EPSILON);
1416 assert_eq!(parsed.metadata.tags, vec!["rag"]);
1417 }
1418
1419 #[test]
1420 fn test_semantic_search_result_serde_roundtrip() {
1421 let result = SemanticSearchResult {
1422 id: "doc-9".into(),
1423 content: "semantic hit".into(),
1424 similarity: 0.91,
1425 metadata: DocumentMetadata::default(),
1426 };
1427 let parsed: SemanticSearchResult =
1428 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1429 assert_eq!(parsed.content, "semantic hit");
1430 assert!((parsed.similarity - 0.91).abs() < f32::EPSILON);
1431 }
1432
1433 #[test]
1434 fn test_app_error_into_response_status_codes() {
1435 let cases = [
1436 (AppError::Auth("denied".into()), 401u16),
1437 (AppError::NotFound("gone".into()), 404),
1438 (AppError::InvalidInput("bad".into()), 400),
1439 (AppError::External("upstream".into()), 502),
1440 (AppError::Unavailable("maintenance".into()), 503),
1441 (AppError::RateLimited("slow".into()), 429),
1442 (AppError::FeatureDisabled("off".into()), 400),
1443 (AppError::Database("db".into()), 500),
1444 ];
1445 for (err, expected) in cases {
1446 assert_eq!(err.status_code(), expected);
1447 }
1448 }
1449
1450 #[test]
1451 fn test_agent_type_from_string_is_case_insensitive() {
1452 assert_eq!(AgentType::from_string("ROUTER"), AgentType::Router);
1453 assert_eq!(AgentType::from_string("Hr"), AgentType::HR);
1454 assert_eq!(
1455 AgentType::from_string("MyCustom"),
1456 AgentType::Custom("MyCustom".into())
1457 );
1458 }
1459}