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