feat: Agent System Prompts MVP (B) - database, backend API, and frontend integration

This commit is contained in:
Apple
2025-11-30 14:04:48 -08:00
parent bca81dc719
commit 1830109a95
10 changed files with 624 additions and 173 deletions

View File

@@ -13,6 +13,7 @@ import asyncio
# Import new modules
import routes_city
import routes_agents
import ws_city
import repo_city
import migrations # Import migrations
@@ -62,6 +63,7 @@ app.add_middleware(
app.include_router(routes_city.router)
app.include_router(routes_city.public_router)
app.include_router(routes_city.api_router)
app.include_router(routes_agents.router)
# Governance API routers
app.include_router(routes_governance.router)

View File

@@ -897,6 +897,23 @@ async def update_agent_prompt(
}
async def upsert_agent_prompts(agent_id: str, prompts: List[dict], created_by: str) -> List[dict]:
"""
Пакетне оновлення промтів агента.
"""
results = []
for p in prompts:
res = await update_agent_prompt(
agent_id=agent_id,
kind=p["kind"],
content=p["content"],
created_by=created_by,
note=p.get("note")
)
results.append(res)
return results
async def get_agent_prompt_history(agent_id: str, kind: str, limit: int = 10) -> List[dict]:
"""
Отримати історію версій промту агента.

View File

@@ -0,0 +1,118 @@
from fastapi import APIRouter, HTTPException, Request, Depends
from typing import List
import logging
import repo_city
from schemas_agents import AgentPromptList, AgentPromptUpsertRequest, AgentPrompt
router = APIRouter(prefix="/agents", tags=["agents"])
logger = logging.getLogger(__name__)
@router.get("/{agent_id}/prompts", response_model=AgentPromptList)
async def get_agent_prompts(agent_id: str):
"""
Отримати системні промти агента.
"""
try:
# Check agent exists
agent = await repo_city.get_agent_by_id(agent_id)
if not agent:
raise HTTPException(status_code=404, detail=f"Agent not found: {agent_id}")
# Get prompts dict from repo
prompts_dict = await repo_city.get_agent_prompts(agent_id)
# Convert to list of AgentPrompt models
prompts_list = []
valid_kinds = ["core", "safety", "governance", "tools"]
for kind in valid_kinds:
p_data = prompts_dict.get(kind)
if p_data:
prompts_list.append(AgentPrompt(
kind=kind,
content=p_data["content"],
version=p_data["version"],
updated_at=p_data["created_at"], # repo returns created_at as isoformat string or datetime? Repo returns isoformat string in dict
note=p_data.get("note")
))
else:
# Should we return empty prompt structure or just skip?
# The frontend expects 4 kinds. If we skip, frontend might need adjustment.
# But AgentPrompt requires content.
# Let's return empty content if missing, or just skip and let frontend handle default.
# Frontend AgentSystemPromptsCard handles missing prompts gracefully?
# Yes: const currentPrompt = systemPrompts?.[activeTab];
pass
# However, the response model is AgentPromptList which has prompts: List[AgentPrompt].
# If we return a list, the frontend needs to map it back to dict by kind.
# The user requested GET returns AgentPromptList.
# Wait, the frontend `useAgentPrompts` implementation in the prompt suggests:
# return { prompts: data?.prompts ?? [] }
# And the component maps it:
# for (const p of prompts) { map[p.kind] = p.content; }
return AgentPromptList(agent_id=agent_id, prompts=prompts_list)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get agent prompts: {e}")
raise HTTPException(status_code=500, detail="Failed to get agent prompts")
@router.put("/{agent_id}/prompts", response_model=AgentPromptList)
async def upsert_agent_prompts_endpoint(agent_id: str, payload: AgentPromptUpsertRequest, request: Request):
"""
Оновити системні промти агента (bulk).
"""
try:
# Check agent exists
agent = await repo_city.get_agent_by_id(agent_id)
if not agent:
raise HTTPException(status_code=404, detail=f"Agent not found: {agent_id}")
# TODO: Get user from auth
created_by = "ARCHITECT"
# Upsert
# Convert Pydantic models to dicts for repo
prompts_to_update = []
for p in payload.prompts:
prompts_to_update.append({
"kind": p.kind,
"content": p.content,
"note": p.note
})
if not prompts_to_update:
# Nothing to update, just return current state
pass
else:
await repo_city.upsert_agent_prompts(agent_id, prompts_to_update, created_by)
# Return updated state
# Re-use get logic
prompts_dict = await repo_city.get_agent_prompts(agent_id)
prompts_list = []
valid_kinds = ["core", "safety", "governance", "tools"]
for kind in valid_kinds:
p_data = prompts_dict.get(kind)
if p_data:
prompts_list.append(AgentPrompt(
kind=kind,
content=p_data["content"],
version=p_data["version"],
updated_at=p_data["created_at"],
note=p_data.get("note")
))
return AgentPromptList(agent_id=agent_id, prompts=prompts_list)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to upsert agent prompts: {e}")
raise HTTPException(status_code=500, detail="Failed to upsert agent prompts")

View File

@@ -0,0 +1,27 @@
from pydantic import BaseModel, Field
from typing import List, Literal, Optional
from datetime import datetime
PromptKind = Literal["core", "safety", "governance", "tools"]
class AgentPrompt(BaseModel):
id: Optional[str] = None
kind: PromptKind
content: str
version: int
updated_at: Optional[datetime] = None
created_by: Optional[str] = None
note: Optional[str] = None
class AgentPromptList(BaseModel):
agent_id: str
prompts: List[AgentPrompt]
class AgentPromptUpsertItem(BaseModel):
kind: PromptKind
content: str
note: Optional[str] = None
class AgentPromptUpsertRequest(BaseModel):
prompts: List[AgentPromptUpsertItem] = Field(default_factory=list)