- models.py: ProviderConfig, RoleConfig, DiscussionConfig, ScenarioConfig, Message, ToolCallRecord, Session — compatible with config.yaml format - events.py: Event constants, Event dataclass, EventEmitter with on/off/emit - 67 tests passing (37 models + 30 events) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
124 lines
3.4 KiB
Python
124 lines
3.4 KiB
Python
"""Pydantic v2 data models for Meeting Room."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Union
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration models — must be compatible with config.yaml parsed by
|
|
# yaml.safe_load() so that DiscussionConfig(**yaml_config) works unchanged.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ProviderConfig(BaseModel):
|
|
"""A single API provider (OpenAI-compatible endpoint)."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
base_url: str
|
|
api_key: str
|
|
|
|
|
|
class RoleConfig(BaseModel):
|
|
"""A discussion participant role bound to a provider/model."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str
|
|
provider: str
|
|
model: str
|
|
temperature: float = 0.7
|
|
tools: Union[str, list[str]] = "none"
|
|
system_prompt: str = ""
|
|
|
|
@field_validator("tools", mode="before")
|
|
@classmethod
|
|
def _coerce_tools(cls, v: object) -> Union[str, list[str]]:
|
|
if isinstance(v, str):
|
|
return v
|
|
if isinstance(v, list):
|
|
return [str(item) for item in v]
|
|
raise ValueError(f"tools must be a string or list of strings, got {type(v).__name__}")
|
|
|
|
|
|
class DefaultsConfig(BaseModel):
|
|
"""Default settings inherited by scenarios."""
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
max_rounds: int = 8
|
|
language: str = "ru"
|
|
framework: str = "custom"
|
|
workdir: str = "."
|
|
|
|
|
|
class DiscussionConfig(BaseModel):
|
|
"""Top-level config — the direct counterpart of config.yaml."""
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
providers: dict[str, ProviderConfig] = Field(default_factory=dict)
|
|
roles: dict[str, RoleConfig] = Field(default_factory=dict)
|
|
defaults: DefaultsConfig = Field(default_factory=DefaultsConfig)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Scenario model
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ScenarioConfig(BaseModel):
|
|
"""A discussion scenario (loaded from .md / .yaml files)."""
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
name: str = "Untitled"
|
|
participants: list[str] = Field(default_factory=list)
|
|
max_rounds: int = 8
|
|
problem: str = ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Runtime models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class Message(BaseModel):
|
|
"""A single message in a discussion session."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
role_id: str
|
|
name: str
|
|
content: str
|
|
|
|
|
|
class ToolCallRecord(BaseModel):
|
|
"""Record of a single tool call made by an agent."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
id: str
|
|
name: str
|
|
arguments: str
|
|
result: str = ""
|
|
|
|
|
|
class Session(BaseModel):
|
|
"""A complete discussion session (scenario + messages + timestamps)."""
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
scenario: ScenarioConfig = Field(default_factory=ScenarioConfig)
|
|
messages: list[Message] = Field(default_factory=list)
|
|
tool_calls: list[ToolCallRecord] = Field(default_factory=list)
|
|
created_at: datetime = Field(
|
|
default_factory=lambda: datetime.now(timezone.utc),
|
|
)
|
|
updated_at: datetime = Field(
|
|
default_factory=lambda: datetime.now(timezone.utc),
|
|
) |