"""Session and Message data models for multi-turn conversations.""" from __future__ import annotations import uuid from dataclasses import dataclass, field from datetime import datetime, timezone from enum import Enum from typing import Any class SessionStatus(str, Enum): """Session lifecycle states.""" ACTIVE = "active" PAUSED = "paused" CLOSED = "closed" class MessageRole(str, Enum): """Message role — mirrors OpenAI chat message roles.""" SYSTEM = "system" USER = "user" ASSISTANT = "assistant" TOOL = "tool" @dataclass class Message: """A single message within a conversation session. Maps directly to the ``messages`` list consumed by the ReAct engine. """ message_id: str session_id: str role: MessageRole content: str tool_call_id: str | None = None agent_name: str | None = None created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) metadata: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: return { "message_id": self.message_id, "session_id": self.session_id, "role": self.role.value, "content": self.content, "tool_call_id": self.tool_call_id, "agent_name": self.agent_name, "created_at": self.created_at.isoformat(), "metadata": self.metadata, } @classmethod def from_dict(cls, data: dict[str, Any]) -> Message: return cls( message_id=data["message_id"], session_id=data["session_id"], role=MessageRole(data["role"]), content=data["content"], tool_call_id=data.get("tool_call_id"), agent_name=data.get("agent_name"), created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else datetime.now(timezone.utc), metadata=data.get("metadata", {}), ) def to_chat_message(self) -> dict[str, str]: """Convert to OpenAI-compatible chat message dict. Returns a dict suitable for the ``messages`` parameter of LLM chat APIs. """ msg: dict[str, str] = {"role": self.role.value, "content": self.content} if self.tool_call_id is not None: msg["tool_call_id"] = self.tool_call_id return msg @dataclass class Session: """A conversation session binding a user to an Agent. Sessions track lifecycle state and accumulate Messages. They are persisted via :class:`SessionStore` backends. """ session_id: str agent_name: str status: SessionStatus = SessionStatus.ACTIVE metadata: dict[str, Any] = field(default_factory=dict) created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) def to_dict(self) -> dict[str, Any]: return { "session_id": self.session_id, "agent_name": self.agent_name, "status": self.status.value, "metadata": self.metadata, "created_at": self.created_at.isoformat(), "updated_at": self.updated_at.isoformat(), } @classmethod def from_dict(cls, data: dict[str, Any]) -> Session: return cls( session_id=data["session_id"], agent_name=data["agent_name"], status=SessionStatus(data.get("status", "active")), metadata=data.get("metadata", {}), created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else datetime.now(timezone.utc), updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else datetime.now(timezone.utc), ) @staticmethod def new_session_id() -> str: """Generate a new session ID.""" return str(uuid.uuid4()) @staticmethod def new_message_id() -> str: """Generate a new message ID.""" return str(uuid.uuid4())