feat: Agent System Prompts MVP (B) - database, backend API, and frontend integration
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
Отримати історію версій промту агента.
|
||||
|
||||
118
services/city-service/routes_agents.py
Normal file
118
services/city-service/routes_agents.py
Normal 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")
|
||||
|
||||
27
services/city-service/schemas_agents.py
Normal file
27
services/city-service/schemas_agents.py
Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user