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