"""Message schema for LLM conversation history.""" from typing import Any, Literal from pydantic import BaseModel, Field from app.models.tool_call import ToolCall class Message(BaseModel): """A single message in the conversation history. Follows the OpenAI chat completions message format with support for system, user, assistant, and tool roles. """ role: Literal["system", "user", "assistant", "tool"] = Field( description="Role of the message sender" ) content: str | None = Field(default=None, description="Text content of the message") tool_calls: list[ToolCall] | None = Field( default=None, description="Tool calls made by the assistant" ) tool_call_id: str | None = Field( default=None, description="ID of the tool call this message responds to (role=tool)" ) name: str | None = Field( default=None, description="Name of the tool that produced this message (role=tool)" ) def to_api_dict(self) -> dict[str, Any]: """Serialize to a dict suitable for the OpenAI-compatible API. Strips None-valued fields to keep the payload clean. """ data: dict[str, Any] = {"role": self.role} # Ollama requires content to be a string, never null/missing — # even on assistant messages that only contain tool_calls. data["content"] = self.content or "" if self.tool_calls is not None: data["tool_calls"] = [tc.model_dump() for tc in self.tool_calls] if self.tool_call_id is not None: data["tool_call_id"] = self.tool_call_id if self.name is not None: data["name"] = self.name return data