- Agent roles integration progress - Various backend and frontend updates - Storybook cache cleanup 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
474 lines
17 KiB
Python
474 lines
17 KiB
Python
"""
|
|
Context Feedback API endpoints for RL Context Curator integration
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
|
from sqlalchemy.orm import Session
|
|
from typing import List, Optional, Dict, Any
|
|
from datetime import datetime, timedelta
|
|
from pydantic import BaseModel, Field
|
|
|
|
from ..core.database import get_db
|
|
from ..models.context_feedback import ContextFeedback, AgentPermissions, PromotionRuleHistory
|
|
from ..models.task import Task
|
|
from ..models.agent import Agent
|
|
from ..services.auth import get_current_user
|
|
from ..models.responses import StatusResponse
|
|
|
|
router = APIRouter(prefix="/api/feedback", tags=["Context Feedback"])
|
|
|
|
|
|
# Pydantic models for API
|
|
class ContextFeedbackRequest(BaseModel):
|
|
"""Request model for context feedback"""
|
|
context_id: str = Field(..., description="HCFS context ID")
|
|
feedback_type: str = Field(..., description="Type of feedback: upvote, downvote, forgetfulness, task_success, task_failure")
|
|
confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence in feedback")
|
|
reason: Optional[str] = Field(None, description="Optional reason for feedback")
|
|
usage_context: Optional[str] = Field(None, description="Context of usage")
|
|
directory_scope: Optional[str] = Field(None, description="Directory where context was used")
|
|
task_type: Optional[str] = Field(None, description="Type of task being performed")
|
|
|
|
|
|
class TaskOutcomeFeedbackRequest(BaseModel):
|
|
"""Request model for task outcome feedback"""
|
|
task_id: str = Field(..., description="Task ID")
|
|
outcome: str = Field(..., description="Task outcome: completed, failed, abandoned")
|
|
completion_time: Optional[int] = Field(None, description="Time to complete in seconds")
|
|
errors_encountered: int = Field(0, description="Number of errors during execution")
|
|
follow_up_questions: int = Field(0, description="Number of follow-up questions")
|
|
context_used: Optional[List[str]] = Field(None, description="Context IDs used in task")
|
|
context_relevance_score: Optional[float] = Field(None, ge=0.0, le=1.0, description="Average relevance of used context")
|
|
outcome_confidence: Optional[float] = Field(None, ge=0.0, le=1.0, description="Confidence in outcome classification")
|
|
|
|
|
|
class AgentPermissionsRequest(BaseModel):
|
|
"""Request model for agent permissions"""
|
|
agent_id: str = Field(..., description="Agent ID")
|
|
role: str = Field(..., description="Agent role")
|
|
directory_patterns: List[str] = Field(..., description="Directory patterns for this role")
|
|
task_types: List[str] = Field(..., description="Task types this agent can handle")
|
|
context_weight: float = Field(1.0, ge=0.1, le=2.0, description="Weight for context relevance")
|
|
|
|
|
|
class ContextFeedbackResponse(BaseModel):
|
|
"""Response model for context feedback"""
|
|
id: int
|
|
context_id: str
|
|
agent_id: str
|
|
task_id: Optional[str]
|
|
feedback_type: str
|
|
role: str
|
|
confidence: float
|
|
reason: Optional[str]
|
|
usage_context: Optional[str]
|
|
directory_scope: Optional[str]
|
|
task_type: Optional[str]
|
|
timestamp: datetime
|
|
|
|
|
|
class FeedbackStatsResponse(BaseModel):
|
|
"""Response model for feedback statistics"""
|
|
total_feedback: int
|
|
feedback_by_type: Dict[str, int]
|
|
feedback_by_role: Dict[str, int]
|
|
average_confidence: float
|
|
recent_feedback_count: int
|
|
top_contexts: List[Dict[str, Any]]
|
|
|
|
|
|
@router.post("/context/{context_id}", response_model=StatusResponse)
|
|
async def submit_context_feedback(
|
|
context_id: str,
|
|
request: ContextFeedbackRequest,
|
|
background_tasks: BackgroundTasks,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Submit feedback for a specific context
|
|
"""
|
|
try:
|
|
# Get agent information
|
|
agent = db.query(Agent).filter(Agent.id == current_user.get("agent_id", "unknown")).first()
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail="Agent not found")
|
|
|
|
# Validate feedback type
|
|
valid_types = ["upvote", "downvote", "forgetfulness", "task_success", "task_failure"]
|
|
if request.feedback_type not in valid_types:
|
|
raise HTTPException(status_code=400, detail=f"Invalid feedback type. Must be one of: {valid_types}")
|
|
|
|
# Create feedback record
|
|
feedback = ContextFeedback(
|
|
context_id=request.context_id,
|
|
agent_id=agent.id,
|
|
feedback_type=request.feedback_type,
|
|
role=agent.role if agent.role else "general",
|
|
confidence=request.confidence,
|
|
reason=request.reason,
|
|
usage_context=request.usage_context,
|
|
directory_scope=request.directory_scope,
|
|
task_type=request.task_type
|
|
)
|
|
|
|
db.add(feedback)
|
|
db.commit()
|
|
db.refresh(feedback)
|
|
|
|
# Send feedback to RL Context Curator in background
|
|
background_tasks.add_task(
|
|
send_feedback_to_rl_curator,
|
|
feedback.id,
|
|
request.context_id,
|
|
request.feedback_type,
|
|
agent.id,
|
|
agent.role if agent.role else "general",
|
|
request.confidence
|
|
)
|
|
|
|
return StatusResponse(
|
|
status="success",
|
|
message="Context feedback submitted successfully",
|
|
data={"feedback_id": feedback.id, "context_id": request.context_id}
|
|
)
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to submit feedback: {str(e)}")
|
|
|
|
|
|
@router.post("/task-outcome/{task_id}", response_model=StatusResponse)
|
|
async def submit_task_outcome_feedback(
|
|
task_id: str,
|
|
request: TaskOutcomeFeedbackRequest,
|
|
background_tasks: BackgroundTasks,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Submit task outcome feedback for RL learning
|
|
"""
|
|
try:
|
|
# Get task
|
|
task = db.query(Task).filter(Task.id == task_id).first()
|
|
if not task:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
|
|
# Update task with outcome metrics
|
|
task.task_outcome = request.outcome
|
|
task.completion_time = request.completion_time
|
|
task.errors_encountered = request.errors_encountered
|
|
task.follow_up_questions = request.follow_up_questions
|
|
task.context_relevance_score = request.context_relevance_score
|
|
task.outcome_confidence = request.outcome_confidence
|
|
task.feedback_collected = True
|
|
|
|
if request.context_used:
|
|
task.context_used = request.context_used
|
|
|
|
if request.outcome in ["completed", "failed", "abandoned"] and not task.completed_at:
|
|
task.completed_at = datetime.utcnow()
|
|
|
|
# Calculate success rate
|
|
if request.outcome == "completed":
|
|
task.success_rate = 1.0 - (request.errors_encountered * 0.1) # Simple calculation
|
|
task.success_rate = max(0.0, min(1.0, task.success_rate))
|
|
else:
|
|
task.success_rate = 0.0
|
|
|
|
db.commit()
|
|
|
|
# Create feedback events for used contexts
|
|
if request.context_used and task.assigned_agent_id:
|
|
agent = db.query(Agent).filter(Agent.id == task.assigned_agent_id).first()
|
|
if agent:
|
|
feedback_type = "task_success" if request.outcome == "completed" else "task_failure"
|
|
|
|
for context_id in request.context_used:
|
|
feedback = ContextFeedback(
|
|
context_id=context_id,
|
|
agent_id=agent.id,
|
|
task_id=task.id,
|
|
feedback_type=feedback_type,
|
|
role=agent.role if agent.role else "general",
|
|
confidence=request.outcome_confidence or 0.8,
|
|
reason=f"Task {request.outcome}",
|
|
usage_context=f"task_execution_{request.outcome}",
|
|
task_type=request.task_type
|
|
)
|
|
db.add(feedback)
|
|
|
|
db.commit()
|
|
|
|
return StatusResponse(
|
|
status="success",
|
|
message="Task outcome feedback submitted successfully",
|
|
data={"task_id": task_id, "outcome": request.outcome}
|
|
)
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to submit task outcome: {str(e)}")
|
|
|
|
|
|
@router.get("/stats", response_model=FeedbackStatsResponse)
|
|
async def get_feedback_stats(
|
|
days: int = 7,
|
|
role: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get feedback statistics for analysis
|
|
"""
|
|
try:
|
|
# Base query
|
|
query = db.query(ContextFeedback)
|
|
|
|
# Filter by date range
|
|
if days > 0:
|
|
since_date = datetime.utcnow() - timedelta(days=days)
|
|
query = query.filter(ContextFeedback.timestamp >= since_date)
|
|
|
|
# Filter by role if specified
|
|
if role:
|
|
query = query.filter(ContextFeedback.role == role)
|
|
|
|
feedback_records = query.all()
|
|
|
|
# Calculate statistics
|
|
total_feedback = len(feedback_records)
|
|
|
|
feedback_by_type = {}
|
|
feedback_by_role = {}
|
|
confidence_values = []
|
|
context_usage = {}
|
|
|
|
for feedback in feedback_records:
|
|
# Count by type
|
|
feedback_by_type[feedback.feedback_type] = feedback_by_type.get(feedback.feedback_type, 0) + 1
|
|
|
|
# Count by role
|
|
feedback_by_role[feedback.role] = feedback_by_role.get(feedback.role, 0) + 1
|
|
|
|
# Collect confidence values
|
|
confidence_values.append(feedback.confidence)
|
|
|
|
# Count context usage
|
|
context_usage[feedback.context_id] = context_usage.get(feedback.context_id, 0) + 1
|
|
|
|
# Calculate average confidence
|
|
average_confidence = sum(confidence_values) / len(confidence_values) if confidence_values else 0.0
|
|
|
|
# Get recent feedback count (last 24 hours)
|
|
recent_since = datetime.utcnow() - timedelta(days=1)
|
|
recent_count = db.query(ContextFeedback).filter(
|
|
ContextFeedback.timestamp >= recent_since
|
|
).count()
|
|
|
|
# Get top contexts by usage
|
|
top_contexts = [
|
|
{"context_id": ctx_id, "usage_count": count}
|
|
for ctx_id, count in sorted(context_usage.items(), key=lambda x: x[1], reverse=True)[:10]
|
|
]
|
|
|
|
return FeedbackStatsResponse(
|
|
total_feedback=total_feedback,
|
|
feedback_by_type=feedback_by_type,
|
|
feedback_by_role=feedback_by_role,
|
|
average_confidence=average_confidence,
|
|
recent_feedback_count=recent_count,
|
|
top_contexts=top_contexts
|
|
)
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get feedback stats: {str(e)}")
|
|
|
|
|
|
@router.get("/recent", response_model=List[ContextFeedbackResponse])
|
|
async def get_recent_feedback(
|
|
limit: int = 50,
|
|
feedback_type: Optional[str] = None,
|
|
role: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get recent feedback events
|
|
"""
|
|
try:
|
|
query = db.query(ContextFeedback).order_by(ContextFeedback.timestamp.desc())
|
|
|
|
if feedback_type:
|
|
query = query.filter(ContextFeedback.feedback_type == feedback_type)
|
|
|
|
if role:
|
|
query = query.filter(ContextFeedback.role == role)
|
|
|
|
feedback_records = query.limit(limit).all()
|
|
|
|
return [
|
|
ContextFeedbackResponse(
|
|
id=fb.id,
|
|
context_id=fb.context_id,
|
|
agent_id=fb.agent_id,
|
|
task_id=str(fb.task_id) if fb.task_id else None,
|
|
feedback_type=fb.feedback_type,
|
|
role=fb.role,
|
|
confidence=fb.confidence,
|
|
reason=fb.reason,
|
|
usage_context=fb.usage_context,
|
|
directory_scope=fb.directory_scope,
|
|
task_type=fb.task_type,
|
|
timestamp=fb.timestamp
|
|
)
|
|
for fb in feedback_records
|
|
]
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get recent feedback: {str(e)}")
|
|
|
|
|
|
@router.post("/agent-permissions", response_model=StatusResponse)
|
|
async def set_agent_permissions(
|
|
request: AgentPermissionsRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Set or update agent permissions for context filtering
|
|
"""
|
|
try:
|
|
# Check if permissions already exist
|
|
existing = db.query(AgentPermissions).filter(
|
|
AgentPermissions.agent_id == request.agent_id,
|
|
AgentPermissions.role == request.role
|
|
).first()
|
|
|
|
if existing:
|
|
# Update existing permissions
|
|
existing.directory_patterns = ",".join(request.directory_patterns)
|
|
existing.task_types = ",".join(request.task_types)
|
|
existing.context_weight = request.context_weight
|
|
existing.updated_at = datetime.utcnow()
|
|
else:
|
|
# Create new permissions
|
|
permissions = AgentPermissions(
|
|
agent_id=request.agent_id,
|
|
role=request.role,
|
|
directory_patterns=",".join(request.directory_patterns),
|
|
task_types=",".join(request.task_types),
|
|
context_weight=request.context_weight
|
|
)
|
|
db.add(permissions)
|
|
|
|
db.commit()
|
|
|
|
return StatusResponse(
|
|
status="success",
|
|
message="Agent permissions updated successfully",
|
|
data={"agent_id": request.agent_id, "role": request.role}
|
|
)
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to set agent permissions: {str(e)}")
|
|
|
|
|
|
@router.get("/agent-permissions/{agent_id}")
|
|
async def get_agent_permissions(
|
|
agent_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get agent permissions for context filtering
|
|
"""
|
|
try:
|
|
permissions = db.query(AgentPermissions).filter(
|
|
AgentPermissions.agent_id == agent_id,
|
|
AgentPermissions.active == "true"
|
|
).all()
|
|
|
|
return [
|
|
{
|
|
"id": perm.id,
|
|
"agent_id": perm.agent_id,
|
|
"role": perm.role,
|
|
"directory_patterns": perm.directory_patterns.split(",") if perm.directory_patterns else [],
|
|
"task_types": perm.task_types.split(",") if perm.task_types else [],
|
|
"context_weight": perm.context_weight,
|
|
"created_at": perm.created_at,
|
|
"updated_at": perm.updated_at
|
|
}
|
|
for perm in permissions
|
|
]
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get agent permissions: {str(e)}")
|
|
|
|
|
|
async def send_feedback_to_rl_curator(
|
|
feedback_id: int,
|
|
context_id: str,
|
|
feedback_type: str,
|
|
agent_id: str,
|
|
role: str,
|
|
confidence: float
|
|
):
|
|
"""
|
|
Background task to send feedback to RL Context Curator
|
|
"""
|
|
try:
|
|
import httpx
|
|
import json
|
|
from datetime import datetime
|
|
|
|
# Prepare feedback event in Bzzz format
|
|
feedback_event = {
|
|
"bzzz_type": "feedback_event",
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"origin": {
|
|
"node_id": "hive",
|
|
"agent_id": agent_id,
|
|
"task_id": f"hive-feedback-{feedback_id}",
|
|
"workspace": "hive://context-feedback",
|
|
"directory": "/feedback/"
|
|
},
|
|
"feedback": {
|
|
"type": feedback_type,
|
|
"category": "general", # Could be enhanced with category detection
|
|
"role": role,
|
|
"context_id": context_id,
|
|
"reason": f"Feedback from Hive agent {agent_id}",
|
|
"confidence": confidence,
|
|
"usage_context": "hive_platform"
|
|
},
|
|
"task_outcome": {
|
|
"completed": feedback_type in ["upvote", "task_success"],
|
|
"completion_time": 0,
|
|
"errors_encountered": 0,
|
|
"follow_up_questions": 0
|
|
}
|
|
}
|
|
|
|
# Send to HCFS RL Tuner Service
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
response = await client.post(
|
|
"http://localhost:8001/api/feedback",
|
|
json=feedback_event,
|
|
timeout=10.0
|
|
)
|
|
if response.status_code == 200:
|
|
print(f"✅ Feedback sent to RL Curator: {feedback_id}")
|
|
else:
|
|
print(f"⚠️ RL Curator responded with status {response.status_code}")
|
|
except httpx.ConnectError:
|
|
print(f"⚠️ Could not connect to RL Curator service (feedback {feedback_id})")
|
|
except Exception as e:
|
|
print(f"❌ Error sending feedback to RL Curator: {e}")
|
|
|
|
except Exception as e:
|
|
print(f"❌ Background feedback task failed: {e}") |