|
27 | 27 | from fastapi.middleware.cors import CORSMiddleware |
28 | 28 | from rich.console import Console |
29 | 29 |
|
| 30 | +from src.goal.context import default_goal_criteria |
30 | 31 | from src.ui_services import build_run_analysis, load_run_context |
31 | 32 |
|
32 | 33 | # UTF-8 on Windows |
@@ -251,6 +252,15 @@ class CreateGoalRequest(BaseModel): |
251 | 252 | time_budget_seconds: Optional[int] = Field(None, ge=1) |
252 | 253 |
|
253 | 254 |
|
| 255 | +class UpdateGoalRequest(BaseModel): |
| 256 | + """Edit mutable finance research goal fields.""" |
| 257 | + |
| 258 | + goal_id: str = Field(..., min_length=1) |
| 259 | + expected_goal_id: str = Field(..., min_length=1) |
| 260 | + objective: Optional[str] = Field(None, min_length=1, max_length=5000) |
| 261 | + ui_summary: Optional[str] = Field(None, max_length=500) |
| 262 | + |
| 263 | + |
254 | 264 | class AddGoalEvidenceRequest(BaseModel): |
255 | 265 | """Append evidence to a finance research goal.""" |
256 | 266 |
|
@@ -295,6 +305,39 @@ class AddGoalEvidenceResponse(BaseModel): |
295 | 305 | snapshot: GoalSnapshotResponse |
296 | 306 |
|
297 | 307 |
|
| 308 | +class GoalAuditRowRequest(BaseModel): |
| 309 | + """One criterion row for goal status audits.""" |
| 310 | + |
| 311 | + criterion_id: str = Field(..., min_length=1) |
| 312 | + result: str = Field(..., min_length=1) |
| 313 | + evidence_ids: List[str] = Field(default_factory=list) |
| 314 | + notes: str = "" |
| 315 | + |
| 316 | + |
| 317 | +class UpdateGoalStatusRequest(BaseModel): |
| 318 | + """Update a finance research goal status.""" |
| 319 | + |
| 320 | + goal_id: str = Field(..., min_length=1) |
| 321 | + expected_goal_id: str = Field(..., min_length=1) |
| 322 | + status: str = Field(..., min_length=1) |
| 323 | + audit: List[GoalAuditRowRequest] = Field(default_factory=list) |
| 324 | + recap: Optional[str] = None |
| 325 | + |
| 326 | + |
| 327 | +class UpdateGoalStatusResponse(BaseModel): |
| 328 | + """Response after changing a goal status.""" |
| 329 | + |
| 330 | + goal: Dict[str, Any] |
| 331 | + snapshot: GoalSnapshotResponse |
| 332 | + |
| 333 | + |
| 334 | +class UpdateGoalResponse(BaseModel): |
| 335 | + """Response after editing a goal.""" |
| 336 | + |
| 337 | + goal: Dict[str, Any] |
| 338 | + snapshot: GoalSnapshotResponse |
| 339 | + |
| 340 | + |
298 | 341 |
|
299 | 342 | # ============================================================================ |
300 | 343 | # FastAPI Application |
@@ -1502,7 +1545,7 @@ async def create_session_goal(session_id: str, req: CreateGoalRequest): |
1502 | 1545 |
|
1503 | 1546 | criteria = [item.strip() for item in req.criteria if item.strip()] |
1504 | 1547 | if not criteria: |
1505 | | - criteria = ["Define research thesis", "Record at least one supporting or contradicting evidence row"] |
| 1548 | + criteria = default_goal_criteria() |
1506 | 1549 | try: |
1507 | 1550 | risk_tier = RiskTier(req.risk_tier) |
1508 | 1551 | except ValueError as exc: |
@@ -1548,6 +1591,41 @@ async def get_session_goal(session_id: str): |
1548 | 1591 | return snapshot |
1549 | 1592 |
|
1550 | 1593 |
|
| 1594 | +@app.patch( |
| 1595 | + "/sessions/{session_id}/goal", |
| 1596 | + response_model=UpdateGoalResponse, |
| 1597 | + dependencies=[Depends(require_auth)], |
| 1598 | +) |
| 1599 | +async def update_session_goal(session_id: str, req: UpdateGoalRequest): |
| 1600 | + """Edit the current finance research goal without replacing the session.""" |
| 1601 | + _validate_path_param(session_id, "session_id") |
| 1602 | + svc, _session = _get_existing_session_or_404(session_id) |
| 1603 | + from src.goal import StaleGoalError |
| 1604 | + |
| 1605 | + if req.objective is None and req.ui_summary is None: |
| 1606 | + raise HTTPException(status_code=400, detail="objective or ui_summary is required") |
| 1607 | + |
| 1608 | + goal_store = _get_goal_store() |
| 1609 | + try: |
| 1610 | + goal = goal_store.update_goal( |
| 1611 | + session_id=session_id, |
| 1612 | + goal_id=req.goal_id, |
| 1613 | + expected_goal_id=req.expected_goal_id, |
| 1614 | + objective=req.objective, |
| 1615 | + ui_summary=req.ui_summary, |
| 1616 | + ) |
| 1617 | + except StaleGoalError as exc: |
| 1618 | + raise HTTPException(status_code=409, detail=str(exc)) from exc |
| 1619 | + except ValueError as exc: |
| 1620 | + raise HTTPException(status_code=400, detail=str(exc)) from exc |
| 1621 | + |
| 1622 | + snapshot = goal_store.get_goal_snapshot(goal.goal_id) |
| 1623 | + if snapshot is None: |
| 1624 | + raise HTTPException(status_code=500, detail="Goal snapshot could not be reloaded") |
| 1625 | + svc.event_bus.emit(session_id, "goal.updated", {"goal": snapshot["goal"], "snapshot": snapshot}) |
| 1626 | + return {"goal": snapshot["goal"], "snapshot": snapshot} |
| 1627 | + |
| 1628 | + |
1551 | 1629 | @app.post( |
1552 | 1630 | "/sessions/{session_id}/goal/evidence", |
1553 | 1631 | response_model=AddGoalEvidenceResponse, |
@@ -1606,6 +1684,52 @@ async def add_session_goal_evidence(session_id: str, req: AddGoalEvidenceRequest |
1606 | 1684 | return {"evidence": asdict(evidence), "snapshot": snapshot} |
1607 | 1685 |
|
1608 | 1686 |
|
| 1687 | +@app.patch( |
| 1688 | + "/sessions/{session_id}/goal/status", |
| 1689 | + response_model=UpdateGoalStatusResponse, |
| 1690 | + dependencies=[Depends(require_auth)], |
| 1691 | +) |
| 1692 | +async def update_session_goal_status(session_id: str, req: UpdateGoalStatusRequest): |
| 1693 | + """Update the current finance research goal status.""" |
| 1694 | + _validate_path_param(session_id, "session_id") |
| 1695 | + svc, _session = _get_existing_session_or_404(session_id) |
| 1696 | + from src.goal import AuditRow, GoalStatus, StaleGoalError |
| 1697 | + |
| 1698 | + try: |
| 1699 | + next_status = GoalStatus(req.status) |
| 1700 | + except ValueError as exc: |
| 1701 | + raise HTTPException(status_code=400, detail=f"invalid goal status: {req.status}") from exc |
| 1702 | + |
| 1703 | + goal_store = _get_goal_store() |
| 1704 | + try: |
| 1705 | + goal = goal_store.update_status( |
| 1706 | + session_id=session_id, |
| 1707 | + goal_id=req.goal_id, |
| 1708 | + expected_goal_id=req.expected_goal_id, |
| 1709 | + status=next_status, |
| 1710 | + audit=[ |
| 1711 | + AuditRow( |
| 1712 | + criterion_id=row.criterion_id, |
| 1713 | + result=row.result, |
| 1714 | + evidence_ids=row.evidence_ids, |
| 1715 | + notes=row.notes, |
| 1716 | + ) |
| 1717 | + for row in req.audit |
| 1718 | + ], |
| 1719 | + recap=req.recap, |
| 1720 | + ) |
| 1721 | + except StaleGoalError as exc: |
| 1722 | + raise HTTPException(status_code=409, detail=str(exc)) from exc |
| 1723 | + except ValueError as exc: |
| 1724 | + raise HTTPException(status_code=400, detail=str(exc)) from exc |
| 1725 | + |
| 1726 | + snapshot = goal_store.get_goal_snapshot(goal.goal_id) |
| 1727 | + if snapshot is None: |
| 1728 | + raise HTTPException(status_code=500, detail="Goal snapshot could not be reloaded") |
| 1729 | + svc.event_bus.emit(session_id, "goal.updated", {"goal": snapshot["goal"], "snapshot": snapshot}) |
| 1730 | + return {"goal": snapshot["goal"], "snapshot": snapshot} |
| 1731 | + |
| 1732 | + |
1609 | 1733 | @app.delete("/sessions/{session_id}", dependencies=[Depends(require_auth)]) |
1610 | 1734 | async def delete_session(session_id: str): |
1611 | 1735 | """Delete a session.""" |
|
0 commit comments