feat(models): Add priority field to Ticket dataclass and update serialization

This commit is contained in:
2026-03-07 15:27:30 -05:00
parent e9d9cdeb28
commit 035c74ed36
4 changed files with 229 additions and 189 deletions

View File

@@ -66,7 +66,7 @@ This file tracks all major tracks for the project. Each track has its own detail
### Manual UX Controls ### Manual UX Controls
14. [ ] **Track: Manual Ticket Queue Management** 14. [~] **Track: Manual Ticket Queue Management**
*Link: [./tracks/ticket_queue_mgmt_20260306/](./tracks/ticket_queue_mgmt_20260306/)* *Link: [./tracks/ticket_queue_mgmt_20260306/](./tracks/ticket_queue_mgmt_20260306/)*
15. [ ] **Track: Kill/Abort Running Workers** 15. [ ] **Track: Kill/Abort Running Workers**

View File

@@ -5,10 +5,10 @@
## Phase 1: Priority Field ## Phase 1: Priority Field
Focus: Add priority to Ticket model Focus: Add priority to Ticket model
- [ ] Task 1.1: Initialize MMA Environment - [x] Task 1.1: Initialize MMA Environment
- Run `activate_skill mma-orchestrator` before starting - Run `activate_skill mma-orchestrator` before starting
- [ ] Task 1.2: Add priority field to Ticket - [~] Task 1.2: Add priority field to Ticket
- WHERE: `src/models.py` `Ticket` dataclass - WHERE: `src/models.py` `Ticket` dataclass
- WHAT: Add `priority: str = "medium"` field - WHAT: Add `priority: str = "medium"` field
- HOW: - HOW:

View File

