Files
manual_slop/src/models.py
2026-03-05 14:07:04 -05:00

229 lines
6.8 KiB
Python

from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any
from datetime import datetime
from pathlib import Path
import os
import tomllib
from src import project_manager
CONFIG_PATH: Path = Path(os.environ.get("SLOP_CONFIG", "config.toml"))
DISC_ROLES: list[str] = ["User", "AI", "Vendor API", "System"]
AGENT_TOOL_NAMES: list[str] = [
"run_powershell",
"read_file",
"list_directory",
"search_files",
"get_file_summary",
"web_search",
"fetch_url",
"py_get_skeleton",
"py_get_code_outline",
"get_file_slice",
"py_get_definition",
"py_get_signature",
"py_get_class_summary",
"py_get_var_declaration",
"get_git_diff",
"py_find_usages",
"py_get_imports",
"py_check_syntax",
"py_get_hierarchy",
"py_get_docstring",
"get_tree",
"get_ui_performance",
# Mutating tools — disabled by default
"set_file_slice",
"py_update_definition",
"py_set_signature",
"py_set_var_declaration",
]
def load_config() -> dict[str, Any]:
with open(CONFIG_PATH, "rb") as f:
return tomllib.load(f)
def parse_history_entries(
history: list[str], roles: list[str] | None = None
) -> list[dict[str, Any]]:
known = roles if roles is not None else DISC_ROLES
entries = []
for raw in history:
entry = project_manager.str_to_entry(raw, known)
entries.append(entry)
return entries
@dataclass
class Ticket:
"""
Represents a discrete unit of work within a track.
"""
id: str
description: str
status: str = "todo"
assigned_to: str = "unassigned"
target_file: Optional[str] = None
context_requirements: List[str] = field(default_factory=list)
depends_on: List[str] = field(default_factory=list)
blocked_reason: Optional[str] = None
step_mode: bool = False
retry_count: int = 0
def mark_blocked(self, reason: str) -> None:
"""Sets the ticket status to 'blocked' and records the reason."""
self.status = "blocked"
self.blocked_reason = reason
def mark_complete(self) -> None:
"""Sets the ticket status to 'completed'."""
self.status = "completed"
def get(self, key: str, default: Any = None) -> Any:
"""Helper to provide dictionary-like access to dataclass fields."""
return getattr(self, key, default)
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"description": self.description,
"status": self.status,
"assigned_to": self.assigned_to,
"target_file": self.target_file,
"context_requirements": self.context_requirements,
"depends_on": self.depends_on,
"blocked_reason": self.blocked_reason,
"step_mode": self.step_mode,
"retry_count": self.retry_count,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Ticket":
return cls(
id=data["id"],
description=data.get("description", ""),
status=data.get("status", "todo"),
assigned_to=data.get("assigned_to", ""),
target_file=data.get("target_file"),
context_requirements=data.get("context_requirements", []),
depends_on=data.get("depends_on", []),
blocked_reason=data.get("blocked_reason"),
step_mode=data.get("step_mode", False),
retry_count=data.get("retry_count", 0),
)
@dataclass
class Track:
"""
Represents a collection of tickets that together form an architectural track or epic.
"""
id: str
description: str
tickets: List[Ticket] = field(default_factory=list)
def get_executable_tickets(self) -> List[Ticket]:
"""
Returns all 'todo' tickets whose dependencies are all 'completed'.
"""
# Map ticket IDs to their current status for efficient lookup
status_map = {t.id: t.status for t in self.tickets}
executable = []
for ticket in self.tickets:
if ticket.status != "todo":
continue
# Check if all dependencies are completed
all_deps_completed = True
for dep_id in ticket.depends_on:
# If a dependency is missing from the track, we treat it as not completed (or we could raise an error)
if status_map.get(dep_id) != "completed":
all_deps_completed = False
break
if all_deps_completed:
executable.append(ticket)
return executable
@dataclass
class WorkerContext:
"""
Represents the context provided to a Tier 3 Worker for a specific ticket.
"""
ticket_id: str
model_name: str
messages: List[Dict[str, Any]]
@dataclass
class Metadata:
id: str
name: str
status: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Metadata":
return cls(
id=data["id"],
name=data["name"],
status=data.get("status"),
created_at=datetime.fromisoformat(data["created_at"])
if data.get("created_at")
else None,
updated_at=datetime.fromisoformat(data["updated_at"])
if data.get("updated_at")
else None,
)
@dataclass
class TrackState:
metadata: Metadata
discussion: List[Dict[str, Any]]
tasks: List[Ticket]
def to_dict(self) -> Dict[str, Any]:
return {
"metadata": self.metadata.to_dict(),
"discussion": [
{
k: v.isoformat() if isinstance(v, datetime) else v
for k, v in item.items()
}
for item in self.discussion
],
"tasks": [task.to_dict() for task in self.tasks],
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "TrackState":
metadata = Metadata.from_dict(data["metadata"])
tasks = [Ticket.from_dict(task_data) for task_data in data["tasks"]]
return cls(
metadata=metadata,
discussion=[
{
k: datetime.fromisoformat(v)
if isinstance(v, str) and "T" in v
else v # Basic check for ISO format
for k, v in item.items()
}
for item in data["discussion"]
],
tasks=tasks,
)