183 lines
5.8 KiB
Python
183 lines
5.8 KiB
Python
"""API 路由 — 黑板 CRUD"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
|
|
from src.blackboard.operations import Blackboard
|
|
from src.blackboard.models import Task, Review
|
|
from src.blackboard.queries import Queries
|
|
from src.blackboard.db import VALID_STATUSES, VALID_TRANSITIONS, COMMENT_TYPES, OUTPUT_TYPES
|
|
|
|
from src.utils import get_data_root
|
|
|
|
router = APIRouter(prefix="/api/projects/{project_id}", tags=["blackboard"])
|
|
|
|
|
|
def _bb(project_id: str) -> Blackboard:
|
|
return Blackboard(get_data_root() / project_id / "blackboard.db")
|
|
|
|
|
|
def _q(project_id: str) -> Queries:
|
|
return Queries(get_data_root() / project_id / "blackboard.db")
|
|
|
|
|
|
# --- Tasks ---
|
|
|
|
@router.get("/tasks")
|
|
async def list_tasks(project_id: str,
|
|
status: Optional[str] = None,
|
|
assignee: Optional[str] = None):
|
|
bb = _bb(project_id)
|
|
tasks = bb.list_tasks(status=status, assignee=assignee)
|
|
return {"tasks": [_task_to_dict(t) for t in tasks]}
|
|
|
|
|
|
@router.get("/tasks/{task_id}")
|
|
async def get_task(project_id: str, task_id: str):
|
|
bb = _bb(project_id)
|
|
task = bb.get_task(task_id)
|
|
if not task:
|
|
raise HTTPException(404, f"Task not found: {task_id}")
|
|
return _task_to_dict(task)
|
|
|
|
|
|
@router.post("/tasks")
|
|
async def create_task(project_id: str, body: Dict[str, Any]):
|
|
bb = _bb(project_id)
|
|
task = Task(
|
|
id=body["id"], title=body["title"],
|
|
description=body.get("description"),
|
|
task_type=body.get("task_type", "coding"),
|
|
priority=body.get("priority", 5),
|
|
assignee=body.get("assignee"),
|
|
assigned_by=body.get("assigned_by", "user"),
|
|
depends_on=json.dumps(body["depends_on"]) if "depends_on" in body else None,
|
|
risk_level=body.get("risk_level", "standard"),
|
|
)
|
|
bb.create_task(task)
|
|
return {"ok": True, "task_id": task.id}
|
|
|
|
|
|
@router.post("/tasks/{task_id}/claim")
|
|
async def claim_task(project_id: str, task_id: str, body: Dict[str, Any]):
|
|
bb = _bb(project_id)
|
|
if not bb.claim_task(task_id, body["agent"]):
|
|
raise HTTPException(409, "Claim failed (already claimed or wrong assignee)")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/tasks/{task_id}/status")
|
|
async def update_status(project_id: str, task_id: str, body: Dict[str, Any]):
|
|
bb = _bb(project_id)
|
|
if not bb.update_task_status(task_id, body["status"],
|
|
agent=body.get("agent")):
|
|
raise HTTPException(409, f"Invalid transition to {body['status']}")
|
|
return {"ok": True}
|
|
|
|
|
|
# --- Comments ---
|
|
|
|
@router.get("/tasks/{task_id}/comments")
|
|
async def get_comments(project_id: str, task_id: str,
|
|
comment_type: Optional[str] = None):
|
|
bb = _bb(project_id)
|
|
comments = bb.get_comments(task_id, comment_type=comment_type)
|
|
return {"comments": [dict(c.__dict__) for c in comments]}
|
|
|
|
|
|
@router.post("/tasks/{task_id}/comments")
|
|
async def add_comment(project_id: str, task_id: str, body: Dict[str, Any]):
|
|
bb = _bb(project_id)
|
|
cid = bb.add_comment(task_id, body["author"], body["body"],
|
|
comment_type=body.get("comment_type", "general"),
|
|
mentions=body.get("mentions"))
|
|
return {"ok": True, "comment_id": cid}
|
|
|
|
|
|
# --- Outputs ---
|
|
|
|
@router.get("/tasks/{task_id}/outputs")
|
|
async def get_outputs(project_id: str, task_id: str):
|
|
bb = _bb(project_id)
|
|
return {"outputs": [dict(o.__dict__) for o in bb.get_outputs(task_id)]}
|
|
|
|
|
|
@router.post("/tasks/{task_id}/outputs")
|
|
async def write_output(project_id: str, task_id: str, body: Dict[str, Any]):
|
|
bb = _bb(project_id)
|
|
oid = bb.write_output(task_id, body["agent"], body["type"],
|
|
body["title"], content_path=body.get("path"),
|
|
summary=body.get("summary"),
|
|
metadata=body.get("metadata"))
|
|
return {"ok": True, "output_id": oid}
|
|
|
|
|
|
# --- Decisions ---
|
|
|
|
@router.post("/tasks/{task_id}/decisions")
|
|
async def add_decision(project_id: str, task_id: str, body: Dict[str, Any]):
|
|
bb = _bb(project_id)
|
|
did = bb.add_decision(task_id, body["decider"], body["decision"],
|
|
body["rationale"],
|
|
alternatives=body.get("alternatives"))
|
|
return {"ok": True, "decision_id": did}
|
|
|
|
|
|
# --- Observations ---
|
|
|
|
@router.post("/tasks/{task_id}/observations")
|
|
async def add_observation(project_id: str, task_id: str, body: Dict[str, Any]):
|
|
bb = _bb(project_id)
|
|
oid = bb.add_observation(task_id, body["observer"], body["body"],
|
|
severity=body.get("severity", "info"))
|
|
return {"ok": True, "observation_id": oid}
|
|
|
|
|
|
# --- Reviews ---
|
|
|
|
@router.get("/tasks/{task_id}/reviews")
|
|
async def get_reviews(project_id: str, task_id: str):
|
|
bb = _bb(project_id)
|
|
return {"reviews": [dict(r.__dict__) for r in bb.get_reviews(task_id)]}
|
|
|
|
|
|
@router.post("/tasks/{task_id}/reviews")
|
|
async def add_review(project_id: str, task_id: str, body: Dict[str, Any]):
|
|
bb = _bb(project_id)
|
|
review = Review(
|
|
id=body["id"], task_id=task_id, reviewer=body["reviewer"],
|
|
review_type=body["review_type"], verdict=body["verdict"],
|
|
summary=body["summary"], confidence=body.get("confidence"),
|
|
round=body.get("round", 1), max_rounds=body.get("max_rounds", 3),
|
|
)
|
|
bb.add_review(review)
|
|
return {"ok": True, "review_id": review.id}
|
|
|
|
|
|
# --- Events ---
|
|
|
|
@router.get("/events")
|
|
async def get_events(project_id: str, limit: int = Query(50, le=200)):
|
|
q = _q(project_id)
|
|
return {"events": q.recent_events(limit)}
|
|
|
|
|
|
# --- Summary ---
|
|
|
|
@router.get("/summary")
|
|
async def task_summary(project_id: str):
|
|
q = _q(project_id)
|
|
return {"summary": q.task_summary()}
|
|
|
|
|
|
# --- Helper ---
|
|
|
|
def _task_to_dict(t: Task) -> Dict[str, Any]:
|
|
d = {k: v for k, v in t.__dict__.items() if v is not None}
|
|
return d
|