@@ -9,230 +9,233 @@ from src.paths import get_config_path
CONFIG_PATH = get_config_path() CONFIG_PATH = get_config_path()
def load_config() -> dict[str, Any]: def load_config() -> dict[str, Any]:
with open(CONFIG_PATH, "rb") as f: with open(CONFIG_PATH, "rb") as f:
return tomllib.load(f) return tomllib.load(f)
def save_config(config: dict[str, Any]) -> None: def save_config(config: dict[str, Any]) -> None:
import tomli_w import tomli_w
with open(CONFIG_PATH, "wb") as f: with open(CONFIG_PATH, "wb") as f:
tomli_w.dump(config, f) tomli_w.dump(config, f)
AGENT_TOOL_NAMES = [ AGENT_TOOL_NAMES = [
"run_powershell", "run_powershell",
"read_file", "read_file",
"list_directory", "list_directory",
"search_files", "search_files",
"web_search", "web_search",
"fetch_url", "fetch_url",
"get_file_summary", "get_file_summary",
"py_get_skeleton", "py_get_skeleton",
"py_get_code_outline", "py_get_code_outline",
"py_get_definition", "py_get_definition",
"py_get_signature", "py_get_signature",
"py_get_class_summary", "py_get_class_summary",
"py_get_var_declaration", "py_get_var_declaration",
"py_get_docstring", "py_get_docstring",
"py_find_usages", "py_find_usages",
"py_get_imports", "py_get_imports",
"py_check_syntax", "py_check_syntax",
"py_get_hierarchy" "py_get_hierarchy"
] ]
def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[dict[str, Any]]: def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[dict[str, Any]]:
import re import re
entries = [] entries = []
for raw in history_strings: for raw in history_strings:
ts = "" ts = ""
rest = raw rest = raw
if rest.startswith("@"): if rest.startswith("@"):
nl = rest.find("\n") nl = rest.find("\n")
if nl != -1: if nl != -1:
ts = rest[1:nl] ts = rest[1:nl]
rest = rest[nl + 1:] rest = rest[nl + 1:]
known = roles or ["User", "AI", "Vendor API", "System"] known = roles or ["User", "AI", "Vendor API", "System"]
role_pat = re.compile(r"^(" + "|".join(re.escape(r) for r in known) + r"):", re.IGNORECASE) role_pat = re.compile(r"^(" + "|".join(re.escape(r) for r in known) + r"):", re.IGNORECASE)
match = role_pat.match(rest) match = role_pat.match(rest)
role = match.group(1) if match else "User" role = match.group(1) if match else "User"
if match: if match:
content = rest[match.end():].strip() content = rest[match.end():].strip()
else: else:
content = rest content = rest
entries.append({"role": role, "content": content, "collapsed": True, "ts": ts}) entries.append({"role": role, "content": content, "collapsed": True, "ts": ts})
return entries return entries
@dataclass @dataclass
class Ticket: class Ticket:
id: str id: str
description: str description: str
status: str = "todo" status: str = "todo"
assigned_to: str = "unassigned" assigned_to: str = "unassigned"
target_file: Optional[str] = None priority: str = "medium"
target_symbols: List[str] = field(default_factory=list) target_file: Optional[str] = None
context_requirements: List[str] = field(default_factory=list) target_symbols: List[str] = field(default_factory=list)
depends_on: List[str] = field(default_factory=list) context_requirements: List[str] = field(default_factory=list)
blocked_reason: Optional[str] = None depends_on: List[str] = field(default_factory=list)
step_mode: bool = False blocked_reason: Optional[str] = None
retry_count: int = 0 step_mode: bool = False
retry_count: int = 0
def mark_blocked(self, reason: str) -> None: def mark_blocked(self, reason: str) -> None:
self.status = "blocked" self.status = "blocked"
self.blocked_reason = reason self.blocked_reason = reason
def mark_complete(self) -> None: def mark_complete(self) -> None:
self.status = "completed" self.status = "completed"
def get(self, key: str, default: Any = None) -> Any: def get(self, key: str, default: Any = None) -> Any:
return getattr(self, key, default) return getattr(self, key, default)
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
return { return {
"id": self.id, "id": self.id,
"description": self.description, "description": self.description,
"status": self.status, "status": self.status,
"assigned_to": self.assigned_to, "assigned_to": self.assigned_to,
"target_file": self.target_file, "priority": self.priority,
"target_symbols": self.target_symbols, "target_file": self.target_file,
"context_requirements": self.context_requirements, "target_symbols": self.target_symbols,
"depends_on": self.depends_on, "context_requirements": self.context_requirements,
"blocked_reason": self.blocked_reason, "depends_on": self.depends_on,
"step_mode": self.step_mode, "blocked_reason": self.blocked_reason,
"retry_count": self.retry_count, "step_mode": self.step_mode,
} "retry_count": self.retry_count,
}
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Ticket": def from_dict(cls, data: Dict[str, Any]) -> "Ticket":
return cls( return cls(
id=data["id"], id=data["id"],
description=data.get("description", ""), description=data.get("description", ""),
status=data.get("status", "todo"), status=data.get("status", "todo"),
assigned_to=data.get("assigned_to", ""), assigned_to=data.get("assigned_to", "unassigned"),
target_file=data.get("target_file"), priority=data.get("priority", "medium"),
target_symbols=data.get("target_symbols", []), target_file=data.get("target_file"),
context_requirements=data.get("context_requirements", []), target_symbols=data.get("target_symbols", []),
depends_on=data.get("depends_on", []), context_requirements=data.get("context_requirements", []),
blocked_reason=data.get("blocked_reason"), depends_on=data.get("depends_on", []),
step_mode=data.get("step_mode", False), blocked_reason=data.get("blocked_reason"),
retry_count=data.get("retry_count", 0), step_mode=data.get("step_mode", False),
) retry_count=data.get("retry_count", 0),
)
@dataclass @dataclass
class Track: class Track:
id: str id: str
description: str description: str
tickets: List[Ticket] = field(default_factory=list) tickets: List[Ticket] = field(default_factory=list)
def get_executable_tickets(self) -> List[Ticket]: def get_executable_tickets(self) -> List[Ticket]:
from src.dag_engine import TrackDAG from src.dag_engine import TrackDAG
dag = TrackDAG(self.tickets) dag = TrackDAG(self.tickets)
return dag.get_ready_tasks() return dag.get_ready_tasks()
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
return { return {
"id": self.id, "id": self.id,
"description": self.description, "description": self.description,
"tickets": [t.to_dict() for t in self.tickets], "tickets": [t.to_dict() for t in self.tickets],
} }
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Track": def from_dict(cls, data: Dict[str, Any]) -> "Track":
return cls( return cls(
id=data["id"], id=data["id"],
description=data.get("description", ""), description=data.get("description", ""),
tickets=[Ticket.from_dict(t) for t in data.get("tickets", [])], tickets=[Ticket.from_dict(t) for t in data.get("tickets", [])],
) )
@dataclass @dataclass
class WorkerContext: class WorkerContext:
ticket_id: str ticket_id: str
model_name: str model_name: str
messages: List[Dict[str, Any]] = field(default_factory=list) messages: List[Dict[str, Any]] = field(default_factory=list)
@dataclass @dataclass
class Metadata: class Metadata:
id: str id: str
name: str name: str
status: Optional[str] = None status: Optional[str] = None
created_at: Optional[datetime.datetime] = None created_at: Optional[datetime.datetime] = None
updated_at: Optional[datetime.datetime] = None updated_at: Optional[datetime.datetime] = None
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
return { return {
"id": self.id, "id": self.id,
"name": self.name, "name": self.name,
"status": self.status, "status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None, "created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None,
} }
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Metadata": def from_dict(cls, data: Dict[str, Any]) -> "Metadata":
created = data.get("created_at") created = data.get("created_at")
updated = data.get("updated_at") updated = data.get("updated_at")
if isinstance(created, str): if isinstance(created, str):
try: try:
created = datetime.datetime.fromisoformat(created) created = datetime.datetime.fromisoformat(created)
except ValueError: except ValueError:
created = None created = None
if isinstance(updated, str): if isinstance(updated, str):
try: try:
updated = datetime.datetime.fromisoformat(updated) updated = datetime.datetime.fromisoformat(updated)
except ValueError: except ValueError:
updated = None updated = None
return cls( return cls(
id=data["id"], id=data["id"],
name=data.get("name", ""), name=data.get("name", ""),
status=data.get("status"), status=data.get("status"),
created_at=created, created_at=created,
updated_at=updated, updated_at=updated,
) )
@dataclass @dataclass
class TrackState: class TrackState:
metadata: Metadata metadata: Metadata
discussion: List[str] = field(default_factory=list) discussion: List[str] = field(default_factory=list)
tasks: List[Ticket] = field(default_factory=list) tasks: List[Ticket] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
serialized_discussion = [] serialized_discussion = []
for item in self.discussion: for item in self.discussion:
if isinstance(item, dict): if isinstance(item, dict):
new_item = dict(item) new_item = dict(item)
if "ts" in new_item and isinstance(new_item["ts"], datetime.datetime): if "ts" in new_item and isinstance(new_item["ts"], datetime.datetime):
new_item["ts"] = new_item["ts"].isoformat() new_item["ts"] = new_item["ts"].isoformat()
serialized_discussion.append(new_item) serialized_discussion.append(new_item)
else: else:
serialized_discussion.append(item) serialized_discussion.append(item)
return { return {
"metadata": self.metadata.to_dict(), "metadata": self.metadata.to_dict(),
"discussion": serialized_discussion, "discussion": serialized_discussion,
"tasks": [t.to_dict() for t in self.tasks], "tasks": [t.to_dict() for t in self.tasks],
} }
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> "TrackState": def from_dict(cls, data: Dict[str, Any]) -> "TrackState":
discussion = data.get("discussion", []) discussion = data.get("discussion", [])
parsed_discussion = [] parsed_discussion = []
for item in discussion: for item in discussion:
if isinstance(item, dict): if isinstance(item, dict):
new_item = dict(item) new_item = dict(item)
ts = new_item.get("ts") ts = new_item.get("ts")
if isinstance(ts, str): if isinstance(ts, str):
try: try:
new_item["ts"] = datetime.datetime.fromisoformat(ts) new_item["ts"] = datetime.datetime.fromisoformat(ts)
except ValueError: except ValueError:
pass pass
parsed_discussion.append(new_item) parsed_discussion.append(new_item)
else: else:
parsed_discussion.append(item) parsed_discussion.append(item)
return cls( return cls(
metadata=Metadata.from_dict(data["metadata"]), metadata=Metadata.from_dict(data["metadata"]),
discussion=parsed_discussion, discussion=parsed_discussion,
tasks=[Ticket.from_dict(t) for t in data.get("tasks", [])], tasks=[Ticket.from_dict(t) for t in data.get("tasks", [])],
) )
@dataclass @dataclass
class FileItem: class FileItem:

View File

@@ -0,0 +1,37 @@
import pytest
from src.models import Ticket
def test_ticket_priority_default():
ticket = Ticket(id="T1", description="Test ticket")
assert ticket.priority == "medium"
def test_ticket_priority_custom():
ticket_high = Ticket(id="T2", description="High priority", priority="high")
assert ticket_high.priority == "high"
ticket_low = Ticket(id="T3", description="Low priority", priority="low")
assert ticket_low.priority == "low"
def test_ticket_to_dict_priority():
ticket = Ticket(id="T4", description="To dict test", priority="high")
d = ticket.to_dict()
assert "priority" in d
assert d["priority"] == "high"
def test_ticket_from_dict_priority():
data = {
"id": "T5",
"description": "From dict test",
"priority": "low",
"status": "todo"
}
ticket = Ticket.from_dict(data)
assert ticket.priority == "low"
def test_ticket_from_dict_default_priority():
data = {
"id": "T6",
"description": "No priority in dict"
}
ticket = Ticket.from_dict(data)
assert ticket.priority == "medium"