Skip to main content

ares_types/types/
mod.rs

1//! Core types used throughout the A.R.E.S server.
2//!
3//! This module contains all the common data structures used for:
4//! - API requests and responses
5//! - Agent configuration and context
6//! - Memory and user preferences
7//! - Tool definitions and calls
8//! - RAG (Retrieval Augmented Generation)
9//! - Authentication
10//! - Error handling
11
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14
15/// Default datetime for serde deserialization
16fn default_datetime() -> DateTime<Utc> {
17    Utc::now()
18}
19
20// ============= API Request/Response Types =============
21
22/// Request payload for chat endpoints.
23#[derive(Debug, Serialize, Deserialize)]
24pub struct ChatRequest {
25    /// The user's message to send to the agent.
26    pub message: String,
27    /// Optional agent type to handle the request. Defaults to router.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub agent_type: Option<AgentType>,
30    /// Optional context ID for conversation continuity.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub context_id: Option<String>,
33    /// Optional Eruka workspace_id for per-user context isolation.
34    /// When set, the Eruka context middleware queries this workspace instead of the default.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub workspace_id: Option<String>,
37    /// Optional per-request model override (Cordis intercept payload).
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub model: Option<String>,
40}
41
42/// Response from chat endpoints.
43#[derive(Debug, Serialize, Deserialize)]
44pub struct ChatResponse {
45    /// The agent's response text.
46    pub response: String,
47    /// The name of the agent that handled the request.
48    pub agent: String,
49    /// Context ID for continuing this conversation.
50    pub context_id: String,
51    /// Optional sources used to generate the response.
52    pub sources: Option<Vec<Source>>,
53}
54
55/// A source reference used in responses.
56#[derive(Debug, Serialize, Deserialize, Clone)]
57pub struct Source {
58    /// Title of the source document or webpage.
59    pub title: String,
60    /// URL of the source, if available.
61    pub url: Option<String>,
62    /// Relevance score (0.0 to 1.0) indicating how relevant this source is.
63    pub relevance_score: f32,
64}
65
66/// Request payload for deep research endpoints.
67#[derive(Debug, Serialize, Deserialize)]
68pub struct ResearchRequest {
69    /// The research query or question.
70    pub query: String,
71    /// Optional maximum depth for recursive research (default: 3).
72    pub depth: Option<u8>,
73    /// Optional maximum iterations across all agents (default: 10).
74    pub max_iterations: Option<u8>,
75}
76
77/// Response from deep research endpoints.
78#[derive(Debug, Serialize, Deserialize)]
79pub struct ResearchResponse {
80    /// The compiled research findings.
81    pub findings: String,
82    /// Sources discovered during research.
83    pub sources: Vec<Source>,
84    /// Time taken for the research in milliseconds.
85    pub duration_ms: u64,
86}
87
88// ============= RAG API Types =============
89
90/// Request to ingest a document into the RAG system.
91#[derive(Debug, Serialize, Deserialize)]
92pub struct RagIngestRequest {
93    /// Collection name to ingest into.
94    pub collection: String,
95    /// The text content to ingest.
96    pub content: String,
97    /// Optional document title.
98    pub title: Option<String>,
99    /// Optional source URL or path.
100    pub source: Option<String>,
101    /// Optional tags for categorization.
102    #[serde(default)]
103    pub tags: Vec<String>,
104    /// Chunking strategy to use.
105    #[serde(default)]
106    pub chunking_strategy: Option<String>,
107}
108
109/// Response from document ingestion.
110#[derive(Debug, Serialize, Deserialize)]
111pub struct RagIngestResponse {
112    /// Number of chunks created.
113    pub chunks_created: usize,
114    /// Document IDs created.
115    pub document_ids: Vec<String>,
116    /// Collection name.
117    pub collection: String,
118}
119
120/// Request to search the RAG system.
121#[derive(Debug, Serialize, Deserialize)]
122pub struct RagSearchRequest {
123    /// Collection to search.
124    pub collection: String,
125    /// The search query.
126    pub query: String,
127    /// Maximum results to return (default: 10).
128    #[serde(default = "default_search_limit")]
129    pub limit: usize,
130    /// Search strategy to use: semantic, bm25, fuzzy, hybrid.
131    #[serde(default)]
132    pub strategy: Option<String>,
133    /// Minimum similarity threshold (0.0 to 1.0).
134    #[serde(default = "default_search_threshold")]
135    pub threshold: f32,
136    /// Whether to enable reranking.
137    #[serde(default)]
138    pub rerank: bool,
139    /// Reranker model to use if reranking.
140    #[serde(default)]
141    pub reranker_model: Option<String>,
142}
143
144fn default_search_limit() -> usize {
145    10
146}
147
148fn default_search_threshold() -> f32 {
149    0.0
150}
151
152/// Single search result.
153#[derive(Debug, Serialize, Deserialize)]
154pub struct RagSearchResult {
155    /// Document ID.
156    pub id: String,
157    /// Matching text content.
158    pub content: String,
159    /// Relevance score.
160    pub score: f32,
161    /// Document metadata.
162    pub metadata: DocumentMetadata,
163}
164
165/// Response from RAG search.
166#[derive(Debug, Serialize, Deserialize)]
167pub struct RagSearchResponse {
168    /// Search results.
169    pub results: Vec<RagSearchResult>,
170    /// Total number of results before limit.
171    pub total: usize,
172    /// Search strategy used.
173    pub strategy: String,
174    /// Whether reranking was applied.
175    pub reranked: bool,
176    /// Query processing time in milliseconds.
177    pub duration_ms: u64,
178}
179
180/// Request to delete a collection.
181#[derive(Debug, Serialize, Deserialize)]
182pub struct RagDeleteCollectionRequest {
183    /// Collection name to delete.
184    pub collection: String,
185}
186
187/// Response from collection deletion.
188#[derive(Debug, Serialize, Deserialize)]
189pub struct RagDeleteCollectionResponse {
190    /// Whether deletion was successful.
191    pub success: bool,
192    /// Collection that was deleted.
193    pub collection: String,
194    /// Number of documents deleted.
195    pub documents_deleted: usize,
196}
197
198// ============= Workflow Types =============
199
200// ============= Semantic Search Types =============
201
202/// Request for semantic document search.
203/// Available only when `ares-vector` feature is enabled.
204#[derive(Debug, Serialize, Deserialize)]
205pub struct SemanticSearchRequest {
206    /// Collection to search (tenant-scoped).
207    pub collection: String,
208    /// The search query text to embed.
209    pub query: String,
210    /// Maximum results to return (default: 10, max: 100).
211    #[serde(default = "default_search_limit")]
212    pub limit: usize,
213    /// Minimum similarity threshold (0.0 to 1.0, default: 0.0).
214    #[serde(default = "default_search_threshold")]
215    pub threshold: f32,
216}
217
218/// Single semantic search result.
219#[derive(Debug, Serialize, Deserialize)]
220pub struct SemanticSearchResult {
221    /// Document ID.
222    pub id: String,
223    /// Matching text content.
224    pub content: String,
225    /// Similarity score (0.0 to 1.0, higher is better).
226    pub similarity: f32,
227    /// Document metadata.
228    pub metadata: DocumentMetadata,
229}
230
231/// Response from semantic search.
232#[derive(Debug, Serialize, Deserialize)]
233pub struct SemanticSearchResponse {
234    /// Search results.
235    pub results: Vec<SemanticSearchResult>,
236    /// Total number of results found.
237    pub total: usize,
238    /// Query processing time in milliseconds.
239    pub duration_ms: u64,
240}
241
242/// Request payload for workflow execution endpoints.
243#[derive(Debug, Serialize, Deserialize)]
244pub struct WorkflowRequest {
245    /// The query to process through the workflow.
246    pub query: String,
247    /// Additional context data as key-value pairs.
248    #[serde(default)]
249    pub context: std::collections::HashMap<String, serde_json::Value>,
250}
251
252// ============= Agent Types =============
253
254/// Available agent types in the system.
255///
256/// This enum supports both built-in agent types and custom user-defined agents.
257/// The `Custom` variant allows for extensibility without modifying this enum.
258#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
259#[serde(rename_all = "lowercase")]
260#[non_exhaustive]
261pub enum AgentType {
262    /// Routes requests to appropriate specialized agents.
263    Router,
264    /// Orchestrates complex multi-step tasks.
265    Orchestrator,
266    /// Handles product-related queries.
267    Product,
268    /// Handles invoice and billing queries.
269    Invoice,
270    /// Handles sales-related queries.
271    Sales,
272    /// Handles financial queries and analysis.
273    Finance,
274    /// Handles HR and employee-related queries.
275    #[serde(rename = "hr")]
276    HR,
277    /// Custom user-defined agent type.
278    /// The string contains the agent's unique identifier/name.
279    #[serde(untagged)]
280    Custom(String),
281}
282
283impl AgentType {
284    /// Returns the agent type name as a string slice.
285    pub fn as_str(&self) -> &str {
286        match self {
287            AgentType::Router => "router",
288            AgentType::Orchestrator => "orchestrator",
289            AgentType::Product => "product",
290            AgentType::Invoice => "invoice",
291            AgentType::Sales => "sales",
292            AgentType::Finance => "finance",
293            AgentType::HR => "hr",
294            AgentType::Custom(name) => name,
295        }
296    }
297
298    /// Creates an AgentType from a string, using built-in types when possible.
299    pub fn from_string(s: &str) -> Self {
300        match s.to_lowercase().as_str() {
301            "router" => AgentType::Router,
302            "orchestrator" => AgentType::Orchestrator,
303            "product" => AgentType::Product,
304            "invoice" => AgentType::Invoice,
305            "sales" => AgentType::Sales,
306            "finance" => AgentType::Finance,
307            "hr" => AgentType::HR,
308            _ => AgentType::Custom(s.to_string()),
309        }
310    }
311
312    /// Returns true if this is a built-in agent type.
313    pub fn is_builtin(&self) -> bool {
314        !matches!(self, AgentType::Custom(_))
315    }
316}
317
318impl std::fmt::Display for AgentType {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        write!(f, "{}", self.as_str())
321    }
322}
323
324/// Context passed to agents during request processing.
325#[derive(Debug, Clone)]
326pub struct AgentContext {
327    /// Unique identifier for the user making the request.
328    pub user_id: String,
329    /// Session identifier for conversation tracking.
330    pub session_id: String,
331    /// Previous messages in the conversation.
332    pub conversation_history: Vec<Message>,
333    /// User's stored memory and preferences.
334    pub user_memory: Option<UserMemory>,
335}
336
337/// A single message in a conversation.
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct Message {
340    /// The role of the message sender.
341    pub role: MessageRole,
342    /// The message content.
343    pub content: String,
344    /// When the message was sent.
345    pub timestamp: DateTime<Utc>,
346}
347
348/// Role of a message sender in a conversation.
349#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(rename_all = "lowercase")]
351pub enum MessageRole {
352    /// System instructions to the model.
353    System,
354    /// Message from the user.
355    User,
356    /// Response from the assistant/agent.
357    Assistant,
358}
359
360// ============= Memory Types =============
361
362/// User memory containing preferences and learned facts.
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct UserMemory {
365    /// The user's unique identifier.
366    pub user_id: String,
367    /// List of user preferences.
368    pub preferences: Vec<Preference>,
369    /// List of facts learned about the user.
370    pub facts: Vec<MemoryFact>,
371}
372
373/// A user preference entry.
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct Preference {
376    /// Category of the preference (e.g., "communication", "output").
377    pub category: String,
378    /// Key identifying the specific preference.
379    pub key: String,
380    /// The preference value.
381    pub value: String,
382    /// Confidence score (0.0 to 1.0) for this preference.
383    pub confidence: f32,
384}
385
386/// A fact learned about a user.
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct MemoryFact {
389    /// Unique identifier for this fact.
390    pub id: String,
391    /// The user this fact belongs to.
392    pub user_id: String,
393    /// Category of the fact (e.g., "personal", "work").
394    pub category: String,
395    /// Key identifying the specific fact.
396    pub fact_key: String,
397    /// The fact value.
398    pub fact_value: String,
399    /// Confidence score (0.0 to 1.0) for this fact.
400    pub confidence: f32,
401    /// When this fact was first recorded.
402    pub created_at: DateTime<Utc>,
403    /// When this fact was last updated.
404    pub updated_at: DateTime<Utc>,
405}
406
407// ============= Tool Types =============
408
409/// Definition of a tool that can be called by an LLM.
410#[derive(Debug, Serialize, Deserialize, Clone)]
411pub struct ToolDefinition {
412    /// Unique name of the tool.
413    pub name: String,
414    /// Human-readable description of what the tool does.
415    pub description: String,
416    /// JSON Schema defining the tool's parameters.
417    pub parameters: serde_json::Value,
418}
419
420/// A request to call a tool.
421#[derive(Debug, Serialize, Deserialize, Clone)]
422pub struct ToolCall {
423    /// Unique identifier for this tool call.
424    pub id: String,
425    /// Name of the tool to call.
426    pub name: String,
427    /// Arguments to pass to the tool.
428    pub arguments: serde_json::Value,
429}
430
431/// Result from executing a tool.
432#[derive(Debug, Serialize, Deserialize)]
433pub struct ToolResult {
434    /// ID of the tool call this result corresponds to.
435    pub tool_call_id: String,
436    /// The result data from the tool execution.
437    pub result: serde_json::Value,
438}
439
440// ============= RAG Types =============
441
442/// A document in the RAG knowledge base.
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct Document {
445    /// Unique identifier for the document.
446    pub id: String,
447    /// The document's text content.
448    pub content: String,
449    /// Metadata about the document.
450    pub metadata: DocumentMetadata,
451    /// Optional embedding vector for semantic search.
452    pub embedding: Option<Vec<f32>>,
453}
454
455/// Metadata associated with a document.
456#[derive(Debug, Clone, Default, Serialize, Deserialize)]
457pub struct DocumentMetadata {
458    /// Title of the document.
459    #[serde(default)]
460    pub title: String,
461    /// Source of the document (e.g., URL, file path).
462    #[serde(default)]
463    pub source: String,
464    /// When the document was created or ingested.
465    #[serde(default = "default_datetime")]
466    pub created_at: DateTime<Utc>,
467    /// Tags for categorization and filtering.
468    #[serde(default)]
469    pub tags: Vec<String>,
470}
471
472/// Query parameters for semantic search.
473#[derive(Debug, Clone)]
474pub struct SearchQuery {
475    /// The search query text.
476    pub query: String,
477    /// Maximum number of results to return.
478    pub limit: usize,
479    /// Minimum similarity threshold (0.0 to 1.0).
480    pub threshold: f32,
481    /// Optional filters to apply to results.
482    pub filters: Option<Vec<SearchFilter>>,
483}
484
485/// A filter to apply during search.
486#[derive(Debug, Clone)]
487pub struct SearchFilter {
488    /// Field name to filter on.
489    pub field: String,
490    /// Value to filter by.
491    pub value: String,
492}
493
494/// A single search result with relevance score.
495#[derive(Debug, Clone)]
496pub struct SearchResult {
497    /// The matching document.
498    pub document: Document,
499    /// Similarity score (0.0 to 1.0).
500    pub score: f32,
501}
502
503// ============= Authentication Types =============
504
505/// Request payload for user login.
506#[derive(Debug, Serialize, Deserialize)]
507pub struct LoginRequest {
508    /// User's email address.
509    pub email: String,
510    /// User's password.
511    pub password: String,
512}
513
514/// Request payload for user registration.
515#[derive(Debug, Serialize, Deserialize)]
516pub struct RegisterRequest {
517    /// Email address for the new account.
518    pub email: String,
519    /// Password for the new account.
520    pub password: String,
521    /// Display name for the user.
522    pub name: String,
523}
524
525/// Response containing authentication tokens.
526#[derive(Debug, Serialize, Deserialize)]
527pub struct TokenResponse {
528    /// JWT access token for API authentication.
529    pub access_token: String,
530    /// Refresh token for obtaining new access tokens.
531    pub refresh_token: String,
532    /// Time in seconds until the access token expires.
533    pub expires_in: i64,
534}
535
536/// JWT claims embedded in access tokens.
537#[derive(Debug, Serialize, Deserialize, Clone)]
538pub struct Claims {
539    /// Subject (user ID).
540    pub sub: String,
541    /// User's email address.
542    pub email: String,
543    /// Expiration time (Unix timestamp).
544    pub exp: usize,
545    /// Issued at time (Unix timestamp).
546    pub iat: usize,
547    /// JWT ID — unique per token (present on refresh tokens).
548    #[serde(default, skip_serializing_if = "String::is_empty")]
549    pub jti: String,
550    /// Tenant that issued or owns this session, when the token is tenant-scoped.
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    pub tenant_id: Option<String>,
553}
554
555// ============= Error Types =============
556
557/// Error codes for programmatic error handling.
558/// These are stable identifiers that clients can use to handle specific error cases.
559#[derive(Debug, Clone, Copy, Serialize)]
560#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
561pub enum ErrorCode {
562    /// Database operation failed
563    DatabaseError,
564    /// LLM/AI model operation failed
565    LlmError,
566    /// Authentication failed (invalid credentials)
567    AuthenticationFailed,
568    /// Authorization failed (valid credentials but insufficient permissions)
569    AuthorizationFailed,
570    /// Requested resource was not found
571    NotFound,
572    /// Input validation failed
573    InvalidInput,
574    /// Server configuration error
575    ConfigurationError,
576    /// External service (API, webhook, etc.) failed
577    ExternalServiceError,
578    /// Internal server error
579    InternalError,
580}
581
582/// Application-wide error type.
583#[derive(Debug, thiserror::Error)]
584pub enum AppError {
585    /// Database operation failed.
586    #[error("Database error: {0}")]
587    Database(String),
588
589    /// LLM operation failed.
590    #[error("LLM error: {0}")]
591    LLM(String),
592
593    /// Authentication or authorization failed.
594    #[error("Authentication error: {0}")]
595    Auth(String),
596
597    /// Requested resource was not found.
598    #[error("Not found: {0}")]
599    NotFound(String),
600
601    /// Input validation failed.
602    #[error("Invalid input: {0}")]
603    InvalidInput(String),
604
605    /// Configuration error.
606    #[error("Configuration error: {0}")]
607    Configuration(String),
608
609    /// External service call failed.
610    #[error("External service error: {0}")]
611    External(String),
612
613    /// Internal server error.
614    #[error("Internal error: {0}")]
615    Internal(String),
616
617    /// Service temporarily unavailable (emergency stop, maintenance).
618    #[error("Service unavailable: {0}")]
619 Unavailable(String),
620    /// RAG feature was disabled by configuration
621    #[error("Feature disabled: {0}")]
622    FeatureDisabled(String),
623
624 /// Rate limit / quota exceeded.
625 #[error("Rate limited: {0}")]
626 RateLimited(String),
627}
628
629impl AppError {
630    /// Get the error code for this error type.
631    pub fn code(&self) -> ErrorCode {
632        match self {
633            AppError::Database(_) => ErrorCode::DatabaseError,
634            AppError::LLM(_) => ErrorCode::LlmError,
635            AppError::Auth(_) => ErrorCode::AuthenticationFailed,
636            AppError::NotFound(_) => ErrorCode::NotFound,
637            AppError::InvalidInput(_) => ErrorCode::InvalidInput,
638            AppError::Configuration(_) => ErrorCode::ConfigurationError,
639            AppError::External(_) => ErrorCode::ExternalServiceError,
640            AppError::Internal(_) => ErrorCode::InternalError,
641            AppError::Unavailable(_) => ErrorCode::InternalError,
642AppError::RateLimited(_) => ErrorCode::InternalError,
643AppError::FeatureDisabled(_) => ErrorCode::InternalError,
644    }
645    }
646
647    /// Check if this error is transient and retrying with a fallback provider
648    /// may succeed (timeout, rate limit, 5xx upstream).
649    pub fn is_retryable(&self) -> bool {
650        matches!(
651            self,
652            AppError::External(_) | AppError::Unavailable(_) | AppError::RateLimited(_)
653        )
654    }
655
656    /// HTTP status code as u16 (no axum dependency).
657    pub fn status_code(&self) -> u16 {
658        match self {
659            AppError::Database(_) => 500,
660            AppError::LLM(_) => 500,
661            AppError::Auth(_) => 401,
662            AppError::NotFound(_) => 404,
663            AppError::InvalidInput(_) => 400,
664            AppError::Configuration(_) => 500,
665            AppError::External(_) => 502,
666            AppError::Internal(_) => 500,
667            AppError::Unavailable(_) => 503,
668            AppError::RateLimited(_) => 429,
669            AppError::FeatureDisabled(_) => 400,
670        }
671    }
672}
673
674// ============= Error Conversions =============
675
676impl From<std::io::Error> for AppError {
677    fn from(err: std::io::Error) -> Self {
678        AppError::Internal(format!("IO error: {}", err))
679    }
680}
681
682impl From<serde_json::Error> for AppError {
683    fn from(err: serde_json::Error) -> Self {
684        AppError::InvalidInput(format!("JSON error: {}", err))
685    }
686}
687
688/// A specialized Result type for A.R.E.S operations.
689pub type Result<T> = std::result::Result<T, AppError>;
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694    use chrono::{TimeZone, Utc};
695
696    #[test]
697    fn test_agent_type_display_all_builtins() {
698        let cases = vec![
699            (AgentType::Router, "router"),
700            (AgentType::Orchestrator, "orchestrator"),
701            (AgentType::Product, "product"),
702            (AgentType::Invoice, "invoice"),
703            (AgentType::Sales, "sales"),
704            (AgentType::Finance, "finance"),
705            (AgentType::HR, "hr"),
706        ];
707        for (agent, expected) in cases {
708            assert_eq!(agent.to_string(), expected);
709            assert_eq!(format!("{}", agent), expected);
710        }
711    }
712
713    #[test]
714    fn test_agent_type_custom_display() {
715        let custom = AgentType::Custom("my-agent".into());
716        assert_eq!(custom.to_string(), "my-agent");
717        assert!(!custom.is_builtin());
718    }
719
720    #[test]
721    fn test_agent_type_from_string_roundtrip() {
722        for name in ["router", "finance", "hr"] {
723            let agent = AgentType::from_string(name);
724            assert_eq!(agent.as_str(), name);
725        }
726        let custom = AgentType::from_string("custom-bot");
727        assert_eq!(custom.as_str(), "custom-bot");
728    }
729
730    #[test]
731    fn test_message_role_serde_roundtrip() {
732        let role = MessageRole::Assistant;
733        let json = serde_json::to_string(&role).unwrap();
734        assert_eq!(json, "\"assistant\"");
735        let parsed: MessageRole = serde_json::from_str(&json).unwrap();
736        assert!(matches!(parsed, MessageRole::Assistant));
737    }
738
739    #[test]
740    fn test_chat_request_serde_roundtrip() {
741        let req = ChatRequest {
742            message: "hello".into(),
743            agent_type: Some(AgentType::Router),
744            context_id: Some("ctx-1".into()),
745            workspace_id: None,
746            model: None,
747        };
748        let json = serde_json::to_string(&req).unwrap();
749        let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
750        assert_eq!(parsed.message, "hello");
751        assert_eq!(parsed.agent_type, Some(AgentType::Router));
752    }
753
754    #[test]
755    fn test_source_serde_roundtrip() {
756        let source = Source {
757            title: "Doc".into(),
758            url: Some("https://example.com".into()),
759            relevance_score: 0.9,
760        };
761        let parsed: Source = serde_json::from_str(&serde_json::to_string(&source).unwrap()).unwrap();
762        assert_eq!(parsed.title, "Doc");
763        assert_eq!(parsed.relevance_score, 0.9);
764    }
765
766    #[test]
767    fn test_document_metadata_default_datetime() {
768        let json = r#"{"title":"t","source":"s"}"#;
769        let meta: DocumentMetadata = serde_json::from_str(json).unwrap();
770        assert_eq!(meta.title, "t");
771        assert!(meta.created_at <= Utc::now());
772    }
773
774    #[test]
775    fn test_tool_call_serde_roundtrip() {
776        let call = ToolCall {
777            id: "c1".into(),
778            name: "search".into(),
779            arguments: serde_json::json!({"q": "ares"}),
780        };
781        let parsed: ToolCall = serde_json::from_str(&serde_json::to_string(&call).unwrap()).unwrap();
782        assert_eq!(parsed.name, "search");
783    }
784
785    #[test]
786    fn test_app_error_code_mapping() {
787        assert!(matches!(AppError::Database("x".into()).code(), ErrorCode::DatabaseError));
788        assert!(matches!(AppError::Auth("x".into()).code(), ErrorCode::AuthenticationFailed));
789        assert!(matches!(AppError::NotFound("x".into()).code(), ErrorCode::NotFound));
790        assert!(matches!(AppError::RateLimited("x".into()).code(), ErrorCode::InternalError));
791    }
792
793    #[test]
794    fn test_app_error_from_io() {
795        let err: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "missing").into();
796        assert!(matches!(err, AppError::Internal(_)));
797        assert!(err.to_string().contains("IO error"));
798    }
799
800    #[test]
801    fn test_app_error_from_serde_json() {
802        let bad = "{not json";
803        let err: AppError = serde_json::from_str::<serde_json::Value>(bad).unwrap_err().into();
804        assert!(matches!(err, AppError::InvalidInput(_)));
805    }
806
807    #[test]
808    fn test_search_filter_application() {
809        let doc = Document {
810            id: "1".into(),
811            content: "body".into(),
812            metadata: DocumentMetadata {
813                title: "Guide".into(),
814                source: "docs/rust".into(),
815                tags: vec!["rust".into(), "rag".into()],
816                ..Default::default()
817            },
818            embedding: None,
819        };
820        let filters = vec![
821            SearchFilter { field: "tags".into(), value: "rust".into() },
822            SearchFilter { field: "source".into(), value: "docs/rust".into() },
823        ];
824        let matches = filters.iter().all(|f| match f.field.as_str() {
825            "tags" => doc.metadata.tags.iter().any(|t| t == &f.value),
826            "source" => doc.metadata.source == f.value,
827            _ => false,
828        });
829        assert!(matches);
830    }
831
832    #[test]
833    fn test_rag_search_request_defaults() {
834        let json = r#"{"collection":"c","query":"q"}"#;
835        let req: RagSearchRequest = serde_json::from_str(json).unwrap();
836        assert_eq!(req.limit, 10);
837        assert!((req.threshold - 0.0).abs() < f32::EPSILON);
838        assert!(!req.rerank);
839    }
840
841    #[test]
842    fn test_chat_response_serde_roundtrip() {
843        let resp = ChatResponse {
844            response: "こんにちは 🌍".into(),
845            agent: "router".into(),
846            context_id: String::new(),
847            sources: Some(vec![]),
848        };
849        let parsed: ChatResponse =
850            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
851        assert_eq!(parsed.response, "こんにちは 🌍");
852        assert_eq!(parsed.context_id, "");
853        assert!(parsed.sources.as_ref().unwrap().is_empty());
854    }
855
856    #[test]
857    fn test_research_request_serde_optional_fields() {
858        let json = r#"{"query":"quantum computing"}"#;
859        let req: ResearchRequest = serde_json::from_str(json).unwrap();
860        assert_eq!(req.query, "quantum computing");
861        assert!(req.depth.is_none());
862        assert!(req.max_iterations.is_none());
863
864        let full = ResearchRequest {
865            query: String::new(),
866            depth: Some(0),
867            max_iterations: Some(u8::MAX),
868        };
869        let parsed: ResearchRequest =
870            serde_json::from_str(&serde_json::to_string(&full).unwrap()).unwrap();
871        assert_eq!(parsed.query, "");
872        assert_eq!(parsed.depth, Some(0));
873        assert_eq!(parsed.max_iterations, Some(u8::MAX));
874    }
875
876    #[test]
877    fn test_research_response_serde_empty_sources() {
878        let resp = ResearchResponse {
879            findings: String::new(),
880            sources: vec![],
881            duration_ms: 0,
882        };
883        let parsed: ResearchResponse =
884            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
885        assert!(parsed.findings.is_empty());
886        assert!(parsed.sources.is_empty());
887        assert_eq!(parsed.duration_ms, 0);
888    }
889
890    #[test]
891    fn test_rag_ingest_request_defaults_and_unicode() {
892        let json = r#"{"collection":"docs","content":"café ☕"}"#;
893        let req: RagIngestRequest = serde_json::from_str(json).unwrap();
894        assert_eq!(req.content, "café ☕");
895        assert!(req.title.is_none());
896        assert!(req.source.is_none());
897        assert!(req.tags.is_empty());
898        assert!(req.chunking_strategy.is_none());
899    }
900
901    #[test]
902    fn test_rag_ingest_response_serde_roundtrip() {
903        let resp = RagIngestResponse {
904            chunks_created: 0,
905            document_ids: vec![],
906            collection: "empty".into(),
907        };
908        let parsed: RagIngestResponse =
909            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
910        assert_eq!(parsed.chunks_created, 0);
911        assert!(parsed.document_ids.is_empty());
912    }
913
914    #[test]
915    fn test_rag_search_response_serde_roundtrip() {
916        let resp = RagSearchResponse {
917            results: vec![RagSearchResult {
918                id: "d1".into(),
919                content: "match".into(),
920                score: 1.0,
921                metadata: DocumentMetadata::default(),
922            }],
923            total: 1,
924            strategy: "hybrid".into(),
925            reranked: false,
926            duration_ms: u64::MAX,
927        };
928        let parsed: RagSearchResponse =
929            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
930        assert_eq!(parsed.total, 1);
931        assert_eq!(parsed.duration_ms, u64::MAX);
932    }
933
934    #[test]
935    fn test_rag_delete_collection_serde_roundtrip() {
936        let req = RagDeleteCollectionRequest {
937            collection: "to-delete".into(),
938        };
939        let parsed: RagDeleteCollectionRequest =
940            serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
941        assert_eq!(parsed.collection, "to-delete");
942
943        let resp = RagDeleteCollectionResponse {
944            success: true,
945            collection: "to-delete".into(),
946            documents_deleted: 0,
947        };
948        let parsed_resp: RagDeleteCollectionResponse =
949            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
950        assert!(parsed_resp.success);
951        assert_eq!(parsed_resp.documents_deleted, 0);
952    }
953
954    #[test]
955    fn test_semantic_search_request_defaults() {
956        let json = r#"{"collection":"c","query":"q"}"#;
957        let req: SemanticSearchRequest = serde_json::from_str(json).unwrap();
958        assert_eq!(req.limit, 10);
959        assert!((req.threshold - 0.0).abs() < f32::EPSILON);
960    }
961
962    #[test]
963    fn test_semantic_search_response_serde_roundtrip() {
964        let resp = SemanticSearchResponse {
965            results: vec![],
966            total: 0,
967            duration_ms: 0,
968        };
969        let parsed: SemanticSearchResponse =
970            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
971        assert!(parsed.results.is_empty());
972        assert_eq!(parsed.total, 0);
973    }
974
975    #[test]
976    fn test_workflow_request_empty_context_default() {
977        let json = r#"{"query":"run workflow"}"#;
978        let req: WorkflowRequest = serde_json::from_str(json).unwrap();
979        assert_eq!(req.query, "run workflow");
980        assert!(req.context.is_empty());
981
982        let with_ctx = WorkflowRequest {
983            query: "q".into(),
984            context: [("key".into(), serde_json::json!(null))]
985                .into_iter()
986                .collect(),
987        };
988        let parsed: WorkflowRequest =
989            serde_json::from_str(&serde_json::to_string(&with_ctx).unwrap()).unwrap();
990        assert!(parsed.context.contains_key("key"));
991    }
992
993    #[test]
994    fn test_agent_type_serde_builtin_and_custom_unicode() {
995        for (agent, expected) in [
996            (AgentType::Router, "\"router\""),
997            (AgentType::HR, "\"hr\""),
998        ] {
999            let json = serde_json::to_string(&agent).unwrap();
1000            assert_eq!(json, expected);
1001            let parsed: AgentType = serde_json::from_str(&json).unwrap();
1002            assert_eq!(parsed, agent);
1003        }
1004        let custom = AgentType::Custom("代理-🤖".into());
1005        let json = serde_json::to_string(&custom).unwrap();
1006        let parsed: AgentType = serde_json::from_str(&json).unwrap();
1007        assert_eq!(parsed.as_str(), "代理-🤖");
1008    }
1009
1010    #[test]
1011    fn test_agent_type_partial_eq_and_clone() {
1012        let a = AgentType::Finance;
1013        let b = a.clone();
1014        assert_eq!(a, b);
1015        assert_ne!(a, AgentType::Sales);
1016        assert!(a.is_builtin());
1017    }
1018
1019    #[test]
1020    fn test_message_role_all_variants_serde() {
1021        for (role, expected) in [
1022            (MessageRole::System, "\"system\""),
1023            (MessageRole::User, "\"user\""),
1024            (MessageRole::Assistant, "\"assistant\""),
1025        ] {
1026            let json = serde_json::to_string(&role).unwrap();
1027            assert_eq!(json, expected);
1028            let parsed: MessageRole = serde_json::from_str(&json).unwrap();
1029            assert_eq!(format!("{:?}", parsed), format!("{:?}", role));
1030        }
1031    }
1032
1033    #[test]
1034    fn test_message_serde_roundtrip_unicode() {
1035        let msg = Message {
1036            role: MessageRole::User,
1037            content: "emoji 🚀 & unicode ñ".into(),
1038            timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1039        };
1040        let parsed: Message =
1041            serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1042        assert_eq!(parsed.content, "emoji 🚀 & unicode ñ");
1043        assert!(matches!(parsed.role, MessageRole::User));
1044    }
1045
1046    #[test]
1047    fn test_user_memory_empty_collections_serde() {
1048        let mem = UserMemory {
1049            user_id: "u0".into(),
1050            preferences: vec![],
1051            facts: vec![],
1052        };
1053        let parsed: UserMemory =
1054            serde_json::from_str(&serde_json::to_string(&mem).unwrap()).unwrap();
1055        assert!(parsed.preferences.is_empty());
1056        assert!(parsed.facts.is_empty());
1057    }
1058
1059    #[test]
1060    fn test_preference_and_memory_fact_boundary_confidence() {
1061        let pref = Preference {
1062            category: String::new(),
1063            key: "lang".into(),
1064            value: "rust".into(),
1065            confidence: 0.0,
1066        };
1067        let parsed: Preference =
1068            serde_json::from_str(&serde_json::to_string(&pref).unwrap()).unwrap();
1069        assert!((parsed.confidence - 0.0).abs() < f32::EPSILON);
1070
1071        let now = Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap();
1072        let fact = MemoryFact {
1073            id: "f1".into(),
1074            user_id: "u1".into(),
1075            category: "work".into(),
1076            fact_key: "role".into(),
1077            fact_value: "engineer".into(),
1078            confidence: 1.0,
1079            created_at: now,
1080            updated_at: now,
1081        };
1082        let parsed_fact: MemoryFact =
1083            serde_json::from_str(&serde_json::to_string(&fact).unwrap()).unwrap();
1084        assert!((parsed_fact.confidence - 1.0).abs() < f32::EPSILON);
1085    }
1086
1087    #[test]
1088    fn test_tool_definition_and_result_serde_roundtrip() {
1089        let def = ToolDefinition {
1090            name: "calc".into(),
1091            description: String::new(),
1092            parameters: serde_json::json!({}),
1093        };
1094        let parsed_def: ToolDefinition =
1095            serde_json::from_str(&serde_json::to_string(&def).unwrap()).unwrap();
1096        assert_eq!(parsed_def.name, "calc");
1097        assert!(parsed_def.description.is_empty());
1098
1099        let result = ToolResult {
1100            tool_call_id: "c1".into(),
1101            result: serde_json::Value::Null,
1102        };
1103        let parsed_result: ToolResult =
1104            serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1105        assert!(parsed_result.result.is_null());
1106    }
1107
1108    #[test]
1109    fn test_document_serde_none_embedding_and_metadata_default() {
1110        let doc = Document {
1111            id: "doc-1".into(),
1112            content: String::new(),
1113            metadata: DocumentMetadata::default(),
1114            embedding: None,
1115        };
1116        let parsed: Document =
1117            serde_json::from_str(&serde_json::to_string(&doc).unwrap()).unwrap();
1118        assert!(parsed.content.is_empty());
1119        assert!(parsed.embedding.is_none());
1120        assert!(parsed.metadata.title.is_empty());
1121
1122        let default_meta = DocumentMetadata::default();
1123        assert!(default_meta.tags.is_empty());
1124        assert!(default_meta.source.is_empty());
1125    }
1126
1127    #[test]
1128    fn test_search_query_and_result_clone_debug() {
1129        let query = SearchQuery {
1130            query: "find".into(),
1131            limit: 0,
1132            threshold: 1.0,
1133            filters: None,
1134        };
1135        let cloned = query.clone();
1136        assert_eq!(cloned.limit, 0);
1137        assert!(cloned.filters.is_none());
1138        assert!(format!("{:?}", cloned).contains("find"));
1139
1140        let result = SearchResult {
1141            document: Document {
1142                id: "1".into(),
1143                content: "x".into(),
1144                metadata: DocumentMetadata::default(),
1145                embedding: Some(vec![]),
1146            },
1147            score: 0.0,
1148        };
1149        let cloned_result = result.clone();
1150        assert!((cloned_result.score - 0.0).abs() < f32::EPSILON);
1151        assert!(cloned_result.document.embedding.as_ref().unwrap().is_empty());
1152    }
1153
1154    #[test]
1155    fn test_agent_context_clone_debug() {
1156        let ctx = AgentContext {
1157            user_id: "u1".into(),
1158            session_id: "s1".into(),
1159            conversation_history: vec![],
1160            user_memory: None,
1161        };
1162        let cloned = ctx.clone();
1163        assert_eq!(cloned.user_id, "u1");
1164        assert!(cloned.user_memory.is_none());
1165        assert!(format!("{:?}", cloned).contains("AgentContext"));
1166    }
1167
1168    #[test]
1169    fn test_login_register_token_claims_serde_roundtrip() {
1170        let login = LoginRequest {
1171            email: "user@example.com".into(),
1172            password: String::new(),
1173        };
1174        let parsed_login: LoginRequest =
1175            serde_json::from_str(&serde_json::to_string(&login).unwrap()).unwrap();
1176        assert!(parsed_login.password.is_empty());
1177
1178        let register = RegisterRequest {
1179            email: "new@example.com".into(),
1180            password: "secret".into(),
1181            name: "新規ユーザー".into(),
1182        };
1183        let parsed_register: RegisterRequest =
1184            serde_json::from_str(&serde_json::to_string(&register).unwrap()).unwrap();
1185        assert_eq!(parsed_register.name, "新規ユーザー");
1186
1187        let token = TokenResponse {
1188            access_token: "access".into(),
1189            refresh_token: "refresh".into(),
1190            expires_in: 0,
1191        };
1192        let parsed_token: TokenResponse =
1193            serde_json::from_str(&serde_json::to_string(&token).unwrap()).unwrap();
1194        assert_eq!(parsed_token.expires_in, 0);
1195
1196        let claims = Claims {
1197            sub: "user-1".into(),
1198            email: "user@example.com".into(),
1199            exp: usize::MAX,
1200            iat: 0,
1201            jti: String::new(),
1202            tenant_id: None,
1203        };
1204        let json = serde_json::to_string(&claims).unwrap();
1205        assert!(!json.contains("jti"));
1206        let parsed_claims: Claims = serde_json::from_str(&json).unwrap();
1207        assert_eq!(parsed_claims.jti, "");
1208    }
1209
1210    #[test]
1211    fn test_error_code_serialize_all_variants() {
1212        let codes = [
1213            (ErrorCode::DatabaseError, "DATABASE_ERROR"),
1214            (ErrorCode::LlmError, "LLM_ERROR"),
1215            (ErrorCode::AuthenticationFailed, "AUTHENTICATION_FAILED"),
1216            (ErrorCode::AuthorizationFailed, "AUTHORIZATION_FAILED"),
1217            (ErrorCode::NotFound, "NOT_FOUND"),
1218            (ErrorCode::InvalidInput, "INVALID_INPUT"),
1219            (ErrorCode::ConfigurationError, "CONFIGURATION_ERROR"),
1220            (ErrorCode::ExternalServiceError, "EXTERNAL_SERVICE_ERROR"),
1221            (ErrorCode::InternalError, "INTERNAL_ERROR"),
1222        ];
1223        for (code, expected) in codes {
1224            let json = serde_json::to_string(&code).unwrap();
1225            assert_eq!(json, format!("\"{}\"", expected));
1226        }
1227    }
1228
1229    #[test]
1230    fn test_app_error_remaining_code_mappings() {
1231        assert!(matches!(
1232            AppError::LLM("x".into()).code(),
1233            ErrorCode::LlmError
1234        ));
1235        assert!(matches!(
1236            AppError::Configuration("x".into()).code(),
1237            ErrorCode::ConfigurationError
1238        ));
1239        assert!(matches!(
1240            AppError::External("x".into()).code(),
1241            ErrorCode::ExternalServiceError
1242        ));
1243        assert!(matches!(
1244            AppError::Unavailable("x".into()).code(),
1245            ErrorCode::InternalError
1246        ));
1247        assert!(matches!(
1248            AppError::FeatureDisabled("x".into()).code(),
1249            ErrorCode::InternalError
1250        ));
1251        assert!(matches!(
1252            AppError::Internal("x".into()).code(),
1253            ErrorCode::InternalError
1254        ));
1255    }
1256
1257    #[test]
1258    fn test_source_clone_and_boundary_scores() {
1259        let source = Source {
1260            title: "t".into(),
1261            url: None,
1262            relevance_score: 0.0,
1263        };
1264        let cloned = source.clone();
1265        assert!(cloned.url.is_none());
1266        assert!((cloned.relevance_score - 0.0).abs() < f32::EPSILON);
1267
1268        let max = Source {
1269            title: "max".into(),
1270            url: Some("https://example.com?q=100%".into()),
1271            relevance_score: 1.0,
1272        };
1273        let parsed: Source =
1274            serde_json::from_str(&serde_json::to_string(&max).unwrap()).unwrap();
1275        assert!((parsed.relevance_score - 1.0).abs() < f32::EPSILON);
1276    }
1277
1278    #[test]
1279    fn test_chat_request_workspace_id_serde_roundtrip() {
1280        let req = ChatRequest {
1281            message: "ping".into(),
1282            agent_type: None,
1283            context_id: None,
1284            workspace_id: Some("ws-éruka-42".into()),
1285            model: None,
1286        };
1287        let json = serde_json::to_string(&req).unwrap();
1288        assert!(json.contains("workspace_id"));
1289        let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
1290        assert_eq!(parsed.workspace_id.as_deref(), Some("ws-éruka-42"));
1291    }
1292
1293    #[test]
1294    fn test_rag_search_result_serde_roundtrip() {
1295        let result = RagSearchResult {
1296            id: "chunk-1".into(),
1297            content: "snippet".into(),
1298            score: 0.75,
1299            metadata: DocumentMetadata {
1300                title: "Guide".into(),
1301                source: "docs/guide.md".into(),
1302                tags: vec!["rag".into()],
1303                ..Default::default()
1304            },
1305        };
1306        let parsed: RagSearchResult =
1307            serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1308        assert_eq!(parsed.id, "chunk-1");
1309        assert!((parsed.score - 0.75).abs() < f32::EPSILON);
1310        assert_eq!(parsed.metadata.tags, vec!["rag"]);
1311    }
1312
1313    #[test]
1314    fn test_semantic_search_result_serde_roundtrip() {
1315        let result = SemanticSearchResult {
1316            id: "doc-9".into(),
1317            content: "semantic hit".into(),
1318            similarity: 0.91,
1319            metadata: DocumentMetadata::default(),
1320        };
1321        let parsed: SemanticSearchResult =
1322            serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1323        assert_eq!(parsed.content, "semantic hit");
1324        assert!((parsed.similarity - 0.91).abs() < f32::EPSILON);
1325    }
1326
1327    #[test]
1328    fn test_app_error_into_response_status_codes() {
1329        let cases = [
1330            (AppError::Auth("denied".into()), 401u16),
1331            (AppError::NotFound("gone".into()), 404),
1332            (AppError::InvalidInput("bad".into()), 400),
1333            (AppError::External("upstream".into()), 502),
1334            (AppError::Unavailable("maintenance".into()), 503),
1335            (AppError::RateLimited("slow".into()), 429),
1336            (AppError::FeatureDisabled("off".into()), 400),
1337            (AppError::Database("db".into()), 500),
1338        ];
1339        for (err, expected) in cases {
1340            assert_eq!(err.status_code(), expected);
1341        }
1342    }
1343
1344    #[test]
1345    fn test_agent_type_from_string_is_case_insensitive() {
1346        assert_eq!(AgentType::from_string("ROUTER"), AgentType::Router);
1347        assert_eq!(AgentType::from_string("Hr"), AgentType::HR);
1348        assert_eq!(
1349            AgentType::from_string("MyCustom"),
1350            AgentType::Custom("MyCustom".into())
1351        );
1352    }
1353}