Phase 5.5-5.9: Plugin-Marketplace, Agent Memory, GraphRAG, Subagents, External Agent API
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
5.5 Plugin-Marketplace: - New plugin: marketplace/ (models, routes, services, schemas, config) - MarketplaceListing model (global, no tenant_id) - Ed25519 signature verification via PluginSignature - Endpoints: list, detail, install, verify, categories - Config: MARKETPLACE_SERVER_URL setting 5.6 Agent Memory (persistent): - New plugin: agent_memory/ (models, routes, services, schemas) - AgentMemory model with embedding vector(768) + HNSW index - store_memory() with auto-embedding - retrieve_relevant_memories() with pgvector cosine similarity - Semantic search endpoint 5.7 GraphRAG: - New plugin: graph_rag/ (models, routes, services, provider, schemas) - EntityRelationship model (source/target type+id, relationship_type, metadata) - BFS graph traversal (bidirectional, configurable depth) - GraphRAGSearchProvider registered in unified_search 5.8 Subagents / Multi-Agent: - AgentCoordinator class (create_subtask, wait_for_subtask, aggregate, cancel) - AgentSubtask model + migration 0002_agent_subtasks.sql - 6 new API endpoints for subtask management - Tools registered in AI tool registry 5.9 External Agent API: - external_api.py: POST /run, GET /status, POST /stream (SSE) - Bearer API token authentication - Rate limiting: 10 req/min per token - ExternalAgentRequest/Response schemas 3 new plugins registered in main.py and __init__.py All files py_compile clean
This commit is contained in:
@@ -624,3 +624,241 @@ async def restore_automation_version(
|
||||
return _automation_to_response(automation)
|
||||
|
||||
|
||||
|
||||
|
||||
# ─── Subtask Endpoints (Phase 5.8) ───
|
||||
|
||||
|
||||
@router.get(
|
||||
"/subtasks",
|
||||
dependencies=[Depends(require_permission("agents:read"))],
|
||||
response_model=SubtaskListResponse,
|
||||
)
|
||||
async def list_subtasks(
|
||||
parent_agent_id: str | None = Query(None),
|
||||
child_agent_id: str | None = Query(None),
|
||||
status: str | None = Query(None),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List subtasks with optional filters."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
try:
|
||||
parent_id = uuid.UUID(parent_agent_id) if parent_agent_id else None
|
||||
child_id = uuid.UUID(child_agent_id) if child_agent_id else None
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="Invalid agent ID")
|
||||
|
||||
from app.plugins.builtins.automation.agent_coordinator import AgentCoordinator
|
||||
|
||||
items, total = await AgentCoordinator.list_subtasks(
|
||||
db, tenant_id,
|
||||
parent_agent_id=parent_id,
|
||||
child_agent_id=child_id,
|
||||
status=status,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
return SubtaskListResponse(
|
||||
items=[_subtask_to_response(s) for s in items],
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/subtasks",
|
||||
dependencies=[Depends(require_permission("agents:execute"))],
|
||||
response_model=SubtaskRead,
|
||||
status_code=201,
|
||||
)
|
||||
async def create_subtask(
|
||||
data: SubtaskCreate,
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Create a new subtask for multi-agent orchestration."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
try:
|
||||
parent_id = uuid.UUID(data.parent_agent_id)
|
||||
child_id = uuid.UUID(data.child_agent_id)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="Invalid agent ID")
|
||||
|
||||
from app.plugins.builtins.automation.agent_coordinator import AgentCoordinator
|
||||
|
||||
subtask = await AgentCoordinator.create_subtask(
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
parent_agent_id=parent_id,
|
||||
child_agent_id=child_id,
|
||||
task_description=data.task_description,
|
||||
)
|
||||
await db.commit()
|
||||
return _subtask_to_response(subtask)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/subtasks/{subtask_id}",
|
||||
dependencies=[Depends(require_permission("agents:read"))],
|
||||
response_model=SubtaskRead,
|
||||
)
|
||||
async def get_subtask(
|
||||
subtask_id: str,
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get a single subtask by ID."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
try:
|
||||
sid = uuid.UUID(subtask_id)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="Invalid subtask ID")
|
||||
|
||||
from app.plugins.builtins.automation.models import AgentSubtask
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await db.execute(
|
||||
select(AgentSubtask)
|
||||
.where(AgentSubtask.id == sid)
|
||||
.where(AgentSubtask.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
)
|
||||
subtask = result.scalar_one_or_none()
|
||||
if subtask is None:
|
||||
raise HTTPException(status_code=404, detail="Subtask not found")
|
||||
return _subtask_to_response(subtask)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/subtasks/{subtask_id}",
|
||||
dependencies=[Depends(require_permission("agents:execute"))],
|
||||
response_model=SubtaskRead,
|
||||
)
|
||||
async def update_subtask(
|
||||
subtask_id: str,
|
||||
data: SubtaskUpdate,
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update a subtask (status, result)."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
try:
|
||||
sid = uuid.UUID(subtask_id)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="Invalid subtask ID")
|
||||
|
||||
from app.plugins.builtins.automation.models import AgentSubtask
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await db.execute(
|
||||
select(AgentSubtask)
|
||||
.where(AgentSubtask.id == sid)
|
||||
.where(AgentSubtask.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
)
|
||||
subtask = result.scalar_one_or_none()
|
||||
if subtask is None:
|
||||
raise HTTPException(status_code=404, detail="Subtask not found")
|
||||
|
||||
update_data = data.model_dump(exclude_none=True)
|
||||
if "status" in update_data:
|
||||
subtask.status = update_data["status"]
|
||||
if update_data["status"] in ("completed", "failed", "cancelled"):
|
||||
subtask.completed_at = __import__("datetime").datetime.now(
|
||||
__import__("datetime").timezone.utc
|
||||
)
|
||||
if "result" in update_data:
|
||||
subtask.result = update_data["result"]
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(subtask)
|
||||
return _subtask_to_response(subtask)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/subtasks/{subtask_id}/cancel",
|
||||
dependencies=[Depends(require_permission("agents:execute"))],
|
||||
)
|
||||
async def cancel_subtask(
|
||||
subtask_id: str,
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Cancel a pending or running subtask."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
try:
|
||||
sid = uuid.UUID(subtask_id)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="Invalid subtask ID")
|
||||
|
||||
from app.plugins.builtins.automation.agent_coordinator import AgentCoordinator
|
||||
|
||||
success = await AgentCoordinator.cancel_subtask(db, tenant_id, sid)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Subtask not found or already in terminal state",
|
||||
)
|
||||
await db.commit()
|
||||
return {"status": "cancelled", "subtask_id": subtask_id}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/subtasks/{subtask_id}/wait",
|
||||
dependencies=[Depends(require_permission("agents:execute"))],
|
||||
)
|
||||
async def wait_for_subtask(
|
||||
subtask_id: str,
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Wait for a subtask to complete (polls until terminal state)."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
try:
|
||||
sid = uuid.UUID(subtask_id)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="Invalid subtask ID")
|
||||
|
||||
from app.plugins.builtins.automation.agent_coordinator import AgentCoordinator
|
||||
|
||||
result = await AgentCoordinator.wait_for_subtask(db, tenant_id, sid)
|
||||
return result
|
||||
|
||||
|
||||
@router.post(
|
||||
"/subtasks/aggregate",
|
||||
dependencies=[Depends(require_permission("agents:read"))],
|
||||
)
|
||||
async def aggregate_subtasks(
|
||||
subtask_ids: list[str],
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Aggregate results from multiple subtasks."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
try:
|
||||
ids = [uuid.UUID(sid) for sid in subtask_ids]
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="Invalid subtask ID in list")
|
||||
|
||||
from app.plugins.builtins.automation.agent_coordinator import AgentCoordinator
|
||||
|
||||
result = await AgentCoordinator.aggregate_results(db, tenant_id, ids)
|
||||
return result
|
||||
|
||||
|
||||
def _subtask_to_response(s) -> SubtaskRead:
|
||||
"""Convert AgentSubtask model to response schema."""
|
||||
return SubtaskRead(
|
||||
id=str(s.id),
|
||||
parent_agent_id=str(s.parent_agent_id),
|
||||
child_agent_id=str(s.child_agent_id),
|
||||
task_description=s.task_description,
|
||||
status=s.status,
|
||||
result=s.result or {},
|
||||
created_at=s.created_at.isoformat() if s.created_at else None,
|
||||
completed_at=s.completed_at.isoformat() if s.completed_at else None,
|
||||
updated_at=s.updated_at.isoformat() if s.updated_at else None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user