Files
agent-maxim/agent/planner/planner.py
T
Anton 052591281d Initial clean version - Agent Maxim
- 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
2026-04-09 18:07:43 +03:00

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", ""