Phase 7: Self-Optimizing Agents (Feedback analysis on rejection, lesson injection on retry)
Browse files
backend/routers/agent_runner.py
CHANGED
|
@@ -363,8 +363,16 @@ async def approve_task(task_id: str, background_tasks: BackgroundTasks):
|
|
| 363 |
return {"message": "Task approved", "task": task}
|
| 364 |
|
| 365 |
@router.post("/{task_id}/reject")
|
| 366 |
-
async def reject_task(task_id: str):
|
| 367 |
task = update_task_status(task_id, "todo")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
await audit_service.log_action(
|
| 369 |
user_id=None,
|
| 370 |
action="task_rejected",
|
|
|
|
| 363 |
return {"message": "Task approved", "task": task}
|
| 364 |
|
| 365 |
@router.post("/{task_id}/reject")
|
| 366 |
+
async def reject_task(task_id: str, background_tasks: BackgroundTasks, feedback: str | None = None):
|
| 367 |
task = update_task_status(task_id, "todo")
|
| 368 |
+
|
| 369 |
+
# Trigger Self-Optimization Loop
|
| 370 |
+
background_tasks.add_task(
|
| 371 |
+
memory_service.analyze_rejection,
|
| 372 |
+
task_id=task_id,
|
| 373 |
+
feedback=feedback
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
await audit_service.log_action(
|
| 377 |
user_id=None,
|
| 378 |
action="task_rejected",
|
backend/services/agent_runner_service.py
CHANGED
|
@@ -104,6 +104,19 @@ class AgentRunnerService:
|
|
| 104 |
for m in memories:
|
| 105 |
memory_blocks.append(f"- Memory: {m['content']}")
|
| 106 |
extra_context += memory_header + "\n".join(memory_blocks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
import time
|
| 109 |
import hashlib
|
|
|
|
| 104 |
for m in memories:
|
| 105 |
memory_blocks.append(f"- Memory: {m['content']}")
|
| 106 |
extra_context += memory_header + "\n".join(memory_blocks)
|
| 107 |
+
|
| 108 |
+
# Fetch Self-Optimization Lessons for this specific task
|
| 109 |
+
lessons_res = supabase.table("project_memory") \
|
| 110 |
+
.select("content") \
|
| 111 |
+
.eq("task_id", task_id) \
|
| 112 |
+
.eq("memory_type", "self_optimization_lesson") \
|
| 113 |
+
.order("created_at", desc=True) \
|
| 114 |
+
.limit(1) \
|
| 115 |
+
.execute()
|
| 116 |
+
|
| 117 |
+
if lessons_res.data:
|
| 118 |
+
lesson = lessons_res.data[0]["content"]
|
| 119 |
+
extra_context += f"\n\n### CRITICAL LESSON FROM PREVIOUS ATTEMPT\n{lesson}\n"
|
| 120 |
|
| 121 |
import time
|
| 122 |
import hashlib
|
backend/services/memory_service.py
CHANGED
|
@@ -109,4 +109,66 @@ class MemoryService:
|
|
| 109 |
}
|
| 110 |
)
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
memory_service = MemoryService()
|
|
|
|
| 109 |
}
|
| 110 |
)
|
| 111 |
|
| 112 |
+
async def analyze_rejection(self, task_id: str, feedback: Optional[str] = None):
|
| 113 |
+
"""
|
| 114 |
+
Analyzes a task rejection to generate a 'Self-Optimization Lesson'.
|
| 115 |
+
Triggered when a human rejects an agent's output.
|
| 116 |
+
"""
|
| 117 |
+
try:
|
| 118 |
+
# 1. Fetch task and its failed output
|
| 119 |
+
task_res = supabase.table("tasks").select("*, projects(name, description)").eq("id", task_id).single().execute()
|
| 120 |
+
if not task_res.data:
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
task = task_res.data
|
| 124 |
+
output = task.get("output_data") or {}
|
| 125 |
+
|
| 126 |
+
# 2. Get an analyst agent
|
| 127 |
+
from agents.agent_factory import AgentFactory
|
| 128 |
+
from services.llm_config import getDefaultProvider, getDefaultModel
|
| 129 |
+
|
| 130 |
+
provider = getDefaultProvider()
|
| 131 |
+
model = getDefaultModel(provider)
|
| 132 |
+
|
| 133 |
+
analyst = AgentFactory.get_agent(
|
| 134 |
+
provider=provider,
|
| 135 |
+
name="Optimization Analyst",
|
| 136 |
+
role="Self-Optimization Expert",
|
| 137 |
+
model=model,
|
| 138 |
+
system_prompt="You analyze task rejections. Your goal is to produce a single, concise 'Lesson Learned' that the next agent should follow to avoid repeating the mistake. Focus on the core reason for rejection."
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# 3. Construct prompt for analysis
|
| 142 |
+
analysis_prompt = f"""
|
| 143 |
+
TASK: {task.get('title')}
|
| 144 |
+
DESCRIPTION: {task.get('description')}
|
| 145 |
+
|
| 146 |
+
REJECTED OUTPUT:
|
| 147 |
+
{str(output)[:2000]}
|
| 148 |
+
|
| 149 |
+
HUMAN FEEDBACK: {feedback or "No explicit feedback provided, but the output did not meet quality standards."}
|
| 150 |
+
|
| 151 |
+
Provide a concise 'LESSON LEARNED' for the next agent. Start with 'Next time, you must...'
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
result = await analyst.run(analysis_prompt, [])
|
| 155 |
+
lesson_text = result.get("raw_output") or result.get("data")
|
| 156 |
+
|
| 157 |
+
if lesson_text:
|
| 158 |
+
await self.save_memory(
|
| 159 |
+
project_id=task.get("project_id"),
|
| 160 |
+
task_id=task_id,
|
| 161 |
+
content=f"Optimization Lesson for '{task.get('title')}': {lesson_text}",
|
| 162 |
+
memory_type="self_optimization_lesson",
|
| 163 |
+
metadata={
|
| 164 |
+
"original_task_id": task_id,
|
| 165 |
+
"was_rejected": True,
|
| 166 |
+
"feedback": feedback
|
| 167 |
+
}
|
| 168 |
+
)
|
| 169 |
+
logger.info(f"Saved self-optimization lesson for task {task_id}")
|
| 170 |
+
|
| 171 |
+
except Exception as e:
|
| 172 |
+
logger.error(f"Failed to analyze rejection for task {task_id}: {e}")
|
| 173 |
+
|
| 174 |
memory_service = MemoryService()
|