178 lines
4.1 KiB
Python
178 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from core.tools.registry import registry
|
|
from core.events import bus
|
|
|
|
|
|
class AgentLoop:
|
|
"""
|
|
Minimal deterministic agent loop.
|
|
|
|
Orchestrates tools like:
|
|
- search
|
|
- intelligent_search
|
|
- crawler
|
|
- research
|
|
|
|
Can later be upgraded with LLM-based planning.
|
|
"""
|
|
|
|
def __init__(self, ctx):
|
|
self.ctx = ctx
|
|
self.max_steps = 5
|
|
|
|
# =========================
|
|
# ENTRY POINT
|
|
# =========================
|
|
|
|
def run(self, goal: str) -> dict[str, Any]:
|
|
state = {
|
|
"goal": goal,
|
|
"steps": [],
|
|
"memory": [],
|
|
"final": None
|
|
}
|
|
|
|
bus.log(
|
|
"AGENT",
|
|
"agent_loop_start",
|
|
"INFO",
|
|
{"goal": goal}
|
|
)
|
|
|
|
for step in range(self.max_steps):
|
|
action = self._decide(state)
|
|
|
|
if action["type"] == "stop":
|
|
state["final"] = action.get("result")
|
|
break
|
|
|
|
result = self._execute(action, state)
|
|
state["steps"].append(action)
|
|
state["memory"].append(result)
|
|
|
|
# simple convergence heuristic
|
|
if self._is_satisfied(state):
|
|
state["final"] = result
|
|
break
|
|
|
|
if not state["final"]:
|
|
state["final"] = state["memory"][-1] if state["memory"] else {}
|
|
|
|
return state
|
|
|
|
# =========================
|
|
# DECISION LOGIC (RULE-BASED FOR NOW)
|
|
# =========================
|
|
|
|
def _decide(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Very simple heuristic planner.
|
|
|
|
Later upgrade point: replace with LLM planner.
|
|
"""
|
|
|
|
goal = state["goal"]
|
|
memory = state["memory"]
|
|
|
|
if not memory:
|
|
return {
|
|
"type": "tool",
|
|
"tool": "research",
|
|
"input": {
|
|
"query": goal,
|
|
"depth": 1
|
|
}
|
|
}
|
|
|
|
last = memory[-1]
|
|
|
|
# if research returned sources, refine or stop
|
|
if isinstance(last, dict) and "sources" in last:
|
|
if len(last["sources"]) >= 2:
|
|
return {
|
|
"type": "stop",
|
|
"result": last
|
|
}
|
|
|
|
# if weak results, deepen search
|
|
return {
|
|
"type": "tool",
|
|
"tool": "research",
|
|
"input": {
|
|
"query": goal,
|
|
"depth": 2,
|
|
"max_sources": 5
|
|
}
|
|
}
|
|
|
|
return {
|
|
"type": "stop",
|
|
"result": last
|
|
}
|
|
|
|
# =========================
|
|
# TOOL EXECUTION
|
|
# =========================
|
|
|
|
def _execute(self, action: dict[str, Any], state: dict[str, Any]) -> Any:
|
|
tool_name = action.get("tool")
|
|
tool_input = action.get("input", {})
|
|
|
|
bus.log(
|
|
"AGENT",
|
|
"agent_tool_call",
|
|
"INFO",
|
|
{
|
|
"tool": tool_name,
|
|
"input": tool_input
|
|
}
|
|
)
|
|
|
|
if not tool_name:
|
|
return {"error": "No tool specified"}
|
|
|
|
try:
|
|
result = registry.run(
|
|
tool_name,
|
|
tool_input,
|
|
self.ctx
|
|
)
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
return {
|
|
"error": str(e),
|
|
"tool": tool_name
|
|
}
|
|
|
|
# =========================
|
|
# STOPPING CONDITION
|
|
# =========================
|
|
|
|
def _is_satisfied(self, state: dict[str, Any]) -> bool:
|
|
"""
|
|
Simple satisfaction heuristic.
|
|
|
|
Later upgrade: LLM-based evaluation.
|
|
"""
|
|
|
|
memory = state["memory"]
|
|
|
|
if not memory:
|
|
return False
|
|
|
|
last = memory[-1]
|
|
|
|
if isinstance(last, dict):
|
|
# if research returned structured sources, assume OK
|
|
if "sources" in last and len(last["sources"]) > 0:
|
|
return True
|
|
|
|
if "error" in last:
|
|
return True
|
|
|
|
return False |
