""" 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": "whoosh", "agent_id": agent_id, "task_id": f"whoosh-feedback-{feedback_id}", "workspace": "whoosh://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 WHOOSH agent {agent_id}", "confidence": confidence, "usage_context": "whoosh_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}")