347 lines
11 KiB
Python
347 lines
11 KiB
Python
"""
|
|
FastAPI Endpoint Additions for Compound Tasks
|
|
==============================================
|
|
|
|
New endpoints to integrate into the existing Symbiont API (/data/symbiont/symbiont/api.py).
|
|
|
|
These endpoints expose the compound task system to external callers:
|
|
1. POST /task/compound - Submit a new compound task for execution
|
|
2. GET /task/{task_id}/progress - Poll for task progress
|
|
3. GET /tasks/recent - List recent tasks (dashboard view)
|
|
|
|
Authentication:
|
|
- Task submission requires a bearer token
|
|
- Progress polling requires no auth (task ID is the "secret")
|
|
- Recent tasks list is unauthenticated (low-sensitivity data)
|
|
|
|
Integration Instructions:
|
|
1. Import task_manager functions at the top of api.py:
|
|
from .task_manager import submit_compound_task, get_task_progress, list_recent_tasks
|
|
|
|
2. Define authentication token (set to your secret):
|
|
TASK_AUTH_TOKEN = "cortex-tasks-2026"
|
|
|
|
3. Add these classes and functions to api.py
|
|
|
|
4. Add the three routes to your FastAPI app instance
|
|
"""
|
|
|
|
from fastapi import FastAPI, HTTPException, Depends, Header
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional, List, Dict, Any
|
|
from .task_manager import submit_compound_task, get_task_progress, list_recent_tasks
|
|
|
|
# These imports should be added to your api.py
|
|
# from .task_manager import submit_compound_task, get_task_progress, list_recent_tasks
|
|
|
|
|
|
# ============================================================================
|
|
# Configuration
|
|
# ============================================================================
|
|
|
|
# Simple shared secret for task submission auth
|
|
# In production, consider using OAuth2, API keys, or other stronger methods
|
|
TASK_AUTH_TOKEN = "cortex-tasks-2026"
|
|
|
|
|
|
# ============================================================================
|
|
# Authentication
|
|
# ============================================================================
|
|
|
|
def verify_task_auth(
|
|
authorization: Optional[str] = Header(None),
|
|
token: Optional[str] = None
|
|
) -> str:
|
|
"""
|
|
Verify task submission authentication.
|
|
|
|
Supports two methods:
|
|
1. Bearer token in Authorization header: "Authorization: Bearer {token}"
|
|
2. Query parameter: ?token={token}
|
|
|
|
Args:
|
|
authorization: Authorization header value
|
|
token: Token from query parameter
|
|
|
|
Returns:
|
|
Verified token
|
|
|
|
Raises:
|
|
HTTPException 401: Invalid or missing token
|
|
"""
|
|
auth_token = token or (
|
|
authorization.replace("Bearer ", "") if authorization else None
|
|
)
|
|
|
|
if auth_token != TASK_AUTH_TOKEN:
|
|
raise HTTPException(status_code=401, detail="Invalid or missing auth token")
|
|
|
|
return auth_token
|
|
|
|
|
|
# ============================================================================
|
|
# Request/Response Models
|
|
# ============================================================================
|
|
|
|
class CompoundTaskRequest(BaseModel):
|
|
"""Request body for compound task submission."""
|
|
|
|
prompt: str = Field(
|
|
...,
|
|
description="The user's request to decompose and execute",
|
|
min_length=1,
|
|
max_length=5000
|
|
)
|
|
token: Optional[str] = Field(
|
|
None,
|
|
description="Auth token (alternative to Bearer header)"
|
|
)
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"prompt": "Search for Python concurrency patterns and summarize the top 3"
|
|
}
|
|
}
|
|
|
|
|
|
class CompoundTaskResponse(BaseModel):
|
|
"""Immediate response from task submission."""
|
|
|
|
id: str = Field(
|
|
...,
|
|
description="Unique task ID for polling progress"
|
|
)
|
|
status: str = Field(
|
|
...,
|
|
description="Current task status (planned, executing, completed, partial)"
|
|
)
|
|
subtask_count: int = Field(
|
|
...,
|
|
description="Total number of subtasks in the plan"
|
|
)
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"id": "compound-a1b2c3d4e5f6",
|
|
"status": "planned",
|
|
"subtask_count": 3
|
|
}
|
|
}
|
|
|
|
|
|
class SubtaskSnapshot(BaseModel):
|
|
"""Current state of a single subtask."""
|
|
|
|
id: str
|
|
index: int
|
|
description: str
|
|
tier_hint: int
|
|
tier_assigned: Optional[int]
|
|
model: Optional[str]
|
|
depends_on: List[int]
|
|
status: str
|
|
result: Optional[str]
|
|
cost: Optional[float]
|
|
started_at: Optional[str]
|
|
completed_at: Optional[str]
|
|
|
|
|
|
class TaskProgressResponse(BaseModel):
|
|
"""Complete progress snapshot for a compound task."""
|
|
|
|
id: str
|
|
prompt: str
|
|
status: str
|
|
reasoning: str
|
|
subtasks: List[SubtaskSnapshot]
|
|
created_at: str
|
|
planned_at: str
|
|
completed_at: Optional[str]
|
|
total_cost: float
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"id": "compound-a1b2c3d4e5f6",
|
|
"prompt": "Search for Python patterns and summarize",
|
|
"status": "executing",
|
|
"reasoning": "Breaking into search, summarization, and output formatting",
|
|
"subtasks": [
|
|
{
|
|
"id": "compound-a1b2c3d4e5f6-sub-0",
|
|
"index": 0,
|
|
"description": "Search for Python concurrency patterns",
|
|
"tier_hint": 2,
|
|
"tier_assigned": 2,
|
|
"model": "sonnet",
|
|
"depends_on": [],
|
|
"status": "completed",
|
|
"result": "Found patterns including...",
|
|
"cost": 0.04,
|
|
"started_at": "2026-03-21T15:30:00Z",
|
|
"completed_at": "2026-03-21T15:30:05Z"
|
|
}
|
|
],
|
|
"created_at": "2026-03-21T15:30:00Z",
|
|
"planned_at": "2026-03-21T15:30:00Z",
|
|
"completed_at": None,
|
|
"total_cost": 0.04
|
|
}
|
|
}
|
|
|
|
|
|
class TaskSummary(BaseModel):
|
|
"""Summary of a task for list view."""
|
|
|
|
id: str
|
|
prompt: str
|
|
status: str
|
|
subtask_count: int
|
|
completed_count: int
|
|
total_cost: float
|
|
created_at: str
|
|
completed_at: Optional[str]
|
|
|
|
|
|
# ============================================================================
|
|
# Endpoints
|
|
# ============================================================================
|
|
|
|
def setup_compound_task_endpoints(app: FastAPI) -> None:
|
|
"""
|
|
Register all compound task endpoints on the FastAPI app.
|
|
|
|
Call this from your main api.py file:
|
|
from .api_additions import setup_compound_task_endpoints
|
|
setup_compound_task_endpoints(app)
|
|
|
|
Or manually add the @app.post/@app.get routes below.
|
|
"""
|
|
|
|
@app.post(
|
|
"/task/compound",
|
|
response_model=CompoundTaskResponse,
|
|
tags=["Compound Tasks"],
|
|
summary="Submit a compound task",
|
|
description="Submit a user prompt to be decomposed into subtasks and executed in parallel"
|
|
)
|
|
async def create_compound_task(
|
|
req: CompoundTaskRequest,
|
|
auth: str = Depends(verify_task_auth)
|
|
) -> CompoundTaskResponse:
|
|
"""
|
|
Submit a compound task for execution.
|
|
|
|
The task is decomposed by Haiku into subtasks that can run in parallel.
|
|
Returns immediately with a task ID for polling progress.
|
|
|
|
Args:
|
|
req: Task request with prompt and optional token
|
|
auth: Verified authentication token
|
|
|
|
Returns:
|
|
Task ID and initial status
|
|
|
|
Example:
|
|
curl -X POST http://localhost:8000/task/compound \\
|
|
-H "Authorization: Bearer cortex-tasks-2026" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{"prompt": "Search for patterns and summarize"}'
|
|
"""
|
|
result = submit_compound_task(req.prompt, auth_token=auth)
|
|
return CompoundTaskResponse(**result)
|
|
|
|
@app.get(
|
|
"/task/{task_id}/progress",
|
|
response_model=TaskProgressResponse,
|
|
tags=["Compound Tasks"],
|
|
summary="Get task progress",
|
|
description="Poll for current execution status of a compound task"
|
|
)
|
|
async def task_progress(task_id: str) -> TaskProgressResponse:
|
|
"""
|
|
Poll for compound task progress.
|
|
|
|
No authentication required - task ID serves as the secret. Clients can poll
|
|
this endpoint to track execution progress and see individual subtask results.
|
|
|
|
Args:
|
|
task_id: The task ID from create_compound_task
|
|
|
|
Returns:
|
|
Complete task snapshot including all subtasks and their progress
|
|
|
|
Raises:
|
|
404: Task not found (expired from cache or invalid ID)
|
|
|
|
Example:
|
|
curl http://localhost:8000/task/compound-a1b2c3d4e5f6/progress
|
|
"""
|
|
progress = get_task_progress(task_id)
|
|
if progress is None:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
return TaskProgressResponse(**progress)
|
|
|
|
@app.get(
|
|
"/tasks/recent",
|
|
response_model=List[TaskSummary],
|
|
tags=["Compound Tasks"],
|
|
summary="List recent tasks",
|
|
description="Get summaries of recently submitted compound tasks"
|
|
)
|
|
async def recent_tasks(limit: int = 20) -> List[TaskSummary]:
|
|
"""
|
|
List recent compound tasks (dashboard view).
|
|
|
|
Useful for monitoring system activity and recent executions.
|
|
No authentication required.
|
|
|
|
Args:
|
|
limit: Maximum number of tasks to return (default 20, max 100)
|
|
|
|
Returns:
|
|
List of task summaries ordered by most recent first
|
|
|
|
Example:
|
|
curl 'http://localhost:8000/tasks/recent?limit=10'
|
|
"""
|
|
if limit > 100:
|
|
limit = 100
|
|
tasks = list_recent_tasks(limit=limit)
|
|
return [TaskSummary(**t) for t in tasks]
|
|
|
|
|
|
# ============================================================================
|
|
# Manual Integration (if not using setup_compound_task_endpoints)
|
|
# ============================================================================
|
|
|
|
"""
|
|
If you prefer to manually add routes instead of using setup_compound_task_endpoints(),
|
|
add these to your api.py after creating the FastAPI app:
|
|
|
|
# At the top of api.py, import these:
|
|
from .task_manager import submit_compound_task, get_task_progress, list_recent_tasks
|
|
|
|
# Then add these route definitions:
|
|
|
|
@app.post("/task/compound", response_model=CompoundTaskResponse, tags=["Compound Tasks"])
|
|
async def create_compound_task(req: CompoundTaskRequest, auth: str = Depends(verify_task_auth)):
|
|
result = submit_compound_task(req.prompt, auth_token=auth)
|
|
return CompoundTaskResponse(**result)
|
|
|
|
@app.get("/task/{task_id}/progress", response_model=TaskProgressResponse, tags=["Compound Tasks"])
|
|
async def task_progress(task_id: str):
|
|
progress = get_task_progress(task_id)
|
|
if progress is None:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
return TaskProgressResponse(**progress)
|
|
|
|
@app.get("/tasks/recent", response_model=List[TaskSummary], tags=["Compound Tasks"])
|
|
async def recent_tasks(limit: int = 20):
|
|
if limit > 100:
|
|
limit = 100
|
|
tasks = list_recent_tasks(limit=limit)
|
|
return [TaskSummary(**t) for t in tasks]
|
|
""" |