| """Tool selector for remote execution steps.""" |
| import sqlite3 |
| from typing import Any |
|
|
| from api.deps import get_logger, load_config |
|
|
| logger = get_logger("kapo.agent.tool_selector") |
|
|
|
|
| class ToolSelectorAgent: |
| def __init__(self): |
| self.cfg = load_config() |
|
|
| def _load_catalog(self) -> list[dict[str, Any]]: |
| tools_db = self.cfg.get("TOOLS_DB_PATH") |
| if not tools_db or not str(tools_db).endswith(".db"): |
| return [] |
|
|
| try: |
| conn = sqlite3.connect(str(tools_db)) |
| cur = conn.cursor() |
| cur.execute( |
| """ |
| SELECT tool_name, install_command, path, description, installed |
| FROM tools |
| """ |
| ) |
| rows = cur.fetchall() |
| conn.close() |
| except sqlite3.OperationalError: |
| return [] |
| except Exception: |
| logger.exception("Tool catalog load failed") |
| return [] |
|
|
| return [ |
| { |
| "tool_name": row[0], |
| "install_command": row[1], |
| "path": row[2], |
| "description": row[3] or "", |
| "installed": bool(row[4]), |
| } |
| for row in rows |
| ] |
|
|
| def _step_text(self, step: dict[str, Any]) -> str: |
| for key in ("input", "description", "title", "summary", "task", "prompt"): |
| value = str(step.get(key, "")).strip() |
| if value: |
| return value |
| return "" |
|
|
| def _fallback_tool(self, tool_hint: str) -> dict[str, Any]: |
| name = f"fallback_{tool_hint or 'command'}" |
| return { |
| "tool_name": name, |
| "description": "Synthetic fallback tool selected by ToolSelectorAgent", |
| "path": tool_hint or "", |
| "installed": True, |
| } |
|
|
| def _fallback_command(self, step: dict[str, Any], tool_hint: str = "") -> str: |
| action = str(step.get("action", "")).strip().lower() |
| step_input = self._step_text(step) |
| explicit_command = str(step.get("command", "")).strip() |
| if explicit_command: |
| return explicit_command |
| if action == "research": |
| return f"echo Research task queued: {step_input}".strip() |
| if action in {"verify", "summarize", "respond", "synthesize"}: |
| if step_input: |
| return f"python -c \"print({step_input!r})\"" |
| return "echo Verification requested" |
| if action in {"execute", "collect_requirements", "analyze", "decompose", "collect_context"}: |
| return f"python -c \"print({step_input!r})\"" if step_input else "python -c \"print('step received')\"" |
| if tool_hint in {"python", "py"} or not tool_hint: |
| return f"python -c \"print({step_input!r})\"" if step_input else "python -c \"print('step received')\"" |
| if step_input: |
| return f"echo {step_input}" |
| return "echo Step received" |
|
|
| def select_tool(self, step: dict[str, Any]) -> dict[str, Any]: |
| action = str(step.get("action", "")).strip().lower() |
| tool_hint = str(step.get("tool_hint", "")).strip().lower() |
| step_input = self._step_text(step).lower() |
| catalog = self._load_catalog() |
|
|
| for tool in catalog: |
| haystack = " ".join([tool["tool_name"], tool["description"], tool["path"] or ""]).lower() |
| if tool_hint and tool_hint in haystack: |
| return {"command": tool["path"] or tool["tool_name"], "files": {}, "env": {}, "tool": tool} |
| if action and action in haystack: |
| return {"command": tool["path"] or tool["tool_name"], "files": {}, "env": {}, "tool": tool} |
| if step_input and any(token in haystack for token in step_input.split()[:4]): |
| return {"command": tool["path"] or tool["tool_name"], "files": {}, "env": {}, "tool": tool} |
|
|
| return { |
| "command": self._fallback_command(step, tool_hint=tool_hint), |
| "files": step.get("files", {}), |
| "env": step.get("env", {}), |
| "tool": self._fallback_tool(tool_hint or "python"), |
| } |
|
|