feat(models): Add priority field to Ticket dataclass and update serialization
This commit is contained in:
@@ -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**
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
375
src/models.py
375
src/models.py
@@ -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:
|
||||||
|
|||||||
37
tests/test_ticket_queue.py
Normal file
37
tests/test_ticket_queue.py
Normal 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"
|
||||||
Reference in New Issue
Block a user