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 abc import ABC, abstractmethod
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from enum import Enum
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import structlog
|
import structlog
|
||||||
from anthropic import AsyncAnthropic
|
from anthropic import AsyncAnthropic
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from anthropic.types import MessageParam
|
from anthropic.types import MessageParam
|
||||||
@@ -26,6 +24,7 @@ if TYPE_CHECKING:
|
|||||||
from roboco.config import settings
|
from roboco.config import settings
|
||||||
from roboco.llm import ToonAdapter
|
from roboco.llm import ToonAdapter
|
||||||
from roboco.models import AgentRole, AgentStatus, Team
|
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)
|
# Type for reasoning stream callback (injected to avoid API layer coupling)
|
||||||
ReasoningStreamCallback = Callable[[UUID, str], Awaitable[None]]
|
ReasoningStreamCallback = Callable[[UUID, str], Awaitable[None]]
|
||||||
@@ -55,65 +54,6 @@ def get_reasoning_stream_callback() -> ReasoningStreamCallback | None:
|
|||||||
logger = structlog.get_logger()
|
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
|
# BASE AGENT CLASS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
+19
-101
@@ -5,17 +5,26 @@ Implementation of Board-level workflows from the blueprint.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from roboco.agents.base import Agent, AgentConfig
|
from roboco.agents.base import Agent
|
||||||
from roboco.models import AgentRole, Team
|
from roboco.models import AgentRole, Team
|
||||||
|
from roboco.models.agents import (
|
||||||
|
AgentConfig,
|
||||||
|
AuditFlag,
|
||||||
|
AuditorFlagSeverity,
|
||||||
|
AuditorPhase,
|
||||||
|
AuditReport,
|
||||||
|
Campaign,
|
||||||
|
Feature,
|
||||||
|
HeadMarketingPhase,
|
||||||
|
ProductOwnerPhase,
|
||||||
|
)
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
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):
|
class ProductOwnerAgent(Agent):
|
||||||
"""
|
"""
|
||||||
Product Owner agent that defines what to build.
|
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):
|
class HeadMarketingAgent(Agent):
|
||||||
"""
|
"""
|
||||||
Head of Marketing 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):
|
class AuditorAgent(Agent):
|
||||||
"""
|
"""
|
||||||
Auditor agent - the CEO's secret ally.
|
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(
|
self._flags.append(
|
||||||
AuditFlag(
|
AuditFlag(
|
||||||
id=uuid4(),
|
id=uuid4(),
|
||||||
severity=FlagSeverity.CONCERN,
|
severity=AuditorFlagSeverity.CONCERN,
|
||||||
category="analysis",
|
category="analysis",
|
||||||
description=analysis[:500],
|
description=analysis[:500],
|
||||||
evidence=["Automated analysis"],
|
evidence=["Automated analysis"],
|
||||||
@@ -542,7 +457,9 @@ efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff step
|
|||||||
"""
|
"""
|
||||||
self.log.debug("FLAG phase")
|
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:
|
if critical_flags:
|
||||||
# Immediate alert to CEO
|
# Immediate alert to CEO
|
||||||
await self._alert_ceo(critical_flags)
|
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
|
self._last_report is None
|
||||||
or hours_elapsed >= hours_in_day
|
or hours_elapsed >= hours_in_day
|
||||||
or any(
|
or any(
|
||||||
f.severity in [FlagSeverity.CONCERN, FlagSeverity.CRITICAL]
|
f.severity
|
||||||
|
in [AuditorFlagSeverity.CONCERN, AuditorFlagSeverity.CRITICAL]
|
||||||
for f in self._flags
|
for f in self._flags
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -607,7 +525,7 @@ efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff step
|
|||||||
self._flags.append(
|
self._flags.append(
|
||||||
AuditFlag(
|
AuditFlag(
|
||||||
id=uuid4(),
|
id=uuid4(),
|
||||||
severity=FlagSeverity.INFO,
|
severity=AuditorFlagSeverity.INFO,
|
||||||
category=audit_type,
|
category=audit_type,
|
||||||
description=findings,
|
description=findings,
|
||||||
evidence=[f"{audit_type} audit"],
|
evidence=[f"{audit_type} audit"],
|
||||||
|
|||||||
@@ -7,50 +7,19 @@ Handles task lifecycle:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import structlog
|
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 import AgentRole, TaskStatus, Team
|
||||||
|
from roboco.models.agents import AgentConfig, DevTaskPhase, TaskContext
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
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):
|
class DeveloperAgent(Agent):
|
||||||
"""
|
"""
|
||||||
Developer agent that follows the Dev Lifecycle.
|
Developer agent that follows the Dev Lifecycle.
|
||||||
|
|||||||
@@ -7,79 +7,26 @@ Handles documentation lifecycle:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import structlog
|
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 import AgentRole, TaskStatus, Team
|
||||||
|
from roboco.models.agents import (
|
||||||
|
AgentConfig,
|
||||||
|
DocContext,
|
||||||
|
DocTaskPhase,
|
||||||
|
DocType,
|
||||||
|
DocumentSpec,
|
||||||
|
)
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
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):
|
class DocumenterAgent(Agent):
|
||||||
"""
|
"""
|
||||||
Documenter agent that follows the Documenter Lifecycle.
|
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.
|
deploying complete cells with their full agent complement.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
import structlog
|
|
||||||
|
|
||||||
from roboco.agents.base import Agent
|
|
||||||
from roboco.agents.board import (
|
from roboco.agents.board import (
|
||||||
AuditorAgent,
|
|
||||||
HeadMarketingAgent,
|
|
||||||
ProductOwnerAgent,
|
|
||||||
create_auditor,
|
create_auditor,
|
||||||
create_head_marketing,
|
create_head_marketing,
|
||||||
create_product_owner,
|
create_product_owner,
|
||||||
)
|
)
|
||||||
from roboco.agents.developer import (
|
from roboco.agents.developer import (
|
||||||
DeveloperAgent,
|
|
||||||
create_backend_developer,
|
create_backend_developer,
|
||||||
create_frontend_developer,
|
create_frontend_developer,
|
||||||
create_ux_developer,
|
create_ux_developer,
|
||||||
)
|
)
|
||||||
from roboco.agents.documenter import (
|
from roboco.agents.documenter import (
|
||||||
DocumenterAgent,
|
|
||||||
create_backend_documenter,
|
create_backend_documenter,
|
||||||
create_frontend_documenter,
|
create_frontend_documenter,
|
||||||
create_ux_documenter,
|
create_ux_documenter,
|
||||||
)
|
)
|
||||||
from roboco.agents.pm import (
|
from roboco.agents.pm import (
|
||||||
CellPMAgent,
|
|
||||||
MainPMAgent,
|
|
||||||
create_backend_pm,
|
create_backend_pm,
|
||||||
create_frontend_pm,
|
create_frontend_pm,
|
||||||
create_main_pm,
|
create_main_pm,
|
||||||
create_ux_pm,
|
create_ux_pm,
|
||||||
)
|
)
|
||||||
from roboco.agents.qa import (
|
from roboco.agents.qa import (
|
||||||
QAAgent,
|
|
||||||
create_backend_qa,
|
create_backend_qa,
|
||||||
create_frontend_qa,
|
create_frontend_qa,
|
||||||
create_ux_qa,
|
create_ux_qa,
|
||||||
)
|
)
|
||||||
from roboco.models import Team
|
from roboco.models import Team
|
||||||
|
from roboco.models.organization import Board, Cell, Organization
|
||||||
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]
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# CELL FACTORIES
|
# CELL FACTORIES
|
||||||
|
|||||||
+9
-59
@@ -9,76 +9,26 @@ Main PM:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import structlog
|
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 import AgentRole, NotificationType, TaskStatus, Team
|
||||||
|
from roboco.models.agents import (
|
||||||
|
AgentConfig,
|
||||||
|
CellPMPhase,
|
||||||
|
CellStatus,
|
||||||
|
Escalation,
|
||||||
|
MainPMPhase,
|
||||||
|
TaskAssignment,
|
||||||
|
)
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
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):
|
class CellPMAgent(Agent):
|
||||||
"""
|
"""
|
||||||
Cell PM agent that manages a single cell (Backend, Frontend, or UX/UI).
|
Cell PM agent that manages a single cell (Backend, Frontend, or UX/UI).
|
||||||
|
|||||||
+8
-51
@@ -7,68 +7,25 @@ Handles review lifecycle:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import structlog
|
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 import AgentRole, TaskStatus, Team
|
||||||
|
from roboco.models.agents import (
|
||||||
|
AgentConfig,
|
||||||
|
QATaskPhase,
|
||||||
|
ReviewContext,
|
||||||
|
TestCase,
|
||||||
|
TestResult,
|
||||||
|
)
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
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):
|
class QAAgent(Agent):
|
||||||
"""
|
"""
|
||||||
QA agent that follows the QA Lifecycle.
|
QA agent that follows the QA Lifecycle.
|
||||||
|
|||||||
@@ -8,79 +8,24 @@ from typing import Annotated
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Query, status
|
from fastapi import APIRouter, HTTPException, Query, status
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from roboco.api.deps import CurrentAgentContext, DbSession, PermissionServiceDep
|
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.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
|
from roboco.utils.converters import require_uuid, to_python_uuid
|
||||||
|
|
||||||
router = APIRouter()
|
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
|
# Routes
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
+11
-102
@@ -6,15 +6,24 @@ Provides aggregated views, alerts, and reporting.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from enum import Enum
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Query, status
|
from fastapi import APIRouter, HTTPException, Query, status
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from roboco.api.deps import DbSession
|
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.db.tables import AgentTable, MessageTable, TaskTable
|
||||||
from roboco.models.base import AgentStatus, Team
|
from roboco.models.base import AgentStatus, Team
|
||||||
from roboco.models.dashboard import CreateFlagParams
|
from roboco.models.dashboard import CreateFlagParams
|
||||||
@@ -25,106 +34,6 @@ from roboco.services.metrics import get_metrics_service
|
|||||||
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
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
|
# AUDITOR DASHBOARD
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ Endpoints for monitoring application health and readiness.
|
|||||||
|
|
||||||
import redis.asyncio as redis
|
import redis.asyncio as redis
|
||||||
from fastapi import APIRouter, status
|
from fastapi import APIRouter, status
|
||||||
from pydantic import BaseModel
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from roboco.api.schemas.health import HealthResponse, ReadinessResponse
|
||||||
from roboco.config import settings
|
from roboco.config import settings
|
||||||
from roboco.db.base import get_db_context
|
from roboco.db.base import get_db_context
|
||||||
|
|
||||||
@@ -36,22 +36,6 @@ async def _check_redis() -> tuple[str, bool]:
|
|||||||
return str(e), False
|
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(
|
@router.get(
|
||||||
"/health",
|
"/health",
|
||||||
response_model=HealthResponse,
|
response_model=HealthResponse,
|
||||||
|
|||||||
+14
-156
@@ -4,14 +4,26 @@ Journal API Routes
|
|||||||
Agent personal journals for reflection, growth tracking, and debugging.
|
Agent personal journals for reflection, growth tracking, and debugging.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from roboco.api.deps import CurrentAgentContext, DbSession
|
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.base import AgentRole, JournalEntryType
|
||||||
from roboco.models.journal import (
|
from roboco.models.journal import (
|
||||||
DecisionLogParams,
|
DecisionLogParams,
|
||||||
@@ -23,163 +35,9 @@ from roboco.models.journal import (
|
|||||||
)
|
)
|
||||||
from roboco.services.journal import ListEntriesFilter, get_journal_service
|
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"])
|
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
|
# JOURNAL ENDPOINTS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -9,13 +9,18 @@ from typing import Annotated
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from roboco.api.deps import CurrentAgentId, DbSession
|
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.db.tables import MessageTable, SessionTable
|
||||||
from roboco.models import MessageType
|
|
||||||
from roboco.services.messaging import (
|
from roboco.services.messaging import (
|
||||||
MessageCreateRequest as ServiceMessageRequest,
|
MessageCreateRequest as ServiceMessageRequest,
|
||||||
)
|
)
|
||||||
@@ -27,75 +32,6 @@ from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid
|
|||||||
router = APIRouter()
|
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
|
# Routes
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -10,81 +10,25 @@ from typing import Annotated, Any
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from roboco.api.deps import CurrentAgentId, DbSession
|
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.db.tables import AgentTable, NotificationTable
|
||||||
from roboco.enforcement import (
|
from roboco.enforcement import (
|
||||||
NotificationPermissionError,
|
NotificationPermissionError,
|
||||||
validate_notification_permission,
|
validate_notification_permission,
|
||||||
)
|
)
|
||||||
from roboco.models import NotificationPriority, NotificationType
|
|
||||||
from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid_list
|
from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid_list
|
||||||
|
|
||||||
router = APIRouter()
|
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
|
# Routes
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
+45
-151
@@ -6,12 +6,29 @@ Knowledge base, RAG queries, and semantic search endpoints.
|
|||||||
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID, uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
from fastapi import APIRouter, HTTPException, status
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from roboco.api.deps import CurrentAgentContext
|
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.models import AgentRole
|
||||||
from roboco.services.optimal import (
|
from roboco.services.optimal import (
|
||||||
IndexType,
|
IndexType,
|
||||||
@@ -22,150 +39,6 @@ from roboco.services.optimal import (
|
|||||||
router = APIRouter(prefix="/optimal", tags=["optimal"])
|
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
|
# 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(
|
@router.post(
|
||||||
@@ -571,7 +464,8 @@ async def create_prompt_template(
|
|||||||
template_id = str(uuid4())
|
template_id = str(uuid4())
|
||||||
created_at = datetime.now(UTC).isoformat()
|
created_at = datetime.now(UTC).isoformat()
|
||||||
|
|
||||||
_prompt_templates[template_id] = {
|
templates = _get_prompt_templates()
|
||||||
|
templates[template_id] = {
|
||||||
"id": template_id,
|
"id": template_id,
|
||||||
"name": request.name,
|
"name": request.name,
|
||||||
"template": request.template,
|
"template": request.template,
|
||||||
@@ -601,7 +495,7 @@ async def list_prompt_templates(
|
|||||||
"""List all prompt templates, optionally filtered by category."""
|
"""List all prompt templates, optionally filtered by category."""
|
||||||
# Any authenticated agent can list templates
|
# Any authenticated agent can list templates
|
||||||
_ = agent # Used for authentication
|
_ = agent # Used for authentication
|
||||||
templates = list(_prompt_templates.values())
|
templates = list(_get_prompt_templates().values())
|
||||||
|
|
||||||
if category:
|
if category:
|
||||||
templates = [t for t in templates if t.get("category") == 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 datetime import datetime
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
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
|
from roboco.runtime import AgentOrchestrator
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -36,56 +41,6 @@ def get_orchestrator() -> AgentOrchestrator:
|
|||||||
return _OrchestratorHolder.instance
|
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
|
# Routes
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -10,11 +10,16 @@ from typing import Annotated
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from roboco.api.deps import CurrentAgentId, DbSession
|
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.db.tables import GroupTable, SessionTable
|
||||||
from roboco.models import SessionStatus
|
from roboco.models import SessionStatus
|
||||||
from roboco.utils.converters import require_uuid
|
from roboco.utils.converters import require_uuid
|
||||||
@@ -22,54 +27,6 @@ from roboco.utils.converters import require_uuid
|
|||||||
router = APIRouter()
|
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
|
# Routes
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -6,74 +6,24 @@ transcription and extraction pipelines.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request, status
|
from fastapi import APIRouter, HTTPException, Request, status
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from roboco.api.deps import CurrentAgentContext, CurrentAgentId, PermissionServiceDep
|
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 import MessageType
|
||||||
from roboco.models.message import RawStream
|
from roboco.models.message import RawStream
|
||||||
|
|
||||||
router = APIRouter()
|
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
|
# STREAM PROCESSING ROUTES
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
+12
-108
@@ -4,20 +4,29 @@ Task API Routes
|
|||||||
Full CRUD operations and lifecycle management for tasks.
|
Full CRUD operations and lifecycle management for tasks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Query, status
|
from fastapi import APIRouter, HTTPException, Query, status
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from roboco.api.deps import (
|
from roboco.api.deps import (
|
||||||
CurrentAgentContext,
|
CurrentAgentContext,
|
||||||
DbSession,
|
DbSession,
|
||||||
PermissionServiceDep,
|
PermissionServiceDep,
|
||||||
)
|
)
|
||||||
|
from roboco.api.schemas.tasks import (
|
||||||
|
CheckpointRequest,
|
||||||
|
CommitRequest,
|
||||||
|
ListTasksQuery,
|
||||||
|
ProgressRequest,
|
||||||
|
QANotes,
|
||||||
|
TaskCountResponse,
|
||||||
|
TaskResponse,
|
||||||
|
TaskUpdate,
|
||||||
|
TeamTasksQuery,
|
||||||
|
)
|
||||||
from roboco.db.tables import TaskTable
|
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.models.task import TaskCreate
|
||||||
from roboco.services.audit import get_audit_service
|
from roboco.services.audit import get_audit_service
|
||||||
from roboco.services.permissions import TaskAction
|
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"])
|
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:
|
def _to_response(task: TaskTable) -> TaskResponse:
|
||||||
"""Convert TaskTable to TaskResponse with proper UUID conversion."""
|
"""Convert TaskTable to TaskResponse with proper UUID conversion."""
|
||||||
return TaskResponse(
|
return TaskResponse(
|
||||||
@@ -112,61 +71,6 @@ def _to_response_list(tasks: list[TaskTable]) -> list[TaskResponse]:
|
|||||||
return [_to_response(t) for t in tasks]
|
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
|
# 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 asyncio
|
||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
from roboco.api.schemas.websocket import (
|
||||||
|
NewMessageBroadcast,
|
||||||
|
)
|
||||||
from roboco.config import settings
|
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()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@@ -215,69 +203,6 @@ async def validate_channel_access(channel_id: UUID, agent_id: UUID) -> bool:
|
|||||||
return False
|
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
|
# WebSocket Routes
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -4,23 +4,10 @@ Task Ownership Enforcement
|
|||||||
Validates task ownership and claim rules.
|
Validates task ownership and claim rules.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from roboco.agents_config import get_agent_role, get_agent_team
|
from roboco.agents_config import get_agent_role, get_agent_team
|
||||||
from roboco.enforcement.task_lifecycle import is_waiting_state
|
from roboco.enforcement.task_lifecycle import is_waiting_state
|
||||||
from roboco.exceptions import RobocoError
|
from roboco.exceptions import RobocoError
|
||||||
|
from roboco.models.enforcement import TaskClaimContext
|
||||||
|
|
||||||
@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]
|
|
||||||
|
|
||||||
|
|
||||||
class TaskOwnershipError(RobocoError):
|
class TaskOwnershipError(RobocoError):
|
||||||
|
|||||||
+1
-91
@@ -6,108 +6,18 @@ Redis-based pub/sub event system for workflow triggers.
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
import json
|
|
||||||
from collections.abc import Callable, Coroutine
|
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 typing import Any
|
||||||
from uuid import UUID, uuid4
|
|
||||||
|
|
||||||
import redis.asyncio as redis
|
import redis.asyncio as redis
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from roboco.config import settings
|
from roboco.config import settings
|
||||||
|
from roboco.models.events import Event, EventType
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
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
|
# Type for event handlers
|
||||||
EventHandler = Callable[[Event], Coroutine[Any, Any, None]]
|
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.
|
Uses dependency injection pattern for services to maintain separation of concerns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from typing import Any
|
||||||
from typing import TYPE_CHECKING, Any, Protocol
|
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from roboco.events.bus import Event, EventType, get_event_bus
|
from roboco.events.bus import Event, EventType, get_event_bus
|
||||||
|
from roboco.models.events import (
|
||||||
if TYPE_CHECKING:
|
EventContext,
|
||||||
from roboco.runtime.orchestrator import WaitingRecord
|
NotificationServiceProtocol,
|
||||||
|
OrchestratorAccessProtocol,
|
||||||
|
)
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
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
|
# Global context instance - set during initialization
|
||||||
_context = EventContext()
|
_context = EventContext()
|
||||||
|
|
||||||
|
|||||||
+1
-66
@@ -4,72 +4,7 @@ TOON Metrics
|
|||||||
Tracks token savings and usage statistics for TOON vs JSON serialization.
|
Tracks token savings and usage statistics for TOON vs JSON serialization.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from roboco.models.llm import ToonMetrics
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# Global metrics holder
|
# Global metrics holder
|
||||||
|
|||||||
@@ -19,25 +19,17 @@ Usage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
import toon
|
import toon
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from roboco.models.llm import ToonConfig
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ToonConfig:
|
|
||||||
"""Configuration for TOON encoding."""
|
|
||||||
|
|
||||||
delimiter: str = ","
|
|
||||||
indent: int = 2
|
|
||||||
include_length: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
class ToonAdapter:
|
class ToonAdapter:
|
||||||
"""
|
"""
|
||||||
Adapter for TOON serialization at LLM boundaries.
|
Adapter for TOON serialization at LLM boundaries.
|
||||||
|
|||||||
@@ -19,107 +19,21 @@ from typing import Any
|
|||||||
import httpx
|
import httpx
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from roboco.config import settings
|
from roboco.config import settings
|
||||||
from roboco.llm import ToonAdapter
|
from roboco.llm import ToonAdapter
|
||||||
|
from roboco.mcp.schemas import (
|
||||||
|
DecisionLogInput,
|
||||||
|
JournalEntryInput,
|
||||||
|
LearningInput,
|
||||||
|
StruggleInput,
|
||||||
|
TaskReflectionInput,
|
||||||
|
)
|
||||||
|
|
||||||
# Global TOON adapter for encoding journal data
|
# Global TOON adapter for encoding journal data
|
||||||
_toon = ToonAdapter()
|
_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
|
# HELPER FUNCTIONS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -19,35 +19,20 @@ from typing import Any
|
|||||||
import httpx
|
import httpx
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from roboco.agents_config import CHANNEL_ACCESS
|
from roboco.agents_config import CHANNEL_ACCESS
|
||||||
from roboco.config import settings
|
from roboco.config import settings
|
||||||
from roboco.llm import ToonAdapter
|
from roboco.llm import ToonAdapter
|
||||||
|
from roboco.mcp.schemas import (
|
||||||
|
AskQuestionInput,
|
||||||
|
ReportBlockerInput,
|
||||||
|
SendMessageInput,
|
||||||
|
)
|
||||||
|
|
||||||
# Global TOON adapter for encoding message data
|
# Global TOON adapter for encoding message data
|
||||||
_toon = ToonAdapter()
|
_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
|
# HELPER FUNCTIONS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -307,24 +292,6 @@ async def _handle_message_get(message_id: str) -> dict[str, Any]:
|
|||||||
return {"message": resp.json()}
|
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(
|
async def _handle_ask_question(
|
||||||
data: AskQuestionInput,
|
data: AskQuestionInput,
|
||||||
send_fn: Callable[[SendMessageInput], Awaitable[dict[str, Any]]],
|
send_fn: Callable[[SendMessageInput], Awaitable[dict[str, Any]]],
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from typing import Any
|
|||||||
import httpx
|
import httpx
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from roboco.agents_config import (
|
from roboco.agents_config import (
|
||||||
NOTIFICATION_PERMISSIONS,
|
NOTIFICATION_PERMISSIONS,
|
||||||
@@ -24,25 +23,7 @@ from roboco.agents_config import (
|
|||||||
get_agent_role,
|
get_agent_role,
|
||||||
)
|
)
|
||||||
from roboco.config import settings
|
from roboco.config import settings
|
||||||
|
from roboco.mcp.schemas import SendNotificationInput
|
||||||
# =============================================================================
|
|
||||||
# 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")
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# HELPER FUNCTIONS
|
# 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 json
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID, uuid4
|
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
|
from roboco.models.runtime import (
|
||||||
|
MODEL_MAP,
|
||||||
|
ROLE_MODEL_MAP,
|
||||||
|
AgentInstance,
|
||||||
|
OrchestratorAgentConfig,
|
||||||
|
OrchestratorAgentState,
|
||||||
|
WaitingRecord,
|
||||||
|
)
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
# Re-export for backwards compatibility
|
||||||
# =============================================================================
|
AgentState = OrchestratorAgentState
|
||||||
# AGENT STATE
|
AgentConfig = OrchestratorAgentConfig
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user