from typing import Literal, List, Optional, Union, Any, Dict, Set from pydantic import BaseModel, Field, model_validator, field_validator, RootModel from datetime import datetime import hashlib def _content_item_type(value: Any) -> Optional[str]: if isinstance(value, dict): item_type = value.get("type") return item_type if isinstance(item_type, str) else None item_type = getattr(value, "type", None) return item_type if isinstance(item_type, str) else None def _content_item_text(value: Any) -> Optional[str]: if isinstance(value, dict): text = value.get("text") return text if isinstance(text, str) else None text = getattr(value, "text", None) return text if isinstance(text, str) else None def _content_item_image_payload(value: Any) -> Any: if isinstance(value, dict): return value.get("image_url") return getattr(value, "image_url", None) def _image_payload_string_field(payload: Any, field_name: str) -> Optional[str]: if isinstance(payload, dict): value = payload.get(field_name) return value if isinstance(value, str) else None value = getattr(payload, field_name, None) return value if isinstance(value, str) else None def _normalize_multimodal_content_item(value: Any) -> Dict[str, Any]: if hasattr(value, "model_dump"): value = value.model_dump() item_type = _content_item_type(value) if item_type == "text": text = _content_item_text(value) if text is None: raise ValueError("Text content item is missing a valid 'text' field") return {"type": "text", "text": text} if item_type == "image_url": image_payload = _content_item_image_payload(value) url = _image_payload_string_field(image_payload, "url") if url is None: raise ValueError( "Image content item is missing a valid 'image_url.url' field" ) normalized_payload: Dict[str, Any] = {"url": url} detail = _image_payload_string_field(image_payload, "detail") if detail in {"auto", "low", "high"}: normalized_payload["detail"] = detail local_path = _image_payload_string_field(image_payload, "local_path") if local_path: normalized_payload["local_path"] = local_path return {"type": "image_url", "image_url": normalized_payload} raise ValueError(f"Unsupported multimodal content item: {type(value).__name__}") def _normalize_message_content(value: Any) -> Any: if value is None or isinstance(value, str): return value if isinstance(value, list): return [_normalize_multimodal_content_item(item) for item in value] return value class TextContent(BaseModel): type: Literal["text"] = Field(..., description="Content block type: plain text") text: str = Field(..., description="Text content of the message") class ImageURL(BaseModel): url: str = Field(..., description="Public access URL for the image") detail: Optional[Literal["auto", "low", "high"]] = Field( None, description="Optional image detail level" ) local_path: Optional[str] = Field(None, description="Local path of the image in the workspace") class ImageContent(BaseModel): type: Literal["image_url"] = Field(..., description="Content block type: image URL") image_url: ImageURL = Field(..., description="Detailed information for the image content") MessageContent = Union[str, None, List[Union[TextContent, ImageContent]]] class FunctionCall(BaseModel): name: str = Field(..., description="Name of the function to call") arguments: str = Field(..., description="JSON-formatted string of arguments to pass") class ToolCall(BaseModel): id: str = Field(..., description="Unique ID of this tool call") type: Literal["function"] = Field(..., description="Type of called tool (function)") function: FunctionCall = Field(..., description="Function call specification") class Message(BaseModel): role: Literal["system", "user", "assistant", "tool"] = Field( ..., description="Role of the message sender" ) content: MessageContent = Field( ..., description=( "Message content. It can be a string, null (when calling tools), or a list of structured multimodal blocks." ), ) name: Optional[str] = Field( default=None, description="Optional sender name, required when the role is 'user' or 'tool'", max_length=64, pattern=r"^[a-zA-Z0-9_]*$", ) tool_calls: Optional[List[ToolCall]] = Field( default=None, description="List of tool calls the assistant wants to invoke" ) tool_call_id: Optional[str] = Field( default=None, description="Tool call ID that this tool message responds to" ) timestamp: Optional[str] = Field(default=None, description="Message timestamp (ISO format)") @field_validator("content", mode="before") @classmethod def normalize_content(cls, value: Any): return _normalize_message_content(value) @model_validator(mode="after") def validate_tool_message_consistency(cls, values): role = values.role content = values.content tool_calls = values.tool_calls tool_call_id = values.tool_call_id if role == "assistant" and tool_calls and content is not None: raise ValueError( "When role is 'assistant' and tool_calls exist, content must be None." ) if role == "tool" and not tool_call_id: raise ValueError("When role is 'tool', tool_call_id must be provided.") return values class ChatMessages(RootModel[List[Message]]): """Chat message list ordered chronologically.""" class SketchPadItem(BaseModel): """Data structure for a SketchPad storage item.""" value: Any = Field(..., description="Stored value") timestamp: datetime = Field(default_factory=datetime.now, description="Creation time") summary: Optional[str] = Field(default=None, description="Content summary") expires_at: Optional[datetime] = Field(default=None, description="Expiration time") access_count: int = Field(default=0, description="Access count") last_accessed: Optional[datetime] = Field(default=None, description="Last access time") tags: Set[str] = Field(default_factory=set, description="Tag set") content_type: str = Field(default="text", description="Content type") content_hash: Optional[str] = Field(default=None, description="Content hash value") @field_validator("last_accessed", mode="before") @classmethod def set_last_accessed(cls, v): """If last_accessed is None, set it to the current time.""" if v is None: return datetime.now() return v @field_validator("content_hash", mode="before") @classmethod def set_content_hash(cls, v, info): """If content_hash is None, compute the hash value.""" if v is None: value = info.data.get("value") if value is not None: content_str = str(value) return hashlib.md5(content_str.encode()).hexdigest()[:8] return v def is_expired(self) -> bool: """Check whether the item has expired.""" return self.expires_at is not None and datetime.now() > self.expires_at def update_access(self): """Update access information for LRU caching.""" self.access_count += 1 self.last_accessed = datetime.now() def to_dict(self) -> Dict[str, Any]: """Convert to a dictionary for serialization.""" return { "value": self.value, "timestamp": self.timestamp.isoformat(), "summary": self.summary, "expires_at": self.expires_at.isoformat() if self.expires_at else None, "access_count": self.access_count, "last_accessed": ( self.last_accessed.isoformat() if self.last_accessed else None ), "tags": list(self.tags), "content_type": self.content_type, "content_hash": self.content_hash, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "SketchPadItem": """Create an instance from a dictionary.""" # Process time fields. if isinstance(data.get("timestamp"), str): data["timestamp"] = datetime.fromisoformat(data["timestamp"]) if data.get("expires_at") and isinstance(data["expires_at"], str): data["expires_at"] = datetime.fromisoformat(data["expires_at"]) if data.get("last_accessed") and isinstance(data["last_accessed"], str): data["last_accessed"] = datetime.fromisoformat(data["last_accessed"]) # Process tags. if data.get("tags") and isinstance(data["tags"], list): data["tags"] = set(data["tags"]) return cls(**data) class SketchPadStatistics(BaseModel): """SketchPad statistics.""" total_items: int = Field(..., description="Total number of items") max_items: int = Field(..., description="Maximum number of items") items_with_summary: int = Field(..., description="Number of items with summaries") total_accesses: int = Field(..., description="Total number of accesses") popular_tags: Dict[str, int] = Field(..., description="Popular tag statistics") content_types: Dict[str, int] = Field(..., description="Content type statistics") avg_access_per_item: float = Field(..., description="Average accesses per item") memory_usage_percent: float = Field(..., description="Memory usage percentage") class SketchPadSearchResult(BaseModel): """SketchPad search result.""" key: str = Field(..., description="Item key") value: Any = Field(..., description="Item value") summary: Optional[str] = Field(default=None, description="Item summary") timestamp: str = Field(..., description="Creation time (ISO format)") tags: List[str] = Field(default_factory=list, description="Tag list") content_type: str = Field(..., description="Content type") access_count: int = Field(..., description="Access count") class SketchPadListItem(BaseModel): """SketchPad list item.""" key: str = Field(..., description="Item key") summary: Optional[str] = Field(default=None, description="Item summary") timestamp: str = Field(..., description="Creation time (ISO format)") tags: List[str] = Field(default_factory=list, description="Tag list") content_type: str = Field(..., description="Content type") access_count: int = Field(..., description="Access count") content_hash: Optional[str] = Field(default=None, description="Content hash value") value: Optional[Any] = Field(default=None, description="Item value, only present when content is included")