Files
hive/backend/app/api/feedback.py
anthonyrawlins b6bff318d9 WIP: Save current work before CHORUS rebrand
- 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>
2025-08-01 02:20:56 +10:00

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}")