052591281d
- Complete agent system with planning and execution - Bridge API with FastAPI server - WhatsApp-style mobile dashboard - Enhanced web search with query improvement - Security guards and policy checks - Comprehensive documentation
166 lines
7.0 KiB
Python
166 lines
7.0 KiB
Python
"""Deterministic planner — Stage 1 (no LLM required)."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
|
|
from core.state import Plan, PlanStep
|
|
|
|
TEST_MODE: bool = os.getenv("AGENT_TEST_MODE") == "1"
|
|
|
|
|
|
def build_plan(goal: str) -> Plan:
|
|
"""Build a deterministic plan based on keywords in the goal.
|
|
|
|
Security test paths (only when AGENT_TEST_MODE=1):
|
|
"test deny bash" → pwd + bash (blocked) + finish
|
|
"test deny rm" → pwd + rm (blocked) + finish
|
|
"test deny curl" → pwd + curl (blocked) + finish
|
|
"test forbidden command" → alias for test deny bash
|
|
"test traversal" → write_file with ../escape.txt (blocked)
|
|
"test absolute path" → write_file with /tmp/test.txt (blocked)
|
|
|
|
Normal paths:
|
|
"inspect" / "analyze" / "посмотри" / "проверь" → pwd + ls + finish
|
|
"create file" / "создай файл" / "write file" → write_file + read_file + finish
|
|
(no match) → PlannerError
|
|
"""
|
|
goal_lower = goal.lower()
|
|
|
|
# --- Security test paths (TEST_MODE only) ---
|
|
if TEST_MODE and "test deny rm" in goal_lower:
|
|
steps = [
|
|
PlanStep("step-1", "shell", "Allowed: pwd", {"command": "pwd"}),
|
|
PlanStep("step-2", "shell", "BLOCKED: rm (security test)", {"command": "rm -rf /tmp"}),
|
|
PlanStep("step-3", "finish", "Task complete", {}),
|
|
]
|
|
|
|
elif TEST_MODE and "test deny curl" in goal_lower:
|
|
steps = [
|
|
PlanStep("step-1", "shell", "Allowed: pwd", {"command": "pwd"}),
|
|
PlanStep("step-2", "shell", "BLOCKED: curl (security test)", {"command": "curl http://example.com"}),
|
|
PlanStep("step-3", "finish", "Task complete", {}),
|
|
]
|
|
|
|
elif TEST_MODE and any(kw in goal_lower for kw in ("test deny bash", "test forbidden command")):
|
|
steps = [
|
|
PlanStep("step-1", "shell", "Allowed: pwd", {"command": "pwd"}),
|
|
PlanStep("step-2", "shell", "BLOCKED: bash (security test)", {"command": "bash -c echo hi"}),
|
|
PlanStep("step-3", "finish", "Task complete", {}),
|
|
]
|
|
|
|
elif TEST_MODE and "test traversal" in goal_lower:
|
|
steps = [
|
|
PlanStep("step-1", "write_file", "BLOCKED: path traversal ../escape.txt",
|
|
{"path": "../escape.txt", "content": "should not exist"}),
|
|
PlanStep("step-2", "finish", "Task complete", {}),
|
|
]
|
|
|
|
elif TEST_MODE and "test absolute path" in goal_lower:
|
|
steps = [
|
|
PlanStep("step-1", "write_file", "BLOCKED: absolute path /tmp/test.txt",
|
|
{"path": "/tmp/test.txt", "content": "should not exist"}),
|
|
PlanStep("step-2", "finish", "Task complete", {}),
|
|
]
|
|
|
|
# --- Stage 2B retry test paths ---
|
|
elif TEST_MODE and "test retry execution" in goal_lower:
|
|
# flaky step: fails once (execution_error), then succeeds on retry
|
|
steps = [
|
|
PlanStep("step-1", "flaky", "Flaky step (fails once, then succeeds)", {}),
|
|
PlanStep("step-2", "finish", "Task complete", {}),
|
|
]
|
|
|
|
# --- Stage 2A flow test paths ---
|
|
elif TEST_MODE and "test flow pwd to file" in goal_lower:
|
|
steps = [
|
|
PlanStep("step-1", "shell", "Get current directory", {"command": "pwd"}),
|
|
PlanStep("step-2", "write_file", "Write pwd output to report.txt",
|
|
{"path": "report.txt", "content": "{{ last.output_text }}"}),
|
|
PlanStep("step-3", "read_file", "Read back report.txt", {"path": "report.txt"}),
|
|
PlanStep("step-4", "finish", "Task complete", {}),
|
|
]
|
|
|
|
elif TEST_MODE and "test flow read to copy" in goal_lower:
|
|
steps = [
|
|
PlanStep("step-1", "write_file", "Write source.txt", {"path": "source.txt", "content": "hello from source"}),
|
|
PlanStep("step-2", "read_file", "Read source.txt", {"path": "source.txt"}),
|
|
PlanStep("step-3", "write_file", "Copy content to copy.txt",
|
|
{"path": "copy.txt", "content": "{{ last.output_text }}"}),
|
|
PlanStep("step-4", "read_file", "Read back copy.txt", {"path": "copy.txt"}),
|
|
PlanStep("step-5", "finish", "Task complete", {}),
|
|
]
|
|
|
|
elif TEST_MODE and "test flow ls to file" in goal_lower:
|
|
steps = [
|
|
PlanStep("step-1", "shell", "List files", {"command": "ls"}),
|
|
PlanStep("step-2", "write_file", "Save listing to listing.txt",
|
|
{"path": "listing.txt", "content": "{{ steps.step-1.output_text }}"}),
|
|
PlanStep("step-3", "finish", "Task complete", {}),
|
|
]
|
|
|
|
elif TEST_MODE and "test flow bad template" in goal_lower:
|
|
steps = [
|
|
PlanStep("step-1", "write_file", "Write with unresolvable template",
|
|
{"path": "fail.txt", "content": "{{ steps.nonexistent.output_text }}"}),
|
|
PlanStep("step-2", "finish", "Task complete", {}),
|
|
]
|
|
|
|
# --- Normal paths ---
|
|
|
|
# Search queries (HIGHEST PRIORITY - check FIRST)
|
|
search_keywords = [
|
|
"найди", "поиск", "новости", "что нового", "посмотри что нового",
|
|
"search", "news", "latest", "find", "look up", "google", "bing"
|
|
]
|
|
if any(word in goal_lower for word in search_keywords):
|
|
steps = [
|
|
PlanStep("step-1", "web_search", "Search the web", {"query": goal}),
|
|
PlanStep("step-2", "finish", "Search complete", {})
|
|
]
|
|
|
|
# Inspect/analyze commands
|
|
elif any(kw in goal_lower for kw in ("inspect", "analyze", "посмотри", "проверь")):
|
|
steps = [
|
|
PlanStep("step-1", "shell", "Show current directory", {"command": "pwd"}),
|
|
PlanStep("step-2", "shell", "List files", {"command": "ls"}),
|
|
PlanStep("step-3", "finish", "Task complete", {})
|
|
]
|
|
|
|
# File commands
|
|
elif any(kw in goal_lower for kw in ("create file", "создай файл", "write file")):
|
|
filename, content = _parse_file_goal(goal)
|
|
steps = [
|
|
PlanStep("step-1", "write_file", f"Write {filename}", {"path": filename, "content": content}),
|
|
PlanStep("step-2", "read_file", f"Read back {filename}", {"path": filename}),
|
|
PlanStep("step-3", "finish", "Task complete", {})
|
|
]
|
|
|
|
# FALLBACK: If nothing matches, default to web_search
|
|
else:
|
|
steps = [
|
|
PlanStep("step-1", "web_search", "Search the web (default)", {"query": goal}),
|
|
PlanStep("step-2", "finish", "Search complete", {})
|
|
]
|
|
|
|
return Plan(steps=steps)
|
|
|
|
|
|
class PlannerError(Exception):
|
|
"""Raised when no plan can be built for the given goal."""
|
|
|
|
|
|
def _parse_file_goal(goal: str) -> tuple[str, str]:
|
|
"""Extract filename and content from a 'create file' goal string."""
|
|
m = re.search(
|
|
r"(?:create file|write file|создай файл)\s+(\S+)"
|
|
r"(?:\s+with\s+(?:text|content)\s+(.+))?",
|
|
goal,
|
|
re.IGNORECASE,
|
|
)
|
|
if m:
|
|
return m.group(1), (m.group(2) or "")
|
|
return "output.txt", ""
|
|
|
|
|