274 lines
9.9 KiB
Python
274 lines
9.9 KiB
Python
"""Pydantic request/response schemas for the MAF roundtable API.
|
|
|
|
These are the wire contracts between the frontend (Phase 6) and the backend.
|
|
Phase 1 ships the session / participant / run / event contracts; the action /
|
|
approval contracts are defined too so Phase 5 can wire the Action Coordinator
|
|
without a schema break. Sensitive fields (credentials / tokens / cookies)
|
|
must already be redacted by the writer before they reach these schemas — the
|
|
backend does not re-redact.
|
|
|
|
``MafMode`` / ``MafWritePolicy`` / ``MafParticipantRole`` are imported from the
|
|
harness config module so API validation and deployment-config validation share
|
|
a single source of truth.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Self
|
|
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
from deerflow.config.maf_roundtable_config import MafMode, MafParticipantRole, MafWritePolicy
|
|
|
|
# ── sessions ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
class ParticipantInput(BaseModel):
|
|
"""One seat / manager agent bound to a session at creation time."""
|
|
|
|
agent_id: str = Field(..., min_length=1, max_length=64)
|
|
display_name: str = Field(default="", max_length=255)
|
|
role: MafParticipantRole = "seat"
|
|
sort_order: int = 0
|
|
# Subtractive only: tools removed from the server-resolved allow-list for this seat.
|
|
excluded_tools: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class CreateSessionRequest(BaseModel):
|
|
title: str = Field(default="", max_length=255)
|
|
task_text: str = Field(..., min_length=1)
|
|
task_payload: dict[str, Any] | None = None
|
|
# Optional: omitted → service layer fills from MafRoundtableConfig.default_mode.
|
|
mode: MafMode | None = None
|
|
model_name: str | None = Field(default=None, max_length=128)
|
|
reasoning_effort: str | None = Field(default=None, max_length=32)
|
|
manager_agent_id: str | None = Field(default=None, max_length=64)
|
|
# Optional: omitted → MafRoundtableConfig.default_max_rounds.
|
|
max_rounds: int | None = Field(default=None, ge=1, le=64)
|
|
# Optional: omitted → MafRoundtableConfig.default_write_policy.
|
|
write_policy: MafWritePolicy | None = None
|
|
participants: list[ParticipantInput] = Field(default_factory=list)
|
|
# Subtractive session-wide tool exclusions (applied per seat when building snapshot).
|
|
excluded_tools: list[str] = Field(default_factory=list)
|
|
|
|
|
|
# Fields that must not be explicitly set to null on PUT (→ 422).
|
|
# Clearable fields (task_payload / model_name / reasoning_effort /
|
|
# manager_agent_id / fact_ledger / final_result) keep null = "clear value".
|
|
_NON_NULLABLE_UPDATE_FIELDS = frozenset(
|
|
{"title", "task_text", "mode", "max_rounds", "write_policy", "participants"}
|
|
)
|
|
|
|
|
|
class UpdateSessionRequest(BaseModel):
|
|
"""Partial update — only allowed when the session is not yet ``running``.
|
|
|
|
Distinguishes "field omitted" (no change) from "field set to null":
|
|
- Non-nullable fields (title / task_text / mode / max_rounds / write_policy /
|
|
participants): explicit ``null`` → 422.
|
|
- Clearable fields (task_payload / model_name / reasoning_effort /
|
|
manager_agent_id / fact_ledger / final_result): explicit ``null`` clears.
|
|
Do **not** use ``exclude_none=True`` — that would collapse the two cases.
|
|
"""
|
|
|
|
title: str | None = Field(default=None, max_length=255)
|
|
task_text: str | None = None
|
|
task_payload: dict[str, Any] | None = None
|
|
mode: MafMode | None = None
|
|
model_name: str | None = Field(default=None, max_length=128)
|
|
reasoning_effort: str | None = Field(default=None, max_length=32)
|
|
manager_agent_id: str | None = Field(default=None, max_length=64)
|
|
max_rounds: int | None = Field(default=None, ge=1, le=64)
|
|
write_policy: MafWritePolicy | None = None
|
|
participants: list[ParticipantInput] | None = None
|
|
# fact_ledger / final_result are mutable runtime fields (updated by the run lifecycle).
|
|
fact_ledger: dict[str, Any] | None = None
|
|
final_result: dict[str, Any] | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def _reject_explicit_null_for_non_nullable(self) -> Self:
|
|
for name in _NON_NULLABLE_UPDATE_FIELDS:
|
|
if name in self.model_fields_set and getattr(self, name) is None:
|
|
raise ValueError(f"{name} cannot be null")
|
|
return self
|
|
|
|
|
|
class ParticipantResponse(BaseModel):
|
|
id: str
|
|
session_id: str
|
|
agent_id: str
|
|
display_name: str
|
|
role: str
|
|
sort_order: int
|
|
thread_id: str | None = None
|
|
sandbox_id: str | None = None
|
|
status: str
|
|
last_turn_index: int | None = None
|
|
capability_snapshot: dict[str, Any] | None = None
|
|
created_at: str | None = None
|
|
updated_at: str | None = None
|
|
|
|
|
|
class SessionResponse(BaseModel):
|
|
id: str
|
|
user_id: str
|
|
title: str
|
|
task_text: str
|
|
task_payload: dict[str, Any] | None = None
|
|
mode: str
|
|
status: str
|
|
model_name: str | None = None
|
|
reasoning_effort: str | None = None
|
|
manager_agent_id: str | None = None
|
|
max_rounds: int
|
|
write_policy: str
|
|
capability_snapshot: dict[str, Any] | None = None
|
|
fact_ledger: dict[str, Any] | None = None
|
|
final_result: dict[str, Any] | None = None
|
|
participants: list[ParticipantResponse] = Field(default_factory=list)
|
|
created_at: str | None = None
|
|
updated_at: str | None = None
|
|
|
|
|
|
class SessionListResponse(BaseModel):
|
|
sessions: list[SessionResponse]
|
|
total: int
|
|
|
|
|
|
# ── runs ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class RunResponse(BaseModel):
|
|
id: str
|
|
session_id: str
|
|
status: str
|
|
workflow_version: str | None = None
|
|
maf_checkpoint: dict[str, Any] | None = None
|
|
last_event_seq: int
|
|
started_at: str | None = None
|
|
finished_at: str | None = None
|
|
error_code: str | None = None
|
|
error_message_safe: str | None = None
|
|
token_usage: dict[str, Any] | None = None
|
|
latency_ms: int | None = None
|
|
created_at: str | None = None
|
|
updated_at: str | None = None
|
|
|
|
|
|
class RunListResponse(BaseModel):
|
|
runs: list[RunResponse]
|
|
|
|
|
|
class CreateRunRequest(BaseModel):
|
|
workflow_version: str | None = Field(default=None, max_length=64)
|
|
|
|
|
|
# ── events ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
class EventResponse(BaseModel):
|
|
id: str
|
|
session_id: str
|
|
run_id: str
|
|
seq: int
|
|
event_type: str
|
|
agent_id: str | None = None
|
|
turn_index: int | None = None
|
|
payload: dict[str, Any] | None = None
|
|
created_at: str | None = None
|
|
|
|
|
|
class EventListResponse(BaseModel):
|
|
events: list[EventResponse]
|
|
latest_seq: int
|
|
|
|
|
|
# ── actions ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
class ActionResponse(BaseModel):
|
|
id: str
|
|
session_id: str
|
|
run_id: str | None = None
|
|
agent_id: str | None = None
|
|
tool_name: str
|
|
logical_action_id: str | None = None
|
|
risk_level: str
|
|
status: str
|
|
arguments: dict[str, Any] | None = None
|
|
result: dict[str, Any] | None = None
|
|
external_object_id: str | None = None
|
|
approval_id: str | None = None
|
|
attempt_count: int
|
|
started_at: str | None = None
|
|
finished_at: str | None = None
|
|
created_at: str | None = None
|
|
updated_at: str | None = None
|
|
|
|
|
|
class ActionListResponse(BaseModel):
|
|
actions: list[ActionResponse]
|
|
|
|
|
|
# ── approvals ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
class ApprovalResponse(BaseModel):
|
|
id: str
|
|
session_id: str
|
|
run_id: str | None = None
|
|
action_id: str | None = None
|
|
requested_by_agent_id: str | None = None
|
|
status: str
|
|
summary: str
|
|
payload: dict[str, Any] | None = None
|
|
decided_by: str | None = None
|
|
decided_at: str | None = None
|
|
expires_at: str | None = None
|
|
created_at: str | None = None
|
|
updated_at: str | None = None
|
|
|
|
|
|
class ApprovalListResponse(BaseModel):
|
|
approvals: list[ApprovalResponse]
|
|
|
|
|
|
class ApprovalDecisionRequest(BaseModel):
|
|
# approved | rejected
|
|
decision: str = Field(..., pattern="^(approved|rejected)$")
|
|
|
|
|
|
# ── health ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class HealthResponse(BaseModel):
|
|
"""Health / feature-flag probe. The frontend reads ``enabled`` to decide
|
|
whether to show the MAF entry. ``store_available`` is false when no DB is
|
|
configured (memory mode) — the router then returns 503 on writes.
|
|
|
|
SSE fields (P1): ``sse_hub_type`` reports the hub implementation
|
|
(``in_memory`` or future ``redis``); ``sse_multi_worker`` is ``False``
|
|
until Redis Pub/Sub is wired; ``sse_warning`` carries a human-readable
|
|
advisory. When multi-worker is unsupported, production deployments must
|
|
use a single worker or fail-closed.
|
|
"""
|
|
|
|
enabled: bool
|
|
conversation_dispatch_ready: bool = False
|
|
conversation_dispatch_status: str = "configuration_unavailable"
|
|
conversation_stream_ready: bool = False
|
|
conversation_stream_status: str = "unknown"
|
|
conversation_event_backend: str = "memory"
|
|
store_available: bool
|
|
default_mode: str
|
|
default_write_policy: str
|
|
default_max_rounds: int
|
|
admin_diagnostic_access: bool
|
|
write_guard_enabled: bool
|
|
write_guard_ready: bool
|
|
action_secret_configured: bool
|
|
write_guard_status: str
|
|
sse_hub_type: str = "in_memory"
|
|
sse_multi_worker: bool = False
|
|
sse_warning: str = "Single-worker only. Multi-worker requires Redis Pub/Sub."
|