mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Finished reorganization
This commit is contained in:
+2
-62
@@ -11,14 +11,12 @@ import contextlib
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from uuid import UUID, uuid4
|
||||
from uuid import UUID
|
||||
|
||||
import httpx
|
||||
import structlog
|
||||
from anthropic import AsyncAnthropic
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from anthropic.types import MessageParam
|
||||
@@ -26,6 +24,7 @@ if TYPE_CHECKING:
|
||||
from roboco.config import settings
|
||||
from roboco.llm import ToonAdapter
|
||||
from roboco.models import AgentRole, AgentStatus, Team
|
||||
from roboco.models.agents import AgentConfig, AgentState
|
||||
|
||||
# Type for reasoning stream callback (injected to avoid API layer coupling)
|
||||
ReasoningStreamCallback = Callable[[UUID, str], Awaitable[None]]
|
||||
@@ -55,65 +54,6 @@ def get_reasoning_stream_callback() -> ReasoningStreamCallback | None:
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ModelProvider(str, Enum):
|
||||
"""LLM provider options."""
|
||||
|
||||
ANTHROPIC = "anthropic"
|
||||
OPENAI = "openai"
|
||||
LOCAL = "local"
|
||||
|
||||
|
||||
class AgentConfig(BaseModel):
|
||||
"""Configuration for an agent instance."""
|
||||
|
||||
# Identity
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
name: str
|
||||
slug: str = Field(..., pattern=r"^[a-z0-9-]+$")
|
||||
role: AgentRole
|
||||
team: Team | None = None
|
||||
|
||||
# Model configuration
|
||||
provider: ModelProvider = ModelProvider.ANTHROPIC
|
||||
model: str = "claude-sonnet-4-20250514"
|
||||
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
|
||||
max_tokens: int = Field(default=4096, ge=1)
|
||||
|
||||
# System prompt (loaded from blueprints)
|
||||
system_prompt: str
|
||||
|
||||
# Capabilities
|
||||
capabilities: list[str] = Field(default_factory=list)
|
||||
|
||||
# Permissions
|
||||
can_notify: bool = False
|
||||
channel_ids: list[UUID] = Field(default_factory=list)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AGENT LIFECYCLE STATE
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AgentState(BaseModel):
|
||||
"""Current state of an agent."""
|
||||
|
||||
status: AgentStatus = AgentStatus.OFFLINE
|
||||
current_task_id: UUID | None = None
|
||||
current_session_id: UUID | None = None
|
||||
last_activity: datetime | None = None
|
||||
error: str | None = None
|
||||
|
||||
# Metrics
|
||||
messages_sent: int = 0
|
||||
tasks_completed: int = 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BASE AGENT CLASS
|
||||
# =============================================================================
|
||||
|
||||
+19
-101
@@ -5,17 +5,26 @@ Implementation of Board-level workflows from the blueprint.
|
||||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent, AgentConfig
|
||||
from roboco.agents.base import Agent
|
||||
from roboco.models import AgentRole, Team
|
||||
from roboco.models.agents import (
|
||||
AgentConfig,
|
||||
AuditFlag,
|
||||
AuditorFlagSeverity,
|
||||
AuditorPhase,
|
||||
AuditReport,
|
||||
Campaign,
|
||||
Feature,
|
||||
HeadMarketingPhase,
|
||||
ProductOwnerPhase,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -25,29 +34,6 @@ logger = structlog.get_logger()
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ProductOwnerPhase(str, Enum):
|
||||
"""Phases of the Product Owner lifecycle."""
|
||||
|
||||
VISION = "vision"
|
||||
ROADMAP = "roadmap"
|
||||
DEFINE = "define"
|
||||
PRIORITIZE = "prioritize"
|
||||
REVIEW = "review"
|
||||
FEEDBACK = "feedback"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Feature:
|
||||
"""A feature or epic."""
|
||||
|
||||
id: UUID
|
||||
title: str
|
||||
description: str
|
||||
acceptance_criteria: list[str]
|
||||
priority: int # 0-3
|
||||
status: str = "backlog"
|
||||
|
||||
|
||||
class ProductOwnerAgent(Agent):
|
||||
"""
|
||||
Product Owner agent that defines what to build.
|
||||
@@ -193,31 +179,6 @@ ACCEPTED: [reason] or NEEDS_CHANGES: [what's missing]
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class HeadMarketingPhase(str, Enum):
|
||||
"""Phases of the Head of Marketing lifecycle."""
|
||||
|
||||
RESEARCH = "research"
|
||||
STRATEGY = "strategy"
|
||||
PLAN = "plan"
|
||||
CREATE = "create"
|
||||
EXECUTE = "execute"
|
||||
ANALYZE = "analyze"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Campaign:
|
||||
"""A marketing campaign."""
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
objective: str
|
||||
channels: list[str]
|
||||
start_date: datetime | None = None
|
||||
end_date: datetime | None = None
|
||||
status: str = "planning"
|
||||
metrics: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class HeadMarketingAgent(Agent):
|
||||
"""
|
||||
Head of Marketing agent.
|
||||
@@ -320,52 +281,6 @@ class HeadMarketingAgent(Agent):
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AuditorPhase(str, Enum):
|
||||
"""Phases of the Auditor lifecycle."""
|
||||
|
||||
OBSERVE = "observe"
|
||||
ANALYZE = "analyze"
|
||||
FLAG = "flag"
|
||||
REPORT = "report"
|
||||
AUDIT = "audit"
|
||||
ADVISE = "advise"
|
||||
|
||||
|
||||
class FlagSeverity(str, Enum):
|
||||
"""Severity of flagged issues."""
|
||||
|
||||
INFO = "info"
|
||||
WARNING = "warning"
|
||||
CONCERN = "concern"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditFlag:
|
||||
"""A flagged issue from audit observation."""
|
||||
|
||||
id: UUID
|
||||
severity: FlagSeverity
|
||||
category: str # quality, process, communication, efficiency
|
||||
description: str
|
||||
evidence: list[str]
|
||||
recommendation: str | None = None
|
||||
reported_to_ceo: bool = False
|
||||
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditReport:
|
||||
"""A report to the CEO."""
|
||||
|
||||
period: str
|
||||
summary: str
|
||||
flags: list[AuditFlag]
|
||||
metrics: dict[str, Any]
|
||||
recommendations: list[str]
|
||||
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class AuditorAgent(Agent):
|
||||
"""
|
||||
Auditor agent - the CEO's secret ally.
|
||||
@@ -527,7 +442,7 @@ efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff step
|
||||
self._flags.append(
|
||||
AuditFlag(
|
||||
id=uuid4(),
|
||||
severity=FlagSeverity.CONCERN,
|
||||
severity=AuditorFlagSeverity.CONCERN,
|
||||
category="analysis",
|
||||
description=analysis[:500],
|
||||
evidence=["Automated analysis"],
|
||||
@@ -542,7 +457,9 @@ efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff step
|
||||
"""
|
||||
self.log.debug("FLAG phase")
|
||||
|
||||
critical_flags = [f for f in self._flags if f.severity == FlagSeverity.CRITICAL]
|
||||
critical_flags = [
|
||||
f for f in self._flags if f.severity == AuditorFlagSeverity.CRITICAL
|
||||
]
|
||||
if critical_flags:
|
||||
# Immediate alert to CEO
|
||||
await self._alert_ceo(critical_flags)
|
||||
@@ -564,7 +481,8 @@ efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff step
|
||||
self._last_report is None
|
||||
or hours_elapsed >= hours_in_day
|
||||
or any(
|
||||
f.severity in [FlagSeverity.CONCERN, FlagSeverity.CRITICAL]
|
||||
f.severity
|
||||
in [AuditorFlagSeverity.CONCERN, AuditorFlagSeverity.CRITICAL]
|
||||
for f in self._flags
|
||||
)
|
||||
)
|
||||
@@ -607,7 +525,7 @@ efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff step
|
||||
self._flags.append(
|
||||
AuditFlag(
|
||||
id=uuid4(),
|
||||
severity=FlagSeverity.INFO,
|
||||
severity=AuditorFlagSeverity.INFO,
|
||||
category=audit_type,
|
||||
description=findings,
|
||||
evidence=[f"{audit_type} audit"],
|
||||
|
||||
@@ -7,50 +7,19 @@ Handles task lifecycle:
|
||||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent, AgentConfig
|
||||
from roboco.agents.base import Agent
|
||||
from roboco.models import AgentRole, TaskStatus, Team
|
||||
from roboco.models.agents import AgentConfig, DevTaskPhase, TaskContext
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class DevTaskPhase(str, Enum):
|
||||
"""Phases of the developer task lifecycle."""
|
||||
|
||||
SCAN = "scan"
|
||||
CLAIM = "claim"
|
||||
UNDERSTAND = "understand"
|
||||
PLAN = "plan"
|
||||
EXECUTE = "execute"
|
||||
VERIFY = "verify"
|
||||
NOTES = "notes"
|
||||
CLOSE = "close"
|
||||
BLOCKED = "blocked"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskContext:
|
||||
"""Context for the current task being worked on."""
|
||||
|
||||
task_id: UUID
|
||||
title: str
|
||||
phase: DevTaskPhase = DevTaskPhase.CLAIM
|
||||
subtasks: list[dict[str, Any]] = field(default_factory=list)
|
||||
current_subtask: int = 0
|
||||
blockers: list[str] = field(default_factory=list)
|
||||
commits: list[str] = field(default_factory=list)
|
||||
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
journal_entries: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class DeveloperAgent(Agent):
|
||||
"""
|
||||
Developer agent that follows the Dev Lifecycle.
|
||||
|
||||
@@ -7,79 +7,26 @@ Handles documentation lifecycle:
|
||||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import aiofiles
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent, AgentConfig
|
||||
from roboco.agents.base import Agent
|
||||
from roboco.models import AgentRole, TaskStatus, Team
|
||||
from roboco.models.agents import (
|
||||
AgentConfig,
|
||||
DocContext,
|
||||
DocTaskPhase,
|
||||
DocType,
|
||||
DocumentSpec,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class DocTaskPhase(str, Enum):
|
||||
"""Phases of the Documenter lifecycle."""
|
||||
|
||||
MONITOR = "monitor"
|
||||
RECEIVE = "receive"
|
||||
GATHER = "gather"
|
||||
SYNTHESIZE = "synthesize"
|
||||
WRITE = "write"
|
||||
REVIEW = "review"
|
||||
PUBLISH = "publish"
|
||||
|
||||
|
||||
class DocType(str, Enum):
|
||||
"""Types of documentation."""
|
||||
|
||||
API = "api"
|
||||
README = "readme"
|
||||
ARCHITECTURE = "architecture"
|
||||
CHANGELOG = "changelog"
|
||||
KNOWLEDGE_BASE = "knowledge_base"
|
||||
COMPONENT = "component"
|
||||
DESIGN_SYSTEM = "design_system"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocumentSpec:
|
||||
"""Specification for a document to create/update."""
|
||||
|
||||
doc_type: DocType
|
||||
title: str
|
||||
path: str
|
||||
priority: str = "required" # required, optional
|
||||
content: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocContext:
|
||||
"""Context for the current documentation task."""
|
||||
|
||||
task_id: UUID
|
||||
title: str
|
||||
phase: DocTaskPhase = DocTaskPhase.RECEIVE
|
||||
# Gathered materials
|
||||
dev_notes: str | None = None
|
||||
qa_feedback: str | None = None
|
||||
commits: list[str] = field(default_factory=list)
|
||||
conversations: list[str] = field(default_factory=list)
|
||||
code_changes: list[str] = field(default_factory=list)
|
||||
# Synthesis
|
||||
summary: str | None = None
|
||||
documents_needed: list[DocumentSpec] = field(default_factory=list)
|
||||
current_doc: int = 0
|
||||
# Output
|
||||
written_docs: list[str] = field(default_factory=list)
|
||||
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
notes: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class DocumenterAgent(Agent):
|
||||
"""
|
||||
Documenter agent that follows the Documenter Lifecycle.
|
||||
|
||||
+1
-146
@@ -5,181 +5,36 @@ Provides factory functions for creating all agent types and
|
||||
deploying complete cells with their full agent complement.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent
|
||||
from roboco.agents.board import (
|
||||
AuditorAgent,
|
||||
HeadMarketingAgent,
|
||||
ProductOwnerAgent,
|
||||
create_auditor,
|
||||
create_head_marketing,
|
||||
create_product_owner,
|
||||
)
|
||||
from roboco.agents.developer import (
|
||||
DeveloperAgent,
|
||||
create_backend_developer,
|
||||
create_frontend_developer,
|
||||
create_ux_developer,
|
||||
)
|
||||
from roboco.agents.documenter import (
|
||||
DocumenterAgent,
|
||||
create_backend_documenter,
|
||||
create_frontend_documenter,
|
||||
create_ux_documenter,
|
||||
)
|
||||
from roboco.agents.pm import (
|
||||
CellPMAgent,
|
||||
MainPMAgent,
|
||||
create_backend_pm,
|
||||
create_frontend_pm,
|
||||
create_main_pm,
|
||||
create_ux_pm,
|
||||
)
|
||||
from roboco.agents.qa import (
|
||||
QAAgent,
|
||||
create_backend_qa,
|
||||
create_frontend_qa,
|
||||
create_ux_qa,
|
||||
)
|
||||
from roboco.models import Team
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Cell:
|
||||
"""A complete cell with all its agents."""
|
||||
|
||||
name: str
|
||||
team: Team
|
||||
pm: CellPMAgent
|
||||
developers: list[DeveloperAgent]
|
||||
qa: QAAgent
|
||||
documenter: DocumenterAgent
|
||||
|
||||
@property
|
||||
def all_agents(self) -> list[Agent]:
|
||||
"""Get all agents in the cell."""
|
||||
return [self.pm, *self.developers, self.qa, self.documenter]
|
||||
|
||||
async def start_all(self) -> None:
|
||||
"""Start all agents in the cell."""
|
||||
for agent in self.all_agents:
|
||||
await agent.start()
|
||||
logger.info("Cell started", cell=self.name, agents=len(self.all_agents))
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
"""Stop all agents in the cell."""
|
||||
for agent in self.all_agents:
|
||||
await agent.stop()
|
||||
logger.info("Cell stopped", cell=self.name)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Board:
|
||||
"""The board level with all board agents."""
|
||||
|
||||
product_owner: ProductOwnerAgent
|
||||
head_marketing: HeadMarketingAgent
|
||||
auditor: AuditorAgent
|
||||
|
||||
@property
|
||||
def all_agents(self) -> list[Agent]:
|
||||
"""Get all board agents."""
|
||||
return [self.product_owner, self.head_marketing, self.auditor]
|
||||
|
||||
async def start_all(self) -> None:
|
||||
"""Start all board agents."""
|
||||
for agent in self.all_agents:
|
||||
await agent.start()
|
||||
logger.info("Board started", agents=len(self.all_agents))
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
"""Stop all board agents."""
|
||||
for agent in self.all_agents:
|
||||
await agent.stop()
|
||||
logger.info("Board stopped")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Organization:
|
||||
"""The complete AI organization."""
|
||||
|
||||
board: Board
|
||||
main_pm: MainPMAgent
|
||||
backend_cell: Cell
|
||||
frontend_cell: Cell
|
||||
ux_cell: Cell
|
||||
|
||||
@property
|
||||
def all_agents(self) -> list[Agent]:
|
||||
"""Get all agents in the organization."""
|
||||
agents: list[Agent] = []
|
||||
agents.extend(self.board.all_agents)
|
||||
agents.append(self.main_pm)
|
||||
agents.extend(self.backend_cell.all_agents)
|
||||
agents.extend(self.frontend_cell.all_agents)
|
||||
agents.extend(self.ux_cell.all_agents)
|
||||
return agents
|
||||
|
||||
@property
|
||||
def agent_count(self) -> int:
|
||||
"""Total number of agents."""
|
||||
return len(self.all_agents)
|
||||
|
||||
async def start_all(self) -> None:
|
||||
"""Start the entire organization."""
|
||||
logger.info("Starting organization")
|
||||
|
||||
# Start board first
|
||||
await self.board.start_all()
|
||||
await self.main_pm.start()
|
||||
|
||||
# Then cells
|
||||
await self.backend_cell.start_all()
|
||||
await self.frontend_cell.start_all()
|
||||
await self.ux_cell.start_all()
|
||||
|
||||
logger.info("Organization started", total_agents=self.agent_count)
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
"""Stop the entire organization."""
|
||||
logger.info("Stopping organization")
|
||||
|
||||
# Stop cells first
|
||||
await self.ux_cell.stop_all()
|
||||
await self.frontend_cell.stop_all()
|
||||
await self.backend_cell.stop_all()
|
||||
|
||||
# Then management
|
||||
await self.main_pm.stop()
|
||||
await self.board.stop_all()
|
||||
|
||||
logger.info("Organization stopped")
|
||||
|
||||
def get_agent_by_id(self, agent_id: UUID) -> Agent | None:
|
||||
"""Find an agent by ID."""
|
||||
for agent in self.all_agents:
|
||||
if agent.id == agent_id:
|
||||
return agent
|
||||
return None
|
||||
|
||||
def get_agent_by_slug(self, slug: str) -> Agent | None:
|
||||
"""Find an agent by slug."""
|
||||
for agent in self.all_agents:
|
||||
if agent.config.slug == slug:
|
||||
return agent
|
||||
return None
|
||||
|
||||
def get_agents_by_team(self, team: Team) -> list[Agent]:
|
||||
"""Get all agents in a team."""
|
||||
return [a for a in self.all_agents if a.team == team]
|
||||
|
||||
from roboco.models.organization import Board, Cell, Organization
|
||||
|
||||
# =============================================================================
|
||||
# CELL FACTORIES
|
||||
|
||||
+9
-59
@@ -9,76 +9,26 @@ Main PM:
|
||||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent, AgentConfig
|
||||
from roboco.agents.base import Agent
|
||||
from roboco.models import AgentRole, NotificationType, TaskStatus, Team
|
||||
from roboco.models.agents import (
|
||||
AgentConfig,
|
||||
CellPMPhase,
|
||||
CellStatus,
|
||||
Escalation,
|
||||
MainPMPhase,
|
||||
TaskAssignment,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class CellPMPhase(str, Enum):
|
||||
"""Phases of the Cell PM lifecycle."""
|
||||
|
||||
MONITOR = "monitor"
|
||||
TRIAGE = "triage"
|
||||
ASSIGN = "assign"
|
||||
FACILITATE = "facilitate"
|
||||
ESCALATE = "escalate"
|
||||
TRACK = "track"
|
||||
REPORT = "report"
|
||||
|
||||
|
||||
class MainPMPhase(str, Enum):
|
||||
"""Phases of the Main PM lifecycle."""
|
||||
|
||||
OVERSEE = "oversee"
|
||||
RECEIVE = "receive"
|
||||
PRIORITIZE = "prioritize"
|
||||
COORDINATE = "coordinate"
|
||||
DISTRIBUTE = "distribute"
|
||||
REPORT_UP = "report_up"
|
||||
FACILITATE = "facilitate"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CellStatus:
|
||||
"""Status of a cell."""
|
||||
|
||||
name: str
|
||||
active_tasks: int = 0
|
||||
blocked_tasks: int = 0
|
||||
completed_today: int = 0
|
||||
available_devs: int = 0
|
||||
concerns: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskAssignment:
|
||||
"""A task assignment decision."""
|
||||
|
||||
task_id: UUID
|
||||
agent_id: UUID
|
||||
agent_name: str
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Escalation:
|
||||
"""An escalation to higher management."""
|
||||
|
||||
issue: str
|
||||
severity: str # low, medium, high, critical
|
||||
task_id: UUID | None = None
|
||||
proposed_solution: str | None = None
|
||||
|
||||
|
||||
class CellPMAgent(Agent):
|
||||
"""
|
||||
Cell PM agent that manages a single cell (Backend, Frontend, or UX/UI).
|
||||
|
||||
+8
-51
@@ -7,68 +7,25 @@ Handles review lifecycle:
|
||||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent, AgentConfig
|
||||
from roboco.agents.base import Agent
|
||||
from roboco.models import AgentRole, TaskStatus, Team
|
||||
from roboco.models.agents import (
|
||||
AgentConfig,
|
||||
QATaskPhase,
|
||||
ReviewContext,
|
||||
TestCase,
|
||||
TestResult,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class QATaskPhase(str, Enum):
|
||||
"""Phases of the QA lifecycle."""
|
||||
|
||||
MONITOR = "monitor"
|
||||
RECEIVE = "receive"
|
||||
UNDERSTAND = "understand"
|
||||
TEST = "test"
|
||||
VERDICT = "verdict"
|
||||
DOCUMENT = "document"
|
||||
RETURN = "return"
|
||||
|
||||
|
||||
class TestResult(str, Enum):
|
||||
"""Test result outcomes."""
|
||||
|
||||
PASS = "pass"
|
||||
FAIL = "fail"
|
||||
BLOCKED = "blocked"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestCase:
|
||||
"""A single test case."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
steps: list[str]
|
||||
expected: str
|
||||
result: TestResult | None = None
|
||||
actual: str | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReviewContext:
|
||||
"""Context for the current review being conducted."""
|
||||
|
||||
task_id: UUID
|
||||
title: str
|
||||
phase: QATaskPhase = QATaskPhase.RECEIVE
|
||||
test_cases: list[TestCase] = field(default_factory=list)
|
||||
current_test: int = 0
|
||||
findings: list[str] = field(default_factory=list)
|
||||
verdict: TestResult | None = None
|
||||
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
notes: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class QAAgent(Agent):
|
||||
"""
|
||||
QA agent that follows the QA Lifecycle.
|
||||
|
||||
@@ -8,79 +8,24 @@ from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from roboco.api.deps import CurrentAgentContext, DbSession, PermissionServiceDep
|
||||
from roboco.api.schemas.channels import (
|
||||
ChannelDetailResponse,
|
||||
ChannelListResponse,
|
||||
ChannelResponse,
|
||||
GroupResponse,
|
||||
ListChannelsQuery,
|
||||
)
|
||||
from roboco.db.tables import ChannelTable
|
||||
from roboco.models import AgentRole, ChannelCreate, ChannelType, ChannelUpdate
|
||||
from roboco.models import AgentRole, ChannelCreate, ChannelUpdate
|
||||
from roboco.utils.converters import require_uuid, to_python_uuid
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Response Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ChannelResponse(BaseModel):
|
||||
"""Channel response with computed fields."""
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
slug: str
|
||||
type: ChannelType
|
||||
description: str | None
|
||||
topic: str | None
|
||||
member_count: int
|
||||
message_count: int
|
||||
group_count: int
|
||||
is_archived: bool
|
||||
is_private: bool
|
||||
can_write: bool # Whether current agent can write
|
||||
|
||||
|
||||
class ChannelListResponse(BaseModel):
|
||||
"""Paginated list of channels."""
|
||||
|
||||
items: list[ChannelResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
class ChannelDetailResponse(ChannelResponse):
|
||||
"""Detailed channel response with groups."""
|
||||
|
||||
groups: list[dict]
|
||||
|
||||
|
||||
class GroupResponse(BaseModel):
|
||||
"""Group within a channel."""
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
hierarchy_level: int
|
||||
is_active: bool
|
||||
total_messages: int
|
||||
active_session_id: UUID | None = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Query Parameter Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ListChannelsQuery(BaseModel):
|
||||
"""Query params for listing channels."""
|
||||
|
||||
page: int = Field(1, ge=1)
|
||||
page_size: int = Field(20, ge=1, le=100)
|
||||
include_archived: bool = False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Routes
|
||||
# =============================================================================
|
||||
|
||||
+11
-102
@@ -6,15 +6,24 @@ Provides aggregated views, alerts, and reporting.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
|
||||
from roboco.api.deps import DbSession
|
||||
from roboco.api.schemas.dashboard import (
|
||||
AuditorDashboard,
|
||||
AuditorFlag,
|
||||
AuditorReport,
|
||||
CEOOverview,
|
||||
ChannelFeed,
|
||||
CreateFlagRequest,
|
||||
CreateReportRequest,
|
||||
FlagSeverity,
|
||||
TeamHealth,
|
||||
)
|
||||
from roboco.db.tables import AgentTable, MessageTable, TaskTable
|
||||
from roboco.models.base import AgentStatus, Team
|
||||
from roboco.models.dashboard import CreateFlagParams
|
||||
@@ -25,106 +34,6 @@ from roboco.services.metrics import get_metrics_service
|
||||
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MODELS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class FlagSeverity(str, Enum):
|
||||
"""Severity levels for auditor flags."""
|
||||
|
||||
INFO = "info"
|
||||
WARNING = "warning"
|
||||
URGENT = "urgent"
|
||||
|
||||
|
||||
class AuditorFlag(BaseModel):
|
||||
"""A flag raised by the auditor."""
|
||||
|
||||
id: UUID
|
||||
severity: FlagSeverity
|
||||
category: str # quality, process, communication, blocked, documentation
|
||||
title: str
|
||||
description: str
|
||||
related_task_id: UUID | None = None
|
||||
related_agent_id: UUID | None = None
|
||||
created_at: datetime
|
||||
resolved_at: datetime | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class AuditorReport(BaseModel):
|
||||
"""An auditor report for the CEO."""
|
||||
|
||||
id: UUID
|
||||
report_type: str # daily, weekly, alert
|
||||
title: str
|
||||
summary: str
|
||||
sections: list[dict[str, Any]]
|
||||
created_at: datetime
|
||||
sent_at: datetime | None = None
|
||||
|
||||
|
||||
class ChannelFeed(BaseModel):
|
||||
"""Live feed status for a channel."""
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
status: str # streaming, idle, offline
|
||||
last_activity: datetime | None
|
||||
message_count_24h: int
|
||||
|
||||
|
||||
class AuditorDashboard(BaseModel):
|
||||
"""Complete auditor dashboard data."""
|
||||
|
||||
live_feeds: list[ChannelFeed]
|
||||
flagged_items: list[AuditorFlag]
|
||||
metrics: dict[str, Any]
|
||||
audit_queue: list[dict[str, Any]]
|
||||
recent_reports: list[AuditorReport]
|
||||
|
||||
|
||||
class TeamHealth(BaseModel):
|
||||
"""Health status for a team."""
|
||||
|
||||
team: str
|
||||
status: str # ok, slow, critical
|
||||
active_tasks: int
|
||||
blocked_tasks: int
|
||||
blocked_ratio: float
|
||||
completed_this_week: int
|
||||
|
||||
|
||||
class CEOOverview(BaseModel):
|
||||
"""Complete CEO overview data."""
|
||||
|
||||
health_status: list[TeamHealth]
|
||||
key_metrics: dict[str, Any]
|
||||
auditor_alerts: dict[str, Any]
|
||||
roadmap_progress: dict[str, Any]
|
||||
|
||||
|
||||
class CreateFlagRequest(BaseModel):
|
||||
"""Request to create an auditor flag."""
|
||||
|
||||
severity: FlagSeverity
|
||||
category: str
|
||||
title: str
|
||||
description: str
|
||||
related_task_id: UUID | None = None
|
||||
related_agent_id: UUID | None = None
|
||||
|
||||
|
||||
class CreateReportRequest(BaseModel):
|
||||
"""Request to create an auditor report."""
|
||||
|
||||
report_type: str
|
||||
title: str
|
||||
summary: str
|
||||
sections: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AUDITOR DASHBOARD
|
||||
# =============================================================================
|
||||
|
||||
@@ -6,9 +6,9 @@ Endpoints for monitoring application health and readiness.
|
||||
|
||||
import redis.asyncio as redis
|
||||
from fastapi import APIRouter, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import text
|
||||
|
||||
from roboco.api.schemas.health import HealthResponse, ReadinessResponse
|
||||
from roboco.config import settings
|
||||
from roboco.db.base import get_db_context
|
||||
|
||||
@@ -36,22 +36,6 @@ async def _check_redis() -> tuple[str, bool]:
|
||||
return str(e), False
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health check response."""
|
||||
|
||||
status: str
|
||||
version: str
|
||||
environment: str
|
||||
|
||||
|
||||
class ReadinessResponse(BaseModel):
|
||||
"""Readiness check response."""
|
||||
|
||||
status: str
|
||||
database: str
|
||||
redis: str
|
||||
|
||||
|
||||
@router.get(
|
||||
"/health",
|
||||
response_model=HealthResponse,
|
||||
|
||||
+14
-156
@@ -4,14 +4,26 @@ Journal API Routes
|
||||
Agent personal journals for reflection, growth tracking, and debugging.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.api.deps import CurrentAgentContext, DbSession
|
||||
from roboco.api.schemas.journals import (
|
||||
CreateEntryRequest,
|
||||
DecisionLogRequest,
|
||||
GeneralEntryRequest,
|
||||
GrowthMetricsResponse,
|
||||
JournalEntryResponse,
|
||||
JournalResponse,
|
||||
JournalStatsResponse,
|
||||
LearningRequest,
|
||||
ListEntriesParams,
|
||||
SearchEntriesRequest,
|
||||
StruggleRequest,
|
||||
TaskReflectionRequest,
|
||||
)
|
||||
from roboco.models.base import AgentRole, JournalEntryType
|
||||
from roboco.models.journal import (
|
||||
DecisionLogParams,
|
||||
@@ -23,163 +35,9 @@ from roboco.models.journal import (
|
||||
)
|
||||
from roboco.services.journal import ListEntriesFilter, get_journal_service
|
||||
|
||||
# =============================================================================
|
||||
# QUERY PARAMETER SCHEMAS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ListEntriesParams(BaseModel):
|
||||
"""Query parameters for listing journal entries."""
|
||||
|
||||
entry_type: str | None = Field(None, description="Filter by entry type")
|
||||
task_id: UUID | None = Field(None, description="Filter by task")
|
||||
limit: int = Field(50, ge=1, le=100, description="Maximum entries to return")
|
||||
offset: int = Field(0, ge=0, description="Number of entries to skip")
|
||||
|
||||
|
||||
router = APIRouter(prefix="/journals", tags=["journals"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# REQUEST/RESPONSE SCHEMAS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class JournalResponse(BaseModel):
|
||||
"""Journal response."""
|
||||
|
||||
id: UUID
|
||||
agent_id: UUID
|
||||
total_entries: int
|
||||
last_entry_at: datetime | None
|
||||
latest_summary: str | None
|
||||
summary_updated_at: datetime | None
|
||||
entries_by_type: dict[str, int]
|
||||
created_at: datetime
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class JournalEntryResponse(BaseModel):
|
||||
"""Journal entry response."""
|
||||
|
||||
id: UUID
|
||||
journal_id: UUID
|
||||
type: str
|
||||
title: str
|
||||
content: str
|
||||
task_id: UUID | None
|
||||
session_id: UUID | None
|
||||
timestamp: datetime
|
||||
tags: list[str]
|
||||
sentiment: str | None
|
||||
is_private: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class CreateEntryRequest(BaseModel):
|
||||
"""Request to create a journal entry."""
|
||||
|
||||
type: str = Field(
|
||||
...,
|
||||
description="Entry type (task_reflection, decision_log, learning, etc.)",
|
||||
)
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
content: str = Field(..., min_length=1)
|
||||
task_id: UUID | None = None
|
||||
session_id: UUID | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
sentiment: str | None = None
|
||||
is_private: bool = False
|
||||
|
||||
|
||||
class TaskReflectionRequest(BaseModel):
|
||||
"""Request to create a task reflection entry."""
|
||||
|
||||
task_id: UUID
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
what_done: str
|
||||
what_learned: str
|
||||
what_struggled: str
|
||||
next_steps: list[str] = Field(default_factory=list)
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class DecisionLogRequest(BaseModel):
|
||||
"""Request to create a decision log entry."""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
context: str
|
||||
options: list[dict[str, str]] = Field(..., min_length=2)
|
||||
chosen: str
|
||||
rationale: str
|
||||
consequences: list[str] = Field(default_factory=list)
|
||||
task_id: UUID | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class LearningRequest(BaseModel):
|
||||
"""Request to create a learning entry."""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
what_learned: str
|
||||
how_applied: str | None = None
|
||||
source: str | None = None
|
||||
task_id: UUID | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class StruggleRequest(BaseModel):
|
||||
"""Request to create a struggle entry."""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
what_struggled: str
|
||||
attempted_solutions: list[str] = Field(default_factory=list)
|
||||
resolution: str | None = None
|
||||
help_needed: str | None = None
|
||||
task_id: UUID | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class GeneralEntryRequest(BaseModel):
|
||||
"""Request to create a general journal entry."""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
content: str
|
||||
task_id: UUID | None = None
|
||||
session_id: UUID | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
is_private: bool = False
|
||||
|
||||
|
||||
class JournalStatsResponse(BaseModel):
|
||||
"""Journal statistics response."""
|
||||
|
||||
total_entries: int
|
||||
entries_by_type: dict[str, int]
|
||||
last_entry_at: datetime | None
|
||||
has_summary: bool
|
||||
|
||||
|
||||
class GrowthMetricsResponse(BaseModel):
|
||||
"""Growth metrics response."""
|
||||
|
||||
total_reflections: int
|
||||
total_learnings: int
|
||||
total_struggles: int
|
||||
total_decisions: int
|
||||
struggle_resolution_rate: float
|
||||
learning_frequency: float
|
||||
sentiment_trend: str
|
||||
|
||||
|
||||
class SearchEntriesRequest(BaseModel):
|
||||
"""Request to search journal entries."""
|
||||
|
||||
query: str = Field(..., min_length=1)
|
||||
top_k: int = Field(5, ge=1, le=20)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# JOURNAL ENDPOINTS
|
||||
# =============================================================================
|
||||
|
||||
@@ -9,13 +9,18 @@ from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from roboco.api.deps import CurrentAgentId, DbSession
|
||||
from roboco.api.schemas.messages import (
|
||||
ListMessagesParams,
|
||||
MessageCreateRequest,
|
||||
MessageEditRequest,
|
||||
MessageListResponse,
|
||||
MessageResponse,
|
||||
)
|
||||
from roboco.db.tables import MessageTable, SessionTable
|
||||
from roboco.models import MessageType
|
||||
from roboco.services.messaging import (
|
||||
MessageCreateRequest as ServiceMessageRequest,
|
||||
)
|
||||
@@ -27,75 +32,6 @@ from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Query Parameter Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ListMessagesParams(BaseModel):
|
||||
"""Query parameters for listing messages."""
|
||||
|
||||
session_id: UUID
|
||||
before: datetime | None = None
|
||||
after: datetime | None = None
|
||||
type_filter: MessageType | None = None
|
||||
limit: int = Field(50, ge=1, le=100)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Response Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
"""Message response."""
|
||||
|
||||
id: UUID
|
||||
agent_id: UUID
|
||||
channel_id: UUID
|
||||
group_id: UUID
|
||||
session_id: UUID
|
||||
type: MessageType
|
||||
content: str
|
||||
content_length: int
|
||||
is_reply: bool
|
||||
reply_to: UUID | None
|
||||
mentions: list[UUID]
|
||||
task_id: UUID | None
|
||||
commit_ref: str | None
|
||||
timestamp: datetime
|
||||
edited_at: datetime | None
|
||||
was_edited: bool
|
||||
|
||||
|
||||
class MessageListResponse(BaseModel):
|
||||
"""List of messages."""
|
||||
|
||||
items: list[MessageResponse]
|
||||
total: int
|
||||
has_more: bool
|
||||
|
||||
|
||||
class MessageCreateRequest(BaseModel):
|
||||
"""Request to create a message."""
|
||||
|
||||
session_id: UUID
|
||||
type: MessageType
|
||||
content: str = Field(..., min_length=1, max_length=10000)
|
||||
is_reply: bool = False
|
||||
reply_to: UUID | None = None
|
||||
mentions: list[UUID] = Field(default_factory=list)
|
||||
task_id: UUID | None = None
|
||||
commit_ref: str | None = None
|
||||
|
||||
|
||||
class MessageEditRequest(BaseModel):
|
||||
"""Request to edit a message."""
|
||||
|
||||
content: str = Field(..., min_length=1, max_length=10000)
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Routes
|
||||
# =============================================================================
|
||||
|
||||
@@ -10,81 +10,25 @@ from typing import Annotated, Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
|
||||
from roboco.api.deps import CurrentAgentId, DbSession
|
||||
from roboco.api.schemas.notifications import (
|
||||
ListNotificationsParams,
|
||||
NotificationCreateRequest,
|
||||
NotificationListResponse,
|
||||
NotificationResponse,
|
||||
)
|
||||
from roboco.db.tables import AgentTable, NotificationTable
|
||||
from roboco.enforcement import (
|
||||
NotificationPermissionError,
|
||||
validate_notification_permission,
|
||||
)
|
||||
from roboco.models import NotificationPriority, NotificationType
|
||||
from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid_list
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Query Parameter Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ListNotificationsParams(BaseModel):
|
||||
"""Query parameters for listing notifications."""
|
||||
|
||||
unread_only: bool = False
|
||||
pending_ack_only: bool = False
|
||||
type_filter: NotificationType | None = None
|
||||
limit: int = Field(50, ge=1, le=100)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Response Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class NotificationResponse(BaseModel):
|
||||
"""Notification response."""
|
||||
|
||||
id: UUID
|
||||
type: NotificationType
|
||||
priority: NotificationPriority
|
||||
from_agent: UUID
|
||||
to_agents: list[UUID]
|
||||
subject: str
|
||||
body: str
|
||||
requires_ack: bool
|
||||
is_acknowledged: bool
|
||||
is_fully_acknowledged: bool
|
||||
is_read: bool
|
||||
related_task_id: UUID | None
|
||||
timestamp: datetime
|
||||
expires_at: datetime | None
|
||||
|
||||
|
||||
class NotificationListResponse(BaseModel):
|
||||
"""List of notifications."""
|
||||
|
||||
items: list[NotificationResponse]
|
||||
total: int
|
||||
unread_count: int
|
||||
pending_ack_count: int
|
||||
|
||||
|
||||
class NotificationCreateRequest(BaseModel):
|
||||
"""Request to create a notification."""
|
||||
|
||||
type: NotificationType
|
||||
priority: NotificationPriority = NotificationPriority.NORMAL
|
||||
to_agents: list[UUID] = Field(..., min_length=1)
|
||||
subject: str = Field(..., min_length=1, max_length=200)
|
||||
body: str
|
||||
requires_ack: bool = True
|
||||
related_task_id: UUID | None = None
|
||||
expires_at: datetime | None = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Routes
|
||||
# =============================================================================
|
||||
|
||||
+45
-151
@@ -6,12 +6,29 @@ Knowledge base, RAG queries, and semantic search endpoints.
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.api.deps import CurrentAgentContext
|
||||
from roboco.api.schemas.optimal import (
|
||||
ClearIndexResponse,
|
||||
IndexCodeRequest,
|
||||
IndexDocsRequest,
|
||||
IndexResponse,
|
||||
IndexStatsResponse,
|
||||
PromptTemplateRequest,
|
||||
PromptTemplateResponse,
|
||||
RAGQueryRequest,
|
||||
RAGQueryResponse,
|
||||
RefreshIndexResponse,
|
||||
RefreshRequest,
|
||||
SearchRequest,
|
||||
SearchResponse,
|
||||
SearchResultResponse,
|
||||
TokenEstimateRequest,
|
||||
TokenEstimateResponse,
|
||||
)
|
||||
from roboco.models import AgentRole
|
||||
from roboco.services.optimal import (
|
||||
IndexType,
|
||||
@@ -22,150 +39,6 @@ from roboco.services.optimal import (
|
||||
router = APIRouter(prefix="/optimal", tags=["optimal"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# REQUEST/RESPONSE SCHEMAS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class IndexCodeRequest(BaseModel):
|
||||
"""Request to index code files."""
|
||||
|
||||
sources: list[str] = Field(
|
||||
..., min_length=1, description="File paths, directories, or globs"
|
||||
)
|
||||
project: str | None = Field(None, description="Project identifier for filtering")
|
||||
|
||||
|
||||
class IndexDocsRequest(BaseModel):
|
||||
"""Request to index documentation."""
|
||||
|
||||
sources: list[str] = Field(
|
||||
..., min_length=1, description="File paths, URLs, or globs"
|
||||
)
|
||||
project: str | None = Field(None, description="Project identifier for filtering")
|
||||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
"""Request for semantic search."""
|
||||
|
||||
query: str = Field(..., min_length=1, description="Natural language query")
|
||||
project: str | None = Field(None, description="Filter by project")
|
||||
task_id: UUID | None = Field(None, description="Filter by task")
|
||||
index_types: list[str] | None = Field(None, description="Index types to search")
|
||||
top_k: int = Field(5, ge=1, le=20, description="Number of results")
|
||||
|
||||
|
||||
class RAGQueryRequest(BaseModel):
|
||||
"""Request for RAG query."""
|
||||
|
||||
query: str = Field(..., min_length=1, description="Natural language question")
|
||||
project: str | None = Field(None, description="Filter by project")
|
||||
task_id: UUID | None = Field(None, description="Filter by task")
|
||||
index_types: list[str] | None = Field(None, description="Index types to query")
|
||||
top_k: int = Field(5, ge=1, le=20, description="Context chunks to use")
|
||||
|
||||
|
||||
class SearchResultResponse(BaseModel):
|
||||
"""A single search result."""
|
||||
|
||||
content: str
|
||||
source: str
|
||||
score: float
|
||||
index_type: str
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class SearchResponse(BaseModel):
|
||||
"""Response from semantic search."""
|
||||
|
||||
results: list[SearchResultResponse]
|
||||
query: str
|
||||
total: int
|
||||
|
||||
|
||||
class RAGQueryResponse(BaseModel):
|
||||
"""Response from RAG query."""
|
||||
|
||||
answer: str
|
||||
citations: list[SearchResultResponse]
|
||||
query: str
|
||||
context_used: int
|
||||
|
||||
|
||||
class IndexStatsResponse(BaseModel):
|
||||
"""Statistics for all indexes."""
|
||||
|
||||
initialized: bool
|
||||
indexes: dict[str, dict[str, Any]]
|
||||
|
||||
|
||||
class RefreshRequest(BaseModel):
|
||||
"""Request to refresh an index."""
|
||||
|
||||
index_type: str = Field(..., description="Index type to refresh")
|
||||
sources: list[str] = Field(..., min_length=1, description="Sources to refresh")
|
||||
|
||||
|
||||
class IndexResponse(BaseModel):
|
||||
"""Response from indexing operations."""
|
||||
|
||||
indexed: int
|
||||
sources: list[str]
|
||||
project: str | None
|
||||
|
||||
|
||||
class ClearIndexResponse(BaseModel):
|
||||
"""Response from clearing an index."""
|
||||
|
||||
status: str
|
||||
index_type: str
|
||||
|
||||
|
||||
class RefreshIndexResponse(BaseModel):
|
||||
"""Response from refreshing an index."""
|
||||
|
||||
status: str
|
||||
index_type: str
|
||||
sources: list[str]
|
||||
|
||||
|
||||
class PromptTemplateRequest(BaseModel):
|
||||
"""Request to create/manage a prompt template."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="Template name")
|
||||
template: str = Field(..., min_length=1, description="Prompt template")
|
||||
description: str | None = Field(None, description="Template description")
|
||||
variables: list[str] = Field(default_factory=list, description="Variables")
|
||||
category: str | None = Field(None, description="Template category")
|
||||
|
||||
|
||||
class PromptTemplateResponse(BaseModel):
|
||||
"""Response for prompt template."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
template: str
|
||||
description: str | None
|
||||
variables: list[str]
|
||||
category: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class TokenEstimateRequest(BaseModel):
|
||||
"""Request to estimate token count."""
|
||||
|
||||
content: str = Field(..., min_length=1, description="Content to estimate")
|
||||
model: str = Field("claude-sonnet-4-20250514", description="Model")
|
||||
|
||||
|
||||
class TokenEstimateResponse(BaseModel):
|
||||
"""Response with token count estimate."""
|
||||
|
||||
token_count: int
|
||||
model: str
|
||||
content_length: int
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# INDEXING ENDPOINTS
|
||||
# =============================================================================
|
||||
@@ -546,11 +419,31 @@ async def refresh_index(
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PROMPT TEMPLATE ENDPOINTS
|
||||
# PROMPT TEMPLATE STORAGE
|
||||
# =============================================================================
|
||||
|
||||
# In-memory prompt template storage (would be database in production)
|
||||
_prompt_templates: dict[str, dict[str, Any]] = {}
|
||||
|
||||
class _PromptTemplateStorageHolder:
|
||||
"""Holder for prompt template storage (would be database in production)."""
|
||||
|
||||
templates: dict[str, dict[str, Any]] | None = None
|
||||
|
||||
|
||||
def _get_prompt_templates() -> dict[str, dict[str, Any]]:
|
||||
"""Get the prompt templates storage."""
|
||||
if _PromptTemplateStorageHolder.templates is None:
|
||||
_PromptTemplateStorageHolder.templates = {}
|
||||
return _PromptTemplateStorageHolder.templates
|
||||
|
||||
|
||||
def reset_prompt_templates() -> None:
|
||||
"""Reset prompt templates (for testing)."""
|
||||
_PromptTemplateStorageHolder.templates = {}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PROMPT TEMPLATE ENDPOINTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -571,7 +464,8 @@ async def create_prompt_template(
|
||||
template_id = str(uuid4())
|
||||
created_at = datetime.now(UTC).isoformat()
|
||||
|
||||
_prompt_templates[template_id] = {
|
||||
templates = _get_prompt_templates()
|
||||
templates[template_id] = {
|
||||
"id": template_id,
|
||||
"name": request.name,
|
||||
"template": request.template,
|
||||
@@ -601,7 +495,7 @@ async def list_prompt_templates(
|
||||
"""List all prompt templates, optionally filtered by category."""
|
||||
# Any authenticated agent can list templates
|
||||
_ = agent # Used for authentication
|
||||
templates = list(_prompt_templates.values())
|
||||
templates = list(_get_prompt_templates().values())
|
||||
|
||||
if category:
|
||||
templates = [t for t in templates if t.get("category") == category]
|
||||
|
||||
@@ -5,11 +5,16 @@ API endpoints for managing the Agent Orchestrator.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.api.schemas.orchestrator import (
|
||||
AgentStatusResponse,
|
||||
OrchestratorStatusResponse,
|
||||
ResolveWaitRequest,
|
||||
SpawnAgentRequest,
|
||||
WaitingAgentResponse,
|
||||
)
|
||||
from roboco.runtime import AgentOrchestrator
|
||||
|
||||
router = APIRouter()
|
||||
@@ -36,56 +41,6 @@ def get_orchestrator() -> AgentOrchestrator:
|
||||
return _OrchestratorHolder.instance
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Response Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AgentStatusResponse(BaseModel):
|
||||
"""Status of a single agent."""
|
||||
|
||||
agent_id: str
|
||||
state: str
|
||||
task_id: str | None
|
||||
error_count: int
|
||||
started_at: datetime | None
|
||||
waiting_for: str | None
|
||||
|
||||
|
||||
class OrchestratorStatusResponse(BaseModel):
|
||||
"""Overall orchestrator status."""
|
||||
|
||||
total_agents: int
|
||||
by_state: dict[str, int]
|
||||
waiting_count: int
|
||||
agents: list[AgentStatusResponse]
|
||||
|
||||
|
||||
class WaitingAgentResponse(BaseModel):
|
||||
"""Agent in WAITING_LONG state."""
|
||||
|
||||
agent_id: str
|
||||
task_id: str | None
|
||||
waiting_for: str
|
||||
waiting_since: datetime
|
||||
context: dict[str, Any]
|
||||
|
||||
|
||||
class SpawnAgentRequest(BaseModel):
|
||||
"""Request to spawn an agent."""
|
||||
|
||||
agent_id: str
|
||||
initial_prompt: str | None = None
|
||||
task_id: str | None = None
|
||||
model: str | None = None
|
||||
|
||||
|
||||
class ResolveWaitRequest(BaseModel):
|
||||
"""Request to resolve a wait condition."""
|
||||
|
||||
resolution: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Routes
|
||||
# =============================================================================
|
||||
|
||||
@@ -10,11 +10,16 @@ from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from roboco.api.deps import CurrentAgentId, DbSession
|
||||
from roboco.api.schemas.sessions import (
|
||||
ListSessionsParams,
|
||||
SessionCreateRequest,
|
||||
SessionListResponse,
|
||||
SessionResponse,
|
||||
)
|
||||
from roboco.db.tables import GroupTable, SessionTable
|
||||
from roboco.models import SessionStatus
|
||||
from roboco.utils.converters import require_uuid
|
||||
@@ -22,54 +27,6 @@ from roboco.utils.converters import require_uuid
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Query Parameter Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ListSessionsParams(BaseModel):
|
||||
"""Query parameters for listing sessions."""
|
||||
|
||||
group_id: UUID
|
||||
status_filter: SessionStatus | None = None
|
||||
limit: int = Field(20, ge=1, le=100)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Response Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class SessionResponse(BaseModel):
|
||||
"""Session response."""
|
||||
|
||||
id: UUID
|
||||
group_id: UUID
|
||||
status: SessionStatus
|
||||
message_count: int
|
||||
total_content_length: int
|
||||
started_at: datetime
|
||||
last_activity_at: datetime
|
||||
closed_at: datetime | None
|
||||
|
||||
|
||||
class SessionListResponse(BaseModel):
|
||||
"""List of sessions."""
|
||||
|
||||
items: list[SessionResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class SessionCreateRequest(BaseModel):
|
||||
"""Request to create a session."""
|
||||
|
||||
group_id: UUID
|
||||
max_time_window_minutes: int | None = 30
|
||||
max_message_count: int | None = 100
|
||||
max_content_length: int | None = 50000
|
||||
timeout_seconds: int = 300
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Routes
|
||||
# =============================================================================
|
||||
|
||||
@@ -6,74 +6,24 @@ transcription and extraction pipelines.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.api.deps import CurrentAgentContext, CurrentAgentId, PermissionServiceDep
|
||||
from roboco.api.schemas.stream import (
|
||||
ExtractedMessageResponse,
|
||||
ExtractionResponse,
|
||||
ExtractRequest,
|
||||
StreamChunkRequest,
|
||||
StreamCompleteRequest,
|
||||
TranscriptionStatsResponse,
|
||||
)
|
||||
from roboco.models import MessageType
|
||||
from roboco.models.message import RawStream
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# REQUEST/RESPONSE SCHEMAS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class StreamChunkRequest(BaseModel):
|
||||
"""Request to process a stream chunk."""
|
||||
|
||||
channel_id: UUID = Field(..., description="Target channel")
|
||||
session_id: UUID = Field(..., description="Current session")
|
||||
chunk: str = Field(..., description="Raw LLM output chunk")
|
||||
|
||||
|
||||
class StreamCompleteRequest(BaseModel):
|
||||
"""Request to mark a stream as complete."""
|
||||
|
||||
session_id: UUID = Field(..., description="Session to complete")
|
||||
|
||||
|
||||
class ExtractRequest(BaseModel):
|
||||
"""Request to extract messages from content."""
|
||||
|
||||
channel_id: UUID = Field(..., description="Target channel")
|
||||
session_id: UUID = Field(..., description="Current session")
|
||||
group_id: UUID = Field(..., description="Group within channel")
|
||||
content: str = Field(..., description="Content to extract from")
|
||||
task_id: UUID | None = Field(default=None, description="Related task")
|
||||
|
||||
|
||||
class ExtractedMessageResponse(BaseModel):
|
||||
"""Response for an extracted message."""
|
||||
|
||||
id: UUID
|
||||
type: str
|
||||
content: str
|
||||
content_length: int
|
||||
confidence: float
|
||||
|
||||
|
||||
class ExtractionResponse(BaseModel):
|
||||
"""Response from extraction."""
|
||||
|
||||
message_count: int
|
||||
messages: list[ExtractedMessageResponse]
|
||||
types_extracted: list[str]
|
||||
|
||||
|
||||
class TranscriptionStatsResponse(BaseModel):
|
||||
"""Response for transcription service stats."""
|
||||
|
||||
active_agents: int
|
||||
total_buffers: int
|
||||
total_buffered_chars: int
|
||||
running: bool
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# STREAM PROCESSING ROUTES
|
||||
# =============================================================================
|
||||
|
||||
+12
-108
@@ -4,20 +4,29 @@ Task API Routes
|
||||
Full CRUD operations and lifecycle management for tasks.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.api.deps import (
|
||||
CurrentAgentContext,
|
||||
DbSession,
|
||||
PermissionServiceDep,
|
||||
)
|
||||
from roboco.api.schemas.tasks import (
|
||||
CheckpointRequest,
|
||||
CommitRequest,
|
||||
ListTasksQuery,
|
||||
ProgressRequest,
|
||||
QANotes,
|
||||
TaskCountResponse,
|
||||
TaskResponse,
|
||||
TaskUpdate,
|
||||
TeamTasksQuery,
|
||||
)
|
||||
from roboco.db.tables import TaskTable
|
||||
from roboco.models.base import Complexity, TaskStatus, Team
|
||||
from roboco.models.base import TaskStatus, Team
|
||||
from roboco.models.task import TaskCreate
|
||||
from roboco.services.audit import get_audit_service
|
||||
from roboco.services.permissions import TaskAction
|
||||
@@ -27,56 +36,6 @@ from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid
|
||||
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# REQUEST/RESPONSE MODELS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
"""Request to update a task."""
|
||||
|
||||
title: str | None = None
|
||||
description: str | None = None
|
||||
acceptance_criteria: list[str] | None = None
|
||||
priority: int | None = Field(default=None, ge=0, le=3)
|
||||
target_date: datetime | None = None
|
||||
estimated_complexity: Complexity | None = None
|
||||
dev_notes: str | None = None
|
||||
quick_context: str | None = None
|
||||
|
||||
|
||||
class TaskResponse(BaseModel):
|
||||
"""Task response model."""
|
||||
|
||||
id: UUID
|
||||
title: str
|
||||
description: str
|
||||
acceptance_criteria: list[str]
|
||||
status: TaskStatus
|
||||
priority: int
|
||||
team: Team
|
||||
created_by: UUID
|
||||
assigned_to: UUID | None
|
||||
parent_task_id: UUID | None
|
||||
dependency_ids: list[UUID]
|
||||
blocker_ids: list[UUID]
|
||||
created_at: datetime
|
||||
updated_at: datetime | None
|
||||
claimed_at: datetime | None
|
||||
started_at: datetime | None
|
||||
completed_at: datetime | None
|
||||
target_date: datetime | None
|
||||
estimated_complexity: Complexity
|
||||
self_verified: bool
|
||||
qa_verified: bool | None
|
||||
dev_notes: str | None
|
||||
qa_notes: str | None
|
||||
quick_context: str | None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
def _to_response(task: TaskTable) -> TaskResponse:
|
||||
"""Convert TaskTable to TaskResponse with proper UUID conversion."""
|
||||
return TaskResponse(
|
||||
@@ -112,61 +71,6 @@ def _to_response_list(tasks: list[TaskTable]) -> list[TaskResponse]:
|
||||
return [_to_response(t) for t in tasks]
|
||||
|
||||
|
||||
class ProgressRequest(BaseModel):
|
||||
"""Request to add progress update."""
|
||||
|
||||
message: str
|
||||
percentage: int | None = Field(default=None, ge=0, le=100)
|
||||
|
||||
|
||||
class CheckpointRequest(BaseModel):
|
||||
"""Request to add checkpoint."""
|
||||
|
||||
state_summary: str
|
||||
remaining_work: list[str]
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class CommitRequest(BaseModel):
|
||||
"""Request to link a commit."""
|
||||
|
||||
hash: str = Field(..., min_length=7, max_length=40)
|
||||
message: str
|
||||
|
||||
|
||||
class QANotes(BaseModel):
|
||||
"""QA review notes."""
|
||||
|
||||
notes: str
|
||||
|
||||
|
||||
class TaskCountResponse(BaseModel):
|
||||
"""Task count by category."""
|
||||
|
||||
counts: dict[str, int]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# QUERY PARAMETER MODELS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ListTasksQuery(BaseModel):
|
||||
"""Query params for listing tasks."""
|
||||
|
||||
team: Team | None = None
|
||||
status: TaskStatus | None = None
|
||||
limit: int = Field(100, ge=1, le=500)
|
||||
offset: int = Field(0, ge=0)
|
||||
|
||||
|
||||
class TeamTasksQuery(BaseModel):
|
||||
"""Query params for team tasks."""
|
||||
|
||||
task_status: TaskStatus | None = None
|
||||
limit: int = Field(100, ge=1, le=500)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CRUD ENDPOINTS
|
||||
# =============================================================================
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
API Schemas
|
||||
|
||||
Pydantic models for request/response serialization.
|
||||
"""
|
||||
|
||||
from roboco.api.schemas.channels import (
|
||||
ChannelDetailResponse,
|
||||
ChannelListResponse,
|
||||
ChannelResponse,
|
||||
GroupResponse,
|
||||
ListChannelsQuery,
|
||||
)
|
||||
from roboco.api.schemas.dashboard import (
|
||||
AuditorDashboard,
|
||||
AuditorFlag,
|
||||
AuditorReport,
|
||||
CEOOverview,
|
||||
ChannelFeed,
|
||||
CreateFlagRequest,
|
||||
CreateReportRequest,
|
||||
FlagSeverity,
|
||||
TeamHealth,
|
||||
)
|
||||
from roboco.api.schemas.health import HealthResponse, ReadinessResponse
|
||||
from roboco.api.schemas.journals import (
|
||||
CreateEntryRequest,
|
||||
DecisionLogRequest,
|
||||
GeneralEntryRequest,
|
||||
GrowthMetricsResponse,
|
||||
JournalEntryResponse,
|
||||
JournalResponse,
|
||||
JournalStatsResponse,
|
||||
LearningRequest,
|
||||
ListEntriesParams,
|
||||
SearchEntriesRequest,
|
||||
StruggleRequest,
|
||||
TaskReflectionRequest,
|
||||
)
|
||||
from roboco.api.schemas.messages import (
|
||||
ListMessagesParams,
|
||||
MessageCreateRequest,
|
||||
MessageEditRequest,
|
||||
MessageListResponse,
|
||||
MessageResponse,
|
||||
)
|
||||
from roboco.api.schemas.notifications import (
|
||||
ListNotificationsParams,
|
||||
NotificationCreateRequest,
|
||||
NotificationListResponse,
|
||||
NotificationResponse,
|
||||
)
|
||||
from roboco.api.schemas.optimal import (
|
||||
ClearIndexResponse,
|
||||
IndexCodeRequest,
|
||||
IndexDocsRequest,
|
||||
IndexResponse,
|
||||
IndexStatsResponse,
|
||||
PromptTemplateRequest,
|
||||
PromptTemplateResponse,
|
||||
RAGQueryRequest,
|
||||
RAGQueryResponse,
|
||||
RefreshIndexResponse,
|
||||
RefreshRequest,
|
||||
SearchRequest,
|
||||
SearchResponse,
|
||||
SearchResultResponse,
|
||||
TokenEstimateRequest,
|
||||
TokenEstimateResponse,
|
||||
)
|
||||
from roboco.api.schemas.orchestrator import (
|
||||
AgentStatusResponse,
|
||||
OrchestratorStatusResponse,
|
||||
ResolveWaitRequest,
|
||||
SpawnAgentRequest,
|
||||
WaitingAgentResponse,
|
||||
)
|
||||
from roboco.api.schemas.sessions import (
|
||||
ListSessionsParams,
|
||||
SessionCreateRequest,
|
||||
SessionListResponse,
|
||||
SessionResponse,
|
||||
)
|
||||
from roboco.api.schemas.stream import (
|
||||
ExtractedMessageResponse,
|
||||
ExtractionResponse,
|
||||
ExtractRequest,
|
||||
StreamChunkRequest,
|
||||
StreamCompleteRequest,
|
||||
TranscriptionStatsResponse,
|
||||
)
|
||||
from roboco.api.schemas.tasks import (
|
||||
CheckpointRequest,
|
||||
CommitRequest,
|
||||
ListTasksQuery,
|
||||
ProgressRequest,
|
||||
QANotes,
|
||||
TaskCountResponse,
|
||||
TaskResponse,
|
||||
TaskUpdate,
|
||||
TeamTasksQuery,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Orchestrator
|
||||
"AgentStatusResponse",
|
||||
# Dashboard
|
||||
"AuditorDashboard",
|
||||
"AuditorFlag",
|
||||
"AuditorReport",
|
||||
"CEOOverview",
|
||||
"ChannelDetailResponse",
|
||||
"ChannelFeed",
|
||||
# Channels
|
||||
"ChannelListResponse",
|
||||
"ChannelResponse",
|
||||
# Tasks
|
||||
"CheckpointRequest",
|
||||
# Optimal
|
||||
"ClearIndexResponse",
|
||||
"CommitRequest",
|
||||
# Journals
|
||||
"CreateEntryRequest",
|
||||
"CreateFlagRequest",
|
||||
"CreateReportRequest",
|
||||
"DecisionLogRequest",
|
||||
"ExtractRequest",
|
||||
# Stream
|
||||
"ExtractedMessageResponse",
|
||||
"ExtractionResponse",
|
||||
"FlagSeverity",
|
||||
"GeneralEntryRequest",
|
||||
"GroupResponse",
|
||||
"GrowthMetricsResponse",
|
||||
# Health
|
||||
"HealthResponse",
|
||||
"IndexCodeRequest",
|
||||
"IndexDocsRequest",
|
||||
"IndexResponse",
|
||||
"IndexStatsResponse",
|
||||
"JournalEntryResponse",
|
||||
"JournalResponse",
|
||||
"JournalStatsResponse",
|
||||
"LearningRequest",
|
||||
"ListChannelsQuery",
|
||||
"ListEntriesParams",
|
||||
# Messages
|
||||
"ListMessagesParams",
|
||||
# Notifications
|
||||
"ListNotificationsParams",
|
||||
# Sessions
|
||||
"ListSessionsParams",
|
||||
"ListTasksQuery",
|
||||
"MessageCreateRequest",
|
||||
"MessageEditRequest",
|
||||
"MessageListResponse",
|
||||
"MessageResponse",
|
||||
"NotificationCreateRequest",
|
||||
"NotificationListResponse",
|
||||
"NotificationResponse",
|
||||
"OrchestratorStatusResponse",
|
||||
"ProgressRequest",
|
||||
"PromptTemplateRequest",
|
||||
"PromptTemplateResponse",
|
||||
"QANotes",
|
||||
"RAGQueryRequest",
|
||||
"RAGQueryResponse",
|
||||
"ReadinessResponse",
|
||||
"RefreshIndexResponse",
|
||||
"RefreshRequest",
|
||||
"ResolveWaitRequest",
|
||||
"SearchEntriesRequest",
|
||||
"SearchRequest",
|
||||
"SearchResponse",
|
||||
"SearchResultResponse",
|
||||
"SessionCreateRequest",
|
||||
"SessionListResponse",
|
||||
"SessionResponse",
|
||||
"SpawnAgentRequest",
|
||||
"StreamChunkRequest",
|
||||
"StreamCompleteRequest",
|
||||
"StruggleRequest",
|
||||
"TaskCountResponse",
|
||||
"TaskReflectionRequest",
|
||||
"TaskResponse",
|
||||
"TaskUpdate",
|
||||
"TeamHealth",
|
||||
"TeamTasksQuery",
|
||||
"TokenEstimateRequest",
|
||||
"TokenEstimateResponse",
|
||||
"TranscriptionStatsResponse",
|
||||
"WaitingAgentResponse",
|
||||
]
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Channels API Schemas
|
||||
|
||||
Request/response models for channel endpoints.
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.models import ChannelType
|
||||
|
||||
|
||||
class ChannelResponse(BaseModel):
|
||||
"""Channel response with computed fields."""
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
slug: str
|
||||
type: ChannelType
|
||||
description: str | None
|
||||
topic: str | None
|
||||
member_count: int
|
||||
message_count: int
|
||||
group_count: int
|
||||
is_archived: bool
|
||||
is_private: bool
|
||||
can_write: bool # Whether current agent can write
|
||||
|
||||
|
||||
class ChannelListResponse(BaseModel):
|
||||
"""Paginated list of channels."""
|
||||
|
||||
items: list[ChannelResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
class ChannelDetailResponse(ChannelResponse):
|
||||
"""Detailed channel response with groups."""
|
||||
|
||||
groups: list[dict]
|
||||
|
||||
|
||||
class GroupResponse(BaseModel):
|
||||
"""Group within a channel."""
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
hierarchy_level: int
|
||||
is_active: bool
|
||||
total_messages: int
|
||||
active_session_id: UUID | None = None
|
||||
|
||||
|
||||
class ListChannelsQuery(BaseModel):
|
||||
"""Query params for listing channels."""
|
||||
|
||||
page: int = Field(1, ge=1)
|
||||
page_size: int = Field(20, ge=1, le=100)
|
||||
include_archived: bool = False
|
||||
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Dashboard API Schemas
|
||||
|
||||
Request/response models for auditor and CEO dashboards.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FlagSeverity(str, Enum):
|
||||
"""Severity levels for auditor flags."""
|
||||
|
||||
INFO = "info"
|
||||
WARNING = "warning"
|
||||
URGENT = "urgent"
|
||||
|
||||
|
||||
class AuditorFlag(BaseModel):
|
||||
"""A flag raised by the auditor."""
|
||||
|
||||
id: UUID
|
||||
severity: FlagSeverity
|
||||
category: str # quality, process, communication, blocked, documentation
|
||||
title: str
|
||||
description: str
|
||||
related_task_id: UUID | None = None
|
||||
related_agent_id: UUID | None = None
|
||||
created_at: datetime
|
||||
resolved_at: datetime | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class AuditorReport(BaseModel):
|
||||
"""An auditor report for the CEO."""
|
||||
|
||||
id: UUID
|
||||
report_type: str # daily, weekly, alert
|
||||
title: str
|
||||
summary: str
|
||||
sections: list[dict[str, Any]]
|
||||
created_at: datetime
|
||||
sent_at: datetime | None = None
|
||||
|
||||
|
||||
class ChannelFeed(BaseModel):
|
||||
"""Live feed status for a channel."""
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
status: str # streaming, idle, offline
|
||||
last_activity: datetime | None
|
||||
message_count_24h: int
|
||||
|
||||
|
||||
class AuditorDashboard(BaseModel):
|
||||
"""Complete auditor dashboard data."""
|
||||
|
||||
live_feeds: list[ChannelFeed]
|
||||
flagged_items: list[AuditorFlag]
|
||||
metrics: dict[str, Any]
|
||||
audit_queue: list[dict[str, Any]]
|
||||
recent_reports: list[AuditorReport]
|
||||
|
||||
|
||||
class TeamHealth(BaseModel):
|
||||
"""Health status for a team."""
|
||||
|
||||
team: str
|
||||
status: str # ok, slow, critical
|
||||
active_tasks: int
|
||||
blocked_tasks: int
|
||||
blocked_ratio: float
|
||||
completed_this_week: int
|
||||
|
||||
|
||||
class CEOOverview(BaseModel):
|
||||
"""Complete CEO overview data."""
|
||||
|
||||
health_status: list[TeamHealth]
|
||||
key_metrics: dict[str, Any]
|
||||
auditor_alerts: dict[str, Any]
|
||||
roadmap_progress: dict[str, Any]
|
||||
|
||||
|
||||
class CreateFlagRequest(BaseModel):
|
||||
"""Request to create an auditor flag."""
|
||||
|
||||
severity: FlagSeverity
|
||||
category: str
|
||||
title: str
|
||||
description: str
|
||||
related_task_id: UUID | None = None
|
||||
related_agent_id: UUID | None = None
|
||||
|
||||
|
||||
class CreateReportRequest(BaseModel):
|
||||
"""Request to create an auditor report."""
|
||||
|
||||
report_type: str
|
||||
title: str
|
||||
summary: str
|
||||
sections: list[dict[str, Any]] = Field(default_factory=list)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Health Check API Schemas
|
||||
|
||||
Request/response models for health check endpoints.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health check response."""
|
||||
|
||||
status: str
|
||||
version: str
|
||||
environment: str
|
||||
|
||||
|
||||
class ReadinessResponse(BaseModel):
|
||||
"""Readiness check response."""
|
||||
|
||||
status: str
|
||||
database: str
|
||||
redis: str
|
||||
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Journals API Schemas
|
||||
|
||||
Request/response models for agent journal endpoints.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ListEntriesParams(BaseModel):
|
||||
"""Query parameters for listing journal entries."""
|
||||
|
||||
entry_type: str | None = Field(None, description="Filter by entry type")
|
||||
task_id: UUID | None = Field(None, description="Filter by task")
|
||||
limit: int = Field(50, ge=1, le=100, description="Maximum entries to return")
|
||||
offset: int = Field(0, ge=0, description="Number of entries to skip")
|
||||
|
||||
|
||||
class JournalResponse(BaseModel):
|
||||
"""Journal response."""
|
||||
|
||||
id: UUID
|
||||
agent_id: UUID
|
||||
total_entries: int
|
||||
last_entry_at: datetime | None
|
||||
latest_summary: str | None
|
||||
summary_updated_at: datetime | None
|
||||
entries_by_type: dict[str, int]
|
||||
created_at: datetime
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class JournalEntryResponse(BaseModel):
|
||||
"""Journal entry response."""
|
||||
|
||||
id: UUID
|
||||
journal_id: UUID
|
||||
type: str
|
||||
title: str
|
||||
content: str
|
||||
task_id: UUID | None
|
||||
session_id: UUID | None
|
||||
timestamp: datetime
|
||||
tags: list[str]
|
||||
sentiment: str | None
|
||||
is_private: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class CreateEntryRequest(BaseModel):
|
||||
"""Request to create a journal entry."""
|
||||
|
||||
type: str = Field(
|
||||
...,
|
||||
description="Entry type (task_reflection, decision_log, learning, etc.)",
|
||||
)
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
content: str = Field(..., min_length=1)
|
||||
task_id: UUID | None = None
|
||||
session_id: UUID | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
sentiment: str | None = None
|
||||
is_private: bool = False
|
||||
|
||||
|
||||
class TaskReflectionRequest(BaseModel):
|
||||
"""Request to create a task reflection entry."""
|
||||
|
||||
task_id: UUID
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
what_done: str
|
||||
what_learned: str
|
||||
what_struggled: str
|
||||
next_steps: list[str] = Field(default_factory=list)
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class DecisionLogRequest(BaseModel):
|
||||
"""Request to create a decision log entry."""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
context: str
|
||||
options: list[dict[str, str]] = Field(..., min_length=2)
|
||||
chosen: str
|
||||
rationale: str
|
||||
consequences: list[str] = Field(default_factory=list)
|
||||
task_id: UUID | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class LearningRequest(BaseModel):
|
||||
"""Request to create a learning entry."""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
what_learned: str
|
||||
how_applied: str | None = None
|
||||
source: str | None = None
|
||||
task_id: UUID | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class StruggleRequest(BaseModel):
|
||||
"""Request to create a struggle entry."""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
what_struggled: str
|
||||
attempted_solutions: list[str] = Field(default_factory=list)
|
||||
resolution: str | None = None
|
||||
help_needed: str | None = None
|
||||
task_id: UUID | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class GeneralEntryRequest(BaseModel):
|
||||
"""Request to create a general journal entry."""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
content: str
|
||||
task_id: UUID | None = None
|
||||
session_id: UUID | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
is_private: bool = False
|
||||
|
||||
|
||||
class JournalStatsResponse(BaseModel):
|
||||
"""Journal statistics response."""
|
||||
|
||||
total_entries: int
|
||||
entries_by_type: dict[str, int]
|
||||
last_entry_at: datetime | None
|
||||
has_summary: bool
|
||||
|
||||
|
||||
class GrowthMetricsResponse(BaseModel):
|
||||
"""Growth metrics response."""
|
||||
|
||||
total_reflections: int
|
||||
total_learnings: int
|
||||
total_struggles: int
|
||||
total_decisions: int
|
||||
struggle_resolution_rate: float
|
||||
learning_frequency: float
|
||||
sentiment_trend: str
|
||||
|
||||
|
||||
class SearchEntriesRequest(BaseModel):
|
||||
"""Request to search journal entries."""
|
||||
|
||||
query: str = Field(..., min_length=1)
|
||||
top_k: int = Field(5, ge=1, le=20)
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Messages API Schemas
|
||||
|
||||
Request/response models for message endpoints.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.models import MessageType
|
||||
|
||||
|
||||
class ListMessagesParams(BaseModel):
|
||||
"""Query parameters for listing messages."""
|
||||
|
||||
session_id: UUID
|
||||
before: datetime | None = None
|
||||
after: datetime | None = None
|
||||
type_filter: MessageType | None = None
|
||||
limit: int = Field(50, ge=1, le=100)
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
"""Message response."""
|
||||
|
||||
id: UUID
|
||||
agent_id: UUID
|
||||
channel_id: UUID
|
||||
group_id: UUID
|
||||
session_id: UUID
|
||||
type: MessageType
|
||||
content: str
|
||||
content_length: int
|
||||
is_reply: bool
|
||||
reply_to: UUID | None
|
||||
mentions: list[UUID]
|
||||
task_id: UUID | None
|
||||
commit_ref: str | None
|
||||
timestamp: datetime
|
||||
edited_at: datetime | None
|
||||
was_edited: bool
|
||||
|
||||
|
||||
class MessageListResponse(BaseModel):
|
||||
"""List of messages."""
|
||||
|
||||
items: list[MessageResponse]
|
||||
total: int
|
||||
has_more: bool
|
||||
|
||||
|
||||
class MessageCreateRequest(BaseModel):
|
||||
"""Request to create a message."""
|
||||
|
||||
session_id: UUID
|
||||
type: MessageType
|
||||
content: str = Field(..., min_length=1, max_length=10000)
|
||||
is_reply: bool = False
|
||||
reply_to: UUID | None = None
|
||||
mentions: list[UUID] = Field(default_factory=list)
|
||||
task_id: UUID | None = None
|
||||
commit_ref: str | None = None
|
||||
|
||||
|
||||
class MessageEditRequest(BaseModel):
|
||||
"""Request to edit a message."""
|
||||
|
||||
content: str = Field(..., min_length=1, max_length=10000)
|
||||
reason: str | None = None
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Notifications API Schemas
|
||||
|
||||
Request/response models for the notification system.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.models import NotificationPriority, NotificationType
|
||||
|
||||
|
||||
class ListNotificationsParams(BaseModel):
|
||||
"""Query parameters for listing notifications."""
|
||||
|
||||
unread_only: bool = False
|
||||
pending_ack_only: bool = False
|
||||
type_filter: NotificationType | None = None
|
||||
limit: int = Field(50, ge=1, le=100)
|
||||
|
||||
|
||||
class NotificationResponse(BaseModel):
|
||||
"""Notification response."""
|
||||
|
||||
id: UUID
|
||||
type: NotificationType
|
||||
priority: NotificationPriority
|
||||
from_agent: UUID
|
||||
to_agents: list[UUID]
|
||||
subject: str
|
||||
body: str
|
||||
requires_ack: bool
|
||||
is_acknowledged: bool
|
||||
is_fully_acknowledged: bool
|
||||
is_read: bool
|
||||
related_task_id: UUID | None
|
||||
timestamp: datetime
|
||||
expires_at: datetime | None
|
||||
|
||||
|
||||
class NotificationListResponse(BaseModel):
|
||||
"""List of notifications."""
|
||||
|
||||
items: list[NotificationResponse]
|
||||
total: int
|
||||
unread_count: int
|
||||
pending_ack_count: int
|
||||
|
||||
|
||||
class NotificationCreateRequest(BaseModel):
|
||||
"""Request to create a notification."""
|
||||
|
||||
type: NotificationType
|
||||
priority: NotificationPriority = NotificationPriority.NORMAL
|
||||
to_agents: list[UUID] = Field(..., min_length=1)
|
||||
subject: str = Field(..., min_length=1, max_length=200)
|
||||
body: str
|
||||
requires_ack: bool = True
|
||||
related_task_id: UUID | None = None
|
||||
expires_at: datetime | None = None
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Optimal API Schemas
|
||||
|
||||
Request/response models for knowledge base and RAG endpoints.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class IndexCodeRequest(BaseModel):
|
||||
"""Request to index code files."""
|
||||
|
||||
sources: list[str] = Field(
|
||||
..., min_length=1, description="File paths, directories, or globs"
|
||||
)
|
||||
project: str | None = Field(None, description="Project identifier for filtering")
|
||||
|
||||
|
||||
class IndexDocsRequest(BaseModel):
|
||||
"""Request to index documentation."""
|
||||
|
||||
sources: list[str] = Field(
|
||||
..., min_length=1, description="File paths, URLs, or globs"
|
||||
)
|
||||
project: str | None = Field(None, description="Project identifier for filtering")
|
||||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
"""Request for semantic search."""
|
||||
|
||||
query: str = Field(..., min_length=1, description="Natural language query")
|
||||
project: str | None = Field(None, description="Filter by project")
|
||||
task_id: UUID | None = Field(None, description="Filter by task")
|
||||
index_types: list[str] | None = Field(None, description="Index types to search")
|
||||
top_k: int = Field(5, ge=1, le=20, description="Number of results")
|
||||
|
||||
|
||||
class RAGQueryRequest(BaseModel):
|
||||
"""Request for RAG query."""
|
||||
|
||||
query: str = Field(..., min_length=1, description="Natural language question")
|
||||
project: str | None = Field(None, description="Filter by project")
|
||||
task_id: UUID | None = Field(None, description="Filter by task")
|
||||
index_types: list[str] | None = Field(None, description="Index types to query")
|
||||
top_k: int = Field(5, ge=1, le=20, description="Context chunks to use")
|
||||
|
||||
|
||||
class SearchResultResponse(BaseModel):
|
||||
"""A single search result."""
|
||||
|
||||
content: str
|
||||
source: str
|
||||
score: float
|
||||
index_type: str
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class SearchResponse(BaseModel):
|
||||
"""Response from semantic search."""
|
||||
|
||||
results: list[SearchResultResponse]
|
||||
query: str
|
||||
total: int
|
||||
|
||||
|
||||
class RAGQueryResponse(BaseModel):
|
||||
"""Response from RAG query."""
|
||||
|
||||
answer: str
|
||||
citations: list[SearchResultResponse]
|
||||
query: str
|
||||
context_used: int
|
||||
|
||||
|
||||
class IndexStatsResponse(BaseModel):
|
||||
"""Statistics for all indexes."""
|
||||
|
||||
initialized: bool
|
||||
indexes: dict[str, dict[str, Any]]
|
||||
|
||||
|
||||
class RefreshRequest(BaseModel):
|
||||
"""Request to refresh an index."""
|
||||
|
||||
index_type: str = Field(..., description="Index type to refresh")
|
||||
sources: list[str] = Field(..., min_length=1, description="Sources to refresh")
|
||||
|
||||
|
||||
class IndexResponse(BaseModel):
|
||||
"""Response from indexing operations."""
|
||||
|
||||
indexed: int
|
||||
sources: list[str]
|
||||
project: str | None
|
||||
|
||||
|
||||
class ClearIndexResponse(BaseModel):
|
||||
"""Response from clearing an index."""
|
||||
|
||||
status: str
|
||||
index_type: str
|
||||
|
||||
|
||||
class RefreshIndexResponse(BaseModel):
|
||||
"""Response from refreshing an index."""
|
||||
|
||||
status: str
|
||||
index_type: str
|
||||
sources: list[str]
|
||||
|
||||
|
||||
class PromptTemplateRequest(BaseModel):
|
||||
"""Request to create/manage a prompt template."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="Template name")
|
||||
template: str = Field(..., min_length=1, description="Prompt template")
|
||||
description: str | None = Field(None, description="Template description")
|
||||
variables: list[str] = Field(default_factory=list, description="Variables")
|
||||
category: str | None = Field(None, description="Template category")
|
||||
|
||||
|
||||
class PromptTemplateResponse(BaseModel):
|
||||
"""Response for prompt template."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
template: str
|
||||
description: str | None
|
||||
variables: list[str]
|
||||
category: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class TokenEstimateRequest(BaseModel):
|
||||
"""Request to estimate token count."""
|
||||
|
||||
content: str = Field(..., min_length=1, description="Content to estimate")
|
||||
model: str = Field("claude-sonnet-4-20250514", description="Model")
|
||||
|
||||
|
||||
class TokenEstimateResponse(BaseModel):
|
||||
"""Response with token count estimate."""
|
||||
|
||||
token_count: int
|
||||
model: str
|
||||
content_length: int
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Orchestrator API Schemas
|
||||
|
||||
Request/response models for agent orchestrator endpoints.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AgentStatusResponse(BaseModel):
|
||||
"""Status of a single agent."""
|
||||
|
||||
agent_id: str
|
||||
state: str
|
||||
task_id: str | None
|
||||
error_count: int
|
||||
started_at: datetime | None
|
||||
waiting_for: str | None
|
||||
|
||||
|
||||
class OrchestratorStatusResponse(BaseModel):
|
||||
"""Overall orchestrator status."""
|
||||
|
||||
total_agents: int
|
||||
by_state: dict[str, int]
|
||||
waiting_count: int
|
||||
agents: list[AgentStatusResponse]
|
||||
|
||||
|
||||
class WaitingAgentResponse(BaseModel):
|
||||
"""Agent in WAITING_LONG state."""
|
||||
|
||||
agent_id: str
|
||||
task_id: str | None
|
||||
waiting_for: str
|
||||
waiting_since: datetime
|
||||
context: dict[str, Any]
|
||||
|
||||
|
||||
class SpawnAgentRequest(BaseModel):
|
||||
"""Request to spawn an agent."""
|
||||
|
||||
agent_id: str
|
||||
initial_prompt: str | None = None
|
||||
task_id: str | None = None
|
||||
model: str | None = None
|
||||
|
||||
|
||||
class ResolveWaitRequest(BaseModel):
|
||||
"""Request to resolve a wait condition."""
|
||||
|
||||
resolution: dict[str, Any] = Field(default_factory=dict)
|
||||
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Sessions API Schemas
|
||||
|
||||
Request/response models for session endpoints.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.models import SessionStatus
|
||||
|
||||
|
||||
class ListSessionsParams(BaseModel):
|
||||
"""Query parameters for listing sessions."""
|
||||
|
||||
group_id: UUID
|
||||
status_filter: SessionStatus | None = None
|
||||
limit: int = Field(20, ge=1, le=100)
|
||||
|
||||
|
||||
class SessionResponse(BaseModel):
|
||||
"""Session response."""
|
||||
|
||||
id: UUID
|
||||
group_id: UUID
|
||||
status: SessionStatus
|
||||
message_count: int
|
||||
total_content_length: int
|
||||
started_at: datetime
|
||||
last_activity_at: datetime
|
||||
closed_at: datetime | None
|
||||
|
||||
|
||||
class SessionListResponse(BaseModel):
|
||||
"""List of sessions."""
|
||||
|
||||
items: list[SessionResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class SessionCreateRequest(BaseModel):
|
||||
"""Request to create a session."""
|
||||
|
||||
group_id: UUID
|
||||
max_time_window_minutes: int | None = 30
|
||||
max_message_count: int | None = 100
|
||||
max_content_length: int | None = 50000
|
||||
timeout_seconds: int = 300
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Stream Processing API Schemas
|
||||
|
||||
Request/response models for stream processing endpoints.
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class StreamChunkRequest(BaseModel):
|
||||
"""Request to process a stream chunk."""
|
||||
|
||||
channel_id: UUID = Field(..., description="Target channel")
|
||||
session_id: UUID = Field(..., description="Current session")
|
||||
chunk: str = Field(..., description="Raw LLM output chunk")
|
||||
|
||||
|
||||
class StreamCompleteRequest(BaseModel):
|
||||
"""Request to mark a stream as complete."""
|
||||
|
||||
session_id: UUID = Field(..., description="Session to complete")
|
||||
|
||||
|
||||
class ExtractRequest(BaseModel):
|
||||
"""Request to extract messages from content."""
|
||||
|
||||
channel_id: UUID = Field(..., description="Target channel")
|
||||
session_id: UUID = Field(..., description="Current session")
|
||||
group_id: UUID = Field(..., description="Group within channel")
|
||||
content: str = Field(..., description="Content to extract from")
|
||||
task_id: UUID | None = Field(default=None, description="Related task")
|
||||
|
||||
|
||||
class ExtractedMessageResponse(BaseModel):
|
||||
"""Response for an extracted message."""
|
||||
|
||||
id: UUID
|
||||
type: str
|
||||
content: str
|
||||
content_length: int
|
||||
confidence: float
|
||||
|
||||
|
||||
class ExtractionResponse(BaseModel):
|
||||
"""Response from extraction."""
|
||||
|
||||
message_count: int
|
||||
messages: list[ExtractedMessageResponse]
|
||||
types_extracted: list[str]
|
||||
|
||||
|
||||
class TranscriptionStatsResponse(BaseModel):
|
||||
"""Response for transcription service stats."""
|
||||
|
||||
active_agents: int
|
||||
total_buffers: int
|
||||
total_buffered_chars: int
|
||||
running: bool
|
||||
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Tasks API Schemas
|
||||
|
||||
Request/response models for task endpoints.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.models.base import Complexity, TaskStatus, Team
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
"""Request to update a task."""
|
||||
|
||||
title: str | None = None
|
||||
description: str | None = None
|
||||
acceptance_criteria: list[str] | None = None
|
||||
priority: int | None = Field(default=None, ge=0, le=3)
|
||||
target_date: datetime | None = None
|
||||
estimated_complexity: Complexity | None = None
|
||||
dev_notes: str | None = None
|
||||
quick_context: str | None = None
|
||||
|
||||
|
||||
class TaskResponse(BaseModel):
|
||||
"""Task response model."""
|
||||
|
||||
id: UUID
|
||||
title: str
|
||||
description: str
|
||||
acceptance_criteria: list[str]
|
||||
status: TaskStatus
|
||||
priority: int
|
||||
team: Team
|
||||
created_by: UUID
|
||||
assigned_to: UUID | None
|
||||
parent_task_id: UUID | None
|
||||
dependency_ids: list[UUID]
|
||||
blocker_ids: list[UUID]
|
||||
created_at: datetime
|
||||
updated_at: datetime | None
|
||||
claimed_at: datetime | None
|
||||
started_at: datetime | None
|
||||
completed_at: datetime | None
|
||||
target_date: datetime | None
|
||||
estimated_complexity: Complexity
|
||||
self_verified: bool
|
||||
qa_verified: bool | None
|
||||
dev_notes: str | None
|
||||
qa_notes: str | None
|
||||
quick_context: str | None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ProgressRequest(BaseModel):
|
||||
"""Request to add progress update."""
|
||||
|
||||
message: str
|
||||
percentage: int | None = Field(default=None, ge=0, le=100)
|
||||
|
||||
|
||||
class CheckpointRequest(BaseModel):
|
||||
"""Request to add checkpoint."""
|
||||
|
||||
state_summary: str
|
||||
remaining_work: list[str]
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class CommitRequest(BaseModel):
|
||||
"""Request to link a commit."""
|
||||
|
||||
hash: str = Field(..., min_length=7, max_length=40)
|
||||
message: str
|
||||
|
||||
|
||||
class QANotes(BaseModel):
|
||||
"""QA review notes."""
|
||||
|
||||
notes: str
|
||||
|
||||
|
||||
class TaskCountResponse(BaseModel):
|
||||
"""Task count by category."""
|
||||
|
||||
counts: dict[str, int]
|
||||
|
||||
|
||||
class ListTasksQuery(BaseModel):
|
||||
"""Query params for listing tasks."""
|
||||
|
||||
team: Team | None = None
|
||||
status: TaskStatus | None = None
|
||||
limit: int = Field(100, ge=1, le=500)
|
||||
offset: int = Field(0, ge=0)
|
||||
|
||||
|
||||
class TeamTasksQuery(BaseModel):
|
||||
"""Query params for team tasks."""
|
||||
|
||||
task_status: TaskStatus | None = None
|
||||
limit: int = Field(100, ge=1, le=500)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
WebSocket API Schemas
|
||||
|
||||
Request/response models for WebSocket messages.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@dataclass
|
||||
class NewMessageBroadcast:
|
||||
"""Data for broadcasting a new message."""
|
||||
|
||||
channel_id: UUID
|
||||
session_id: UUID
|
||||
message_id: UUID
|
||||
agent_id: UUID
|
||||
content: str
|
||||
message_type: str
|
||||
|
||||
|
||||
class WSMessage(BaseModel):
|
||||
"""Base WebSocket message."""
|
||||
|
||||
type: str
|
||||
timestamp: datetime = datetime.now(UTC)
|
||||
|
||||
|
||||
class WSMessageNew(WSMessage):
|
||||
"""New message event."""
|
||||
|
||||
type: str = "message.new"
|
||||
message_id: UUID
|
||||
agent_id: UUID
|
||||
content: str
|
||||
message_type: str
|
||||
|
||||
|
||||
class WSMessageEdit(WSMessage):
|
||||
"""Message edited event."""
|
||||
|
||||
type: str = "message.edit"
|
||||
message_id: UUID
|
||||
content: str
|
||||
|
||||
|
||||
class WSMessageDelete(WSMessage):
|
||||
"""Message deleted event."""
|
||||
|
||||
type: str = "message.delete"
|
||||
message_id: UUID
|
||||
|
||||
|
||||
class WSAgentStream(WSMessage):
|
||||
"""Agent stream chunk."""
|
||||
|
||||
type: str = "agent.stream"
|
||||
agent_id: UUID
|
||||
chunk: str
|
||||
|
||||
|
||||
class WSSessionClosed(WSMessage):
|
||||
"""Session closed event."""
|
||||
|
||||
type: str = "session.closed"
|
||||
session_id: UUID
|
||||
reason: str
|
||||
|
||||
|
||||
class WSNotification(WSMessage):
|
||||
"""Notification event."""
|
||||
|
||||
type: str = "notification"
|
||||
notification_id: UUID
|
||||
notification_type: str
|
||||
subject: str
|
||||
priority: str
|
||||
+3
-78
@@ -9,30 +9,18 @@ Real-time communication via WebSocket connections for:
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from roboco.api.schemas.websocket import (
|
||||
NewMessageBroadcast,
|
||||
)
|
||||
from roboco.config import settings
|
||||
|
||||
|
||||
@dataclass
|
||||
class NewMessageBroadcast:
|
||||
"""Data for broadcasting a new message."""
|
||||
|
||||
channel_id: UUID
|
||||
session_id: UUID
|
||||
message_id: UUID
|
||||
agent_id: UUID
|
||||
content: str
|
||||
message_type: str
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -215,69 +203,6 @@ async def validate_channel_access(channel_id: UUID, agent_id: UUID) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WebSocket Message Types
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class WSMessage(BaseModel):
|
||||
"""Base WebSocket message."""
|
||||
|
||||
type: str
|
||||
timestamp: datetime = datetime.now(UTC)
|
||||
|
||||
|
||||
class WSMessageNew(WSMessage):
|
||||
"""New message event."""
|
||||
|
||||
type: str = "message.new"
|
||||
message_id: UUID
|
||||
agent_id: UUID
|
||||
content: str
|
||||
message_type: str
|
||||
|
||||
|
||||
class WSMessageEdit(WSMessage):
|
||||
"""Message edited event."""
|
||||
|
||||
type: str = "message.edit"
|
||||
message_id: UUID
|
||||
content: str
|
||||
|
||||
|
||||
class WSMessageDelete(WSMessage):
|
||||
"""Message deleted event."""
|
||||
|
||||
type: str = "message.delete"
|
||||
message_id: UUID
|
||||
|
||||
|
||||
class WSAgentStream(WSMessage):
|
||||
"""Agent stream chunk."""
|
||||
|
||||
type: str = "agent.stream"
|
||||
agent_id: UUID
|
||||
chunk: str
|
||||
|
||||
|
||||
class WSSessionClosed(WSMessage):
|
||||
"""Session closed event."""
|
||||
|
||||
type: str = "session.closed"
|
||||
session_id: UUID
|
||||
reason: str
|
||||
|
||||
|
||||
class WSNotification(WSMessage):
|
||||
"""Notification event."""
|
||||
|
||||
type: str = "notification"
|
||||
notification_id: UUID
|
||||
notification_type: str
|
||||
subject: str
|
||||
priority: str
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WebSocket Routes
|
||||
# =============================================================================
|
||||
|
||||
@@ -4,23 +4,10 @@ Task Ownership Enforcement
|
||||
Validates task ownership and claim rules.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from roboco.agents_config import get_agent_role, get_agent_team
|
||||
from roboco.enforcement.task_lifecycle import is_waiting_state
|
||||
from roboco.exceptions import RobocoError
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskClaimContext:
|
||||
"""Context for validating a task claim."""
|
||||
|
||||
agent_id: str
|
||||
task_id: str
|
||||
task_status: str
|
||||
task_team: str
|
||||
agent_active_tasks: list[dict]
|
||||
agent_paused_tasks: list[dict]
|
||||
from roboco.models.enforcement import TaskClaimContext
|
||||
|
||||
|
||||
class TaskOwnershipError(RobocoError):
|
||||
|
||||
+1
-91
@@ -6,108 +6,18 @@ Redis-based pub/sub event system for workflow triggers.
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import redis.asyncio as redis
|
||||
import structlog
|
||||
|
||||
from roboco.config import settings
|
||||
from roboco.models.events import Event, EventType
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class EventType(str, Enum):
|
||||
"""Types of events in the system."""
|
||||
|
||||
# Task lifecycle events
|
||||
TASK_CREATED = "task.created"
|
||||
TASK_CLAIMED = "task.claimed"
|
||||
TASK_STARTED = "task.started"
|
||||
TASK_BLOCKED = "task.blocked"
|
||||
TASK_UNBLOCKED = "task.unblocked"
|
||||
TASK_PAUSED = "task.paused"
|
||||
TASK_RESUMED = "task.resumed"
|
||||
TASK_VERIFYING = "task.verifying"
|
||||
TASK_AWAITING_QA = "task.awaiting_qa"
|
||||
TASK_QA_PASSED = "task.qa_passed"
|
||||
TASK_QA_FAILED = "task.qa_failed"
|
||||
TASK_AWAITING_DOCS = "task.awaiting_docs"
|
||||
TASK_COMPLETED = "task.completed"
|
||||
TASK_CANCELLED = "task.cancelled"
|
||||
|
||||
# Session events
|
||||
SESSION_CREATED = "session.created"
|
||||
SESSION_CLOSED = "session.closed"
|
||||
SESSION_TIMEOUT = "session.timeout"
|
||||
|
||||
# Handoff events
|
||||
HANDOFF_CREATED = "handoff.created"
|
||||
HANDOFF_ACCEPTED = "handoff.accepted"
|
||||
|
||||
# Agent events
|
||||
AGENT_SPAWNED = "agent.spawned"
|
||||
AGENT_STOPPED = "agent.stopped"
|
||||
AGENT_WAITING = "agent.waiting"
|
||||
AGENT_RESUMED = "agent.resumed"
|
||||
AGENT_ERROR = "agent.error"
|
||||
|
||||
# Notification events
|
||||
NOTIFICATION_SENT = "notification.sent"
|
||||
NOTIFICATION_ACKED = "notification.acked"
|
||||
|
||||
# Blocker events
|
||||
BLOCKER_REPORTED = "blocker.reported"
|
||||
BLOCKER_RESOLVED = "blocker.resolved"
|
||||
|
||||
# Question events
|
||||
QUESTION_ASKED = "question.asked"
|
||||
QUESTION_ANSWERED = "question.answered"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
"""An event in the system."""
|
||||
|
||||
type: EventType
|
||||
data: dict[str, Any]
|
||||
id: UUID = field(default_factory=uuid4)
|
||||
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
source_agent: str | None = None
|
||||
correlation_id: str | None = None # For tracking related events
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Serialize to JSON."""
|
||||
return json.dumps(
|
||||
{
|
||||
"id": str(self.id),
|
||||
"type": self.type.value,
|
||||
"data": self.data,
|
||||
"timestamp": self.timestamp.isoformat(),
|
||||
"source_agent": self.source_agent,
|
||||
"correlation_id": self.correlation_id,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> "Event":
|
||||
"""Deserialize from JSON."""
|
||||
data = json.loads(json_str)
|
||||
return cls(
|
||||
id=UUID(data["id"]),
|
||||
type=EventType(data["type"]),
|
||||
data=data["data"],
|
||||
timestamp=datetime.fromisoformat(data["timestamp"]),
|
||||
source_agent=data.get("source_agent"),
|
||||
correlation_id=data.get("correlation_id"),
|
||||
)
|
||||
|
||||
|
||||
# Type for event handlers
|
||||
EventHandler = Callable[[Event], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
@@ -5,91 +5,20 @@ Workflow trigger handlers that respond to system events.
|
||||
Uses dependency injection pattern for services to maintain separation of concerns.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.events.bus import Event, EventType, get_event_bus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from roboco.runtime.orchestrator import WaitingRecord
|
||||
from roboco.models.events import (
|
||||
EventContext,
|
||||
NotificationServiceProtocol,
|
||||
OrchestratorAccessProtocol,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SERVICE PROTOCOLS (for dependency injection)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class NotificationServiceProtocol(Protocol):
|
||||
"""Protocol for notification service."""
|
||||
|
||||
async def send_blocker_notification(
|
||||
self,
|
||||
task_id: str,
|
||||
blocker_reason: str,
|
||||
from_agent: str | None,
|
||||
to_pm: str,
|
||||
) -> None: ...
|
||||
|
||||
async def send_qa_ready_notification(
|
||||
self,
|
||||
task_id: str,
|
||||
from_agent: str | None,
|
||||
to_qa: str,
|
||||
) -> None: ...
|
||||
|
||||
async def send_qa_failed_notification(
|
||||
self,
|
||||
task_id: str,
|
||||
qa_notes: str,
|
||||
to_developer: str,
|
||||
) -> None: ...
|
||||
|
||||
async def send_docs_ready_notification(
|
||||
self,
|
||||
task_id: str,
|
||||
from_agent: str | None,
|
||||
to_documenter: str,
|
||||
) -> None: ...
|
||||
|
||||
async def send_handoff_notification(
|
||||
self,
|
||||
task_id: str,
|
||||
handoff_id: str,
|
||||
from_agent: str | None,
|
||||
to_documenter: str,
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class OrchestratorAccessProtocol(Protocol):
|
||||
"""Protocol for orchestrator access."""
|
||||
|
||||
def get_waiting_agents(self) -> dict[str, "WaitingRecord"]: ...
|
||||
|
||||
async def resolve_wait(self, agent_id: str, resolution: dict[str, Any]) -> Any: ...
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EVENT CONTEXT (dependency container)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class EventContext:
|
||||
"""
|
||||
Dependency container for event handlers.
|
||||
|
||||
Set once during application initialization, then used by all handlers.
|
||||
This avoids runtime imports inside handler functions.
|
||||
"""
|
||||
|
||||
notification_service: NotificationServiceProtocol | None = None
|
||||
orchestrator: OrchestratorAccessProtocol | None = None
|
||||
|
||||
|
||||
# Global context instance - set during initialization
|
||||
_context = EventContext()
|
||||
|
||||
|
||||
+1
-66
@@ -4,72 +4,7 @@ TOON Metrics
|
||||
Tracks token savings and usage statistics for TOON vs JSON serialization.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToonMetrics:
|
||||
"""
|
||||
Metrics for tracking TOON serialization efficiency.
|
||||
|
||||
Tracks character counts (as proxy for tokens) for JSON vs TOON
|
||||
to measure actual savings in production.
|
||||
"""
|
||||
|
||||
json_chars: int = 0
|
||||
toon_chars: int = 0
|
||||
encode_count: int = 0
|
||||
decode_count: int = 0
|
||||
decode_fallback_count: int = 0
|
||||
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
@property
|
||||
def savings_percent(self) -> float:
|
||||
"""Calculate percentage of characters saved using TOON."""
|
||||
if self.json_chars == 0:
|
||||
return 0.0
|
||||
return (1 - self.toon_chars / self.json_chars) * 100
|
||||
|
||||
@property
|
||||
def fallback_rate(self) -> float:
|
||||
"""Calculate rate of fallback to JSON decoding."""
|
||||
if self.decode_count == 0:
|
||||
return 0.0
|
||||
return (self.decode_fallback_count / self.decode_count) * 100
|
||||
|
||||
def record_encode(self, json_chars: int, toon_chars: int) -> None:
|
||||
"""Record an encode operation with character counts."""
|
||||
self.json_chars += json_chars
|
||||
self.toon_chars += toon_chars
|
||||
self.encode_count += 1
|
||||
|
||||
def record_decode(self, used_fallback: bool = False) -> None:
|
||||
"""Record a decode operation."""
|
||||
self.decode_count += 1
|
||||
if used_fallback:
|
||||
self.decode_fallback_count += 1
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert metrics to dictionary for logging/reporting."""
|
||||
return {
|
||||
"json_chars": self.json_chars,
|
||||
"toon_chars": self.toon_chars,
|
||||
"savings_percent": round(self.savings_percent, 2),
|
||||
"encode_count": self.encode_count,
|
||||
"decode_count": self.decode_count,
|
||||
"fallback_rate": round(self.fallback_rate, 2),
|
||||
"started_at": self.started_at.isoformat(),
|
||||
}
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset all metrics."""
|
||||
self.json_chars = 0
|
||||
self.toon_chars = 0
|
||||
self.encode_count = 0
|
||||
self.decode_count = 0
|
||||
self.decode_fallback_count = 0
|
||||
self.started_at = datetime.now(UTC)
|
||||
from roboco.models.llm import ToonMetrics
|
||||
|
||||
|
||||
# Global metrics holder
|
||||
|
||||
@@ -19,25 +19,17 @@ Usage:
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
import toon
|
||||
from pydantic import BaseModel
|
||||
|
||||
from roboco.models.llm import ToonConfig
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToonConfig:
|
||||
"""Configuration for TOON encoding."""
|
||||
|
||||
delimiter: str = ","
|
||||
indent: int = 2
|
||||
include_length: bool = True
|
||||
|
||||
|
||||
class ToonAdapter:
|
||||
"""
|
||||
Adapter for TOON serialization at LLM boundaries.
|
||||
|
||||
@@ -19,107 +19,21 @@ from typing import Any
|
||||
import httpx
|
||||
from fastapi import status
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.config import settings
|
||||
from roboco.llm import ToonAdapter
|
||||
from roboco.mcp.schemas import (
|
||||
DecisionLogInput,
|
||||
JournalEntryInput,
|
||||
LearningInput,
|
||||
StruggleInput,
|
||||
TaskReflectionInput,
|
||||
)
|
||||
|
||||
# Global TOON adapter for encoding journal data
|
||||
_toon = ToonAdapter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# INPUT MODELS (Pydantic models to reduce argument count)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class JournalEntryInput(BaseModel):
|
||||
"""Input for creating a general journal entry."""
|
||||
|
||||
title: str = Field(..., description="Entry title (short description)")
|
||||
content: str = Field(..., description="Entry content (detailed text)")
|
||||
entry_type: str = Field(
|
||||
default="general",
|
||||
description="Type: general, task_reflection, decision_log, learning, struggle",
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task")
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
is_private: bool = Field(
|
||||
default=False, description="If true, only you and CEO/Auditor can see"
|
||||
)
|
||||
|
||||
|
||||
class TaskReflectionInput(BaseModel):
|
||||
"""Input for creating a task reflection entry."""
|
||||
|
||||
task_id: str = Field(..., description="The task UUID you're reflecting on")
|
||||
title: str = Field(..., description="Reflection title")
|
||||
what_done: str = Field(..., description="What was accomplished")
|
||||
what_learned: str = Field(..., description="Key learnings from this task")
|
||||
what_struggled: str = Field(..., description="What was difficult or challenging")
|
||||
next_steps: list[str] = Field(
|
||||
default_factory=list, description="Optional follow-up items"
|
||||
)
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
|
||||
|
||||
class DecisionOption(BaseModel):
|
||||
"""A decision option with pros/cons."""
|
||||
|
||||
option: str
|
||||
pros_cons: str
|
||||
|
||||
|
||||
class DecisionLogInput(BaseModel):
|
||||
"""Input for logging a decision."""
|
||||
|
||||
title: str = Field(..., description="Decision title")
|
||||
context: str = Field(..., description="What situation led to this decision")
|
||||
options: list[DecisionOption] = Field(
|
||||
..., min_length=2, description="Options considered (at least 2)"
|
||||
)
|
||||
chosen: str = Field(..., description="Which option was chosen")
|
||||
rationale: str = Field(..., description="Why this option was chosen")
|
||||
consequences: list[str] = Field(
|
||||
default_factory=list, description="Expected consequences"
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task")
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
|
||||
|
||||
class LearningInput(BaseModel):
|
||||
"""Input for logging a learning."""
|
||||
|
||||
title: str = Field(..., description="Learning title")
|
||||
what_learned: str = Field(..., description="The actual learning/insight")
|
||||
how_applied: str | None = Field(
|
||||
default=None, description="How you applied or plan to apply this"
|
||||
)
|
||||
source: str | None = Field(
|
||||
default=None, description="Where you learned this (docs, experiment, etc.)"
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task")
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
|
||||
|
||||
class StruggleInput(BaseModel):
|
||||
"""Input for logging a struggle."""
|
||||
|
||||
title: str = Field(..., description="Struggle title")
|
||||
what_struggled: str = Field(..., description="What the challenge was")
|
||||
attempted_solutions: list[str] = Field(
|
||||
default_factory=list, description="What you tried (even if it didn't work)"
|
||||
)
|
||||
resolution: str | None = Field(
|
||||
default=None, description="How it was resolved (if resolved)"
|
||||
)
|
||||
help_needed: str | None = Field(
|
||||
default=None, description="What help you need (if unresolved)"
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task")
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
@@ -19,35 +19,20 @@ from typing import Any
|
||||
import httpx
|
||||
from fastapi import status
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.agents_config import CHANNEL_ACCESS
|
||||
from roboco.config import settings
|
||||
from roboco.llm import ToonAdapter
|
||||
from roboco.mcp.schemas import (
|
||||
AskQuestionInput,
|
||||
ReportBlockerInput,
|
||||
SendMessageInput,
|
||||
)
|
||||
|
||||
# Global TOON adapter for encoding message data
|
||||
_toon = ToonAdapter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# INPUT MODELS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class SendMessageInput(BaseModel):
|
||||
"""Input for sending a message."""
|
||||
|
||||
channel_slug: str = Field(..., description="Channel slug (e.g., 'backend-cell')")
|
||||
content: str = Field(..., description="Message content")
|
||||
message_type: str = Field(
|
||||
default="dialogue",
|
||||
description="Type: reasoning, dialogue, decision, action, blocker, technical",
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task ID")
|
||||
reply_to: str | None = Field(default=None, description="Message ID to reply to")
|
||||
mentions: list[str] = Field(default_factory=list, description="Agents to mention")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
@@ -307,24 +292,6 @@ async def _handle_message_get(message_id: str) -> dict[str, Any]:
|
||||
return {"message": resp.json()}
|
||||
|
||||
|
||||
class AskQuestionInput(BaseModel):
|
||||
"""Input for asking a question."""
|
||||
|
||||
channel_slug: str
|
||||
question: str
|
||||
context: str | None = None
|
||||
task_id: str | None = None
|
||||
|
||||
|
||||
class ReportBlockerInput(BaseModel):
|
||||
"""Input for reporting a blocker."""
|
||||
|
||||
channel_slug: str
|
||||
blocker_description: str
|
||||
what_needed: str
|
||||
task_id: str | None = None
|
||||
|
||||
|
||||
async def _handle_ask_question(
|
||||
data: AskQuestionInput,
|
||||
send_fn: Callable[[SendMessageInput], Awaitable[dict[str, Any]]],
|
||||
|
||||
@@ -16,7 +16,6 @@ from typing import Any
|
||||
import httpx
|
||||
from fastapi import status
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.agents_config import (
|
||||
NOTIFICATION_PERMISSIONS,
|
||||
@@ -24,25 +23,7 @@ from roboco.agents_config import (
|
||||
get_agent_role,
|
||||
)
|
||||
from roboco.config import settings
|
||||
|
||||
# =============================================================================
|
||||
# INPUT MODELS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class SendNotificationInput(BaseModel):
|
||||
"""Input for sending a notification."""
|
||||
|
||||
recipients: list[str] = Field(..., description="Agent IDs to notify")
|
||||
subject: str = Field(..., description="Notification subject")
|
||||
body: str = Field(..., description="Notification body")
|
||||
notification_type: str = Field(
|
||||
default="info", description="Type: info, alert, task, escalation, approval"
|
||||
)
|
||||
priority: str = Field(default="normal", description="low, normal, high, urgent")
|
||||
requires_ack: bool = Field(default=True, description="Require acknowledgment")
|
||||
related_task_id: str | None = Field(default=None, description="Related task")
|
||||
|
||||
from roboco.mcp.schemas import SendNotificationInput
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
MCP Input Schemas
|
||||
|
||||
Pydantic models for MCP tool input validation.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# =============================================================================
|
||||
# JOURNAL SCHEMAS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class JournalEntryInput(BaseModel):
|
||||
"""Input for creating a general journal entry."""
|
||||
|
||||
title: str = Field(..., description="Entry title (short description)")
|
||||
content: str = Field(..., description="Entry content (detailed text)")
|
||||
entry_type: str = Field(
|
||||
default="general",
|
||||
description="Type: general, task_reflection, decision_log, learning, struggle",
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task")
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
is_private: bool = Field(
|
||||
default=False, description="If true, only you and CEO/Auditor can see"
|
||||
)
|
||||
|
||||
|
||||
class TaskReflectionInput(BaseModel):
|
||||
"""Input for creating a task reflection entry."""
|
||||
|
||||
task_id: str = Field(..., description="The task UUID you're reflecting on")
|
||||
title: str = Field(..., description="Reflection title")
|
||||
what_done: str = Field(..., description="What was accomplished")
|
||||
what_learned: str = Field(..., description="Key learnings from this task")
|
||||
what_struggled: str = Field(..., description="What was difficult or challenging")
|
||||
next_steps: list[str] = Field(
|
||||
default_factory=list, description="Optional follow-up items"
|
||||
)
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
|
||||
|
||||
class DecisionOption(BaseModel):
|
||||
"""A decision option with pros/cons."""
|
||||
|
||||
option: str
|
||||
pros_cons: str
|
||||
|
||||
|
||||
class DecisionLogInput(BaseModel):
|
||||
"""Input for logging a decision."""
|
||||
|
||||
title: str = Field(..., description="Decision title")
|
||||
context: str = Field(..., description="What situation led to this decision")
|
||||
options: list[DecisionOption] = Field(
|
||||
..., min_length=2, description="Options considered (at least 2)"
|
||||
)
|
||||
chosen: str = Field(..., description="Which option was chosen")
|
||||
rationale: str = Field(..., description="Why this option was chosen")
|
||||
consequences: list[str] = Field(
|
||||
default_factory=list, description="Expected consequences"
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task")
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
|
||||
|
||||
class LearningInput(BaseModel):
|
||||
"""Input for logging a learning."""
|
||||
|
||||
title: str = Field(..., description="Learning title")
|
||||
what_learned: str = Field(..., description="The actual learning/insight")
|
||||
how_applied: str | None = Field(
|
||||
default=None, description="How you applied or plan to apply this"
|
||||
)
|
||||
source: str | None = Field(
|
||||
default=None, description="Where you learned this (docs, experiment, etc.)"
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task")
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
|
||||
|
||||
class StruggleInput(BaseModel):
|
||||
"""Input for logging a struggle."""
|
||||
|
||||
title: str = Field(..., description="Struggle title")
|
||||
what_struggled: str = Field(..., description="What the challenge was")
|
||||
attempted_solutions: list[str] = Field(
|
||||
default_factory=list, description="What you tried (even if it didn't work)"
|
||||
)
|
||||
resolution: str | None = Field(
|
||||
default=None, description="How it was resolved (if resolved)"
|
||||
)
|
||||
help_needed: str | None = Field(
|
||||
default=None, description="What help you need (if unresolved)"
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task")
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MESSAGE SCHEMAS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class SendMessageInput(BaseModel):
|
||||
"""Input for sending a message."""
|
||||
|
||||
channel_slug: str = Field(..., description="Channel slug (e.g., 'backend-cell')")
|
||||
content: str = Field(..., description="Message content")
|
||||
message_type: str = Field(
|
||||
default="dialogue",
|
||||
description="Type: reasoning, dialogue, decision, action, blocker, technical",
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task ID")
|
||||
reply_to: str | None = Field(default=None, description="Message ID to reply to")
|
||||
mentions: list[str] = Field(default_factory=list, description="Agents to mention")
|
||||
|
||||
|
||||
class AskQuestionInput(BaseModel):
|
||||
"""Input for asking a question."""
|
||||
|
||||
channel_slug: str
|
||||
question: str
|
||||
context: str | None = None
|
||||
task_id: str | None = None
|
||||
|
||||
|
||||
class ReportBlockerInput(BaseModel):
|
||||
"""Input for reporting a blocker."""
|
||||
|
||||
channel_slug: str
|
||||
blocker_description: str
|
||||
what_needed: str
|
||||
task_id: str | None = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# NOTIFICATION SCHEMAS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class SendNotificationInput(BaseModel):
|
||||
"""Input for sending a notification."""
|
||||
|
||||
recipients: list[str] = Field(..., description="Agent IDs to notify")
|
||||
subject: str = Field(..., description="Notification subject")
|
||||
body: str = Field(..., description="Notification body")
|
||||
notification_type: str = Field(
|
||||
default="info", description="Type: info, alert, task, escalation, approval"
|
||||
)
|
||||
priority: str = Field(default="normal", description="low, normal, high, urgent")
|
||||
requires_ack: bool = Field(default=True, description="Require acknowledgment")
|
||||
related_task_id: str | None = Field(default=None, description="Related task")
|
||||
@@ -0,0 +1,383 @@
|
||||
"""
|
||||
Agent Models
|
||||
|
||||
Domain types for the agent system including phases, configs, and contexts.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.models import AgentRole, AgentStatus, Team
|
||||
|
||||
# =============================================================================
|
||||
# MODEL PROVIDER
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ModelProvider(str, Enum):
|
||||
"""LLM provider options."""
|
||||
|
||||
ANTHROPIC = "anthropic"
|
||||
OPENAI = "openai"
|
||||
LOCAL = "local"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AGENT CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AgentConfig(BaseModel):
|
||||
"""Configuration for an agent instance."""
|
||||
|
||||
# Identity
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
name: str
|
||||
slug: str = Field(..., pattern=r"^[a-z0-9-]+$")
|
||||
role: AgentRole
|
||||
team: Team | None = None
|
||||
|
||||
# Model configuration
|
||||
provider: ModelProvider = ModelProvider.ANTHROPIC
|
||||
model: str = "claude-sonnet-4-20250514"
|
||||
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
|
||||
max_tokens: int = Field(default=4096, ge=1)
|
||||
|
||||
# System prompt (loaded from blueprints)
|
||||
system_prompt: str
|
||||
|
||||
# Capabilities
|
||||
capabilities: list[str] = Field(default_factory=list)
|
||||
|
||||
# Permissions
|
||||
can_notify: bool = False
|
||||
channel_ids: list[UUID] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AgentState(BaseModel):
|
||||
"""Current state of an agent."""
|
||||
|
||||
status: AgentStatus = AgentStatus.OFFLINE
|
||||
current_task_id: UUID | None = None
|
||||
current_session_id: UUID | None = None
|
||||
last_activity: datetime | None = None
|
||||
error: str | None = None
|
||||
|
||||
# Metrics
|
||||
messages_sent: int = 0
|
||||
tasks_completed: int = 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DEVELOPER PHASES AND CONTEXT
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class DevTaskPhase(str, Enum):
|
||||
"""Phases of the developer task lifecycle."""
|
||||
|
||||
SCAN = "scan"
|
||||
CLAIM = "claim"
|
||||
UNDERSTAND = "understand"
|
||||
PLAN = "plan"
|
||||
EXECUTE = "execute"
|
||||
VERIFY = "verify"
|
||||
NOTES = "notes"
|
||||
CLOSE = "close"
|
||||
BLOCKED = "blocked"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskContext:
|
||||
"""Context for the current task being worked on by a developer."""
|
||||
|
||||
task_id: UUID
|
||||
title: str
|
||||
phase: DevTaskPhase = DevTaskPhase.CLAIM
|
||||
subtasks: list[dict[str, Any]] = field(default_factory=list)
|
||||
current_subtask: int = 0
|
||||
blockers: list[str] = field(default_factory=list)
|
||||
commits: list[str] = field(default_factory=list)
|
||||
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
journal_entries: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# QA PHASES AND CONTEXT
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class QATaskPhase(str, Enum):
|
||||
"""Phases of the QA lifecycle."""
|
||||
|
||||
MONITOR = "monitor"
|
||||
RECEIVE = "receive"
|
||||
UNDERSTAND = "understand"
|
||||
TEST = "test"
|
||||
VERDICT = "verdict"
|
||||
DOCUMENT = "document"
|
||||
RETURN = "return"
|
||||
|
||||
|
||||
class TestResult(str, Enum):
|
||||
"""Test result outcomes."""
|
||||
|
||||
PASS = "pass"
|
||||
FAIL = "fail"
|
||||
BLOCKED = "blocked"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestCase:
|
||||
"""A single test case."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
steps: list[str]
|
||||
expected: str
|
||||
result: TestResult | None = None
|
||||
actual: str | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReviewContext:
|
||||
"""Context for the current review being conducted by QA."""
|
||||
|
||||
task_id: UUID
|
||||
title: str
|
||||
phase: QATaskPhase = QATaskPhase.RECEIVE
|
||||
test_cases: list[TestCase] = field(default_factory=list)
|
||||
current_test: int = 0
|
||||
findings: list[str] = field(default_factory=list)
|
||||
verdict: TestResult | None = None
|
||||
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
notes: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PM PHASES AND CONTEXT
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class CellPMPhase(str, Enum):
|
||||
"""Phases of the Cell PM lifecycle."""
|
||||
|
||||
MONITOR = "monitor"
|
||||
TRIAGE = "triage"
|
||||
ASSIGN = "assign"
|
||||
FACILITATE = "facilitate"
|
||||
ESCALATE = "escalate"
|
||||
TRACK = "track"
|
||||
REPORT = "report"
|
||||
|
||||
|
||||
class MainPMPhase(str, Enum):
|
||||
"""Phases of the Main PM lifecycle."""
|
||||
|
||||
OVERSEE = "oversee"
|
||||
RECEIVE = "receive"
|
||||
PRIORITIZE = "prioritize"
|
||||
COORDINATE = "coordinate"
|
||||
DISTRIBUTE = "distribute"
|
||||
REPORT_UP = "report_up"
|
||||
FACILITATE = "facilitate"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CellStatus:
|
||||
"""Status of a cell."""
|
||||
|
||||
name: str
|
||||
active_tasks: int = 0
|
||||
blocked_tasks: int = 0
|
||||
completed_today: int = 0
|
||||
available_devs: int = 0
|
||||
concerns: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskAssignment:
|
||||
"""A task assignment decision."""
|
||||
|
||||
task_id: UUID
|
||||
agent_id: UUID
|
||||
agent_name: str
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Escalation:
|
||||
"""An escalation to higher management."""
|
||||
|
||||
issue: str
|
||||
severity: str # low, medium, high, critical
|
||||
task_id: UUID | None = None
|
||||
proposed_solution: str | None = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DOCUMENTER PHASES AND CONTEXT
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class DocTaskPhase(str, Enum):
|
||||
"""Phases of the Documenter lifecycle."""
|
||||
|
||||
MONITOR = "monitor"
|
||||
RECEIVE = "receive"
|
||||
GATHER = "gather"
|
||||
SYNTHESIZE = "synthesize"
|
||||
WRITE = "write"
|
||||
REVIEW = "review"
|
||||
PUBLISH = "publish"
|
||||
|
||||
|
||||
class DocType(str, Enum):
|
||||
"""Types of documentation."""
|
||||
|
||||
API = "api"
|
||||
README = "readme"
|
||||
ARCHITECTURE = "architecture"
|
||||
CHANGELOG = "changelog"
|
||||
KNOWLEDGE_BASE = "knowledge_base"
|
||||
COMPONENT = "component"
|
||||
DESIGN_SYSTEM = "design_system"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocumentSpec:
|
||||
"""Specification for a document to create/update."""
|
||||
|
||||
doc_type: DocType
|
||||
title: str
|
||||
path: str
|
||||
priority: str = "required" # required, optional
|
||||
content: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocContext:
|
||||
"""Context for the current documentation task."""
|
||||
|
||||
task_id: UUID
|
||||
title: str
|
||||
phase: DocTaskPhase = DocTaskPhase.RECEIVE
|
||||
# Gathered materials
|
||||
dev_notes: str | None = None
|
||||
qa_feedback: str | None = None
|
||||
commits: list[str] = field(default_factory=list)
|
||||
conversations: list[str] = field(default_factory=list)
|
||||
code_changes: list[str] = field(default_factory=list)
|
||||
# Synthesis
|
||||
summary: str | None = None
|
||||
documents_needed: list[DocumentSpec] = field(default_factory=list)
|
||||
current_doc: int = 0
|
||||
# Output
|
||||
written_docs: list[str] = field(default_factory=list)
|
||||
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
notes: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BOARD PHASES AND CONTEXT
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ProductOwnerPhase(str, Enum):
|
||||
"""Phases of the Product Owner lifecycle."""
|
||||
|
||||
VISION = "vision"
|
||||
ROADMAP = "roadmap"
|
||||
DEFINE = "define"
|
||||
PRIORITIZE = "prioritize"
|
||||
REVIEW = "review"
|
||||
FEEDBACK = "feedback"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Feature:
|
||||
"""A feature or epic."""
|
||||
|
||||
id: UUID
|
||||
title: str
|
||||
description: str
|
||||
acceptance_criteria: list[str]
|
||||
priority: int # 0-3
|
||||
status: str = "backlog"
|
||||
|
||||
|
||||
class HeadMarketingPhase(str, Enum):
|
||||
"""Phases of the Head of Marketing lifecycle."""
|
||||
|
||||
RESEARCH = "research"
|
||||
STRATEGY = "strategy"
|
||||
PLAN = "plan"
|
||||
CREATE = "create"
|
||||
EXECUTE = "execute"
|
||||
ANALYZE = "analyze"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Campaign:
|
||||
"""A marketing campaign."""
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
objective: str
|
||||
channels: list[str]
|
||||
start_date: datetime | None = None
|
||||
end_date: datetime | None = None
|
||||
status: str = "planning"
|
||||
metrics: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class AuditorPhase(str, Enum):
|
||||
"""Phases of the Auditor lifecycle."""
|
||||
|
||||
OBSERVE = "observe"
|
||||
ANALYZE = "analyze"
|
||||
FLAG = "flag"
|
||||
REPORT = "report"
|
||||
AUDIT = "audit"
|
||||
ADVISE = "advise"
|
||||
|
||||
|
||||
class AuditorFlagSeverity(str, Enum):
|
||||
"""Severity of flagged issues from auditor."""
|
||||
|
||||
INFO = "info"
|
||||
WARNING = "warning"
|
||||
CONCERN = "concern"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditFlag:
|
||||
"""A flagged issue from audit observation."""
|
||||
|
||||
id: UUID
|
||||
severity: AuditorFlagSeverity
|
||||
category: str # quality, process, communication, efficiency
|
||||
description: str
|
||||
evidence: list[str]
|
||||
recommendation: str | None = None
|
||||
reported_to_ceo: bool = False
|
||||
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditReport:
|
||||
"""A report to the CEO."""
|
||||
|
||||
period: str
|
||||
summary: str
|
||||
flags: list[AuditFlag]
|
||||
metrics: dict[str, Any]
|
||||
recommendations: list[str]
|
||||
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Enforcement Models
|
||||
|
||||
Domain types for enforcement rules.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskClaimContext:
|
||||
"""Context for validating a task claim."""
|
||||
|
||||
agent_id: str
|
||||
task_id: str
|
||||
task_status: str
|
||||
task_team: str
|
||||
agent_active_tasks: list[dict]
|
||||
agent_paused_tasks: list[dict]
|
||||
|
||||
|
||||
@dataclass
|
||||
class OwnershipContext:
|
||||
"""Context for validating task ownership."""
|
||||
|
||||
agent_id: str
|
||||
task_id: str
|
||||
current_owner: str | None
|
||||
current_status: str
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Event Models
|
||||
|
||||
Domain types for the event bus system.
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from roboco.runtime.orchestrator import WaitingRecord
|
||||
|
||||
|
||||
class EventType(str, Enum):
|
||||
"""Types of events in the system."""
|
||||
|
||||
# Task lifecycle events
|
||||
TASK_CREATED = "task.created"
|
||||
TASK_CLAIMED = "task.claimed"
|
||||
TASK_STARTED = "task.started"
|
||||
TASK_BLOCKED = "task.blocked"
|
||||
TASK_UNBLOCKED = "task.unblocked"
|
||||
TASK_PAUSED = "task.paused"
|
||||
TASK_RESUMED = "task.resumed"
|
||||
TASK_VERIFYING = "task.verifying"
|
||||
TASK_AWAITING_QA = "task.awaiting_qa"
|
||||
TASK_QA_PASSED = "task.qa_passed"
|
||||
TASK_QA_FAILED = "task.qa_failed"
|
||||
TASK_AWAITING_DOCS = "task.awaiting_docs"
|
||||
TASK_COMPLETED = "task.completed"
|
||||
TASK_CANCELLED = "task.cancelled"
|
||||
|
||||
# Session events
|
||||
SESSION_CREATED = "session.created"
|
||||
SESSION_CLOSED = "session.closed"
|
||||
SESSION_TIMEOUT = "session.timeout"
|
||||
|
||||
# Handoff events
|
||||
HANDOFF_CREATED = "handoff.created"
|
||||
HANDOFF_ACCEPTED = "handoff.accepted"
|
||||
|
||||
# Agent events
|
||||
AGENT_SPAWNED = "agent.spawned"
|
||||
AGENT_STOPPED = "agent.stopped"
|
||||
AGENT_WAITING = "agent.waiting"
|
||||
AGENT_RESUMED = "agent.resumed"
|
||||
AGENT_ERROR = "agent.error"
|
||||
|
||||
# Notification events
|
||||
NOTIFICATION_SENT = "notification.sent"
|
||||
NOTIFICATION_ACKED = "notification.acked"
|
||||
|
||||
# Blocker events
|
||||
BLOCKER_REPORTED = "blocker.reported"
|
||||
BLOCKER_RESOLVED = "blocker.resolved"
|
||||
|
||||
# Question events
|
||||
QUESTION_ASKED = "question.asked"
|
||||
QUESTION_ANSWERED = "question.answered"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
"""An event in the system."""
|
||||
|
||||
type: EventType
|
||||
data: dict[str, Any]
|
||||
id: UUID = field(default_factory=uuid4)
|
||||
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
source_agent: str | None = None
|
||||
correlation_id: str | None = None # For tracking related events
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Serialize to JSON."""
|
||||
return json.dumps(
|
||||
{
|
||||
"id": str(self.id),
|
||||
"type": self.type.value,
|
||||
"data": self.data,
|
||||
"timestamp": self.timestamp.isoformat(),
|
||||
"source_agent": self.source_agent,
|
||||
"correlation_id": self.correlation_id,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> "Event":
|
||||
"""Deserialize from JSON."""
|
||||
data = json.loads(json_str)
|
||||
return cls(
|
||||
id=UUID(data["id"]),
|
||||
type=EventType(data["type"]),
|
||||
data=data["data"],
|
||||
timestamp=datetime.fromisoformat(data["timestamp"]),
|
||||
source_agent=data.get("source_agent"),
|
||||
correlation_id=data.get("correlation_id"),
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SERVICE PROTOCOLS (for dependency injection)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class NotificationServiceProtocol(Protocol):
|
||||
"""Protocol for notification service."""
|
||||
|
||||
async def send_blocker_notification(
|
||||
self,
|
||||
task_id: str,
|
||||
blocker_reason: str,
|
||||
from_agent: str | None,
|
||||
to_pm: str,
|
||||
) -> None: ...
|
||||
|
||||
async def send_qa_ready_notification(
|
||||
self,
|
||||
task_id: str,
|
||||
from_agent: str | None,
|
||||
to_qa: str,
|
||||
) -> None: ...
|
||||
|
||||
async def send_qa_failed_notification(
|
||||
self,
|
||||
task_id: str,
|
||||
qa_notes: str,
|
||||
to_developer: str,
|
||||
) -> None: ...
|
||||
|
||||
async def send_docs_ready_notification(
|
||||
self,
|
||||
task_id: str,
|
||||
from_agent: str | None,
|
||||
to_documenter: str,
|
||||
) -> None: ...
|
||||
|
||||
async def send_handoff_notification(
|
||||
self,
|
||||
task_id: str,
|
||||
handoff_id: str,
|
||||
from_agent: str | None,
|
||||
to_documenter: str,
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class OrchestratorAccessProtocol(Protocol):
|
||||
"""Protocol for orchestrator access."""
|
||||
|
||||
def get_waiting_agents(self) -> dict[str, "WaitingRecord"]: ...
|
||||
|
||||
async def resolve_wait(self, agent_id: str, resolution: dict[str, Any]) -> Any: ...
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EVENT CONTEXT (dependency container)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class EventContext:
|
||||
"""
|
||||
Dependency container for event handlers.
|
||||
|
||||
Set once during application initialization, then used by all handlers.
|
||||
This avoids runtime imports inside handler functions.
|
||||
"""
|
||||
|
||||
notification_service: NotificationServiceProtocol | None = None
|
||||
orchestrator: OrchestratorAccessProtocol | None = None
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
LLM Models
|
||||
|
||||
Domain types for LLM integration.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToonConfig:
|
||||
"""Configuration for TOON encoding."""
|
||||
|
||||
delimiter: str = ","
|
||||
indent: int = 2
|
||||
include_length: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class EncodedBlock:
|
||||
"""A TOON-encoded block with metadata."""
|
||||
|
||||
content: str
|
||||
label: str
|
||||
token_estimate: int = 0
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return the formatted block."""
|
||||
return f"[{self.label}]\n{self.content}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMUsage:
|
||||
"""Token usage statistics for an LLM call."""
|
||||
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
cache_creation_input_tokens: int = 0
|
||||
cache_read_input_tokens: int = 0
|
||||
|
||||
@property
|
||||
def total_tokens(self) -> int:
|
||||
"""Total tokens used."""
|
||||
return self.input_tokens + self.output_tokens
|
||||
|
||||
@property
|
||||
def total_input_with_cache(self) -> int:
|
||||
"""Total input including cache operations."""
|
||||
return (
|
||||
self.input_tokens
|
||||
+ self.cache_creation_input_tokens
|
||||
+ self.cache_read_input_tokens
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToonMetrics:
|
||||
"""
|
||||
Metrics for tracking TOON serialization efficiency.
|
||||
|
||||
Tracks character counts (as proxy for tokens) for JSON vs TOON
|
||||
to measure actual savings in production.
|
||||
"""
|
||||
|
||||
json_chars: int = 0
|
||||
toon_chars: int = 0
|
||||
encode_count: int = 0
|
||||
decode_count: int = 0
|
||||
decode_fallback_count: int = 0
|
||||
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
@property
|
||||
def savings_percent(self) -> float:
|
||||
"""Calculate percentage of characters saved using TOON."""
|
||||
if self.json_chars == 0:
|
||||
return 0.0
|
||||
return (1 - self.toon_chars / self.json_chars) * 100
|
||||
|
||||
@property
|
||||
def fallback_rate(self) -> float:
|
||||
"""Calculate rate of fallback to JSON decoding."""
|
||||
if self.decode_count == 0:
|
||||
return 0.0
|
||||
return (self.decode_fallback_count / self.decode_count) * 100
|
||||
|
||||
def record_encode(self, json_chars: int, toon_chars: int) -> None:
|
||||
"""Record an encode operation with character counts."""
|
||||
self.json_chars += json_chars
|
||||
self.toon_chars += toon_chars
|
||||
self.encode_count += 1
|
||||
|
||||
def record_decode(self, used_fallback: bool = False) -> None:
|
||||
"""Record a decode operation."""
|
||||
self.decode_count += 1
|
||||
if used_fallback:
|
||||
self.decode_fallback_count += 1
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert metrics to dictionary for logging/reporting."""
|
||||
return {
|
||||
"json_chars": self.json_chars,
|
||||
"toon_chars": self.toon_chars,
|
||||
"savings_percent": round(self.savings_percent, 2),
|
||||
"encode_count": self.encode_count,
|
||||
"decode_count": self.decode_count,
|
||||
"fallback_rate": round(self.fallback_rate, 2),
|
||||
"started_at": self.started_at.isoformat(),
|
||||
}
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset all metrics."""
|
||||
self.json_chars = 0
|
||||
self.toon_chars = 0
|
||||
self.encode_count = 0
|
||||
self.decode_count = 0
|
||||
self.decode_fallback_count = 0
|
||||
self.started_at = datetime.now(UTC)
|
||||
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Organization Models
|
||||
|
||||
Domain types for the organizational structure (cells, board, organization).
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.models import Team
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from roboco.agents.base import Agent
|
||||
from roboco.agents.board import (
|
||||
AuditorAgent,
|
||||
HeadMarketingAgent,
|
||||
ProductOwnerAgent,
|
||||
)
|
||||
from roboco.agents.developer import DeveloperAgent
|
||||
from roboco.agents.documenter import DocumenterAgent
|
||||
from roboco.agents.pm import CellPMAgent, MainPMAgent
|
||||
from roboco.agents.qa import QAAgent
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Cell:
|
||||
"""A complete cell with all its agents."""
|
||||
|
||||
name: str
|
||||
team: Team
|
||||
pm: "CellPMAgent"
|
||||
developers: list["DeveloperAgent"]
|
||||
qa: "QAAgent"
|
||||
documenter: "DocumenterAgent"
|
||||
|
||||
@property
|
||||
def all_agents(self) -> list["Agent"]:
|
||||
"""Get all agents in the cell."""
|
||||
return [self.pm, *self.developers, self.qa, self.documenter]
|
||||
|
||||
async def start_all(self) -> None:
|
||||
"""Start all agents in the cell."""
|
||||
for agent in self.all_agents:
|
||||
await agent.start()
|
||||
logger.info("Cell started", cell=self.name, agents=len(self.all_agents))
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
"""Stop all agents in the cell."""
|
||||
for agent in self.all_agents:
|
||||
await agent.stop()
|
||||
logger.info("Cell stopped", cell=self.name)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Board:
|
||||
"""The board level with all board agents."""
|
||||
|
||||
product_owner: "ProductOwnerAgent"
|
||||
head_marketing: "HeadMarketingAgent"
|
||||
auditor: "AuditorAgent"
|
||||
|
||||
@property
|
||||
def all_agents(self) -> list["Agent"]:
|
||||
"""Get all board agents."""
|
||||
return [self.product_owner, self.head_marketing, self.auditor]
|
||||
|
||||
async def start_all(self) -> None:
|
||||
"""Start all board agents."""
|
||||
for agent in self.all_agents:
|
||||
await agent.start()
|
||||
logger.info("Board started", agents=len(self.all_agents))
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
"""Stop all board agents."""
|
||||
for agent in self.all_agents:
|
||||
await agent.stop()
|
||||
logger.info("Board stopped")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Organization:
|
||||
"""The complete AI organization."""
|
||||
|
||||
board: Board
|
||||
main_pm: "MainPMAgent"
|
||||
backend_cell: Cell
|
||||
frontend_cell: Cell
|
||||
ux_cell: Cell
|
||||
|
||||
@property
|
||||
def all_agents(self) -> list["Agent"]:
|
||||
"""Get all agents in the organization."""
|
||||
agents: list[Agent] = []
|
||||
agents.extend(self.board.all_agents)
|
||||
agents.append(self.main_pm)
|
||||
agents.extend(self.backend_cell.all_agents)
|
||||
agents.extend(self.frontend_cell.all_agents)
|
||||
agents.extend(self.ux_cell.all_agents)
|
||||
return agents
|
||||
|
||||
@property
|
||||
def agent_count(self) -> int:
|
||||
"""Total number of agents."""
|
||||
return len(self.all_agents)
|
||||
|
||||
async def start_all(self) -> None:
|
||||
"""Start the entire organization."""
|
||||
logger.info("Starting organization")
|
||||
|
||||
# Start board first
|
||||
await self.board.start_all()
|
||||
await self.main_pm.start()
|
||||
|
||||
# Then cells
|
||||
await self.backend_cell.start_all()
|
||||
await self.frontend_cell.start_all()
|
||||
await self.ux_cell.start_all()
|
||||
|
||||
logger.info("Organization started", total_agents=self.agent_count)
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
"""Stop the entire organization."""
|
||||
logger.info("Stopping organization")
|
||||
|
||||
# Stop cells first
|
||||
await self.ux_cell.stop_all()
|
||||
await self.frontend_cell.stop_all()
|
||||
await self.backend_cell.stop_all()
|
||||
|
||||
# Then management
|
||||
await self.main_pm.stop()
|
||||
await self.board.stop_all()
|
||||
|
||||
logger.info("Organization stopped")
|
||||
|
||||
def get_agent_by_id(self, agent_id: UUID) -> "Agent | None":
|
||||
"""Find an agent by ID."""
|
||||
for agent in self.all_agents:
|
||||
if agent.id == agent_id:
|
||||
return agent
|
||||
return None
|
||||
|
||||
def get_agent_by_slug(self, slug: str) -> "Agent | None":
|
||||
"""Find an agent by slug."""
|
||||
for agent in self.all_agents:
|
||||
if agent.config.slug == slug:
|
||||
return agent
|
||||
return None
|
||||
|
||||
def get_agents_by_team(self, team: Team) -> list["Agent"]:
|
||||
"""Get all agents in a team."""
|
||||
return [a for a in self.all_agents if a.team == team]
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Runtime Models
|
||||
|
||||
Domain types for the agent orchestrator system.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
|
||||
class OrchestratorAgentState(str, Enum):
|
||||
"""Agent lifecycle states in the orchestrator."""
|
||||
|
||||
OFFLINE = "offline"
|
||||
STARTING = "starting"
|
||||
ACTIVE = "active"
|
||||
WAITING_SHORT = "waiting_short" # Polling, agent still running
|
||||
WAITING_LONG = "waiting_long" # Terminated, will respawn on event
|
||||
IDLE = "idle"
|
||||
STOPPING = "stopping"
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrchestratorAgentConfig:
|
||||
"""Configuration for an agent in the orchestrator."""
|
||||
|
||||
agent_id: str
|
||||
blueprint_path: Path
|
||||
model: str = "sonnet" # sonnet, opus, haiku
|
||||
mcp_config_path: Path | None = None
|
||||
working_directory: Path | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentInstance:
|
||||
"""A running Claude Code agent instance."""
|
||||
|
||||
id: UUID = field(default_factory=uuid4)
|
||||
agent_id: str = ""
|
||||
state: OrchestratorAgentState = OrchestratorAgentState.OFFLINE
|
||||
process: asyncio.subprocess.Process | None = None
|
||||
config: OrchestratorAgentConfig | None = None
|
||||
started_at: datetime | None = None
|
||||
last_activity: datetime | None = None
|
||||
current_task_id: str | None = None
|
||||
error_count: int = 0
|
||||
waiting_for: str | None = None # For WAITING_LONG state
|
||||
waiting_context: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.id:
|
||||
self.id = uuid4()
|
||||
|
||||
|
||||
@dataclass
|
||||
class WaitingRecord:
|
||||
"""Tracks what a WAITING_LONG agent is waiting for."""
|
||||
|
||||
agent_id: str
|
||||
task_id: str | None
|
||||
waiting_for: str # "blocker_resolution", "qa_result", "answer", "assignment"
|
||||
waiting_since: datetime
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# Model mapping for cost optimization
|
||||
MODEL_MAP: dict[str, str] = {
|
||||
"sonnet": "claude-sonnet-4-20250514",
|
||||
"opus": "claude-opus-4-20250514",
|
||||
"haiku": "claude-haiku-4-20250514",
|
||||
}
|
||||
|
||||
|
||||
# Default model by role
|
||||
ROLE_MODEL_MAP: dict[str, str] = {
|
||||
"developer": "sonnet",
|
||||
"qa": "sonnet",
|
||||
"documenter": "haiku",
|
||||
"cell_pm": "sonnet",
|
||||
"main_pm": "sonnet",
|
||||
"auditor": "sonnet",
|
||||
"product_owner": "opus",
|
||||
"head_marketing": "opus",
|
||||
"ceo": "opus",
|
||||
}
|
||||
@@ -10,113 +10,26 @@ import contextlib
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.models.runtime import (
|
||||
MODEL_MAP,
|
||||
ROLE_MODEL_MAP,
|
||||
AgentInstance,
|
||||
OrchestratorAgentConfig,
|
||||
OrchestratorAgentState,
|
||||
WaitingRecord,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AGENT STATE
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AgentState(str, Enum):
|
||||
"""Agent lifecycle states."""
|
||||
|
||||
OFFLINE = "offline"
|
||||
STARTING = "starting"
|
||||
ACTIVE = "active"
|
||||
WAITING_SHORT = "waiting_short" # Polling, agent still running
|
||||
WAITING_LONG = "waiting_long" # Terminated, will respawn on event
|
||||
IDLE = "idle"
|
||||
STOPPING = "stopping"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AGENT CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentConfig:
|
||||
"""Configuration for an agent."""
|
||||
|
||||
agent_id: str
|
||||
blueprint_path: Path
|
||||
model: str = "sonnet" # sonnet, opus, haiku
|
||||
mcp_config_path: Path | None = None
|
||||
working_directory: Path | None = None
|
||||
|
||||
|
||||
# Model mapping for cost optimization
|
||||
MODEL_MAP = {
|
||||
"sonnet": "claude-sonnet-4-20250514",
|
||||
"opus": "claude-opus-4-20250514",
|
||||
"haiku": "claude-haiku-4-20250514",
|
||||
}
|
||||
|
||||
|
||||
# Default model by role
|
||||
ROLE_MODEL_MAP = {
|
||||
"developer": "sonnet",
|
||||
"qa": "sonnet",
|
||||
"documenter": "haiku",
|
||||
"cell_pm": "sonnet",
|
||||
"main_pm": "sonnet",
|
||||
"auditor": "sonnet",
|
||||
"product_owner": "opus",
|
||||
"head_marketing": "opus",
|
||||
"ceo": "opus",
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AGENT INSTANCE
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentInstance:
|
||||
"""A running Claude Code agent instance."""
|
||||
|
||||
id: UUID = field(default_factory=uuid4)
|
||||
agent_id: str = ""
|
||||
state: AgentState = AgentState.OFFLINE
|
||||
process: asyncio.subprocess.Process | None = None
|
||||
config: AgentConfig | None = None
|
||||
started_at: datetime | None = None
|
||||
last_activity: datetime | None = None
|
||||
current_task_id: str | None = None
|
||||
error_count: int = 0
|
||||
waiting_for: str | None = None # For WAITING_LONG state
|
||||
waiting_context: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.id:
|
||||
self.id = uuid4()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WAITING RECORD
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class WaitingRecord:
|
||||
"""Tracks what a WAITING_LONG agent is waiting for."""
|
||||
|
||||
agent_id: str
|
||||
task_id: str | None
|
||||
waiting_for: str # "blocker_resolution", "qa_result", "answer", "assignment"
|
||||
waiting_since: datetime
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
# Re-export for backwards compatibility
|
||||
AgentState = OrchestratorAgentState
|
||||
AgentConfig = OrchestratorAgentConfig
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user