symbiont/symbiont/api_additions.py

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]
"""