| | """
|
| | API Request/Response Models
|
| | ===========================
|
| | Pydantic models with comprehensive input validation and Field validators.
|
| | """
|
| |
|
| | from typing import Optional, Dict, Any, List
|
| | from pydantic import BaseModel, Field, field_validator, model_validator
|
| | import re
|
| |
|
| |
|
| | class StoreRequest(BaseModel):
|
| | """Request model for storing a memory."""
|
| | content: str = Field(
|
| | ...,
|
| | max_length=100_000,
|
| | description="The content to store as a memory",
|
| | examples=["This is a sample memory content"]
|
| | )
|
| | metadata: Optional[Dict[str, Any]] = Field(
|
| | default=None,
|
| | description="Optional metadata associated with the memory"
|
| | )
|
| | agent_id: Optional[str] = Field(
|
| | default=None,
|
| | max_length=256,
|
| | description="Optional agent identifier"
|
| | )
|
| | ttl: Optional[int] = Field(
|
| | default=None,
|
| | ge=1,
|
| | le=86400 * 365,
|
| | description="Time-to-live in seconds (1 to 31536000)"
|
| | )
|
| |
|
| | @field_validator('content')
|
| | @classmethod
|
| | def validate_content(cls, v: str) -> str:
|
| | """Ensure content is not empty or whitespace only."""
|
| | if not v or not v.strip():
|
| | raise ValueError('Content cannot be empty or whitespace only')
|
| | return v
|
| |
|
| | @field_validator('metadata')
|
| | @classmethod
|
| | def check_metadata_size(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
| | """Validate metadata constraints."""
|
| | if v is None:
|
| | return v
|
| | if len(v) > 50:
|
| | raise ValueError('Too many metadata keys (max 50)')
|
| | for key, value in v.items():
|
| | if len(key) > 64:
|
| | raise ValueError(f'Metadata key "{key[:20]}..." too long (max 64 chars)')
|
| | if not re.match(r'^[a-zA-Z0-9_\-\.]+$', key):
|
| | raise ValueError(f'Metadata key "{key}" contains invalid characters (only alphanumeric, underscore, hyphen, dot allowed)')
|
| |
|
| | if isinstance(value, str) and len(value) > 1000:
|
| | raise ValueError(f'Metadata value for "{key}" too long (max 1000 chars)')
|
| |
|
| | if isinstance(value, (dict, list)):
|
| | raise ValueError(f'Metadata value for "{key}" must be a primitive type (str, int, float, bool, null)')
|
| | return v
|
| |
|
| | @field_validator('agent_id')
|
| | @classmethod
|
| | def validate_agent_id(cls, v: Optional[str]) -> Optional[str]:
|
| | """Validate agent_id format."""
|
| | if v is None:
|
| | return v
|
| | if not re.match(r'^[a-zA-Z0-9_\-\:]+$', v):
|
| | raise ValueError('Agent ID contains invalid characters')
|
| | return v
|
| |
|
| |
|
| | class QueryRequest(BaseModel):
|
| | """Request model for querying memories."""
|
| | query: str = Field(
|
| | ...,
|
| | max_length=10000,
|
| | description="The search query string",
|
| | examples=["sample search query"]
|
| | )
|
| | top_k: int = Field(
|
| | default=5,
|
| | ge=1,
|
| | le=100,
|
| | description="Maximum number of results to return (1-100)"
|
| | )
|
| | agent_id: Optional[str] = Field(
|
| | default=None,
|
| | max_length=256,
|
| | description="Optional agent identifier to filter by"
|
| | )
|
| |
|
| | @field_validator('query')
|
| | @classmethod
|
| | def validate_query(cls, v: str) -> str:
|
| | """Ensure query is not empty or whitespace only."""
|
| | if not v or not v.strip():
|
| | raise ValueError('Query cannot be empty or whitespace only')
|
| | return v
|
| |
|
| |
|
| | class ConceptRequest(BaseModel):
|
| | """Request model for defining a concept."""
|
| | name: str = Field(
|
| | ...,
|
| | max_length=256,
|
| | description="Name of the concept",
|
| | examples=["animal"]
|
| | )
|
| | attributes: Dict[str, str] = Field(
|
| | ...,
|
| | description="Key-value attributes for the concept"
|
| | )
|
| |
|
| | @field_validator('name')
|
| | @classmethod
|
| | def validate_name(cls, v: str) -> str:
|
| | """Validate concept name."""
|
| | if not v or not v.strip():
|
| | raise ValueError('Concept name cannot be empty')
|
| | if not re.match(r'^[a-zA-Z0-9_\-\s]+$', v):
|
| | raise ValueError('Concept name contains invalid characters')
|
| | return v.strip()
|
| |
|
| | @field_validator('attributes')
|
| | @classmethod
|
| | def check_attributes_size(cls, v: Dict[str, str]) -> Dict[str, str]:
|
| | """Validate attributes constraints."""
|
| | if len(v) == 0:
|
| | raise ValueError('At least one attribute is required')
|
| | if len(v) > 50:
|
| | raise ValueError('Too many attributes (max 50)')
|
| | for key, value in v.items():
|
| | if len(key) > 64:
|
| | raise ValueError(f'Attribute key "{key[:20]}..." too long (max 64 chars)')
|
| | if not re.match(r'^[a-zA-Z0-9_\-\.]+$', key):
|
| | raise ValueError(f'Attribute key "{key}" contains invalid characters')
|
| | if len(value) > 1000:
|
| | raise ValueError(f'Attribute value for "{key}" too long (max 1000 chars)')
|
| | return v
|
| |
|
| |
|
| | class AnalogyRequest(BaseModel):
|
| | """Request model for solving analogies."""
|
| | source_concept: str = Field(
|
| | ...,
|
| | max_length=256,
|
| | description="The source concept in the analogy"
|
| | )
|
| | source_value: str = Field(
|
| | ...,
|
| | max_length=1000,
|
| | description="The value associated with the source concept"
|
| | )
|
| | target_concept: str = Field(
|
| | ...,
|
| | max_length=256,
|
| | description="The target concept in the analogy"
|
| | )
|
| |
|
| | @field_validator('source_concept', 'target_concept')
|
| | @classmethod
|
| | def validate_concept(cls, v: str) -> str:
|
| | """Validate concept names."""
|
| | if not v or not v.strip():
|
| | raise ValueError('Concept cannot be empty')
|
| | return v.strip()
|
| |
|
| | @field_validator('source_value')
|
| | @classmethod
|
| | def validate_value(cls, v: str) -> str:
|
| | """Validate source value."""
|
| | if not v or not v.strip():
|
| | raise ValueError('Source value cannot be empty')
|
| | return v.strip()
|
| |
|
| |
|
| | class MemoryResponse(BaseModel):
|
| | """Response model for memory retrieval."""
|
| | id: str
|
| | content: str
|
| | metadata: Dict[str, Any]
|
| | created_at: str
|
| | epistemic_value: float = 0.0
|
| | ltp_strength: float = 0.0
|
| | tier: str = "unknown"
|
| |
|
| |
|
| | class QueryResult(BaseModel):
|
| | """Single result from a query."""
|
| | id: str
|
| | content: str
|
| | score: float
|
| | metadata: Dict[str, Any]
|
| | tier: str
|
| |
|
| |
|
| | class QueryResponse(BaseModel):
|
| | """Response model for query results."""
|
| | ok: bool = True
|
| | query: str
|
| | results: List[QueryResult]
|
| |
|
| |
|
| | class StoreResponse(BaseModel):
|
| | """Response model for store operation."""
|
| | ok: bool = True
|
| | memory_id: str
|
| | message: str
|
| |
|
| |
|
| | class DeleteResponse(BaseModel):
|
| | """Response model for delete operation."""
|
| | ok: bool = True
|
| | deleted: str
|
| |
|
| |
|
| | class ConceptResponse(BaseModel):
|
| | """Response model for concept definition."""
|
| | ok: bool = True
|
| | concept: str
|
| |
|
| |
|
| | class AnalogyResult(BaseModel):
|
| | """Single result from an analogy query."""
|
| | value: str
|
| | score: float
|
| |
|
| |
|
| | class AnalogyResponse(BaseModel):
|
| | """Response model for analogy query."""
|
| | ok: bool = True
|
| | analogy: str
|
| | results: List[AnalogyResult]
|
| |
|
| |
|
| | class ErrorResponse(BaseModel):
|
| | """Error response model."""
|
| | detail: str
|
| | error_type: Optional[str] = None
|
| |
|
| |
|
| | class HealthResponse(BaseModel):
|
| | """Health check response model."""
|
| | status: str
|
| | redis_connected: bool
|
| | storage_circuit_breaker: str
|
| | qdrant_circuit_breaker: str
|
| | engine_ready: bool
|
| | timestamp: str
|
| |
|
| |
|
| | class RootResponse(BaseModel):
|
| | """Root endpoint response model."""
|
| | status: str
|
| | service: str
|
| | version: str
|
| | phase: str
|
| | timestamp: str
|
| |
|