Add architecture package and application skeleton

This commit is contained in:
2026-07-03 21:09:47 +03:00
parent 7d6cc1c83e
commit ce2b263e52
47 changed files with 2377 additions and 53 deletions
+2
View File
@@ -0,0 +1,2 @@
"""AI Orchestrator package."""
@@ -0,0 +1,2 @@
"""Application layer."""
+102
View File
@@ -0,0 +1,102 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Protocol
from ai_orchestrator.domain.events import DomainEvent
from ai_orchestrator.domain.models import (
ActionDescriptor,
ConfirmationRequest,
ExecutionGraph,
PolicyDecision,
Task,
WorkerSession,
)
@dataclass(slots=True)
class ModelInvocationResult:
status: str
provider: str
model: str
message: dict[str, Any]
tool_calls: list[dict[str, Any]] = field(default_factory=list)
usage: dict[str, Any] = field(default_factory=dict)
error: dict[str, Any] | None = None
@dataclass(slots=True)
class ToolInvocationResult:
status: str
content: dict[str, Any]
artifacts: list[dict[str, Any]] = field(default_factory=list)
logs: list[str] = field(default_factory=list)
error: dict[str, Any] | None = None
class TaskRepository(Protocol):
def create(self, task: Task) -> Task: ...
def save(self, task: Task) -> Task: ...
def get(self, task_id: str) -> Task | None: ...
class GraphRepository(Protocol):
def save(self, graph: ExecutionGraph) -> ExecutionGraph: ...
def get(self, task_id: str) -> ExecutionGraph | None: ...
class ConfirmationRepository(Protocol):
def create(self, confirmation: ConfirmationRequest) -> ConfirmationRequest: ...
def get(self, confirmation_id: str) -> ConfirmationRequest | None: ...
def save(self, confirmation: ConfirmationRequest) -> ConfirmationRequest: ...
class WorkerRepository(Protocol):
def save(self, worker: WorkerSession) -> WorkerSession: ...
def get(self, session_id: str) -> WorkerSession | None: ...
def list_active(self) -> list[WorkerSession]: ...
class EventStore(Protocol):
def append(self, event: DomainEvent) -> DomainEvent: ...
def list_by_task(self, task_id: str) -> list[DomainEvent]: ...
class PolicyEvaluator(Protocol):
def evaluate(self, action: ActionDescriptor, project_id: str) -> PolicyDecision: ...
class ModelRouter(Protocol):
def run(
self,
*,
project_id: str,
slot: str,
messages: list[dict[str, Any]],
task_context: dict[str, Any],
) -> ModelInvocationResult: ...
class ToolGateway(Protocol):
def call(
self,
*,
project_id: str,
server_id: str,
tool_name: str,
args: dict[str, Any],
task_context: dict[str, Any],
) -> ToolInvocationResult: ...
class WorkerGateway(Protocol):
def dispatch(
self,
*,
project_id: str,
worker_session_id: str,
command_name: str,
args: dict[str, Any],
task_context: dict[str, Any],
) -> ToolInvocationResult: ...
@@ -0,0 +1,2 @@
"""Application services."""
@@ -0,0 +1,134 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from ai_orchestrator.application.ports import (
ConfirmationRepository,
EventStore,
GraphRepository,
PolicyEvaluator,
TaskRepository,
)
from ai_orchestrator.domain.enums import NodeType, PolicyDecisionType
from ai_orchestrator.domain.events import DomainEvent
from ai_orchestrator.domain.models import (
ActionDescriptor,
ConfirmationRequest,
ExecutionGraph,
ExecutionNode,
Task,
)
@dataclass(slots=True)
class CreateTaskRequest:
project_id: str
goal: str
inputs: dict[str, Any]
conversation_id: str | None = None
requested_mode: str = "auto"
@dataclass(slots=True)
class OrchestratorService:
task_repository: TaskRepository
graph_repository: GraphRepository
confirmation_repository: ConfirmationRepository
event_store: EventStore
policy_evaluator: PolicyEvaluator
def create_task(self, request: CreateTaskRequest) -> Task:
task = Task(
project_id=request.project_id,
goal=request.goal,
inputs=request.inputs,
conversation_id=request.conversation_id,
requested_mode=request.requested_mode,
effective_mode=request.requested_mode,
)
self.task_repository.create(task)
self.event_store.append(
DomainEvent(
event_type="task_created",
task_id=task.task_id,
conversation_id=task.conversation_id,
payload={"goal": task.goal, "requested_mode": task.requested_mode},
)
)
return task
def plan_task(self, task_id: str) -> ExecutionGraph:
task = self._require_task(task_id)
planner = ExecutionNode(
task_id=task.task_id,
node_type=NodeType.PLANNER,
input_data={"goal": task.goal, "inputs": task.inputs},
)
finalizer = ExecutionNode(
task_id=task.task_id,
node_type=NodeType.FINALIZER,
input_data={"task_id": task.task_id},
dependencies=[planner.node_id],
retryable=False,
)
graph = ExecutionGraph(task_id=task.task_id, nodes=[planner, finalizer])
self.graph_repository.save(graph)
task.mark_planned()
self.task_repository.save(task)
self.event_store.append(
DomainEvent(
event_type="task_planned",
task_id=task.task_id,
conversation_id=task.conversation_id,
payload={"node_count": len(graph.nodes)},
)
)
return graph
def evaluate_action(
self,
*,
task_id: str,
node_id: str,
action: ActionDescriptor,
) -> ConfirmationRequest | None:
task = self._require_task(task_id)
decision = self.policy_evaluator.evaluate(action, task.project_id)
self.event_store.append(
DomainEvent(
event_type="policy_decision_made",
task_id=task.task_id,
conversation_id=task.conversation_id,
node_id=node_id,
payload={
"action_name": action.action_name,
"resource": action.resource,
"risk_level": action.risk_level.value,
"decision": decision.decision.value,
},
)
)
if decision.decision != PolicyDecisionType.CONFIRM:
return None
confirmation = ConfirmationRequest(task_id=task.task_id, node_id=node_id, decision=decision)
self.confirmation_repository.create(confirmation)
task.wait_for_confirmation(node_id=node_id)
self.task_repository.save(task)
self.event_store.append(
DomainEvent(
event_type="confirmation_requested",
task_id=task.task_id,
conversation_id=task.conversation_id,
node_id=node_id,
payload={"confirmation_id": confirmation.confirmation_id},
)
)
return confirmation
def _require_task(self, task_id: str) -> Task:
task = self.task_repository.get(task_id)
if task is None:
raise LookupError(f"Task not found: {task_id}")
return task
@@ -0,0 +1,47 @@
from __future__ import annotations
from dataclasses import dataclass
from ai_orchestrator.application.ports import EventStore, WorkerRepository
from ai_orchestrator.domain.events import DomainEvent
from ai_orchestrator.domain.models import WorkerSession
@dataclass(slots=True)
class RegisterWorkerRequest:
worker_id: str
name: str
machine: str
os: str
version: str
capabilities: list[str]
@dataclass(slots=True)
class WorkerService:
worker_repository: WorkerRepository
event_store: EventStore
def register(self, request: RegisterWorkerRequest) -> WorkerSession:
worker = WorkerSession(
worker_id=request.worker_id,
name=request.name,
machine=request.machine,
os=request.os,
version=request.version,
capabilities=request.capabilities,
)
worker.mark_online()
self.worker_repository.save(worker)
self.event_store.append(
DomainEvent(
event_type="worker_registered",
task_id="system",
payload={
"session_id": worker.session_id,
"worker_id": worker.worker_id,
"capabilities": worker.capabilities,
},
)
)
return worker
+82
View File
@@ -0,0 +1,82 @@
from __future__ import annotations
from enum import StrEnum
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class ProviderType(StrEnum):
LOCAL = "local"
EXTERNAL = "external"
DISABLED = "disabled"
class PolicyMode(StrEnum):
FULL_AUTO = "full_auto"
CONFIRM = "confirm"
MANUAL = "manual"
DISABLED = "disabled"
class ModelSlotConfig(BaseModel):
provider: ProviderType
model: str | None = None
base_url: str | None = None
api_key_env: str | None = None
class ModelsConfig(BaseModel):
weak: ModelSlotConfig
strong: ModelSlotConfig
vision: ModelSlotConfig
embedding: ModelSlotConfig
class ExecutionConfig(BaseModel):
max_graph_nodes: int = 20
max_agent_steps: int = 8
max_local_retries: int = 2
allow_external_models: bool = False
allow_paid_fallback: bool = False
class PolicyResourceConfig(BaseModel):
filesystem: PolicyMode = PolicyMode.CONFIRM
shell: PolicyMode = PolicyMode.CONFIRM
sql: PolicyMode = PolicyMode.CONFIRM
mcp: PolicyMode = PolicyMode.CONFIRM
external_models: PolicyMode = PolicyMode.CONFIRM
desktop: PolicyMode = PolicyMode.CONFIRM
browser: PolicyMode = PolicyMode.CONFIRM
network: PolicyMode = PolicyMode.CONFIRM
cost: PolicyMode = PolicyMode.CONFIRM
system: PolicyMode = PolicyMode.CONFIRM
class PolicyConfig(BaseModel):
default_mode: PolicyMode = PolicyMode.CONFIRM
resources: PolicyResourceConfig = Field(default_factory=PolicyResourceConfig)
class ProjectMetadata(BaseModel):
id: str
name: str
class ProjectConfig(BaseModel):
project: ProjectMetadata
models: ModelsConfig | None = None
execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
policy: PolicyConfig = Field(default_factory=PolicyConfig)
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="AI_ORCH_", extra="ignore")
app_name: str = "ai-orchestrator"
env: str = "dev"
host: str = "127.0.0.1"
port: int = 8080
default_project_config: str = "configs/examples/local_only.yaml"
+2
View File
@@ -0,0 +1,2 @@
"""Delivery layer."""
@@ -0,0 +1,2 @@
"""HTTP delivery adapters."""
+190
View File
@@ -0,0 +1,190 @@
from __future__ import annotations
from fastapi import FastAPI, HTTPException
from sse_starlette.sse import EventSourceResponse
from ai_orchestrator.application.services.orchestrator import CreateTaskRequest, OrchestratorService
from ai_orchestrator.application.services.workers import RegisterWorkerRequest, WorkerService
from ai_orchestrator.config import AppSettings
from ai_orchestrator.delivery.http.schemas import (
ChatRequest,
ChatResponse,
ConfirmationApproveRequest,
ConfirmationRejectRequest,
CreateTaskRequestSchema,
TaskStatusResponse,
WorkerListItem,
WorkerListResponse,
WorkerRegisterRequest,
)
from ai_orchestrator.infrastructure.config_loader import load_project_config
from ai_orchestrator.infrastructure.policy import StaticProjectPolicyEvaluator
from ai_orchestrator.infrastructure.storage.memory import (
InMemoryConfirmationRepository,
InMemoryEventStore,
InMemoryGraphRepository,
InMemoryTaskRepository,
InMemoryWorkerRepository,
)
def create_app(settings: AppSettings | None = None) -> FastAPI:
settings = settings or AppSettings()
project_config = load_project_config(settings.default_project_config)
task_repository = InMemoryTaskRepository()
graph_repository = InMemoryGraphRepository()
confirmation_repository = InMemoryConfirmationRepository()
worker_repository = InMemoryWorkerRepository()
event_store = InMemoryEventStore()
policy_evaluator = StaticProjectPolicyEvaluator(
projects={project_config.project.id: project_config}
)
orchestrator = OrchestratorService(
task_repository=task_repository,
graph_repository=graph_repository,
confirmation_repository=confirmation_repository,
event_store=event_store,
policy_evaluator=policy_evaluator,
)
app = FastAPI(title="AI Orchestrator", version="0.1.0")
app.state.task_repository = task_repository
app.state.graph_repository = graph_repository
app.state.confirmation_repository = confirmation_repository
app.state.worker_repository = worker_repository
app.state.event_store = event_store
app.state.orchestrator = orchestrator
app.state.worker_service = WorkerService(
worker_repository=worker_repository,
event_store=event_store,
)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/chat", response_model=ChatResponse)
def post_chat(payload: ChatRequest) -> ChatResponse:
task = orchestrator.create_task(
CreateTaskRequest(
project_id=payload.project_id,
goal=payload.message,
inputs={"attachments": payload.attachments, "preferences": payload.preferences},
conversation_id=payload.conversation_id or "conv_default",
requested_mode=payload.mode,
)
)
orchestrator.plan_task(task.task_id)
return ChatResponse(
conversation_id=task.conversation_id or "conv_default",
task_id=task.task_id,
response_type="task_started",
message="Task accepted and planned.",
cards=[{"type": "plan_card", "task_id": task.task_id}],
)
@app.post("/tasks", response_model=TaskStatusResponse)
def post_tasks(payload: CreateTaskRequestSchema) -> TaskStatusResponse:
task = orchestrator.create_task(
CreateTaskRequest(
project_id=payload.project_id,
goal=payload.goal,
inputs=payload.inputs,
requested_mode=payload.execution_mode,
)
)
graph = orchestrator.plan_task(task.task_id)
return TaskStatusResponse(
task_id=task.task_id,
status=task.status.value,
current_node=task.current_node_id,
progress={"completed": 0, "total": len(graph.nodes)},
)
@app.get("/tasks/{task_id}", response_model=TaskStatusResponse)
def get_task(task_id: str) -> TaskStatusResponse:
task = task_repository.get(task_id)
graph = graph_repository.get(task_id)
if task is None or graph is None:
raise HTTPException(status_code=404, detail="Task not found")
completed = sum(1 for node in graph.nodes if node.status.value == "completed")
return TaskStatusResponse(
task_id=task.task_id,
status=task.status.value,
current_node=task.current_node_id,
progress={"completed": completed, "total": len(graph.nodes)},
)
@app.get("/tasks/{task_id}/events")
async def get_task_events(task_id: str) -> EventSourceResponse:
events = event_store.list_by_task(task_id)
async def iterator():
for event in events:
yield {
"event": event.event_type,
"data": {
"event_id": event.event_id,
"task_id": event.task_id,
"node_id": event.node_id,
"timestamp": event.occurred_at.isoformat(),
"payload": event.payload,
},
}
return EventSourceResponse(iterator())
@app.post("/confirmations/{confirmation_id}/approve")
def approve_confirmation(
confirmation_id: str,
payload: ConfirmationApproveRequest,
) -> dict[str, str]:
confirmation = confirmation_repository.get(confirmation_id)
if confirmation is None:
raise HTTPException(status_code=404, detail="Confirmation not found")
confirmation.approve(payload.comment)
confirmation_repository.save(confirmation)
return {"status": confirmation.status.value}
@app.post("/confirmations/{confirmation_id}/reject")
def reject_confirmation(
confirmation_id: str,
payload: ConfirmationRejectRequest,
) -> dict[str, str]:
confirmation = confirmation_repository.get(confirmation_id)
if confirmation is None:
raise HTTPException(status_code=404, detail="Confirmation not found")
confirmation.reject(payload.reason)
confirmation_repository.save(confirmation)
return {"status": confirmation.status.value}
@app.get("/workers", response_model=WorkerListResponse)
def list_workers() -> WorkerListResponse:
return WorkerListResponse(
workers=[
WorkerListItem(
session_id=worker.session_id,
worker_id=worker.worker_id,
name=worker.name,
status=worker.status.value,
capabilities=worker.capabilities,
)
for worker in worker_repository.list_active()
]
)
@app.post("/workers/register")
def register_worker(payload: WorkerRegisterRequest) -> dict[str, object]:
worker = app.state.worker_service.register(
RegisterWorkerRequest(
worker_id=payload.worker_id,
name=payload.name,
machine=payload.machine or payload.name,
os=payload.os or "unknown",
version=payload.version,
capabilities=payload.capabilities,
)
)
return {"session_id": worker.session_id, "status": worker.status.value}
return app
@@ -0,0 +1,66 @@
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
class ChatRequest(BaseModel):
project_id: str = "default"
conversation_id: str | None = None
message: str
attachments: list[dict[str, Any]] = Field(default_factory=list)
mode: str = "auto"
preferences: dict[str, Any] = Field(default_factory=dict)
class ChatResponse(BaseModel):
conversation_id: str
task_id: str
response_type: str
message: str
cards: list[dict[str, Any]] = Field(default_factory=list)
class CreateTaskRequestSchema(BaseModel):
project_id: str
goal: str
inputs: dict[str, Any] = Field(default_factory=dict)
execution_mode: str = "agent_graph"
class TaskStatusResponse(BaseModel):
task_id: str
status: str
current_node: str | None
progress: dict[str, int]
class ConfirmationApproveRequest(BaseModel):
scope: str = "once"
comment: str | None = None
class ConfirmationRejectRequest(BaseModel):
reason: str
class WorkerRegisterRequest(BaseModel):
worker_id: str
name: str
capabilities: list[str] = Field(default_factory=list)
version: str
machine: str | None = None
os: str | None = None
class WorkerListItem(BaseModel):
session_id: str
worker_id: str
name: str
status: str
capabilities: list[str] = Field(default_factory=list)
class WorkerListResponse(BaseModel):
workers: list[WorkerListItem] = Field(default_factory=list)
+2
View File
@@ -0,0 +1,2 @@
"""Domain layer."""
+86
View File
@@ -0,0 +1,86 @@
from __future__ import annotations
from enum import StrEnum
class TaskStatus(StrEnum):
CREATED = "created"
PLANNED = "planned"
RUNNING = "running"
WAITING_CONFIRMATION = "waiting_confirmation"
WAITING_MANUAL = "waiting_manual"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class NodeStatus(StrEnum):
PENDING = "pending"
READY = "ready"
RUNNING = "running"
WAITING_CONFIRMATION = "waiting_confirmation"
COMPLETED = "completed"
FAILED = "failed"
SKIPPED = "skipped"
CANCELLED = "cancelled"
class NodeType(StrEnum):
PLANNER = "planner"
MODEL_CALL = "model_call"
TOOL_CALL = "tool_call"
LOCAL_WORKER_CALL = "local_worker_call"
REVIEWER = "reviewer"
FINALIZER = "finalizer"
CONFIRMATION = "confirmation"
class ConfirmationStatus(StrEnum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
EXPIRED = "expired"
CANCELLED = "cancelled"
class WorkerSessionStatus(StrEnum):
CONNECTING = "connecting"
ONLINE = "online"
BUSY = "busy"
STALE = "stale"
DISCONNECTED = "disconnected"
class ResourceType(StrEnum):
FILESYSTEM = "filesystem"
SHELL = "shell"
SQL = "sql"
MCP = "mcp"
EXTERNAL_MODELS = "external_models"
DESKTOP = "desktop"
BROWSER = "browser"
NETWORK = "network"
COST = "cost"
SYSTEM = "system"
class RiskLevel(StrEnum):
SAFE = "safe"
WRITE = "write"
DESTRUCTIVE = "destructive"
SYSTEM = "system"
COST = "cost"
class PolicyDecisionType(StrEnum):
ALLOW = "allow"
CONFIRM = "confirm"
MANUAL = "manual"
DISABLED_BY_CONFIG = "disabled_by_config"
class ConfirmationScope(StrEnum):
ONCE = "once"
TASK = "task"
PROJECT = "project"
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
class OrchestratorError(Exception):
"""Base class for orchestrator errors."""
class ValidationError(OrchestratorError):
"""Raised when request or data validation fails."""
class PolicyError(OrchestratorError):
"""Raised when policy blocks or pauses execution."""
class RetryableInfrastructureError(OrchestratorError):
"""Raised for transient upstream or transport errors."""
class NonRetryableInfrastructureError(OrchestratorError):
"""Raised for permanent upstream or integration errors."""
class ExecutionError(OrchestratorError):
"""Raised when node execution fails semantically."""
class CancellationError(OrchestratorError):
"""Raised when task or node execution was cancelled."""
+20
View File
@@ -0,0 +1,20 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any
from uuid import uuid4
@dataclass(slots=True, frozen=True)
class DomainEvent:
event_type: str
task_id: str
payload: dict[str, Any]
conversation_id: str | None = None
node_id: str | None = None
correlation_id: str | None = None
causation_id: str | None = None
event_id: str = field(default_factory=lambda: f"evt_{uuid4().hex}")
occurred_at: datetime = field(default_factory=lambda: datetime.now(UTC))
+192
View File
@@ -0,0 +1,192 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any
from uuid import uuid4
from ai_orchestrator.domain.enums import (
ConfirmationScope,
ConfirmationStatus,
NodeStatus,
NodeType,
PolicyDecisionType,
RiskLevel,
TaskStatus,
WorkerSessionStatus,
)
def _utcnow() -> datetime:
return datetime.now(UTC)
@dataclass(slots=True)
class Task:
project_id: str
goal: str
inputs: dict[str, Any]
conversation_id: str | None = None
requested_mode: str = "auto"
effective_mode: str = "auto"
task_id: str = field(default_factory=lambda: f"task_{uuid4().hex}")
status: TaskStatus = TaskStatus.CREATED
current_node_id: str | None = None
result_summary: dict[str, Any] = field(default_factory=dict)
created_at: datetime = field(default_factory=_utcnow)
updated_at: datetime = field(default_factory=_utcnow)
def mark_planned(self) -> None:
self.status = TaskStatus.PLANNED
self.updated_at = _utcnow()
def mark_running(self, node_id: str | None = None) -> None:
self.status = TaskStatus.RUNNING
self.current_node_id = node_id
self.updated_at = _utcnow()
def wait_for_confirmation(self, node_id: str | None = None) -> None:
self.status = TaskStatus.WAITING_CONFIRMATION
self.current_node_id = node_id
self.updated_at = _utcnow()
def wait_for_manual(self, node_id: str | None = None) -> None:
self.status = TaskStatus.WAITING_MANUAL
self.current_node_id = node_id
self.updated_at = _utcnow()
def complete(self, summary: dict[str, Any]) -> None:
self.status = TaskStatus.COMPLETED
self.result_summary = summary
self.updated_at = _utcnow()
def fail(self, summary: dict[str, Any]) -> None:
self.status = TaskStatus.FAILED
self.result_summary = summary
self.updated_at = _utcnow()
@dataclass(slots=True)
class ExecutionNode:
task_id: str
node_type: NodeType
input_data: dict[str, Any]
dependencies: list[str] = field(default_factory=list)
node_id: str = field(default_factory=lambda: f"node_{uuid4().hex}")
status: NodeStatus = NodeStatus.PENDING
output_data: dict[str, Any] = field(default_factory=dict)
assigned_runner: str | None = None
attempts: int = 0
retryable: bool = True
timeout_ms: int | None = None
created_at: datetime = field(default_factory=_utcnow)
updated_at: datetime = field(default_factory=_utcnow)
def is_ready(self, completed_node_ids: set[str]) -> bool:
return all(dep in completed_node_ids for dep in self.dependencies)
def mark_ready(self) -> None:
self.status = NodeStatus.READY
self.updated_at = _utcnow()
def mark_running(self) -> None:
self.status = NodeStatus.RUNNING
self.attempts += 1
self.updated_at = _utcnow()
def mark_waiting_confirmation(self) -> None:
self.status = NodeStatus.WAITING_CONFIRMATION
self.updated_at = _utcnow()
def mark_completed(self, output_data: dict[str, Any]) -> None:
self.status = NodeStatus.COMPLETED
self.output_data = output_data
self.updated_at = _utcnow()
def mark_failed(self, output_data: dict[str, Any] | None = None) -> None:
self.status = NodeStatus.FAILED
self.output_data = output_data or {}
self.updated_at = _utcnow()
@dataclass(slots=True)
class ExecutionGraph:
task_id: str
nodes: list[ExecutionNode]
def completed_node_ids(self) -> set[str]:
return {node.node_id for node in self.nodes if node.status == NodeStatus.COMPLETED}
def ready_nodes(self) -> list[ExecutionNode]:
completed = self.completed_node_ids()
ready: list[ExecutionNode] = []
for node in self.nodes:
if node.status == NodeStatus.PENDING and node.is_ready(completed):
node.mark_ready()
if node.status == NodeStatus.READY:
ready.append(node)
return ready
@dataclass(slots=True)
class ActionDescriptor:
resource: str
risk_level: RiskLevel
action_name: str
preview_available: bool = False
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(slots=True)
class PolicyDecision:
decision: PolicyDecisionType
reason: str
requires_confirmation: bool = False
preview: dict[str, Any] = field(default_factory=dict)
@dataclass(slots=True)
class ConfirmationRequest:
task_id: str
node_id: str
decision: PolicyDecision
scope: ConfirmationScope = ConfirmationScope.ONCE
confirmation_id: str = field(default_factory=lambda: f"conf_{uuid4().hex}")
status: ConfirmationStatus = ConfirmationStatus.PENDING
comment: str | None = None
created_at: datetime = field(default_factory=_utcnow)
resolved_at: datetime | None = None
def approve(self, comment: str | None = None) -> None:
self.status = ConfirmationStatus.APPROVED
self.comment = comment
self.resolved_at = _utcnow()
def reject(self, comment: str | None = None) -> None:
self.status = ConfirmationStatus.REJECTED
self.comment = comment
self.resolved_at = _utcnow()
@dataclass(slots=True)
class WorkerSession:
worker_id: str
name: str
machine: str
os: str
version: str
capabilities: list[str]
session_id: str = field(default_factory=lambda: f"wrk_{uuid4().hex}")
status: WorkerSessionStatus = WorkerSessionStatus.CONNECTING
current_task_id: str | None = None
last_heartbeat_at: datetime = field(default_factory=_utcnow)
connected_at: datetime = field(default_factory=_utcnow)
disconnected_at: datetime | None = None
def mark_online(self) -> None:
self.status = WorkerSessionStatus.ONLINE
self.last_heartbeat_at = _utcnow()
def heartbeat(self) -> None:
self.last_heartbeat_at = _utcnow()
@@ -0,0 +1,2 @@
"""Infrastructure layer."""
@@ -0,0 +1,15 @@
from __future__ import annotations
from pathlib import Path
import yaml
from ai_orchestrator.config import ProjectConfig
def load_project_config(path: str | Path) -> ProjectConfig:
config_path = Path(path)
with config_path.open("r", encoding="utf-8") as fh:
raw = yaml.safe_load(fh) or {}
return ProjectConfig.model_validate(raw)
@@ -0,0 +1,14 @@
from __future__ import annotations
import json
import logging
from typing import Any
def configure_logging(level: int = logging.INFO) -> None:
logging.basicConfig(level=level, format="%(message)s")
def log_structured(logger: logging.Logger, event_type: str, **payload: Any) -> None:
logger.info(json.dumps({"event_type": event_type, **payload}, ensure_ascii=True, default=str))
@@ -0,0 +1,34 @@
from __future__ import annotations
from dataclasses import dataclass
from ai_orchestrator.application.ports import PolicyEvaluator
from ai_orchestrator.config import PolicyMode, ProjectConfig
from ai_orchestrator.domain.enums import PolicyDecisionType
from ai_orchestrator.domain.models import ActionDescriptor, PolicyDecision
@dataclass(slots=True)
class StaticProjectPolicyEvaluator(PolicyEvaluator):
projects: dict[str, ProjectConfig]
def evaluate(self, action: ActionDescriptor, project_id: str) -> PolicyDecision:
project = self.projects[project_id]
resource_mode = getattr(
project.policy.resources,
action.resource,
project.policy.default_mode,
)
mapping = {
PolicyMode.FULL_AUTO: PolicyDecisionType.ALLOW,
PolicyMode.CONFIRM: PolicyDecisionType.CONFIRM,
PolicyMode.MANUAL: PolicyDecisionType.MANUAL,
PolicyMode.DISABLED: PolicyDecisionType.DISABLED_BY_CONFIG,
}
decision = mapping[resource_mode]
return PolicyDecision(
decision=decision,
reason=f"Policy mode for {action.resource}: {resource_mode.value}",
requires_confirmation=decision == PolicyDecisionType.CONFIRM,
preview={"preview_available": action.preview_available, **action.metadata},
)
@@ -0,0 +1,2 @@
"""Storage adapters."""
@@ -0,0 +1,85 @@
from __future__ import annotations
from dataclasses import dataclass, field
from ai_orchestrator.application.ports import (
ConfirmationRepository,
EventStore,
GraphRepository,
TaskRepository,
WorkerRepository,
)
from ai_orchestrator.domain.events import DomainEvent
from ai_orchestrator.domain.models import ConfirmationRequest, ExecutionGraph, Task, WorkerSession
@dataclass(slots=True)
class InMemoryTaskRepository(TaskRepository):
items: dict[str, Task] = field(default_factory=dict)
def create(self, task: Task) -> Task:
self.items[task.task_id] = task
return task
def save(self, task: Task) -> Task:
self.items[task.task_id] = task
return task
def get(self, task_id: str) -> Task | None:
return self.items.get(task_id)
@dataclass(slots=True)
class InMemoryGraphRepository(GraphRepository):
items: dict[str, ExecutionGraph] = field(default_factory=dict)
def save(self, graph: ExecutionGraph) -> ExecutionGraph:
self.items[graph.task_id] = graph
return graph
def get(self, task_id: str) -> ExecutionGraph | None:
return self.items.get(task_id)
@dataclass(slots=True)
class InMemoryConfirmationRepository(ConfirmationRepository):
items: dict[str, ConfirmationRequest] = field(default_factory=dict)
def create(self, confirmation: ConfirmationRequest) -> ConfirmationRequest:
self.items[confirmation.confirmation_id] = confirmation
return confirmation
def get(self, confirmation_id: str) -> ConfirmationRequest | None:
return self.items.get(confirmation_id)
def save(self, confirmation: ConfirmationRequest) -> ConfirmationRequest:
self.items[confirmation.confirmation_id] = confirmation
return confirmation
@dataclass(slots=True)
class InMemoryWorkerRepository(WorkerRepository):
items: dict[str, WorkerSession] = field(default_factory=dict)
def save(self, worker: WorkerSession) -> WorkerSession:
self.items[worker.session_id] = worker
return worker
def get(self, session_id: str) -> WorkerSession | None:
return self.items.get(session_id)
def list_active(self) -> list[WorkerSession]:
return list(self.items.values())
@dataclass(slots=True)
class InMemoryEventStore(EventStore):
items: list[DomainEvent] = field(default_factory=list)
def append(self, event: DomainEvent) -> DomainEvent:
self.items.append(event)
return event
def list_by_task(self, task_id: str) -> list[DomainEvent]:
return [event for event in self.items if event.task_id == task_id]
+6
View File
@@ -0,0 +1,6 @@
from __future__ import annotations
from ai_orchestrator.delivery.http.app import create_app
app = create_app()