Commit ·
98bb628
1
Parent(s): 55890db
refactor: let Controller reason from chat history
Browse files- agents/orchestrator.py +20 -45
- app.py +5 -4
agents/orchestrator.py
CHANGED
|
@@ -603,19 +603,13 @@ Return JSON:
|
|
| 603 |
|
| 604 |
# Autonomous reasoning features
|
| 605 |
self._reasoning_trace: Optional[ReasoningTrace] = None
|
| 606 |
-
self._pending_follow_up: Optional[CompletenessAnalysis] = None
|
| 607 |
-
|
| 608 |
-
# Accumulated context from incomplete input gathering (for multi-turn)
|
| 609 |
-
self._accumulated_context: Dict[str, Any] = {}
|
| 610 |
-
# Track the last question we asked (for context-aware classification)
|
| 611 |
-
self._last_question: Optional[str] = None
|
| 612 |
|
| 613 |
# Location data captured from tool results for map updates
|
| 614 |
self._captured_location: Optional[Dict[str, Any]] = None
|
| 615 |
|
| 616 |
-
#
|
| 617 |
self._conversation_history: List[Dict[str, Any]] = []
|
| 618 |
-
self._max_history_turns: int = 5
|
| 619 |
|
| 620 |
# Eager initialization to avoid delay on first request
|
| 621 |
if eager_init and self.api_key:
|
|
@@ -993,7 +987,7 @@ Return JSON:
|
|
| 993 |
self,
|
| 994 |
user_message: str,
|
| 995 |
image_analysis: Optional[str] = None,
|
| 996 |
-
|
| 997 |
) -> Generator[Tuple[str, dict], None, None]:
|
| 998 |
"""
|
| 999 |
Process infrastructure report through TRUE AUTONOMOUS multi-agent system.
|
|
@@ -1008,7 +1002,7 @@ Return JSON:
|
|
| 1008 |
Args:
|
| 1009 |
user_message: User's description of the issue
|
| 1010 |
image_analysis: Optional image analysis from Claude Vision
|
| 1011 |
-
|
| 1012 |
|
| 1013 |
Yields:
|
| 1014 |
Tuples of (event_type, event_data) for UI updates
|
|
@@ -1047,15 +1041,19 @@ Return JSON:
|
|
| 1047 |
except Empty:
|
| 1048 |
break
|
| 1049 |
|
| 1050 |
-
#
|
| 1051 |
-
|
| 1052 |
-
if
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1059 |
|
| 1060 |
trace.think("Delegating full control to Controller agent", "autonomy")
|
| 1061 |
yield ("planning", {"status": "started", "message": "Controller is planning..."})
|
|
@@ -1064,11 +1062,10 @@ Return JSON:
|
|
| 1064 |
# TRUE AUTONOMOUS TASK - Controller has full decision-making power
|
| 1065 |
task = f"""You are the autonomous Controller for FixMyNeighborhood, an NYC infrastructure reporting system.
|
| 1066 |
|
| 1067 |
-
USER INPUT: "{user_message}"
|
| 1068 |
{f'IMAGE ANALYSIS: {image_analysis}' if image_analysis else 'No image provided.'}
|
| 1069 |
|
| 1070 |
-
|
| 1071 |
-
{context}
|
| 1072 |
|
| 1073 |
YOUR AGENTS:
|
| 1074 |
- triage_agent: Validates NYC addresses (call validate_address tool), classifies images as infrastructure or not
|
|
@@ -1239,6 +1236,7 @@ NOW: Analyze the input and decide what to do. You have full autonomy."""
|
|
| 1239 |
print(f"[Controller] Raw result: {raw_result[:500]}...")
|
| 1240 |
|
| 1241 |
# Detect response type (Controller decided what to do)
|
|
|
|
| 1242 |
if "FOLLOW_UP:" in raw_result:
|
| 1243 |
# Controller decided more info is needed
|
| 1244 |
follow_up_match = re.search(r'FOLLOW_UP:\s*(.+?)(?:$|REJECTION:|REPORT:)', raw_result, re.DOTALL)
|
|
@@ -1246,10 +1244,6 @@ NOW: Analyze the input and decide what to do. You have full autonomy."""
|
|
| 1246 |
follow_up_text = follow_up_match.group(1).strip()
|
| 1247 |
trace.decide("Controller decided: need more information", confidence=0.9)
|
| 1248 |
|
| 1249 |
-
# Extract what Controller understood for context
|
| 1250 |
-
self._accumulated_context = self._extract_context_from_result(raw_result)
|
| 1251 |
-
self._last_question = follow_up_text
|
| 1252 |
-
|
| 1253 |
yield ("reasoning_update", {"trace": trace.to_display()})
|
| 1254 |
yield ("needs_info", {
|
| 1255 |
"message": follow_up_text,
|
|
@@ -1264,10 +1258,6 @@ NOW: Analyze the input and decide what to do. You have full autonomy."""
|
|
| 1264 |
rejection_text = rejection_match.group(1).strip()
|
| 1265 |
trace.decide("Controller decided: reject request", confidence=0.9)
|
| 1266 |
|
| 1267 |
-
# Clear context on rejection
|
| 1268 |
-
self._accumulated_context = {}
|
| 1269 |
-
self._last_question = None
|
| 1270 |
-
|
| 1271 |
yield ("reasoning_update", {"trace": trace.to_display()})
|
| 1272 |
yield ("needs_info", {
|
| 1273 |
"message": rejection_text,
|
|
@@ -1310,8 +1300,6 @@ NOW: Analyze the input and decide what to do. You have full autonomy."""
|
|
| 1310 |
"response_days": self._extract_field(raw_result, r'(?:Expected Response|response)[:\s]+~?(\d+)', "7"),
|
| 1311 |
}
|
| 1312 |
self._add_to_history(user_message, report_summary)
|
| 1313 |
-
self._accumulated_context = {}
|
| 1314 |
-
self._last_question = None
|
| 1315 |
trace.think(f"Report stored (total: {len(self._conversation_history)})", "multi-turn")
|
| 1316 |
|
| 1317 |
yield ("reasoning_update", {"trace": trace.to_display()})
|
|
@@ -1325,19 +1313,6 @@ NOW: Analyze the input and decide what to do. You have full autonomy."""
|
|
| 1325 |
"reasoning_trace": trace.to_display(),
|
| 1326 |
})
|
| 1327 |
|
| 1328 |
-
def _extract_context_from_result(self, result: str) -> Dict[str, Any]:
|
| 1329 |
-
"""Extract understood context from Controller's response."""
|
| 1330 |
-
context = {}
|
| 1331 |
-
# Try to extract what Controller understood
|
| 1332 |
-
if "pothole" in result.lower():
|
| 1333 |
-
context["issue_type"] = "pothole"
|
| 1334 |
-
elif "streetlight" in result.lower():
|
| 1335 |
-
context["issue_type"] = "streetlight"
|
| 1336 |
-
elif "drain" in result.lower():
|
| 1337 |
-
context["issue_type"] = "drain"
|
| 1338 |
-
# Add more as needed
|
| 1339 |
-
return context
|
| 1340 |
-
|
| 1341 |
def _format_result(self, result: str, user_message: str) -> str:
|
| 1342 |
"""Format the controller result into a user-friendly message."""
|
| 1343 |
# More flexible patterns to handle various agent output formats
|
|
|
|
| 603 |
|
| 604 |
# Autonomous reasoning features
|
| 605 |
self._reasoning_trace: Optional[ReasoningTrace] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
|
| 607 |
# Location data captured from tool results for map updates
|
| 608 |
self._captured_location: Optional[Dict[str, Any]] = None
|
| 609 |
|
| 610 |
+
# Completed reports history (for session awareness)
|
| 611 |
self._conversation_history: List[Dict[str, Any]] = []
|
| 612 |
+
self._max_history_turns: int = 5
|
| 613 |
|
| 614 |
# Eager initialization to avoid delay on first request
|
| 615 |
if eager_init and self.api_key:
|
|
|
|
| 987 |
self,
|
| 988 |
user_message: str,
|
| 989 |
image_analysis: Optional[str] = None,
|
| 990 |
+
chat_history: Optional[List[Dict[str, str]]] = None,
|
| 991 |
) -> Generator[Tuple[str, dict], None, None]:
|
| 992 |
"""
|
| 993 |
Process infrastructure report through TRUE AUTONOMOUS multi-agent system.
|
|
|
|
| 1002 |
Args:
|
| 1003 |
user_message: User's description of the issue
|
| 1004 |
image_analysis: Optional image analysis from Claude Vision
|
| 1005 |
+
chat_history: Full conversation history for Controller to reason about
|
| 1006 |
|
| 1007 |
Yields:
|
| 1008 |
Tuples of (event_type, event_data) for UI updates
|
|
|
|
| 1041 |
except Empty:
|
| 1042 |
break
|
| 1043 |
|
| 1044 |
+
# Format conversation history for Controller (TRUE AUTONOMOUS - Controller reasons about context)
|
| 1045 |
+
conversation_context = ""
|
| 1046 |
+
if chat_history and len(chat_history) > 0:
|
| 1047 |
+
conv_lines = ["CONVERSATION HISTORY (you have full context - reason about what user needs):"]
|
| 1048 |
+
for msg in chat_history:
|
| 1049 |
+
role = msg.get("role", "unknown")
|
| 1050 |
+
content = msg.get("content", "")
|
| 1051 |
+
if role == "user":
|
| 1052 |
+
conv_lines.append(f"User: {content}")
|
| 1053 |
+
elif role == "assistant":
|
| 1054 |
+
# Truncate long assistant responses
|
| 1055 |
+
conv_lines.append(f"Agent: {content[:500]}{'...' if len(content) > 500 else ''}")
|
| 1056 |
+
conversation_context = "\n".join(conv_lines)
|
| 1057 |
|
| 1058 |
trace.think("Delegating full control to Controller agent", "autonomy")
|
| 1059 |
yield ("planning", {"status": "started", "message": "Controller is planning..."})
|
|
|
|
| 1062 |
# TRUE AUTONOMOUS TASK - Controller has full decision-making power
|
| 1063 |
task = f"""You are the autonomous Controller for FixMyNeighborhood, an NYC infrastructure reporting system.
|
| 1064 |
|
| 1065 |
+
CURRENT USER INPUT: "{user_message}"
|
| 1066 |
{f'IMAGE ANALYSIS: {image_analysis}' if image_analysis else 'No image provided.'}
|
| 1067 |
|
| 1068 |
+
{conversation_context if conversation_context else 'No previous conversation.'}
|
|
|
|
| 1069 |
|
| 1070 |
YOUR AGENTS:
|
| 1071 |
- triage_agent: Validates NYC addresses (call validate_address tool), classifies images as infrastructure or not
|
|
|
|
| 1236 |
print(f"[Controller] Raw result: {raw_result[:500]}...")
|
| 1237 |
|
| 1238 |
# Detect response type (Controller decided what to do)
|
| 1239 |
+
# No Python context extraction - Controller reasons from chat history
|
| 1240 |
if "FOLLOW_UP:" in raw_result:
|
| 1241 |
# Controller decided more info is needed
|
| 1242 |
follow_up_match = re.search(r'FOLLOW_UP:\s*(.+?)(?:$|REJECTION:|REPORT:)', raw_result, re.DOTALL)
|
|
|
|
| 1244 |
follow_up_text = follow_up_match.group(1).strip()
|
| 1245 |
trace.decide("Controller decided: need more information", confidence=0.9)
|
| 1246 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1247 |
yield ("reasoning_update", {"trace": trace.to_display()})
|
| 1248 |
yield ("needs_info", {
|
| 1249 |
"message": follow_up_text,
|
|
|
|
| 1258 |
rejection_text = rejection_match.group(1).strip()
|
| 1259 |
trace.decide("Controller decided: reject request", confidence=0.9)
|
| 1260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1261 |
yield ("reasoning_update", {"trace": trace.to_display()})
|
| 1262 |
yield ("needs_info", {
|
| 1263 |
"message": rejection_text,
|
|
|
|
| 1300 |
"response_days": self._extract_field(raw_result, r'(?:Expected Response|response)[:\s]+~?(\d+)', "7"),
|
| 1301 |
}
|
| 1302 |
self._add_to_history(user_message, report_summary)
|
|
|
|
|
|
|
| 1303 |
trace.think(f"Report stored (total: {len(self._conversation_history)})", "multi-turn")
|
| 1304 |
|
| 1305 |
yield ("reasoning_update", {"trace": trace.to_display()})
|
|
|
|
| 1313 |
"reasoning_trace": trace.to_display(),
|
| 1314 |
})
|
| 1315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1316 |
def _format_result(self, result: str, user_message: str) -> str:
|
| 1317 |
"""Format the controller result into a user-friendly message."""
|
| 1318 |
# More flexible patterns to handle various agent output formats
|
app.py
CHANGED
|
@@ -330,6 +330,7 @@ def process_report(message: str, image, history: list, session_logs: list, sessi
|
|
| 330 |
"""Build all output values for yield."""
|
| 331 |
return (
|
| 332 |
history,
|
|
|
|
| 333 |
get_processing_status(),
|
| 334 |
get_status_text(agent_states["research"]["status"], agent_states["research"].get("duration_formatted")),
|
| 335 |
get_tools_markdown(agent_states["research"]["tools"]),
|
|
@@ -428,8 +429,8 @@ def process_report(message: str, image, history: list, session_logs: list, sessi
|
|
| 428 |
progress_msg_idx += 1
|
| 429 |
|
| 430 |
try:
|
| 431 |
-
# Process through orchestrator with
|
| 432 |
-
for event_type, data in orchestrator.process(message, image_analysis,
|
| 433 |
|
| 434 |
# Handle reasoning trace updates (Autonomous Feature #1)
|
| 435 |
if event_type == "reasoning_update":
|
|
@@ -816,7 +817,7 @@ with gr.Blocks(title="FixMyNeighborhood - Multi-Agent Reporter") as demo:
|
|
| 816 |
process_report,
|
| 817 |
inputs=[message_input, image_input, chatbot, session_logs, session_orchestrator, reasoning_display],
|
| 818 |
outputs=[
|
| 819 |
-
chatbot, processing_status, research_status, research_tools,
|
| 820 |
report_status, report_tools, map_display, logs_display,
|
| 821 |
session_logs, session_orchestrator, reasoning_display
|
| 822 |
]
|
|
@@ -826,7 +827,7 @@ with gr.Blocks(title="FixMyNeighborhood - Multi-Agent Reporter") as demo:
|
|
| 826 |
process_report,
|
| 827 |
inputs=[message_input, image_input, chatbot, session_logs, session_orchestrator, reasoning_display],
|
| 828 |
outputs=[
|
| 829 |
-
chatbot, processing_status, research_status, research_tools,
|
| 830 |
report_status, report_tools, map_display, logs_display,
|
| 831 |
session_logs, session_orchestrator, reasoning_display
|
| 832 |
]
|
|
|
|
| 330 |
"""Build all output values for yield."""
|
| 331 |
return (
|
| 332 |
history,
|
| 333 |
+
None, # Clear image_input after submit
|
| 334 |
get_processing_status(),
|
| 335 |
get_status_text(agent_states["research"]["status"], agent_states["research"].get("duration_formatted")),
|
| 336 |
get_tools_markdown(agent_states["research"]["tools"]),
|
|
|
|
| 429 |
progress_msg_idx += 1
|
| 430 |
|
| 431 |
try:
|
| 432 |
+
# Process through orchestrator with chat history (Controller reasons about context)
|
| 433 |
+
for event_type, data in orchestrator.process(message, image_analysis, chat_history=history):
|
| 434 |
|
| 435 |
# Handle reasoning trace updates (Autonomous Feature #1)
|
| 436 |
if event_type == "reasoning_update":
|
|
|
|
| 817 |
process_report,
|
| 818 |
inputs=[message_input, image_input, chatbot, session_logs, session_orchestrator, reasoning_display],
|
| 819 |
outputs=[
|
| 820 |
+
chatbot, image_input, processing_status, research_status, research_tools,
|
| 821 |
report_status, report_tools, map_display, logs_display,
|
| 822 |
session_logs, session_orchestrator, reasoning_display
|
| 823 |
]
|
|
|
|
| 827 |
process_report,
|
| 828 |
inputs=[message_input, image_input, chatbot, session_logs, session_orchestrator, reasoning_display],
|
| 829 |
outputs=[
|
| 830 |
+
chatbot, image_input, processing_status, research_status, research_tools,
|
| 831 |
report_status, report_tools, map_display, logs_display,
|
| 832 |
session_logs, session_orchestrator, reasoning_display
|
| 833 |
]
|