├── src
├── __init__.py
├── utils
│ ├── __init__.py
│ ├── web_search.py
│ ├── helpers.py
│ ├── xml_schema.py
│ ├── feedback.py
│ ├── shell.py
│ ├── xml_operations.py
│ ├── file_ops.py
│ ├── xml_tools.py
│ └── input_schema.py
├── interface
│ ├── __init__.py
│ ├── input_handler.py
│ ├── display.py
│ ├── cli.py
│ ├── vim_input.py
│ ├── interface.py
│ └── input.py
├── run_agent.py
├── agent
│ ├── repository.py
│ ├── core.py
│ ├── execution.py
│ ├── task.py
│ └── plan.py
├── config.py
├── spec.md
├── task_list.md
└── agent_main.py
├── bin
└── agent
├── tests
├── test_dummy.py
├── conftest.py
├── test_task.py
├── test_commands.py
├── test_interface.py
├── test_core.py
├── test_web_search.py
├── test_file_ops.py
├── test_xml_schema.py
├── test_feedback.py
├── test_display.py
└── test_xml_tools.py
├── spec.md
├── setup.py
├── c
├── .pylintrc
├── LICENSE
├── start_agent
├── demos
└── dopamine_demo.py
├── watch-git-diff
├── README.md
├── .gitignore
├── info.md
├── aider_multi_agent
├── project_agent
└── prompt_cycle
/src/__init__.py:
--------------------------------------------------------------------------------
1 | # Make src a proper package
2 |
--------------------------------------------------------------------------------
/src/utils/__init__.py:
--------------------------------------------------------------------------------
1 | # Package initialization for utils module
2 | # Package initialization for utils module
3 |
--------------------------------------------------------------------------------
/bin/agent:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from src.interface.cli import main
4 |
5 | if __name__ == "__main__":
6 | main()
7 |
--------------------------------------------------------------------------------
/tests/test_dummy.py:
--------------------------------------------------------------------------------
1 | def test_dummy():
2 | """Simple dummy test that always passes"""
3 | assert True, "This test should always pass"
4 |
--------------------------------------------------------------------------------
/spec.md:
--------------------------------------------------------------------------------
1 | - has a cli interface
2 | - uses rich to create good looking tables
3 | - shows the ping jitter to locations in a table
4 | - by default without args analyses and shows ping jitter to new york as a demo
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from pathlib import Path
3 |
4 | # Add project root to Python path
5 | project_root = str(Path(__file__).parent.parent)
6 | if project_root not in sys.path:
7 | sys.path.insert(0, project_root)
8 |
--------------------------------------------------------------------------------
/src/interface/__init__.py:
--------------------------------------------------------------------------------
1 | """Interface package initialization."""
2 | from .display import (
3 | get_system_info,
4 | display_welcome,
5 | display_help,
6 | display_models,
7 | display_plan_tree,
8 | display_from_top,
9 | )
10 |
--------------------------------------------------------------------------------
/src/run_agent.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 |
4 | def main():
5 | """Entry point for the agent interface"""
6 | from src.interface.interface import main as interface_main
7 |
8 | interface_main()
9 |
10 |
11 | if __name__ == "__main__":
12 | main()
13 |
--------------------------------------------------------------------------------
/tests/test_task.py:
--------------------------------------------------------------------------------
1 | """Tests for task execution functionality."""
2 |
3 | from src.agent.task import execute_task
4 | from src.agent.core import Agent
5 |
6 |
7 | def test_execute_task_with_no_plan():
8 | """Test executing a task when no plan exists."""
9 | agent = Agent()
10 |
11 | # Execute task with no plan
12 | result = execute_task(agent, "task1")
13 |
14 | # Verify error response
15 | assert "error" in result
16 | assert "No plan exists" in result
17 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name="path-scripts",
5 | version="0.1.0",
6 | packages=find_packages(),
7 | install_requires=[
8 | "rich<13.0.0,>=12.6.0", # Compatible with textual 0.1.18
9 | "litellm",
10 | "textual==0.1.18", # Pin to the installed version
11 | "lxml", # For better XML handling
12 | "requests", # For web search
13 | ],
14 | entry_points={
15 | "console_scripts": [
16 | "agent=src.interface.cli:main",
17 | ],
18 | },
19 | )
20 |
--------------------------------------------------------------------------------
/tests/test_commands.py:
--------------------------------------------------------------------------------
1 | """Tests for command processing."""
2 | from unittest.mock import Mock
3 | from src.interface.commands import process_command # pylint: disable=no-name-in-module
4 |
5 | def test_process_help_command():
6 | """Test help command displays help."""
7 | mock_console = Mock()
8 | process_command(
9 | agent=Mock(),
10 | command=["help"],
11 | chat_history=[],
12 | history_file="test.json",
13 | console=mock_console,
14 | multiline_input_mode=False,
15 | multiline_input_buffer=[]
16 | )
17 | mock_console.print.assert_called() # Verify help was displayed
18 |
--------------------------------------------------------------------------------
/tests/test_interface.py:
--------------------------------------------------------------------------------
1 | """Tests for interface components."""
2 | from rich.console import Console
3 | from src.interface.display import get_system_info, display_welcome # pylint: disable=no-name-in-module
4 |
5 | def test_get_system_info():
6 | """Test system info returns expected fields."""
7 | info = get_system_info()
8 | assert isinstance(info, dict)
9 | for key in ["platform", "python", "shell"]:
10 | assert key in info
11 | assert isinstance(info[key], str)
12 |
13 | def test_display_welcome():
14 | """Test welcome message displays without errors."""
15 | console = Console()
16 | display_welcome(console)
17 | # Basic smoke test - just verify function runs without exceptions
18 |
--------------------------------------------------------------------------------
/c:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Default filters as single string without extra quoting
4 | DEFAULT_FILTERS="+PENDING -bu"
5 |
6 | # Handle arguments
7 | if [[ $# -eq 0 ]]; then
8 | echo "Usage: c [OPTIONS] [FILTERS...] [REPORT_NAME]"
9 | echo "Options:"
10 | echo " -n, --no_default_filters Disable default filters (+PENDING -bu)"
11 | exit 1
12 | fi
13 |
14 | # Check for no-default-filters flag
15 | NO_DEFAULTS=false
16 | if [[ "$1" == "-n" || "$1" == "--no_default_filters" ]]; then
17 | NO_DEFAULTS=true
18 | shift
19 | fi
20 |
21 | # Build arguments
22 | ARGS=("--once")
23 | if ! $NO_DEFAULTS; then
24 | ARGS+=("$DEFAULT_FILTERS")
25 | fi
26 |
27 | # Combine remaining arguments into single string and add to ARGS
28 | USER_ARGS="$*"
29 | ARGS+=("$USER_ARGS")
30 |
31 | # Pass all arguments as individual quoted elements
32 | "/home/tom/git/scripts/show_tw_tasks.py" "${ARGS[@]}"
33 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MAIN]
2 | # Load all checks except those we disable below
3 | disable=
4 |
5 | [MESSAGES CONTROL]
6 | # Disable these specific checks
7 | disable=
8 | missing-docstring,
9 | too-few-public-methods,
10 | too-many-arguments,
11 | too-many-locals,
12 | too-many-instance-attributes,
13 | too-many-public-methods,
14 | too-many-branches,
15 | too-many-statements,
16 | duplicate-code,
17 | protected-access,
18 | redefined-builtin,
19 | broad-except,
20 | fixme,
21 | invalid-name,
22 | line-too-long,
23 | import-error,
24 | wrong-import-position,
25 | unnecessary-dunder-call,
26 | consider-using-f-string,
27 | unspecified-encoding,
28 | unnecessary-lambda-assignment,
29 | use-dict-literal,
30 | use-list-literal
31 |
32 | [REPORTS]
33 | # Output configuration
34 | output-format=colorized
35 | reports=no
36 |
37 | [FORMAT]
38 | # Formatting options
39 | max-line-length=120
40 | indent-string=' '
41 | single-line-if-stmt=no
42 | ignore-long-lines=^\s*(# )??$
43 |
--------------------------------------------------------------------------------
/tests/test_core.py:
--------------------------------------------------------------------------------
1 | """Tests for core agent functionality."""
2 |
3 | from src.agent.core import Agent # pylint: disable=no-name-in-module
4 |
5 |
6 | def test_agent_initialization():
7 | """Test agent initializes with correct repository info."""
8 | test_repo_path = "/test/path"
9 | agent = Agent()
10 |
11 | # Test initial state before initialization
12 | assert not agent.repository_info # Should be empty
13 |
14 | agent.initialize(test_repo_path)
15 |
16 | # Test state after initialization
17 | assert "path" in agent.repository_info
18 | assert agent.repository_info["path"] == test_repo_path
19 |
20 |
21 | def test_agent_initial_state():
22 | """Test agent starts with empty repository info."""
23 | agent = Agent()
24 | assert (
25 | agent.repository_info == {}
26 | ), "Repository info should be empty before initialization"
27 |
28 | def test_initial_plan_tree_is_none():
29 | """Test plan tree is None after initialization."""
30 | agent = Agent()
31 | assert agent.plan_tree is None, "Plan tree should be None initially"
32 |
--------------------------------------------------------------------------------
/tests/test_web_search.py:
--------------------------------------------------------------------------------
1 | """Tests for web search functionality."""
2 |
3 | import requests
4 | from src.utils.web_search import search_web # pylint: disable=no-name-in-module
5 |
6 |
7 | def test_search_web_success():
8 | """Test successful web search returns results."""
9 | query = "Python programming language"
10 | results = search_web(query)
11 |
12 | assert isinstance(results, list)
13 | if results: # Only check structure if we got results
14 | for result in results:
15 | assert "title" in result
16 | assert "link" in result
17 | assert "snippet" in result
18 |
19 |
20 | def test_search_web_empty_query():
21 | """Test empty query returns empty results."""
22 | results = search_web("")
23 | assert results == []
24 |
25 |
26 | def test_search_web_error_handling(monkeypatch):
27 | """Test error handling when API fails."""
28 | def mock_get(*args, **kwargs):
29 | raise requests.RequestException("API Error")
30 |
31 | monkeypatch.setattr(requests, "get", mock_get)
32 | results = search_web("test")
33 | assert results == []
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Tom Dörr
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/start_agent:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Default model if not specified
4 | MODEL="r1"
5 |
6 | # Parse command line arguments
7 | while [[ $# -gt 0 ]]; do
8 | case $1 in
9 | --model)
10 | MODEL="$2"
11 | shift 2
12 | ;;
13 | *)
14 | echo "Unknown option: $1"
15 | echo "Usage: $0 [--model MODEL_NAME]"
16 | exit 1
17 | ;;
18 | esac
19 | done
20 |
21 | cd src
22 | for i in {1..1000}
23 | do
24 | echo "$i - $(date) ====================================================="
25 | aider --architect --model $MODEL --subtree-only --read plex.md --read context.txt --yes-always --no-show-model-warnings --weak-model 'openrouter/google/gemini-2.0-flash-001' --message 'if there are errors, work on fixing the errors. if there are no errors, work on cleaning up the code a little bit. If you see that something is not defined or implemented, please work on implementing until there are no errors. The tests need to work as they are right now. Do not edit tests. Do not add tests to the chat. Do not edit the linting rules. Do not run any commands. Do not try to install anything. Do not mock any functionality, actually implement it. Is there any functionality that is not yet implemented? Replace all mocking with actual implementations. Only use small search replace blocks. You can however use many search replace blocks.' **/*.py
26 | sleep 1
27 | done
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/utils/web_search.py:
--------------------------------------------------------------------------------
1 | """Web search functionality using DuckDuckGo API."""
2 |
3 | import requests
4 | from typing import List, Dict
5 |
6 |
7 | def search_web(query: str) -> List[Dict[str, str]]:
8 | """
9 | Perform a web search using DuckDuckGo Instant Answer API.
10 |
11 | Args:
12 | query: Search query string
13 |
14 | Returns:
15 | List of search results with title, link and snippet
16 | """
17 | if not query.strip():
18 | return []
19 |
20 | try:
21 | response = requests.get(
22 | "https://api.duckduckgo.com/",
23 | params={
24 | "q": query,
25 | "format": "json",
26 | "no_html": 1,
27 | "skip_disambig": 1,
28 | },
29 | timeout=5,
30 | )
31 | response.raise_for_status()
32 |
33 | data = response.json()
34 | results = []
35 |
36 | # Extract basic results from RelatedTopics
37 | for topic in data.get("RelatedTopics", []):
38 | if "FirstURL" in topic and "Text" in topic:
39 | results.append({
40 | "title": topic["Text"].split(" - ")[0],
41 | "link": topic["FirstURL"],
42 | "snippet": topic["Text"]
43 | })
44 |
45 | return results[:3] # Return top 3 results to keep it simple
46 |
47 | except requests.RequestException:
48 | return [] # Fail silently for now
49 |
--------------------------------------------------------------------------------
/demos/dopamine_demo.py:
--------------------------------------------------------------------------------
1 | from rich.console import Console
2 | import sys
3 | import os
4 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5 | from src.utils.feedback import DopamineReward
6 |
7 | def demo_dopamine_optimization():
8 | """Demo dopamine-based prompt optimization"""
9 | console = Console()
10 | reward = DopamineReward(console)
11 |
12 | # Initial state
13 | console.print(f"\nStarting dopamine level: {reward.dopamine_level:.1f}")
14 |
15 | # Simulate positive interaction
16 | console.print("\n[bold]Test 1: Positive feedback[/bold]")
17 | feedback = reward.reward_for_xml_response("", "Perfect! Exactly what I needed!")
18 | console.print(f"Reward: {feedback}")
19 | console.print(f"New dopamine level: {reward.dopamine_level:.1f}")
20 |
21 | # Simulate negative interaction
22 | console.print("\n[bold]Test 2: Negative feedback[/bold]")
23 | feedback = reward.reward_for_xml_response("", "Wrong! This is completely incorrect.")
24 | console.print(f"Reward: {feedback}")
25 | console.print(f"New dopamine level: {reward.dopamine_level:.1f}")
26 |
27 | # Simulate mixed interaction
28 | console.print("\n[bold]Test 3: Mixed feedback[/bold]")
29 | feedback = reward.reward_for_xml_response("", "Partially correct but needs improvement")
30 | console.print(f"Reward: {feedback}")
31 | console.print(f"New dopamine level: {reward.dopamine_level:.1f}")
32 |
33 | if __name__ == "__main__":
34 | demo_dopamine_optimization()
35 |
--------------------------------------------------------------------------------
/src/utils/helpers.py:
--------------------------------------------------------------------------------
1 | """Common helper functions reused across the codebase."""
2 |
3 | import os
4 | import json
5 | from typing import List, Dict, Any
6 |
7 | def load_persistent_memory() -> str:
8 | """
9 | Load memory from file.
10 |
11 | Returns:
12 | Memory content as string
13 | """
14 | memory_file = "agent_memory.xml"
15 | try:
16 | if os.path.exists(memory_file):
17 | with open(memory_file, "r") as f:
18 | return f.read()
19 | # Create default memory structure
20 | default_memory = "\n \n"
21 | with open(memory_file, "w") as f:
22 | f.write(default_memory)
23 | return default_memory
24 | except Exception as e:
25 | print(f"Could not load memory: {e}")
26 | return ""
27 |
28 | def save_chat_history(chat_history: List[Dict[str, Any]], history_file: str) -> None:
29 | """
30 | Save chat history to file.
31 |
32 | Args:
33 | chat_history: List of chat history entries
34 | history_file: Path to the history file
35 | """
36 | try:
37 | os.makedirs(os.path.dirname(history_file), exist_ok=True)
38 | with open(history_file, "w") as f:
39 | json.dump(chat_history, f, indent=2)
40 | except Exception as e:
41 | print(f"Could not save chat history: {e}")
42 |
43 | def get_terminal_height() -> int:
44 | """
45 | Get terminal height in lines.
46 |
47 | Returns:
48 | Terminal height or 40 as default
49 | """
50 | try:
51 | import shutil
52 | return shutil.get_terminal_size().lines
53 | except Exception:
54 | return 40
55 |
--------------------------------------------------------------------------------
/tests/test_file_ops.py:
--------------------------------------------------------------------------------
1 | """Tests for file operations."""
2 |
3 | import os
4 | import tempfile
5 | from src.utils.file_ops import read_file, write_file, edit_file, append_to_file
6 |
7 | def test_read_file_success():
8 | """Test reading an existing file."""
9 | with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
10 | f.write("test content")
11 | path = f.name
12 |
13 | success, content = read_file(path)
14 | assert success is True
15 | assert content == "test content"
16 | os.unlink(path)
17 |
18 | def test_read_file_not_found():
19 | """Test reading a non-existent file."""
20 | success, content = read_file("/nonexistent/file")
21 | assert success is False
22 | assert "not found" in content.lower()
23 |
24 | def test_write_file_new():
25 | """Test writing to a new file."""
26 | with tempfile.TemporaryDirectory() as tmpdir:
27 | path = os.path.join(tmpdir, "new.txt")
28 | success, _ = write_file(path, "new content")
29 | assert success is True
30 | assert os.path.exists(path)
31 | with open(path) as f:
32 | assert f.read() == "new content"
33 |
34 | def test_edit_file_success():
35 | """Test editing a file."""
36 | with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
37 | f.write("old content")
38 | path = f.name
39 |
40 | success, _ = edit_file(path, "old", "new")
41 | assert success is True
42 | with open(path) as f:
43 | assert f.read() == "new content"
44 | os.unlink(path)
45 |
46 | def test_append_to_file():
47 | """Test appending to a file."""
48 | with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
49 | f.write("original")
50 | path = f.name
51 |
52 | success, _ = append_to_file(path, "\nappended")
53 | assert success is True
54 | with open(path) as f:
55 | assert f.read() == "original\nappended"
56 | os.unlink(path)
57 |
--------------------------------------------------------------------------------
/src/agent/repository.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """Repository analysis functionality."""
3 |
4 | import os
5 | import subprocess
6 | from typing import Dict, Any
7 |
8 |
9 | def analyze_repository(repo_path: str = ".") -> Dict[str, Any]:
10 | """
11 | Analyze the repository structure and return information.
12 |
13 | Args:
14 | repo_path: Path to the repository
15 |
16 | Returns:
17 | Dictionary containing repository information
18 | """
19 | repo_info = {"files": [], "directories": [], "git_info": {}}
20 |
21 | # Get list of files (excluding .git directory)
22 | try:
23 | result = subprocess.run(
24 | ["git", "ls-files"],
25 | cwd=repo_path,
26 | capture_output=True,
27 | text=True,
28 | check=True,
29 | )
30 | repo_info["files"] = result.stdout.strip().split("\n")
31 | except subprocess.CalledProcessError:
32 | # Fallback if git command fails
33 | for root, dirs, files in os.walk(repo_path):
34 | if ".git" in root:
35 | continue
36 | for file in files:
37 | full_path = os.path.join(root, file)
38 | rel_path = os.path.relpath(full_path, repo_path)
39 | repo_info["files"].append(rel_path)
40 | for dir in dirs:
41 | if dir != ".git":
42 | full_path = os.path.join(root, dir)
43 | rel_path = os.path.relpath(full_path, repo_path)
44 | repo_info["directories"].append(rel_path)
45 |
46 | # Get git info if available
47 | try:
48 | result = subprocess.run(
49 | ["git", "branch", "--show-current"],
50 | cwd=repo_path,
51 | capture_output=True,
52 | text=True,
53 | check=True,
54 | )
55 | repo_info["git_info"]["current_branch"] = result.stdout.strip()
56 | except subprocess.CalledProcessError:
57 | repo_info["git_info"]["current_branch"] = "unknown"
58 |
59 | return repo_info
60 |
--------------------------------------------------------------------------------
/src/interface/input_handler.py:
--------------------------------------------------------------------------------
1 | """
2 | Input handler for the agent CLI with Vim-like functionality.
3 | """
4 |
5 | from typing import List, Dict, Any, Optional
6 | from rich.console import Console
7 |
8 | # Try to import the Vim input module, fall back to regular input if not available
9 | try:
10 | from src.interface.vim_input import get_vim_input
11 |
12 | VIM_INPUT_AVAILABLE = True
13 | except ImportError:
14 | VIM_INPUT_AVAILABLE = False
15 |
16 |
17 | def get_user_input(
18 | console: Console,
19 | prompt: str = "> ",
20 | history: List[str] = None,
21 | vim_mode: bool = True,
22 | config: Dict[str, Any] = None,
23 | ) -> str:
24 | """
25 | Get input from the user with optional Vim-like interface.
26 |
27 | Args:
28 | console: Rich console instance
29 | prompt: Input prompt to display
30 | history: Command history
31 | vim_mode: Whether to use Vim-like input mode
32 | config: Configuration dictionary
33 |
34 | Returns:
35 | User input string
36 | """
37 | # Check config for vim_mode setting if provided
38 | if config is not None and "vim_mode" in config:
39 | vim_mode = config["vim_mode"]
40 |
41 | try:
42 | if vim_mode and VIM_INPUT_AVAILABLE:
43 | console.print(f"[dim]{prompt}[/dim]", end="")
44 | return get_vim_input(console, history or [])
45 | else:
46 | # Fall back to regular input
47 | return console.input(prompt)
48 | except Exception as e:
49 | console.print(f"[bold red]Error getting input: {str(e)}[/bold red]")
50 | # Fall back to basic input on error
51 | return input(prompt)
52 |
53 |
54 | def process_input_with_history(
55 | input_text: str, history: List[str], max_history: int = 100
56 | ) -> None:
57 | """
58 | Process input and update history.
59 |
60 | Args:
61 | input_text: The input text to process
62 | history: The history list to update
63 | max_history: Maximum history items to keep
64 | """
65 | if input_text and (not history or input_text != history[-1]):
66 | history.append(input_text)
67 |
68 | # Trim history if needed
69 | if len(history) > max_history:
70 | history.pop(0)
71 |
--------------------------------------------------------------------------------
/src/interface/display.py:
--------------------------------------------------------------------------------
1 | """Display formatting for the agent interface."""
2 |
3 | import platform
4 | from typing import Dict
5 | from rich.console import Console
6 | from rich.panel import Panel
7 |
8 |
9 | def get_system_info() -> Dict[str, str]:
10 | """Get basic system information."""
11 | return {
12 | "platform": platform.platform(),
13 | "python": platform.python_version(),
14 | "shell": platform.system(),
15 | }
16 |
17 |
18 | def display_welcome(console: Console, system_info: Dict[str, str] = None):
19 | """Display welcome message."""
20 | if system_info is None:
21 | system_info = get_system_info()
22 |
23 | console.print(
24 | Panel.fit(
25 | "[bold blue]Agent Interface[/bold blue]\n"
26 | f"Running on: {system_info['platform']} | Python {system_info['python']}",
27 | title="Welcome",
28 | border_style="blue",
29 | )
30 | )
31 |
32 |
33 | def display_help(console: Console):
34 | """Display available commands."""
35 | console.print("[bold]Available Commands:[/bold]")
36 | console.print("- /help: Show this help")
37 | console.print("- /exit: Exit the interface")
38 | console.print("- /models: Show available models")
39 | console.print("- /plan: Generate or display plan")
40 | console.print("- /execute: Execute a task")
41 |
42 |
43 | def display_models(agent, console: Console):
44 | """Display available models."""
45 | model_name = agent.model_name if agent else "No model selected"
46 | console.print(f"[bold blue]Current model:[/bold blue] {model_name}")
47 |
48 |
49 | def display_plan_tree(console: Console, xml_content: str):
50 | """Display plan tree from XML."""
51 | if not xml_content:
52 | console.print("[bold red]Error: No plan content[/bold red]")
53 | return
54 |
55 | console.print("[bold blue]Plan Tree:[/bold blue]")
56 | console.print(xml_content)
57 |
58 |
59 | def display_from_top(console: Console, content: str, _preserve_history: bool = True):
60 | """
61 | Display content without clearing terminal history.
62 |
63 | Args:
64 | console: Rich console instance
65 | content: Content to display
66 | preserve_history: Not used, kept for backward compatibility
67 | """
68 | console.print(content)
69 |
--------------------------------------------------------------------------------
/src/utils/xml_schema.py:
--------------------------------------------------------------------------------
1 | """XML schema definitions for agent responses."""
2 |
3 | RESPONSE_SCHEMA = """
4 |
5 |
6 |
7 |
8 | Your response text here. Can include markdown formatting.
9 |
10 |
11 |
12 |
13 |
14 | # Python code here
15 |
16 |
17 |
18 |
19 |
20 | def old_function():
21 | def new_function():
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | def old_function():
34 | def new_function():
35 |
36 |
37 |
38 | # New file content here
39 |
40 |
41 |
42 |
43 |
44 | echo "Hello World"
45 | rm -rf some_directory
46 |
47 |
48 |
49 |
50 |
51 | Old information to replace
52 | Updated information
53 |
54 | New information to remember
55 |
56 |
57 |
58 |
59 | Status message explaining what's done or what's needed
60 |
61 |
62 |
63 | """
64 |
65 |
66 | def get_schema():
67 | """Return the XML schema for agent responses."""
68 | return RESPONSE_SCHEMA
69 |
--------------------------------------------------------------------------------
/tests/test_xml_schema.py:
--------------------------------------------------------------------------------
1 | """Tests for XML schema functionality."""
2 |
3 | import xml.etree.ElementTree as ET
4 | from src.utils.xml_schema import get_schema # pylint: disable=no-name-in-module
5 | from src.utils.xml_tools import validate_xml # pylint: disable=no-name-in-module
6 |
7 |
8 | def test_schema_is_valid_xml():
9 | """Test that the schema itself is valid XML."""
10 | schema = get_schema()
11 | assert validate_xml(schema), "Schema should be valid XML"
12 |
13 |
14 | def test_schema_contains_required_elements():
15 | """Test that the schema contains required elements."""
16 | schema = get_schema()
17 | required_elements = {
18 | "response",
19 | "actions",
20 | "file_edits",
21 | "shell_commands",
22 | "memory_updates",
23 | "execution_status",
24 | }
25 |
26 | for element in required_elements:
27 | assert f"<{element}" in schema, f"Schema should contain {element} element"
28 | assert f"{element}>" in schema, f"Schema should close {element} element"
29 |
30 |
31 | def test_schema_example_structures():
32 | """Test that example structures in schema are valid"""
33 | schema = get_schema()
34 | assert 'type="create_file"' in schema, "Should contain file creation example"
35 | assert 'type="modify_file"' in schema, "Should contain file modification example"
36 | assert 'type="run_command"' in schema, "Should contain command execution example"
37 |
38 |
39 | def test_get_schema_returns_string():
40 | """Test that get_schema returns a non-empty string."""
41 | schema = get_schema()
42 | assert isinstance(schema, str), "Schema should be a string"
43 | assert len(schema) > 100, "Schema should be a meaningful length string"
44 |
45 |
46 | def test_execution_status_structure():
47 | """Test execution_status element has required attributes"""
48 | schema = get_schema()
49 | root = ET.fromstring(schema)
50 | status_elem = root.find(".//execution_status")
51 |
52 | assert status_elem is not None, "execution_status element missing"
53 | assert "complete" in status_elem.attrib, "Missing complete attribute"
54 | assert (
55 | "needs_user_input" in status_elem.attrib
56 | ), "Missing needs_user_input attribute"
57 |
58 | def test_schema_root_element():
59 | """Test schema contains root xml_schema element"""
60 | schema = get_schema()
61 | root = ET.fromstring(schema)
62 | assert root.tag == "xml_schema", "Schema should have xml_schema root element"
63 |
--------------------------------------------------------------------------------
/src/config.py:
--------------------------------------------------------------------------------
1 | """
2 | Configuration management for the agent.
3 | """
4 |
5 | import os
6 | import json
7 | from typing import Dict, Any, Optional
8 |
9 | DEFAULT_CONFIG = {
10 | "vim_mode": True,
11 | "stream_reasoning": True,
12 | "verbose": True,
13 | "default_model": "openrouter/deepseek/deepseek-r1",
14 | "history_size": 100,
15 | "model_aliases": {
16 | "flash": "openrouter/google/gemini-2.0-flash-001",
17 | "r1": "deepseek/deepseek-reasoner",
18 | "claude": "openrouter/anthropic/claude-3.7-sonnet",
19 | },
20 | }
21 |
22 | CONFIG_PATH = os.path.expanduser("~/.config/agent/config.json")
23 |
24 |
25 | def load_config() -> Dict[str, Any]:
26 | """
27 | Load configuration from file or return default.
28 |
29 | Returns:
30 | Configuration dictionary
31 | """
32 | if os.path.exists(CONFIG_PATH):
33 | try:
34 | with open(CONFIG_PATH, "r") as f:
35 | config = json.load(f)
36 | # Merge with defaults for any missing keys
37 | return {**DEFAULT_CONFIG, **config}
38 | except Exception:
39 | return DEFAULT_CONFIG
40 | else:
41 | return DEFAULT_CONFIG
42 |
43 |
44 | def save_config(config: Dict[str, Any]) -> bool:
45 | """
46 | Save configuration to file.
47 |
48 | Args:
49 | config: Configuration dictionary
50 |
51 | Returns:
52 | True if successful, False otherwise
53 | """
54 | try:
55 | # Ensure directory exists
56 | os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
57 |
58 | with open(CONFIG_PATH, "w") as f:
59 | json.dump(config, f, indent=2)
60 | return True
61 | except Exception:
62 | return False
63 |
64 |
65 | def update_config(key: str, value: Any) -> bool:
66 | """
67 | Update a specific configuration value.
68 |
69 | Args:
70 | key: Configuration key
71 | value: New value
72 |
73 | Returns:
74 | True if successful, False otherwise
75 | """
76 | config = load_config()
77 | config[key] = value
78 | return save_config(config)
79 |
80 |
81 | def get_config_value(key: str, default: Optional[Any] = None) -> Any:
82 | """
83 | Get a specific configuration value.
84 |
85 | Args:
86 | key: Configuration key
87 | default: Default value if key not found
88 |
89 | Returns:
90 | Configuration value or default
91 | """
92 | config = load_config()
93 | return config.get(key, default)
94 |
--------------------------------------------------------------------------------
/tests/test_feedback.py:
--------------------------------------------------------------------------------
1 | """Tests for feedback functionality."""
2 |
3 | from rich.console import Console
4 | from src.utils.feedback import DopamineReward # pylint: disable=no-name-in-module
5 |
6 |
7 | def test_initial_score_neutral():
8 | """Test initial score is neutral."""
9 | reward = DopamineReward(Console())
10 | assert "NEUTRAL" in reward.generate_reward()
11 |
12 |
13 | def test_positive_feedback_high_score():
14 | """Test high score generates positive feedback."""
15 | reward = DopamineReward(Console())
16 | feedback = reward.generate_reward(95)
17 | assert "SURGE" in feedback
18 |
19 |
20 | def test_negative_feedback_low_score():
21 | """Test low score generates negative feedback."""
22 | reward = DopamineReward(Console())
23 | feedback = reward.generate_reward(15)
24 | assert "LOW" in feedback
25 |
26 |
27 | def test_mixed_feedback_mid_score():
28 | """Test mid-range score generates mixed feedback."""
29 | reward = DopamineReward(Console())
30 | feedback = reward.generate_reward(65)
31 | assert "TRICKLE" in feedback # 65 should be in the TRICKLE range
32 |
33 |
34 | def test_positive_feedback_edge_case():
35 | """Test edge case for positive feedback."""
36 | reward = DopamineReward(Console())
37 | feedback = reward.generate_reward(75)
38 | assert "BOOST" in feedback
39 |
40 |
41 | def test_negative_feedback_edge_case():
42 | """Test edge case for negative feedback."""
43 | reward = DopamineReward(Console())
44 | feedback = reward.generate_reward(39)
45 | assert "DIP" in feedback
46 |
47 |
48 | def test_reward_with_positive_observation():
49 | """Test reward generation with positive user feedback affects dopamine level."""
50 | reward = DopamineReward(Console())
51 | initial_level = reward.dopamine_level
52 | reward.reward_for_xml_response("", "Good job! This is perfect!")
53 | assert reward.dopamine_level > initial_level
54 |
55 |
56 | def test_reward_with_negative_observation():
57 | """Test reward generation with negative user feedback affects dopamine level."""
58 | reward = DopamineReward(Console())
59 | initial_level = reward.dopamine_level
60 | reward.reward_for_xml_response("", "Bad result! Wrong and useless!")
61 | assert reward.dopamine_level < initial_level
62 |
63 |
64 | def test_reward_with_neutral_observation():
65 | """Test reward generation with mixed feedback."""
66 | reward = DopamineReward(Console())
67 | feedback = reward.reward_for_xml_response("", "OK but could be better")
68 | assert "NEUTRAL" in feedback
69 |
70 |
71 | def test_empty_feedback_defaults_neutral():
72 | """Test empty feedback defaults to neutral."""
73 | reward = DopamineReward(Console())
74 | feedback = reward.reward_for_xml_response("", "")
75 | assert "NEUTRAL" in feedback
76 |
77 |
78 | def test_default_reward_score():
79 | """Test reward generation with default scoring."""
80 | reward = DopamineReward(Console())
81 | feedback = reward.generate_reward()
82 | assert "DOPAMINE" in feedback # Should handle None score
83 |
--------------------------------------------------------------------------------
/watch-git-diff:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # watch-git-diff - Continuously display the last git commit diff with color
4 | # Usage: watch-git-diff [check_interval_seconds] [additional_git_diff_args]
5 |
6 | # Default check interval in seconds
7 | INTERVAL=${1:-2}
8 | shift 2>/dev/null
9 |
10 | # Check if we're in a git repository
11 | if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
12 | echo "Error: Not in a git repository"
13 | exit 1
14 | fi
15 |
16 | # Get the initial HEAD commit hash
17 | LAST_KNOWN_COMMIT=$(git rev-parse HEAD 2>/dev/null)
18 |
19 | # Use a more compatible approach for terminal handling
20 | setup_display() {
21 | # Function to clear screen without flickering
22 | clear_screen() {
23 | clear
24 | }
25 | }
26 |
27 | # Function to check if there's a new commit
28 | check_for_new_commit() {
29 | # Get current HEAD commit
30 | CURRENT_COMMIT=$(git rev-parse HEAD 2>/dev/null)
31 |
32 | # Compare with last known commit
33 | if [[ "$CURRENT_COMMIT" != "$LAST_KNOWN_COMMIT" ]]; then
34 | LAST_KNOWN_COMMIT=$CURRENT_COMMIT
35 | return 0 # New commit detected
36 | fi
37 |
38 | return 1 # No new commit
39 | }
40 |
41 | # Function to display the diff with a header
42 | show_diff() {
43 | clear_screen
44 |
45 | # Get the last commit hash and message
46 | LAST_COMMIT=$(git log -1 --pretty=format:"%h - %s (%cr) by %an")
47 |
48 | # Print header with timestamp
49 | echo -e "\033[1;36m=== Last Commit Diff (Updated: $(date '+%Y-%m-%d %H:%M:%S')) ===\033[0m"
50 | echo -e "\033[1;33m$LAST_COMMIT\033[0m"
51 | echo -e "\033[1;36m=======================================================\033[0m"
52 | echo ""
53 |
54 | # Show the diff with color
55 | git --no-pager diff HEAD~1 HEAD --color=always "$@"
56 |
57 | echo ""
58 | echo -e "\033[1;36m=== Press Ctrl+C or 'q' to exit ===\033[0m"
59 | }
60 |
61 | # Main execution
62 | echo "Starting git diff watch, checking for new commits every $INTERVAL seconds..."
63 | setup_display
64 |
65 | # Show initial diff
66 | show_diff "$@"
67 |
68 | # Function to check for 'q' keypress without blocking
69 | check_for_quit() {
70 | # Check if input is available (non-blocking)
71 | if read -t 0.1 -N 1 key; then
72 | if [[ "$key" == "q" ]]; then
73 | echo -e "\nUser pressed 'q'. Exiting..."
74 | exit 0
75 | fi
76 | fi
77 | }
78 |
79 | # Set terminal to read input without requiring Enter key
80 | old_stty_settings=$(stty -g)
81 | stty -icanon min 1 time 0
82 |
83 | # Ensure terminal settings are restored on exit
84 | cleanup() {
85 | stty "$old_stty_settings"
86 | echo -e "\nExiting git diff watch"
87 | exit 0
88 | }
89 | trap cleanup INT TERM EXIT
90 |
91 | # Main loop
92 | while true; do
93 | # Check for new commits
94 | if check_for_new_commit; then
95 | # New commit detected, update the display
96 | show_diff "$@"
97 | echo -e "\033[1;32mNew commit detected, updated diff.\033[0m" >&2
98 | fi
99 |
100 | # Update status on the same line (overwrite previous status)
101 | echo -ne "\r\033[K\033[1;36mLast check: $(date '+%H:%M:%S') | Press Ctrl+C to exit or 'q' to quit\033[0m"
102 |
103 | # Check for 'q' keypress
104 | check_for_quit
105 |
106 | # Wait before next check (with smaller intervals to check for keypress)
107 | for ((i=0; i<$INTERVAL*10; i++)); do
108 | check_for_quit
109 | sleep 0.1
110 | done
111 | done
112 |
--------------------------------------------------------------------------------
/src/spec.md:
--------------------------------------------------------------------------------
1 | - no actions visible under the model suggests the following actions
2 | - don't think we need an action xml tag, all of those are kind of actions
3 |
4 |
5 | ### Project Initialization
6 | - Write a `spec.md` for each project to define requirements.
7 | - Use `aider` to generate structured output based on the spec.
8 |
9 | ### Task Management
10 | - Split tasks into small, manageable pieces to increase success rates.
11 | - Use `pytest` for testing and to identify failures.
12 | - Include feedback from test results in prompts for better coordination.
13 |
14 | ### Monitoring and Coordination
15 | - Use `tmux` for session management, starting new sessions with custom endings to avoid conflicts.
16 | - Display and refresh stats constantly for easy monitoring.
17 | - Coordinate with `agent-aider-worktree` without modifying it to preserve its integrity.
18 |
19 | ### Planning and Execution
20 | - Create a plan tree using XML to structure tasks.
21 | - Execute tasks based on the generated plan.
22 | - Track task dependencies and show progress.
23 | - Allow the agent to modify its own plan as needed.
24 | - Enable the agent to indicate task completion or request further input from the user.
25 |
26 | ### User Interaction
27 | - Implement an interactive mode with chat functionality for direct user interaction.
28 | - Require user confirmation for critical actions to ensure control.
29 | - Handle multi-line inputs properly to process pasted content effectively.
30 |
31 | ### Technical Implementation
32 | - Stream and display reasoning with proper formatting for transparency.
33 | - Manage terminal output to preserve history and avoid overwriting existing text.
34 | - Integrate with different models and APIs for flexibility.
35 | - Use `deepseekr1` for reasoning and evaluation of results.
36 | - Handle API overload issues, possibly by using native DeepSeek APIs when necessary.
37 |
38 | ### Code Maintenance
39 | - Keep complexity low to ensure maintainability.
40 | - Refactor code into smaller, independent modules for easier testing and development.
41 | - Fix bugs related to API overload and module imports.
42 | - Avoid modifying existing code like `agent-aider-worktree`; duplicate functionality if needed.
43 |
44 | ### Enhancements
45 | - Include system information (e.g., date, time, timezone) in the context for better awareness.
46 | - Implement multi-step execution to handle complex tasks over multiple interactions.
47 | - Maintain conversation history to provide context for the agent.
48 | - Add a vim-like interface for command navigation (e.g., using `j`, `k` to move through commands).
49 | - Implement persistent memory for the agent to retain and modify information over time.
50 | - Allow the agent to edit files using XML tags (e.g., search and replace with filename specifications).
51 | - Enable the agent to update its plan based on new information learned during execution.
52 |
53 | ### Context Management
54 | - Format input messages to the model in XML to set a consistent example.
55 | - Provide an XML schema for the agent’s responses to clarify expected output structure.
56 | - Exclude reasoning tokens from the model’s context to avoid confusion, as they weren’t part of training.
57 | - Truncate shell command outputs to the last 5000 characters per command to manage context size.
58 | - Log command execution details (e.g., auto-run, confirmed, rejected) for valuable context.
59 |
60 | ### Debugging
61 | - Print the complete message sent to the model for debugging purposes.
62 | - Remove unnecessary status messages (e.g., "generating plan", "end of reasoning") and rely on color-coding.
63 |
64 | ### Alternative Approaches
65 | - Explore using `litellm` with `deepseekr1` as an alternative to `aider` for building the agent.
66 |
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Path Scripts
2 |
3 | A collection of powerful CLI tools for automating development workflows and task management. These scripts are designed to be added to your system PATH for easy command-line access.
4 |
5 | Repository: https://github.com/tom-doerr/path_scripts
6 |
7 | ## Tools Overview
8 |
9 | ### 1. Agent Aider Worktree
10 |
11 | Automates code improvements using AI agents in isolated git worktrees.
12 |
13 | #### Key Features
14 | - Creates isolated git worktrees for safe experimentation
15 | - Runs AI-powered code analysis and improvements
16 | - Automatically merges changes back to main branch
17 | - Handles merge conflicts intelligently
18 | - Test-driven development workflow
19 | - Exponential retry strategy for complex tasks
20 |
21 | #### Installation
22 | ```bash
23 | # Clone and install
24 | git clone https://github.com/tom-doerr/path_scripts.git
25 | cd path_scripts
26 | pip install -r requirements.txt
27 |
28 | # Add to PATH (optional)
29 | ln -s $PWD/agent-aider-worktree ~/.local/bin/
30 | ```
31 |
32 | #### Usage
33 | ```bash
34 | # Basic usage
35 | agent-aider-worktree "Add user authentication"
36 |
37 | # With custom iterations and model
38 | agent-aider-worktree --max-iterations 20 --model claude-3-opus "Refactor database code"
39 |
40 | # From a subdirectory
41 | agent-aider-worktree --exponential-retries "Fix login form validation"
42 | ```
43 |
44 | #### Command Line Options
45 | | Option | Description | Default |
46 | |--------|-------------|---------|
47 | | `--path` | Repository path | Current directory |
48 | | `--model` | AI model to use | deepseek-reasoner |
49 | | `--weak-model` | Secondary model | deepseek |
50 | | `--max-iterations` | Maximum iterations | 10 |
51 | | `--inner-loop` | Inner loop iterations | 10 |
52 | | `--exponential-retries` | Use exponential retry strategy | False |
53 | | `--no-push` | Skip pushing changes | False |
54 | | `--read` | Additional files to analyze | [] |
55 |
56 | ### 2. Aider Multi-Agent System
57 |
58 | Runs multiple AI agents in parallel for distributed code analysis.
59 |
60 | #### Key Features
61 | - Parallel agent execution in tmux sessions
62 | - Configurable models and iterations
63 | - Session persistence and recovery
64 | - Centralized management interface
65 |
66 | #### Usage
67 | ```bash
68 | # Start 3 parallel agents
69 | aider_multi_agent -n 3
70 |
71 | # Use specific model
72 | aider_multi_agent -m claude-3-opus -n 2
73 |
74 | # Kill all sessions
75 | aider_multi_agent -k
76 | ```
77 |
78 | #### Options
79 | | Option | Description | Default |
80 | |--------|-------------|---------|
81 | | `-n SESSIONS` | Number of agents | 1 |
82 | | `-m MODEL` | AI model | r1 |
83 | | `-w WEAK_MODEL` | Secondary model | gemini-2.0-flash-001 |
84 | | `-i ITERATIONS` | Max iterations | 1000 |
85 | | `-k` | Kill all sessions | - |
86 |
87 | ### 3. Task Management (c)
88 |
89 | Streamlined interface for Taskwarrior with smart filtering.
90 |
91 | #### Installation
92 | ```bash
93 | # Install to PATH
94 | cp c ~/.local/bin/
95 | chmod +x ~/.local/bin/c
96 |
97 | # Configure task script path
98 | export TASK_SCRIPT_PATH="/path/to/show_tw_tasks.py"
99 | ```
100 |
101 | #### Usage
102 | ```bash
103 | # Show pending tasks
104 | c
105 |
106 | # Filter by tag and report
107 | c "+work" "active"
108 |
109 | # Custom filters
110 | c -n "+project:home" "next"
111 | ```
112 |
113 | ## Dependencies
114 |
115 | - Python 3.8+
116 | - Git 2.25+
117 | - tmux
118 | - Taskwarrior (for task management)
119 | - Required Python packages in requirements.txt
120 |
121 | ## Contributing
122 |
123 | 1. Fork the repository
124 | 2. Create a feature branch
125 | 3. Submit a pull request
126 |
127 | ## License
128 |
129 | MIT License - See LICENSE file for details
130 |
--------------------------------------------------------------------------------
/tests/test_display.py:
--------------------------------------------------------------------------------
1 | """Tests for display functionality."""
2 |
3 | import sys
4 | from pathlib import Path
5 |
6 | # Add project root to Python path
7 | project_root = str(Path(__file__).parent.parent)
8 | if project_root not in sys.path:
9 | sys.path.insert(0, project_root)
10 |
11 | from rich.console import Console
12 | from src.interface.display import (
13 | get_system_info,
14 | display_welcome,
15 | display_help,
16 | display_models,
17 | display_plan_tree,
18 | display_from_top,
19 | )
20 |
21 |
22 | def test_get_system_info_returns_dict():
23 | """Test get_system_info returns a dictionary with expected keys."""
24 | info = get_system_info()
25 | assert isinstance(info, dict)
26 | assert "platform" in info
27 | assert "python" in info
28 | assert "shell" in info
29 |
30 |
31 | def test_display_welcome_prints(capsys):
32 | """Test display_welcome prints output."""
33 | console = Console()
34 | display_welcome(console)
35 | captured = capsys.readouterr()
36 | assert "Welcome" in captured.out
37 | assert "Agent Interface" in captured.out
38 |
39 |
40 | def test_display_help_prints_commands(capsys):
41 | """Test display_help prints available commands."""
42 | console = Console()
43 | display_help(console)
44 | captured = capsys.readouterr()
45 | assert "/help" in captured.out
46 | assert "/exit" in captured.out
47 |
48 |
49 | def test_display_models_shows_current_model(capsys):
50 | """Test display_models shows current model."""
51 | console = Console()
52 | display_models(None, console) # Pass None as agent since we just test output
53 | captured = capsys.readouterr()
54 | assert "Current model" in captured.out
55 |
56 |
57 | def test_display_plan_tree_handles_empty_xml(capsys):
58 | """Test display_plan_tree handles empty XML gracefully."""
59 | console = Console()
60 | display_plan_tree(console, "")
61 | captured = capsys.readouterr()
62 | assert "No plan" in captured.out or "Error" in captured.out
63 |
64 |
65 | def test_display_from_top_prints_content(capsys):
66 | """Test display_from_top prints content without clearing."""
67 | console = Console()
68 | test_content = "Test output"
69 | display_from_top(console, test_content)
70 | captured = capsys.readouterr()
71 | assert test_content in captured.out
72 |
73 |
74 | def test_display_plan_tree_shows_xml_content(capsys):
75 | """Test display_plan_tree shows XML content."""
76 | console = Console()
77 | test_xml = "Test"
78 | display_plan_tree(console, test_xml)
79 | captured = capsys.readouterr()
80 | assert "Test" in captured.out
81 |
82 |
83 | def test_display_welcome_includes_system_info(capsys):
84 | """Test display_welcome includes system information."""
85 | console = Console()
86 | display_welcome(console)
87 | captured = capsys.readouterr()
88 | info = get_system_info()
89 | assert info["platform"] in captured.out
90 | assert info["python"] in captured.out
91 |
92 |
93 | def test_display_help_includes_all_commands(capsys):
94 | """Test display_help includes all expected commands."""
95 | console = Console()
96 | display_help(console)
97 | captured = capsys.readouterr()
98 | expected_commands = ["/help", "/exit", "/models", "/plan", "/execute"]
99 | for cmd in expected_commands:
100 | assert cmd in captured.out
101 |
102 |
103 | def test_display_models_handles_missing_agent(capsys):
104 | """Test display_models handles missing agent gracefully."""
105 | console = Console()
106 | display_models(None, console)
107 | captured = capsys.readouterr()
108 | assert "Current model" in captured.out
109 |
--------------------------------------------------------------------------------
/tests/test_xml_tools.py:
--------------------------------------------------------------------------------
1 | """Tests for XML tools functionality."""
2 |
3 | from src.utils.xml_tools import ( # pylint: disable=no-name-in-module
4 | extract_xml_from_response,
5 | pretty_format_xml,
6 | validate_xml,
7 | escape_xml_content,
8 | )
9 |
10 | SAMPLE_XML = """
11 | Test content
12 | 123
13 | """
14 |
15 |
16 | def test_extract_xml_simple():
17 | """Test basic XML extraction."""
18 | wrapped_xml = f"Some text before {SAMPLE_XML} some text after"
19 | result = extract_xml_from_response(wrapped_xml, "response")
20 | assert result.strip() == SAMPLE_XML.strip()
21 |
22 |
23 | def test_extract_xml_no_match():
24 | """Test XML extraction when no match exists."""
25 | result = extract_xml_from_response("No XML here", "response")
26 | assert result is None
27 |
28 |
29 | def test_extract_xml_nested_tag():
30 | """Test extraction of nested XML tags."""
31 | nested_xml = (
32 | """BeforeNestedAfter"""
33 | )
34 | result = extract_xml_from_response(nested_xml, "response")
35 | assert "Nested" in result
36 |
37 |
38 | def test_extract_xml_multiple_matches():
39 | """Test extraction returns first match when multiple exist."""
40 | multi_xml = "FirstSecond"
41 | result = extract_xml_from_response(multi_xml, "response")
42 | assert "First" in result and "Second" not in result
43 |
44 |
45 | def test_pretty_format_xml():
46 | """Test XML formatting produces indented output."""
47 | compressed_xml = (
48 | "Test- 1
"
49 | )
50 | formatted = pretty_format_xml(compressed_xml)
51 | # Check indentation
52 | assert formatted == (
53 | "\n"
54 | " \n"
55 | " Test\n"
56 | " \n"
57 | " \n"
58 | " - \n"
59 | " 1\n"
60 | "
\n"
61 | " \n"
62 | ""
63 | )
64 |
65 |
66 | def test_pretty_format_invalid_xml():
67 | """Test formatting handles invalid XML gracefully."""
68 | invalid_xml = "test"
69 | formatted = pretty_format_xml(invalid_xml)
70 | assert invalid_xml in formatted # Should return original string
71 |
72 |
73 | def test_extract_xml_with_whitespace():
74 | """Test XML extraction with surrounding whitespace."""
75 | wrapped_xml = "\n\n \n Content\n \n\n"
76 | result = extract_xml_from_response(wrapped_xml, "response")
77 | assert result.strip() == "\n Content\n "
78 |
79 |
80 | def test_validate_good_xml():
81 | """Test validation of properly formatted XML."""
82 | valid_xml = "text"
83 | assert validate_xml(valid_xml) is True
84 |
85 |
86 | def test_escape_xml_content():
87 | """Test XML content escaping."""
88 | test_cases = [
89 | ('Hello "World" & Co. <3 >', "Hello "World" & Co. <3 >"),
90 | ('content', "<tag>content</tag>"),
91 | ('', ''),
92 | ('No special chars', 'No special chars'),
93 | ('&&&&&', "&&&&&")
94 | ]
95 |
96 | for original, expected in test_cases:
97 | assert escape_xml_content(original) == expected, f"Failed for: {original}"
98 |
99 |
100 | def test_validate_bad_xml():
101 | """Test validation detects malformed XML."""
102 | invalid_xml = "text"
103 | assert validate_xml(invalid_xml) is False
104 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # UV
98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | #uv.lock
102 |
103 | # poetry
104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | # This is especially recommended for binary packages to ensure reproducibility, and is more
106 | # commonly ignored for libraries.
107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 |
110 | # pdm
111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | # in version control.
115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 |
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 |
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 |
127 | # SageMath parsed files
128 | *.sage.py
129 |
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 |
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 |
143 | # Rope project settings
144 | .ropeproject
145 |
146 | # mkdocs documentation
147 | /site
148 |
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 |
154 | # Pyre type checker
155 | .pyre/
156 |
157 | # pytype static type analyzer
158 | .pytype/
159 |
160 | # Cython debug symbols
161 | cython_debug/
162 |
163 | # PyCharm
164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | # and can be added to the global gitignore or merged into this file. For a more nuclear
167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 |
170 | # PyPI configuration file
171 | .pypirc
172 | .aider*
173 |
174 | context.txt
175 | custom_aider_history.md
176 | aider_history.md
--------------------------------------------------------------------------------
/src/utils/feedback.py:
--------------------------------------------------------------------------------
1 | """Feedback mechanisms for the agent, including dopamine rewards."""
2 |
3 | from typing import Optional
4 | import random
5 | from rich.console import Console
6 |
7 |
8 | class DopamineReward:
9 | """Manages dopamine rewards for the agent based on performance."""
10 |
11 | def __init__(self, console: Optional[Console] = None):
12 | self.console = console or Console()
13 | self.dopamine_level = 50.0 # Track as float for more precise calculations
14 | self.history = []
15 |
16 | def generate_reward(self, quality_score: Optional[int] = None) -> str:
17 | """
18 | Generate a dopamine reward message based on performance quality.
19 |
20 | Args:
21 | quality_score: Optional score from 0-100 indicating quality of performance
22 | If None, will use current dopamine level as base
23 |
24 | Returns:
25 | A dopamine reward message
26 |
27 | Examples:
28 | >>> reward = DopamineReward(Console())
29 | >>> reward.generate_reward(95)
30 | '🌟 [bold green]DOPAMINE SURGE![/bold green] Exceptional work!'
31 | >>> reward.generate_reward(65)
32 | '🙂 [blue]DOPAMINE TRICKLE[/blue] Good progress.'
33 | """
34 | if quality_score is None:
35 | # Use current dopamine level with small random variation
36 | quality_score = max(0, min(100, self.dopamine_level + random.randint(-10, 10)))
37 |
38 | self.dopamine_level = max(0, min(100, quality_score)) # Update dopamine level directly
39 |
40 | if quality_score >= 90:
41 | return "🌟 [bold green]DOPAMINE SURGE![/bold green] Exceptional work!"
42 | if quality_score >= 75:
43 | return "😊 [green]DOPAMINE BOOST![/green] Great job!"
44 | if quality_score >= 60:
45 | return "🙂 [blue]DOPAMINE TRICKLE[/blue] Good progress."
46 | if quality_score >= 40:
47 | return "😐 [yellow]DOPAMINE NEUTRAL[/yellow] Acceptable."
48 | if quality_score >= 20:
49 | return "😕 [orange]DOPAMINE DIP[/orange] Could be better."
50 | return "😟 [red]DOPAMINE LOW[/red] Needs improvement."
51 |
52 | def reward_for_xml_response(self, _response: str, observation: str) -> str:
53 | """
54 | Analyze the XML response and observation to determine a reward.
55 | Updates dopamine level based on feedback quality.
56 |
57 | Args:
58 | response: The XML response from the agent
59 | observation: The user's observation/feedback
60 |
61 | Returns:
62 | A dopamine reward message
63 | """
64 | # Simple heuristic based on positive/negative words in observation
65 | positive_words = [
66 | "good", "great", "excellent", "perfect", "nice", "helpful",
67 | "useful", "correct", "right", "well", "thanks", "thank"
68 | ]
69 | negative_words = [
70 | "bad", "wrong", "incorrect", "error", "mistake", "useless",
71 | "unhelpful", "poor", "terrible", "fail", "failed", "not working"
72 | ]
73 |
74 | observation_lower = observation.lower()
75 | positive_count = sum(1 for word in positive_words if word in observation_lower)
76 | negative_count = sum(1 for word in negative_words if word in observation_lower)
77 |
78 | # Calculate score and update dopamine level
79 | if positive_count + negative_count == 0:
80 | return self.generate_reward(None) # Neutral
81 |
82 | score = 100 * positive_count / (positive_count + negative_count)
83 | self._update_dopamine_level(score)
84 | return self.generate_reward(score)
85 |
86 | def _update_dopamine_level(self, score: float):
87 | """Update dopamine level using a moving average with decay factor."""
88 | # Keep last 5 scores for smoothing
89 | self.history = (self.history + [score])[-5:]
90 | # Calculate weighted average with decay
91 | weights = [0.5**i for i in range(len(self.history),0,-1)]
92 | weighted_avg = sum(w*s for w,s in zip(weights, self.history)) / sum(weights)
93 | # Update dopamine level (clamped between 0-100)
94 | self.dopamine_level = max(0, min(100, weighted_avg))
95 | return self.dopamine_level
96 |
97 | def get_current_dopamine_level(self) -> float:
98 | """Get the current dopamine level for prompt optimization."""
99 | return self.dopamine_level
100 |
--------------------------------------------------------------------------------
/src/interface/cli.py:
--------------------------------------------------------------------------------
1 | """
2 | Command-line interface for the agent.
3 | """
4 |
5 | import os
6 | import sys
7 | import datetime
8 | from typing import List, Dict, Any
9 | from rich.console import Console
10 | from rich.panel import Panel
11 | from rich.markdown import Markdown
12 |
13 | from src.agent.core import Agent
14 | from src.interface.display import display_welcome, get_system_info
15 | from src.interface.commands import process_command
16 | from src.interface.input import process_user_input, save_chat_history
17 | from src.interface.input_handler import get_user_input
18 | from src.config import load_config
19 |
20 |
21 | def main():
22 | """Main entry point for the agent CLI."""
23 | # Initialize console
24 | console = Console()
25 |
26 | # Display welcome message
27 | system_info = get_system_info()
28 | display_welcome(console, system_info)
29 |
30 | # Initialize agent
31 | agent = Agent()
32 |
33 | # Load configuration
34 | config = load_config()
35 |
36 | # Set up history
37 | history_dir = os.path.expanduser("~/.config/agent/history")
38 | os.makedirs(history_dir, exist_ok=True)
39 | history_file = os.path.join(history_dir, "chat_history.json")
40 |
41 | # Initialize chat history
42 | chat_history = []
43 | try:
44 | if os.path.exists(history_file):
45 | with open(history_file, "r") as f:
46 | import json
47 |
48 | chat_history = json.load(f)
49 | except Exception as e:
50 | console.print(f"[yellow]Could not load chat history: {e}[/yellow]")
51 |
52 | # Command history for up/down navigation
53 | command_history = []
54 |
55 | # Multiline input mode flag and buffer
56 | multiline_input_mode = False
57 | multiline_input_buffer = []
58 |
59 | # Main input loop
60 | while True:
61 | try:
62 | # Handle multiline input mode
63 | if multiline_input_mode:
64 | line = get_user_input(console, "... ", command_history, config=config)
65 | if line.strip() == "/end":
66 | multiline_input_mode = False
67 | if multiline_input_buffer:
68 | full_input = "\n".join(multiline_input_buffer)
69 | console.print(
70 | f"[dim]Processing {len(multiline_input_buffer)} lines of input...[/dim]"
71 | )
72 | process_user_input(
73 | agent, full_input, chat_history, history_file, console
74 | )
75 | multiline_input_buffer.clear()
76 | else:
77 | console.print("[yellow]No input to process[/yellow]")
78 | else:
79 | multiline_input_buffer.append(line)
80 | continue
81 |
82 | # Get user input
83 | user_input = get_user_input(console, "> ", command_history, config=config)
84 |
85 | # Add to command history if not empty
86 | if user_input.strip() and (
87 | not command_history or user_input != command_history[-1]
88 | ):
89 | command_history.append(user_input)
90 | # Trim history if needed
91 | if len(command_history) > config.get("history_size", 100):
92 | command_history.pop(0)
93 |
94 | # Check for empty input
95 | if not user_input.strip():
96 | continue
97 |
98 | # Check for slash commands
99 | if user_input.startswith("/"):
100 | # Split the command and arguments
101 | parts = user_input[1:].split()
102 | process_command(
103 | agent,
104 | parts,
105 | chat_history,
106 | history_file,
107 | console,
108 | multiline_input_mode,
109 | multiline_input_buffer,
110 | )
111 | else:
112 | # Process as regular input to the model
113 | process_user_input(
114 | agent, user_input, chat_history, history_file, console
115 | )
116 |
117 | except KeyboardInterrupt:
118 | console.print("\n[bold yellow]Operation cancelled by user[/bold yellow]")
119 | continue
120 | except EOFError:
121 | console.print("\n[bold blue]Exiting...[/bold blue]")
122 | break
123 | except Exception as e:
124 | console.print(f"[bold red]Error:[/bold red] {str(e)}")
125 | import traceback
126 |
127 | console.print(traceback.format_exc())
128 |
--------------------------------------------------------------------------------
/src/interface/vim_input.py:
--------------------------------------------------------------------------------
1 | """
2 | Vim-like input interface for the agent CLI using Textual.
3 | """
4 |
5 | from enum import Enum
6 | from typing import List, Dict, Any, Callable, Optional
7 |
8 | from rich.console import Console
9 | from textual.app import App
10 | from textual.widgets import Input
11 | from textual.events import Key
12 |
13 | # Import necessary modules for input processing
14 | from src.interface.display import get_system_info
15 |
16 |
17 | class Mode(Enum):
18 | NORMAL = "normal"
19 | INSERT = "insert"
20 |
21 |
22 | class VimInput(App):
23 | """A Vim-like input widget using Textual."""
24 |
25 | def __init__(
26 | self,
27 | console: Console,
28 | history: List[str] = None,
29 | on_submit: Callable[[str], None] = None,
30 | ):
31 | super().__init__()
32 | self.console = console
33 | self.history = history or []
34 | self.history_index = len(self.history)
35 | self.on_submit = on_submit
36 | self.mode = Mode.INSERT
37 | self.result: Optional[str] = None
38 |
39 | def compose(self):
40 | """Create child widgets."""
41 | self.input = Input(placeholder="Enter command (Esc for normal mode)")
42 | yield self.input
43 |
44 | def on_mount(self):
45 | """Called when app is mounted."""
46 | self.input.focus()
47 |
48 | def on_key(self, event: Key):
49 | """Handle key events."""
50 | # Handle mode switching
51 | if event.key == "escape":
52 | self.mode = Mode.NORMAL
53 | self.input.placeholder = "[Normal Mode] j/k: history, i: insert, :/search"
54 | return
55 |
56 | if self.mode == Mode.NORMAL:
57 | self._handle_normal_mode(event)
58 | # In insert mode, let default handlers work
59 |
60 | def _handle_normal_mode(self, event: Key):
61 | """Handle keys in normal mode."""
62 | key = event.key
63 |
64 | if key == "i":
65 | # Switch to insert mode
66 | self.mode = Mode.INSERT
67 | self.input.placeholder = "Enter command (Esc for normal mode)"
68 |
69 | elif key == "a":
70 | # Append (move cursor to end and switch to insert)
71 | self.mode = Mode.INSERT
72 | self.input.placeholder = "Enter command (Esc for normal mode)"
73 | # Move cursor to end
74 | self.input.cursor_position = len(self.input.value)
75 |
76 | elif key == "0":
77 | # Move to beginning of line
78 | self.input.cursor_position = 0
79 |
80 | elif key == "$":
81 | # Move to end of line
82 | self.input.cursor_position = len(self.input.value)
83 |
84 | elif key == "j":
85 | # Navigate down in history
86 | if self.history and self.history_index < len(self.history) - 1:
87 | self.history_index += 1
88 | self.input.value = self.history[self.history_index]
89 |
90 | elif key == "k":
91 | # Navigate up in history
92 | if self.history and self.history_index > 0:
93 | self.history_index -= 1
94 | self.input.value = self.history[self.history_index]
95 |
96 | elif key == "d":
97 | # dd to clear line
98 | if getattr(self, "_last_key", None) == "d":
99 | self.input.value = ""
100 | self._last_key = "d"
101 | return
102 |
103 | elif key == "x":
104 | # Delete character under cursor
105 | if self.input.value and self.input.cursor_position < len(self.input.value):
106 | pos = self.input.cursor_position
107 | self.input.value = self.input.value[:pos] + self.input.value[pos + 1 :]
108 |
109 | elif key == "enter":
110 | # Submit in normal mode too
111 | self.result = self.input.value
112 | self.exit()
113 |
114 | # Store last key for combinations like dd
115 | self._last_key = key
116 |
117 | def on_input_submitted(self):
118 | """Handle input submission."""
119 | self.result = self.input.value
120 | self.exit()
121 |
122 |
123 | def get_vim_input(
124 | console: Console, history: List[str] = None, on_submit: Callable[[str], None] = None
125 | ) -> str:
126 | """
127 | Display a Vim-like input prompt and return the entered text.
128 |
129 | Args:
130 | console: Rich console instance
131 | history: Command history list
132 | on_submit: Callback for when input is submitted
133 |
134 | Returns:
135 | The entered text
136 | """
137 | app = VimInput(console, history, on_submit)
138 | app.run()
139 | return app.result or ""
140 |
--------------------------------------------------------------------------------
/info.md:
--------------------------------------------------------------------------------
1 | import os
2 | from camel.agents import ChatAgent
3 | from camel.memory import ChatHistoryMemory
4 | from camel.messages import BaseMessage
5 | from camel.models import ModelFactory
6 |
7 | # Step 1: Define a function to read files
8 | def read_file(file_path):
9 | if not os.path.exists(file_path):
10 | raise FileNotFoundError(f"File {file_path} not found")
11 | with open(file_path, 'r') as file:
12 | return file.read()
13 |
14 | # Step 2: Initialize the CAMEL agent with memory
15 | def create_spec_review_agent():
16 | # Configure the memory to retain context
17 | memory = ChatHistoryMemory(window_size=10) # Retains last 10 messages
18 |
19 | # Use OpenAI's GPT-4 as the model (replace with your API key)
20 | model = ModelFactory.create(model_type="openai", model_config={"api_key": "your_openai_api_key"})
21 |
22 | # Initialize the ChatAgent
23 | agent = ChatAgent(model=model, memory=memory)
24 | return agent
25 |
26 | # Step 3: Function to review specs and code
27 | def review_specs_and_code(agent, spec_file_path, code_file_path):
28 | # Read the spec and code files
29 | spec_content = read_file(spec_file_path)
30 | code_content = read_file(code_file_path)
31 |
32 | # Create an instruction message for the agent
33 | instruction = (
34 | "You are a code review assistant. Your task is to:\n"
35 | "1. Review the specifications in the provided spec.md file.\n"
36 | "2. Analyze the code in the provided main.py file.\n"
37 | "3. Identify and list any specifications that are violated by the code.\n"
38 | "Return the result in the format: 'Violations: [list of violated specs]'.\n\n"
39 | f"Here is the spec content:\n{spec_content}\n\n"
40 | f"Here is the code content:\n{code_content}"
41 | )
42 |
43 | # Create a user message
44 | user_msg = BaseMessage.make_user_message(role_name="User", content=instruction)
45 |
46 | # Record the message to memory
47 | agent.record_message(user_msg)
48 |
49 | # Generate a response based on the memory context
50 | response = agent.step()
51 |
52 | # Extract the agent's response content
53 | return response.content
54 |
55 | # Step 4: Main execution
56 | def main():
57 | # File paths (adjust these to your actual file locations)
58 | spec_file_path = "spec.md"
59 | code_file_path = "main.py"
60 |
61 | # Create the agent
62 | agent = create_spec_review_agent()
63 |
64 | # Review specs and code
65 | result = review_specs_and_code(agent, spec_file_path, code_file_path)
66 |
67 | # Print the result
68 | print("Agent's Review Result:")
69 | print(result)
70 |
71 | # Optionally, retrieve the memory context for debugging
72 | context = agent.memory.get_context()
73 | print("\nMemory Context:")
74 | print(context)
75 |
76 | # Example spec.md content (save this as spec.md)
77 | """
78 | # Project Specifications
79 | 1. The function `add_numbers` must take two parameters: `a` and `b`.
80 | 2. The function `add_numbers` must return the sum of `a` and `b`.
81 | 3. The code must include a function called `greet_user` that prints "Hello, User!".
82 | """
83 |
84 | # Example main.py content (save this as main.py)
85 | """
86 | def add_numbers(a, b, c):
87 | return a + b + c
88 |
89 | def say_hello():
90 | print("Hi there!")
91 | """
92 |
93 | if __name__ == "__main__":
94 | main()
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | from camel.agents import ChatAgent
112 | from camel.memory import LongtermAgentMemory, ScoreBasedContextCreator
113 | from camel.storage import InMemoryKeyValueStorage, QdrantStorage
114 |
115 | # Step 1: Initialize storage backends
116 | chat_history_storage = InMemoryKeyValueStorage()
117 | vector_db_storage = QdrantStorage(path="./vector_db", prefer_grpc=True)
118 |
119 | # Step 2: Initialize memory with context creator
120 | memory = LongtermAgentMemory(
121 | context_creator=ScoreBasedContextCreator(),
122 | chat_history_block=chat_history_storage,
123 | vector_db_block=vector_db_storage,
124 | retrieve_limit=3
125 | )
126 |
127 | # Step 3: Initialize the agent with memory
128 | agent = ChatAgent(model="gpt-4", memory=memory)
129 |
130 | # Step 4: Retrieve memory context (using retrieve() for LongtermAgentMemory)
131 | context = agent.memory.retrieve()
132 | print("Combined memory context:", context)
133 |
134 | # Step 5: Record a new message
135 | new_user_msg = BaseMessage.make_user_message(role_name="User", content="What's the weather like today?")
136 | agent.record_message(new_user_msg)
137 |
138 | # Step 6: Retrieve updated context
139 | updated_context = agent.memory.retrieve()
140 | print("Updated combined memory context:", updated_context)
141 |
--------------------------------------------------------------------------------
/src/utils/shell.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """Shell command execution utilities."""
3 |
4 | import subprocess
5 | from typing import Tuple, Dict, Any
6 |
7 |
8 | def execute_command(command: str, cwd: str = None) -> Tuple[bool, Dict[str, Any]]:
9 | """
10 | Execute a shell command and return the result.
11 |
12 | Args:
13 | command: The command to execute
14 | cwd: Working directory for the command
15 |
16 | Returns:
17 | Tuple of (success, result_dict)
18 | """
19 | result = {
20 | "command": command,
21 | "returncode": None,
22 | "stdout": "",
23 | "stderr": "",
24 | "success": False,
25 | }
26 |
27 | try:
28 | process = subprocess.Popen(
29 | command,
30 | shell=True,
31 | stdout=subprocess.PIPE,
32 | stderr=subprocess.PIPE,
33 | text=True,
34 | cwd=cwd,
35 | )
36 |
37 | # Collect output while streaming
38 | output_lines = []
39 |
40 | # Stream output in real-time
41 | for line in process.stdout:
42 | output_lines.append(line.rstrip())
43 |
44 | # Wait for process to complete
45 | process.wait()
46 |
47 | # Get stderr if any
48 | stderr = process.stderr.read()
49 |
50 | # Truncate output to last 5000 characters
51 | full_output = "\n".join(output_lines)
52 | if len(full_output) > 5000:
53 | truncated_output = "... (output truncated) ...\n" + full_output[-5000:]
54 | else:
55 | truncated_output = full_output
56 |
57 | result["stdout"] = truncated_output
58 | result["stderr"] = stderr
59 | result["returncode"] = process.returncode
60 | result["success"] = process.returncode == 0
61 |
62 | return result["success"], result
63 |
64 | except Exception as e:
65 | result["stderr"] = str(e)
66 | return False, result
67 |
68 |
69 | def is_command_safe(command: str) -> bool:
70 | """
71 | Check if a command is safe to run automatically.
72 |
73 | Args:
74 | command: The command to check
75 |
76 | Returns:
77 | True if the command is considered safe, False otherwise
78 | """
79 | # List of dangerous commands or patterns
80 | dangerous_patterns = [
81 | "rm -rf",
82 | "rm -r",
83 | "rmdir",
84 | "dd",
85 | "> /dev/",
86 | "mkfs",
87 | "fdisk",
88 | "format",
89 | "chmod -R",
90 | "chown -R",
91 | ":(){:|:&};:", # Fork bomb
92 | "wget",
93 | "curl",
94 | "> /etc/",
95 | "> ~/.ssh/",
96 | "sudo",
97 | "su",
98 | "shutdown",
99 | "reboot",
100 | "halt",
101 | "mv /* ",
102 | "find / -delete",
103 | ]
104 |
105 | # Check for dangerous patterns
106 | for pattern in dangerous_patterns:
107 | if pattern in command:
108 | return False
109 |
110 | # List of safe commands
111 | safe_commands = [
112 | "ls",
113 | "dir",
114 | "echo",
115 | "cat",
116 | "head",
117 | "tail",
118 | "pwd",
119 | "cd",
120 | "mkdir",
121 | "touch",
122 | "grep",
123 | "find",
124 | "wc",
125 | "sort",
126 | "uniq",
127 | "git status",
128 | "git log",
129 | "git branch",
130 | "git diff",
131 | "python",
132 | "python3",
133 | "pip",
134 | "pip3",
135 | "pytest",
136 | "npm test",
137 | "npm run",
138 | "ps",
139 | "top",
140 | "htop",
141 | "df",
142 | "du",
143 | ]
144 |
145 | # Check if command starts with a safe command
146 | for safe in safe_commands:
147 | if command.startswith(safe):
148 | return True
149 |
150 | # By default, consider commands unsafe
151 | return False
152 |
153 |
154 | if __name__ == "__main__":
155 | # Simple test when run directly
156 | test_commands = [
157 | "echo 'Hello, world!'",
158 | "ls -la",
159 | "rm -rf /", # Should be unsafe
160 | "sudo apt-get update", # Should be unsafe
161 | "git status",
162 | ]
163 |
164 | print("Safety check:")
165 | for cmd in test_commands:
166 | print(f"{cmd}: {'Safe' if is_command_safe(cmd) else 'Unsafe'}")
167 |
168 | print("\nExecution test (safe commands only):")
169 | for cmd in test_commands:
170 | if is_command_safe(cmd):
171 | success, result = execute_command(cmd)
172 | print(f"{cmd}: {'Success' if success else 'Failed'}")
173 | print(f" stdout: {result['stdout'][:50]}...")
174 | if result["stderr"]:
175 | print(f" stderr: {result['stderr']}")
176 |
--------------------------------------------------------------------------------
/src/utils/xml_operations.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """XML operations for the agent."""
3 |
4 | import xml.etree.ElementTree as ET
5 | import xml.dom.minidom as minidom
6 | from typing import Dict, Any, Optional
7 |
8 |
9 | def extract_xml_from_response(response: str, tag_name: str) -> Optional[str]:
10 | """
11 | Extract XML content for a specific tag from the response.
12 |
13 | Args:
14 | response: The response text
15 | tag_name: The tag name to extract
16 |
17 | Returns:
18 | The XML content or None if not found
19 | """
20 | try:
21 | # Look for XML content in the response
22 | start_tag = f"<{tag_name}"
23 | end_tag = f"{tag_name}>"
24 |
25 | start_index = response.find(start_tag)
26 | end_index = response.find(end_tag, start_index) + len(end_tag)
27 |
28 | if start_index != -1 and end_index != -1:
29 | return response[start_index:end_index]
30 | return None
31 | except Exception as e:
32 | print(f"Error extracting XML: {e}")
33 | return None
34 |
35 |
36 | def format_xml_response(content_dict: Dict[str, Any]) -> str:
37 | """
38 | Format various content pieces into an XML response.
39 |
40 | Args:
41 | content_dict: Dictionary of content to format
42 |
43 | Returns:
44 | Formatted XML string
45 | """
46 | root = ET.Element("agent-response")
47 |
48 | for key, value in content_dict.items():
49 | if value is None:
50 | continue
51 |
52 | if (
53 | isinstance(value, str)
54 | and value.strip().startswith("<")
55 | and value.strip().endswith(">")
56 | ):
57 | # This is already XML content, parse it and add as a subtree
58 | try:
59 | # Parse the XML string
60 | element = ET.fromstring(value)
61 | root.append(element)
62 | except ET.ParseError:
63 | # If parsing fails, add as text
64 | child = ET.SubElement(root, key)
65 | child.text = value
66 | else:
67 | # Add as regular element
68 | child = ET.SubElement(root, key)
69 | if isinstance(value, dict):
70 | child.text = str(value)
71 | else:
72 | child.text = str(value)
73 |
74 | # Convert to string with pretty formatting
75 | xml_str = ET.tostring(root, encoding="unicode")
76 |
77 | # Use a custom function to format XML more cleanly
78 | return pretty_format_xml(xml_str)
79 |
80 |
81 | def pretty_format_xml(xml_string: str) -> str:
82 | """
83 | Format XML string in a cleaner way than minidom.
84 |
85 | Args:
86 | xml_string: Raw XML string
87 |
88 | Returns:
89 | Formatted XML string with proper indentation
90 | """
91 | try:
92 | # Parse the XML
93 | root = ET.fromstring(xml_string)
94 |
95 | # Function to recursively format XML
96 | def format_elem(elem, level=0):
97 | indent = " " * level
98 | result = []
99 |
100 | # Add opening tag with attributes
101 | attrs = " ".join([f'{k}="{v}"' for k, v in elem.attrib.items()])
102 | tag_open = f"{indent}<{elem.tag}{' ' + attrs if attrs else ''}>"
103 |
104 | # Check if element has children or text
105 | children = list(elem)
106 | if children or (elem.text and elem.text.strip()):
107 | result.append(tag_open)
108 |
109 | # Add text if present
110 | if elem.text and elem.text.strip():
111 | text_lines = elem.text.strip().split("\n")
112 | if len(text_lines) > 1:
113 | # Multi-line text
114 | result.append("")
115 | for line in text_lines:
116 | result.append(f"{indent} {line}")
117 | result.append("")
118 | else:
119 | # Single line text
120 | result.append(f"{indent} {elem.text.strip()}")
121 |
122 | # Add children
123 | for child in children:
124 | result.extend(format_elem(child, level + 1))
125 |
126 | # Add closing tag
127 | result.append(f"{indent}{elem.tag}>")
128 | else:
129 | # Empty element
130 | result.append(f"{tag_open}{elem.tag}>")
131 |
132 | return result
133 |
134 | # Format the XML
135 | formatted = format_elem(root)
136 | return "\n".join(formatted)
137 |
138 | except ET.ParseError:
139 | # Fallback to minidom if our custom formatter fails
140 | try:
141 | pretty_xml = minidom.parseString(xml_string).toprettyxml(indent=" ")
142 | lines = [line for line in pretty_xml.split("\n") if line.strip()]
143 | return "\n".join(lines)
144 | except:
145 | # If all else fails, return the original string
146 | return xml_string
147 |
--------------------------------------------------------------------------------
/src/utils/file_ops.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """File operation utilities."""
3 |
4 | import os
5 | from typing import Tuple
6 |
7 |
8 | def read_file(path: str) -> Tuple[bool, str]:
9 | """
10 | Read a file and return its contents.
11 |
12 | Args:
13 | path: Path to the file
14 |
15 | Returns:
16 | Tuple of (success, content_or_error_message)
17 | """
18 | try:
19 | if not os.path.exists(path):
20 | return False, f"File not found: {path}"
21 | if not os.path.isfile(path):
22 | return False, f"Path is not a file: {path}"
23 |
24 | with open(path, "r", encoding='utf-8') as f:
25 | file_content = f.read()
26 | return True, file_content
27 | except FileNotFoundError:
28 | return False, f"File not found: {path}"
29 | except PermissionError:
30 | return False, f"Permission denied: {path}"
31 | except Exception as e:
32 | return False, f"Error reading file: {str(e)}"
33 |
34 |
35 | def write_file(
36 | path: str, file_content: str, create_dirs: bool = True
37 | ) -> Tuple[bool, str]:
38 | """
39 | Write content to a file.
40 |
41 | Args:
42 | path: Path to the file
43 | content: Content to write
44 | create_dirs: Whether to create parent directories if they don't exist
45 |
46 | Returns:
47 | Tuple of (success, message)
48 | """
49 | try:
50 | if os.path.exists(path) and not os.path.isfile(path):
51 | return False, f"Path exists but is not a file: {path}"
52 |
53 | # Create parent directories if needed
54 | if create_dirs:
55 | directory = os.path.dirname(path)
56 | if directory and not os.path.exists(directory):
57 | os.makedirs(directory, exist_ok=True)
58 |
59 | with open(path, "w", encoding='utf-8') as f:
60 | f.write(file_content)
61 |
62 | # Make executable if it's a Python file or has no extension
63 | if path.endswith(".py") or not os.path.splitext(path)[1]:
64 | os.chmod(path, 0o755)
65 | return True, f"File written and made executable: {path}"
66 |
67 | return True, f"File written: {path}"
68 | except PermissionError:
69 | return False, f"Permission denied: {path}"
70 | except Exception as e:
71 | return False, f"Error writing file: {str(e)}"
72 |
73 |
74 | def edit_file(path: str, search_text: str, replace_text: str) -> Tuple[bool, str]:
75 | """
76 | Edit a file by replacing text.
77 |
78 | Args:
79 | path: Path to the file
80 | search: Text to search for
81 | replace: Text to replace with
82 |
83 | Returns:
84 | Tuple of (success, message)
85 | """
86 | try:
87 | if not os.path.exists(path):
88 | return False, f"File not found: {path}"
89 | if not os.path.isfile(path):
90 | return False, f"Path is not a file: {path}"
91 |
92 | with open(path, "r", encoding='utf-8') as f:
93 | existing_content = f.read()
94 |
95 | if search_text not in existing_content:
96 | return False, f"Search text not found in {path}"
97 |
98 | new_content = existing_content.replace(search_text, replace_text, 1)
99 |
100 | with open(path, "w", encoding='utf-8') as f:
101 | f.write(new_content)
102 |
103 | return True, f"File edited: {path}"
104 | except PermissionError:
105 | return False, f"Permission denied: {path}"
106 | except Exception as e:
107 | return False, f"Error editing file: {str(e)}"
108 |
109 |
110 | def append_to_file(path: str, text_to_append: str) -> Tuple[bool, str]:
111 | """
112 | Append content to a file.
113 |
114 | Args:
115 | path: Path to the file
116 | content: Content to append
117 |
118 | Returns:
119 | Tuple of (success, message)
120 | """
121 | try:
122 | if not os.path.exists(path):
123 | return False, f"File not found: {path}"
124 | if not os.path.isfile(path):
125 | return False, f"Path is not a file: {path}"
126 |
127 | with open(path, "a", encoding='utf-8') as f:
128 | f.write(text_to_append)
129 |
130 | return True, f"Content appended to: {path}"
131 | except PermissionError:
132 | return False, f"Permission denied: {path}"
133 | except Exception as e:
134 | return False, f"Error appending to file: {str(e)}"
135 |
136 |
137 | if __name__ == "__main__":
138 | # Simple test when run directly
139 | import tempfile
140 |
141 | # Create a temporary file
142 | with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as temp:
143 | temp_path = temp.name
144 |
145 | # Test write
146 | success, msg = write_file(temp_path, "Hello, world!")
147 | print(f"Write: {success} - {msg}")
148 |
149 | # Test read
150 | success, content = read_file(temp_path)
151 | print(f"Read: {success} - {content}")
152 |
153 | # Test edit
154 | success, msg = edit_file(temp_path, "Hello", "Goodbye")
155 | print(f"Edit: {success} - {msg}")
156 |
157 | # Test append
158 | success, msg = append_to_file(temp_path, "\nAppended text")
159 | print(f"Append: {success} - {msg}")
160 |
161 | # Read final content
162 | success, content = read_file(temp_path)
163 | print(f"Final content: {content}")
164 |
165 | # Clean up
166 | os.unlink(temp_path)
167 |
--------------------------------------------------------------------------------
/src/utils/xml_tools.py:
--------------------------------------------------------------------------------
1 | """XML parsing and formatting utilities."""
2 |
3 | import xml.etree.ElementTree as ET
4 | from xml.dom import minidom
5 | import json
6 | from typing import Optional, Dict, Any
7 |
8 |
9 | def extract_xml_from_response(response: str, tag_name: str) -> Optional[str]:
10 | """Extract the first XML section with the specified tag from a response string.
11 |
12 | Args:
13 | response: String containing potential XML content
14 | tag_name: Name of the root XML tag to look for
15 |
16 | Returns:
17 | Extracted XML string or None if not found
18 | """
19 | try:
20 | start_tag = f"<{tag_name}"
21 | end_tag = f"{tag_name}>"
22 |
23 | start_index = response.find(start_tag)
24 | if start_index == -1:
25 | return None
26 |
27 | end_index = response.find(end_tag, start_index)
28 | if end_index == -1:
29 | return None
30 |
31 | return response[start_index : end_index + len(end_tag)]
32 | except Exception:
33 | return None
34 |
35 |
36 | def validate_xml(xml_str: str) -> bool:
37 | """Basic XML validation."""
38 | try:
39 | ET.fromstring(xml_str)
40 | return True
41 | except ET.ParseError:
42 | return False
43 |
44 |
45 | def escape_xml_content(content: str) -> str:
46 | """Escape special XML characters."""
47 | return (
48 | content.replace("&", "&")
49 | .replace("<", "<")
50 | .replace(">", ">")
51 | .replace('"', """)
52 | .replace("'", "'")
53 | )
54 |
55 |
56 | def format_xml_response(content_dict: Dict[str, Any]) -> str:
57 | """
58 | Format various content pieces into an XML response.
59 |
60 | Args:
61 | content_dict: Dictionary of content to format
62 |
63 | Returns:
64 | Formatted XML string
65 | """
66 | root = ET.Element("agent-response")
67 |
68 | for key, value in content_dict.items():
69 | if value is None:
70 | continue
71 |
72 | if (
73 | isinstance(value, str)
74 | and value.strip().startswith("<")
75 | and value.strip().endswith(">")
76 | ):
77 | # This is already XML content, parse it and add as a subtree
78 | try:
79 | # Parse the XML string
80 | element = ET.fromstring(value)
81 | root.append(element)
82 | except ET.ParseError:
83 | # If parsing fails, add as text
84 | child = ET.SubElement(root, key)
85 | child.text = value
86 | else:
87 | # Add as regular element
88 | child = ET.SubElement(root, key)
89 | if isinstance(value, dict):
90 | child.text = json.dumps(value)
91 | else:
92 | child.text = str(value)
93 |
94 | # Convert to string with pretty formatting
95 | xml_str = ET.tostring(root, encoding="unicode")
96 |
97 | # Use a custom function to format XML more cleanly
98 | return pretty_format_xml(xml_str)
99 |
100 |
101 | def pretty_format_xml(xml_string: str) -> str:
102 | """Format XML string with consistent indentation.
103 |
104 | Args:
105 | xml_string: Raw XML string to format
106 |
107 | Returns:
108 | Beautifully formatted XML string. Returns original string if parsing fails.
109 | """
110 | try:
111 | # Parse the XML safely
112 | parser = ET.XMLParser()
113 | root = ET.fromstring(xml_string, parser=parser)
114 |
115 | # Function to recursively format XML
116 | def format_elem(elem, level=0):
117 | indent = " " * level
118 | result = []
119 |
120 | # Add opening tag with attributes
121 | attrs = " ".join([f'{k}="{v}"' for k, v in elem.attrib.items()])
122 | tag_open = f"{indent}<{elem.tag}{' ' + attrs if attrs else ''}>"
123 |
124 | # Check if element has children or text
125 | children = list(elem)
126 | if children or (elem.text and elem.text.strip()):
127 | result.append(tag_open)
128 |
129 | # Add text if present
130 | if elem.text and elem.text.strip():
131 | text_lines = elem.text.strip().split("\n")
132 | if len(text_lines) > 1:
133 | # Multi-line text
134 | result.append("")
135 | for line in text_lines:
136 | result.append(f"{indent} {line}")
137 | result.append("")
138 | else:
139 | # Single line text
140 | result.append(f"{indent} {elem.text.strip()}")
141 |
142 | # Add children
143 | for child in children:
144 | result.extend(format_elem(child, level + 1))
145 |
146 | # Add closing tag
147 | result.append(f"{indent}{elem.tag}>")
148 | else:
149 | # Empty element
150 | result.append(f"{tag_open}{elem.tag}>")
151 |
152 | return result
153 |
154 | # Format the XML
155 | formatted = format_elem(root)
156 | return "\n".join(formatted)
157 |
158 | except ET.ParseError:
159 | # Fallback to minidom if our custom formatter fails
160 | try:
161 |
162 | pretty_xml = minidom.parseString(xml_string).toprettyxml(indent=" ")
163 | lines = [line for line in pretty_xml.split("\n") if line.strip()]
164 | return "\n".join(lines)
165 | except Exception:
166 | # If all else fails, return the original string
167 | return xml_string
168 |
169 |
170 | if __name__ == "__main__":
171 | # Simple test when run directly
172 | test_xml = """Text"""
173 | print(pretty_format_xml(test_xml))
174 |
--------------------------------------------------------------------------------
/aider_multi_agent:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Define prompts
4 | PROMPTS=(
5 | "please look at context.txt. are there issues? if so what are they? where do they occur and why do they occur? please go through each issue and update the report.md and make sure we have for each issue documented why it occurs, how we can fix it or what pytest tests we could add to find out why it occurs"
6 | "please go through each line in the spec and check if the code fulfills the requirement and why it does or does not fulfill it. document it in detail in the report.md"
7 | "are there features in the codebase that are not part of the specs in spec.md? what are they? can we remove them? should we remove them? how much additional code do they cause? do they add a lot of complexity? are they really useful and would be appreciated? please document them in report.md"
8 | "consider the current code architecture. could it be restructured someway? what are alternatives? what are alternatives that would be easy to implement? can we restructure it in a way that would reduce total lines of code? can we restructure it in a way that reduces complexity or makes it easier to work with in general? those do not need to be large things, even tiny changes can have an impact. please add report.md with your findings"
9 | "please look through the content in report.md. what are the highest priority items in there? can you identify dependencies between tasks? would implementing some of those tasks make other tasks easier? please update the report with a list of tasks in a markdown table with each task containing task description, task-id, priority(1-9), the tasks it depends on, the tasks that depend on it (just enter the task ids for those two in the table). do not make the task list ordered since reprioritizing tasks is harder if reordering is required as well."
10 | "please go through report.md and refactor it. are there items that should be grouped together? should some sections be restructered?"
11 | "please go through report.md and check if there are duplicate section or if there is duplicate content. please remove any duplication"
12 | "please go through report.md and create a plan for how we can takle the highest priority items. please update the report.md with your plan, do not implement it just yet"
13 | "please go through report.md and work on the highest priority tasks"
14 | "please go through report.md and check if the plan was successfully implemented. remove the plan if it was completed and mark the tasks as done. if there are issues with the implementation update the report.md accordingly"
15 | )
16 |
17 | show_help() {
18 | echo "Usage: aider_multi_agent [options]"
19 | echo
20 | echo "Options:"
21 | echo " -h, --help Show this help message and exit"
22 | echo " -i ITERATIONS Number of iterations to run (default: 1000)"
23 | echo " -m MODEL Model to use (default: r1)"
24 | echo " -w WEAK_MODEL Weak model to use (default: gemini-2.0-flash-001)"
25 | echo " -n SESSIONS Number of tmux sessions to create (default: 1)"
26 | echo " -k Kill all running aider_multi_agent tmux sessions"
27 | echo
28 | echo "This script runs multiple aider agents through a series of prompts to analyze and improve code."
29 | echo
30 | echo "To start multiple sessions:"
31 | echo " aider_multi_agent -n 3 # Starts 3 vertical splits"
32 | echo
33 | echo "To stop all sessions:"
34 | echo " aider_multi_agent -k"
35 | }
36 |
37 | # Main script
38 | if [[ "$1" == "-h" || "$1" == "--help" ]]; then
39 | show_help
40 | exit 0
41 | fi
42 |
43 | # Parse arguments
44 | ITERATIONS=1000
45 | MODEL="r1"
46 | WEAK_MODEL="gemini-2.0-flash-001"
47 | SESSIONS=1
48 | KILL_MODE=false
49 |
50 | while getopts "i:m:w:n:kh" opt; do
51 | case $opt in
52 | i) ITERATIONS=$OPTARG ;;
53 | m) MODEL=$OPTARG ;;
54 | w) WEAK_MODEL=$OPTARG ;;
55 | n) SESSIONS=$OPTARG ;;
56 | k) KILL_MODE=true ;;
57 | h) show_help; exit 0 ;;
58 | *) show_help; exit 1 ;;
59 | esac
60 | done
61 |
62 | # Kill all sessions if -k flag is set
63 | if $KILL_MODE; then
64 | echo "Killing all aider_multi_agent sessions..."
65 | tmux list-sessions -F '#{session_name}' | grep '^aider_multi_agent_' | while read -r session; do
66 | tmux kill-session -t "$session"
67 | done
68 | exit 0
69 | fi
70 |
71 | # Create tmux session
72 | SESSION_NAME="aider_multi_agent_$(date +%s)"
73 | CUSTOM_HISTORY_FILE=custom_aider_history.md
74 |
75 | # Create new tmux session with horizontal splits
76 | if [ "$SESSIONS" -gt 1 ]; then
77 | echo "Creating $SESSIONS tmux sessions..."
78 | # Get current working directory
79 | CURRENT_DIR=$(pwd)
80 |
81 | # Create initial session with first window
82 | tmux new-session -d -s "$SESSION_NAME" -n "Session 1" "cd '$CURRENT_DIR' && ./$0 -i $ITERATIONS -m $MODEL -w $WEAK_MODEL"
83 |
84 | # Create additional windows
85 | for ((s=2; s<=SESSIONS; s++)); do
86 | # Create new window
87 | tmux new-window -t "$SESSION_NAME" -n "Session $s" "cd '$CURRENT_DIR' && ./$0 -i $ITERATIONS -m $MODEL -w $WEAK_MODEL"
88 | done
89 |
90 | # Split each window horizontally after brief delay
91 | for ((s=1; s<=SESSIONS; s++)); do
92 | tmux split-window -h -t "$SESSION_NAME:$s" "sleep 0.1; cd '$CURRENT_DIR' && ./$0 -i $ITERATIONS -m $MODEL -w $WEAK_MODEL"
93 | tmux select-layout -t "$SESSION_NAME:$s" even-horizontal
94 | sleep 0.1 # Allow time for window creation
95 | done
96 |
97 | # Select first window and attach
98 | tmux select-window -t "$SESSION_NAME:1"
99 | tmux attach-session -t "$SESSION_NAME"
100 | exit 0
101 | fi
102 |
103 | # Normal execution for single session
104 | for ((i=1; i<=ITERATIONS; i++)); do
105 | echo "Iteration $i"' ========================= '$(date)' ============================'
106 |
107 | for prompt in "${PROMPTS[@]}"; do
108 | rm -f $CUSTOM_HISTORY_FILE
109 | for run in {1..3}; do
110 | aider --yes-always \
111 | --read "${PWD}/plex.md" \
112 | --read "${HOME}/git/dotfiles/instruction.md" \
113 | --read "${PWD}/spec.md" \
114 | --read "${PWD}/context.txt" \
115 | --edit-format diff \
116 | --model "openrouter/deepseek/deepseek-r1" \
117 | --no-show-model-warnings \
118 | --weak-model $WEAK_MODEL \
119 | --architect \
120 | --chat-history-file $CUSTOM_HISTORY_FILE \
121 | --restore-chat-history \
122 | report.md **/*py \
123 | --message "$prompt"
124 | done
125 | done
126 |
127 | sleep 1
128 | done
129 |
--------------------------------------------------------------------------------
/project_agent:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Unified project maintenance agent
4 | # Combines task list management, reporting, and development tasks
5 |
6 | typeset -A CONFIG=(
7 | MODEL "r1"
8 | MAX_ITERATIONS 1000
9 | BASE_SLEEP 1
10 | TASK_SLEEP 60
11 | REPORT_SLEEP 60
12 | MODE "all"
13 | EDIT_FORMAT "diff"
14 | INSTANCES 1
15 | )
16 |
17 | # Define a function to build AIDER_ARGS after parsing arguments
18 | build_aider_args() {
19 | AIDER_ARGS=(
20 | --read spec.md
21 | --read issues.txt
22 | --model ${CONFIG[MODEL]}
23 | --no-auto-lint
24 | --no-auto-test
25 | --edit-format ${CONFIG[EDIT_FORMAT]}
26 | --no-browser
27 | --no-suggest-shell-commands
28 | --no-detect-urls
29 | --subtree-only
30 | --architect
31 | --yes-always
32 | )
33 | }
34 |
35 | update_task_list() {
36 | echo "\n=== Updating Task List ==="
37 | local worker_msg=""
38 | [[ -n "${CONFIG[WORKER_ID]}" ]] && worker_msg=" You are worker ${CONFIG[WORKER_ID]}."
39 |
40 | aider ${AIDER_ARGS} task_list.md --message \
41 | "Review project and update task_list.md with prioritized, non-duplicated tasks.$worker_msg"
42 | sleep ${CONFIG[TASK_SLEEP]}
43 | }
44 |
45 | update_project_report() {
46 | echo "\n=== Updating Project Report ==="
47 | local worker_msg=""
48 | [[ -n "${CONFIG[WORKER_ID]}" ]] && worker_msg=" You are worker ${CONFIG[WORKER_ID]}."
49 |
50 | aider ${AIDER_ARGS} project_report.md --message \
51 | "Update project_report.md with current status, critical path, architecture issues, simplification opportunities. No long-term planning. Ensure accuracy and no duplication.$worker_msg"
52 | sleep ${CONFIG[REPORT_SLEEP]}
53 | }
54 |
55 | handle_development_tasks() {
56 | echo "\n=== Handling Development Tasks ==="
57 | local worker_msg=""
58 | [[ -n "${CONFIG[WORKER_ID]}" ]] && worker_msg=" You are worker ${CONFIG[WORKER_ID]}."
59 |
60 | aider ${AIDER_ARGS} --auto-test --message \
61 | "Review spec.md and issues.txt and work on the highest priority items. Only work on pylint issues if the code is rated below 9. If you don't work on linting, go through spec.md. Quote each requirement and tell me if the code fulfills it. If it doesn't fulfill it, create one or more edit blocks to fix the issue before moving on to the next spec requirement. $worker_msg"
62 | sleep ${CONFIG[BASE_SLEEP]}
63 | }
64 |
65 | parse_arguments() {
66 | while [[ $# -gt 0 ]]; do
67 | case $1 in
68 | --tasks)
69 | CONFIG[MODE]="tasks"
70 | ;;
71 | --report)
72 | CONFIG[MODE]="report"
73 | ;;
74 | --dev)
75 | CONFIG[MODE]="dev"
76 | ;;
77 | --all)
78 | CONFIG[MODE]="all"
79 | ;;
80 | -i|--iterations)
81 | CONFIG[MAX_ITERATIONS]=$2
82 | shift
83 | ;;
84 | --edit-format)
85 | CONFIG[EDIT_FORMAT]="$2"
86 | shift
87 | ;;
88 | -n|--instances)
89 | CONFIG[INSTANCES]=$2
90 | shift
91 | ;;
92 | --worker-id)
93 | CONFIG[WORKER_ID]=$2
94 | shift
95 | ;;
96 | --model)
97 | CONFIG[MODEL]="$2"
98 | shift
99 | ;;
100 | --help)
101 | help
102 | exit 0
103 | ;;
104 | *)
105 | echo "Unknown option: $1"
106 | help
107 | exit 1
108 | ;;
109 | esac
110 | shift
111 | done
112 | }
113 |
114 | run_cycle() {
115 | local cycle=$1
116 | echo "\n\n=== Cycle $cycle/${CONFIG[MAX_ITERATIONS]} ==="
117 |
118 | case $CONFIG[MODE] in
119 | "all")
120 | if (( cycle % 60 == 0 )); then update_task_list; fi
121 | if (( cycle % 60 == 30 )); then update_project_report; fi
122 | handle_development_tasks
123 | ;;
124 | "tasks")
125 | update_task_list
126 | ;;
127 | "report")
128 | update_project_report
129 | ;;
130 | "dev")
131 | handle_development_tasks
132 | ;;
133 | esac
134 | }
135 |
136 | help() {
137 | echo "Project Maintenance Agent - Unified Development Orchestrator"
138 | echo "Version: 1.2.0 | License: MIT | Model: ${CONFIG[MODEL]}"
139 | echo "Usage: ./project_agent [OPTIONS]"
140 |
141 | echo "\nOPERATIONAL MODES:"
142 | echo " --tasks Focus only on task list maintenance (task_list.md)"
143 | echo " --report Generate project status reports (project_report.md)"
144 | echo " --dev Execute development tasks only (code changes)"
145 | echo " --all Full operational mode (default)"
146 |
147 | echo "\nOPTIONS:"
148 | echo " -i, --iterations Set execution cycles (default: ${CONFIG[MAX_ITERATIONS]})"
149 | echo " --edit-format (diff|whole) Edit format (default: ${CONFIG[EDIT_FORMAT]})"
150 | echo " -n, --instances Run N parallel background instances (default: ${CONFIG[INSTANCES]})"
151 | echo " --model Set the AI model to use (default: ${CONFIG[MODEL]})"
152 | echo " --help Show this help menu"
153 |
154 | echo "\nCONFIGURATION DEFAULTS:"
155 | echo " Max Iterations: ${CONFIG[MAX_ITERATIONS]}"
156 | echo " Base Sleep: ${CONFIG[BASE_SLEEP]}s"
157 | echo " Task Sleep: ${CONFIG[TASK_SLEEP]}s"
158 | echo " Report Sleep: ${CONFIG[REPORT_SLEEP]}s"
159 |
160 | echo "\nEXAMPLES:"
161 | echo " ./project_agent --tasks -i 5 # Refresh task list 5 times"
162 | echo " ./project_agent --report # Generate status report"
163 | echo " ./project_agent --dev # Continuous development mode"
164 | echo " ./project_agent --all -i 100 # Full operation for 100 cycles"
165 | echo " ./project_agent --model gpt-4 # Use GPT-4 model"
166 |
167 | echo "\nNOTES:"
168 | echo " - Task priorities update automatically based on project state"
169 | echo " - Reports include architecture analysis and simplification opportunities"
170 | echo " - Development mode prefers isolated, low-complexity changes"
171 | echo " - Use 'aider --help' for details on the underlying AI agent"
172 | }
173 |
174 | launch_instance() {
175 | local instance_id=$1
176 | # Use direct command execution instead of nested instance management
177 | kitty --title "Agent $instance_id" zsh -ic "project_agent --${CONFIG[MODE]} --worker-id $instance_id --model ${CONFIG[MODEL]} || echo 'Agent failed - press enter to exit'; read" &
178 | }
179 |
180 | main() {
181 | parse_arguments "$@"
182 | build_aider_args
183 |
184 | if [[ ${CONFIG[INSTANCES]} -gt 1 ]]; then
185 | for ((instance=1; instance<=${CONFIG[INSTANCES]}; instance++)); do
186 | # Set worker ID for each instance
187 | launch_instance $instance "$@"
188 | done
189 | exit 0
190 | fi
191 |
192 | for ((i=1; i<=${CONFIG[MAX_ITERATIONS]}; i++)); do
193 | run_cycle $i
194 | done
195 | }
196 |
197 | main "$@"
198 | sleep 3
199 |
--------------------------------------------------------------------------------
/src/agent/core.py:
--------------------------------------------------------------------------------
1 | """Core agent functionality."""
2 |
3 | import threading
4 | import shutil
5 | from typing import Dict, Optional, Callable, Any, List, Tuple
6 | import litellm
7 | from rich.console import Console
8 | from .plan import (
9 | generate_plan,
10 | update_plan,
11 | check_dependencies,
12 | apply_plan_updates,
13 | )
14 | from .task import execute_task
15 | from ..utils.xml_tools import format_xml_response
16 |
17 |
18 | class Agent:
19 | """Main agent class for handling model interactions and reasoning."""
20 |
21 | def __init__(self, model_name: str = "openrouter/deepseek/deepseek-r1"):
22 | self.console = Console()
23 | self.model_name = model_name
24 | self.plan_tree = None
25 | self.plan_lock = threading.Lock() # Thread safety for plan_tree access
26 | self.repository_info: Dict[str, Any] = {}
27 | self.config = {
28 | "stream_reasoning": True,
29 | "verbose": True,
30 | "rate_limit": 5, # Requests per minute
31 | }
32 | self.stream_callback: Optional[Callable[[str, bool], None]] = None
33 |
34 | def initialize(self, repo_path: str = ".") -> None:
35 | """Initialize the agent with repository information"""
36 |
37 | self.repository_info = {"path": repo_path}
38 | print(f"Agent initialized for repository: {repo_path}")
39 |
40 | def _get_terminal_height(self) -> int:
41 | """Get terminal height using shutil"""
42 | try:
43 | return shutil.get_terminal_size().lines
44 | except Exception:
45 | return 40 # Fallback default
46 |
47 | def stream_reasoning(self, prompt: str) -> str:
48 | """Stream the reasoning process from the model and return the final response"""
49 | messages = [{"role": "user", "content": prompt}]
50 |
51 | # Get terminal height and add that many newlines to preserve history
52 | terminal_height = self._get_terminal_height()
53 | print("\n" * terminal_height)
54 |
55 | if not self.config["stream_reasoning"]:
56 | # Non-streaming mode
57 | try:
58 | response = litellm.completion(
59 | model=self.model_name,
60 | messages=messages,
61 | timeout=60, # Add timeout to prevent hanging
62 | )
63 | return response.choices[0].message.content
64 | except Exception as e:
65 | print(f"Error in non-streaming mode: {e}")
66 | return f"Error: {str(e)}"
67 |
68 | # Streaming mode
69 | full_response = ""
70 | reasoning_output = ""
71 |
72 | try:
73 | response = litellm.completion(
74 | model=self.model_name, messages=messages, stream=True, timeout=60
75 | )
76 |
77 | for chunk in response:
78 | self.handle_response_content(chunk, full_response, reasoning_output)
79 |
80 | self.finalize_response(reasoning_output)
81 |
82 | return full_response
83 |
84 | except KeyboardInterrupt:
85 | print("\n\nOperation cancelled by user")
86 | return full_response
87 | except Exception as e:
88 | print(f"\nError during streaming: {e}")
89 | return full_response or f"Error: {str(e)}"
90 |
91 | def handle_response_content(self, chunk, full_response: str, reasoning_output: str) -> None:
92 | """Handle different types of response content."""
93 | if hasattr(chunk.choices[0].delta, "content"):
94 | self.handle_regular_content(chunk, full_response)
95 | elif hasattr(chunk.choices[0].delta, "reasoning_content"):
96 | self.handle_reasoning_content(chunk, reasoning_output)
97 |
98 | def handle_regular_content(self, chunk, full_response: str) -> None:
99 | """Process regular content chunks."""
100 | content = chunk.choices[0].delta.content
101 | if content:
102 | clean_content = content.replace("\r", "").replace("\b", "")
103 | if self.stream_callback and callable(self.stream_callback):
104 | self.stream_callback(clean_content, False) # pylint: disable=not-callable
105 | else:
106 | print(clean_content, end="", flush=True)
107 | full_response += clean_content
108 |
109 | def handle_reasoning_content(self, chunk, reasoning_output: str) -> None:
110 | """Process reasoning content chunks."""
111 | reasoning = chunk.choices[0].delta.reasoning_content
112 | if reasoning:
113 | clean_reasoning = reasoning.replace("\r", "").replace("\b", "")
114 | if self.stream_callback and callable(self.stream_callback):
115 | self.stream_callback(clean_reasoning, True) # pylint: disable=not-callable
116 | else:
117 | self.console.print(f"[yellow]{clean_reasoning}[/yellow]",
118 | end="", highlight=False)
119 | reasoning_output += clean_reasoning
120 |
121 | def finalize_response(self, reasoning_output: str) -> None:
122 | """Save reasoning output to file."""
123 | if reasoning_output:
124 | try:
125 | with open("last_reasoning.txt", "w") as f:
126 | f.write(reasoning_output)
127 | except Exception as e:
128 | print(f"Warning: Could not save reasoning to file: {e}")
129 |
130 | # Plan management methods
131 | def generate_plan(self, spec: str) -> str:
132 | """Generate a plan tree based on the specification"""
133 | return generate_plan(self, spec)
134 |
135 | def update_plan(
136 | self,
137 | task_id: str,
138 | new_status: str,
139 | notes: Optional[str] = None,
140 | progress: Optional[str] = None,
141 | ) -> str:
142 | """Update the status of a task in the plan"""
143 | return update_plan(self, task_id, new_status, notes, progress)
144 |
145 | def display_plan_tree(self) -> str:
146 | """Display the current plan tree"""
147 | if not self.plan_tree:
148 | return format_xml_response({"error": "No plan exists"})
149 | return format_xml_response({"plan": self.plan_tree})
150 |
151 | def apply_plan_updates(self, plan_update_xml: str) -> None:
152 | """Apply updates to the plan tree based on the plan_update XML"""
153 | apply_plan_updates(self, plan_update_xml)
154 |
155 | def check_dependencies(self, task_id: str) -> Tuple[bool, List[str]]:
156 | """Check if all dependencies for a task are completed"""
157 | return check_dependencies(self, task_id)
158 |
159 | def execute_task(self, task_id: str) -> str:
160 | """Execute a specific task from the plan"""
161 | return execute_task(self, task_id)
162 |
--------------------------------------------------------------------------------
/src/task_list.md:
--------------------------------------------------------------------------------
1 | # Agent Development Task List
2 |
3 | ## High Priority Tasks
4 |
5 | 1. **Implement Direct Model Chat** - Enhance the chat interface to allow direct interaction with the model.
6 | - Priority: HIGH
7 | - Status: Completed
8 | - Notes: Successfully implemented direct model chat functionality
9 |
10 | 2. **Fix Command Line Interface** - Make the agent command work globally.
11 | - Priority: HIGH
12 | - Status: Completed
13 | - Notes: Fixed setup.py to use entry_points instead of scripts for better compatibility
14 |
15 | ## Medium Priority Tasks
16 |
17 | 1. **Allow Agent to Modify Its Own Plan** - Enable the agent to update its plan based on new information.
18 | - Priority: HIGH
19 | - Status: Basic implementation
20 | - Notes: Need to improve the plan modification logic and validation
21 |
22 | 2. **Show Progress as Tasks are Completed** - Better visual feedback for task completion.
23 | - Priority: MEDIUM
24 | - Status: Basic implementation exists
25 | - Notes: Add progress bars and better status indicators
26 |
27 | 3. **Improve Memory Management** - Enhance the persistent memory system with better organization.
28 | - Priority: MEDIUM
29 | - Status: Not started
30 | - Notes: Add categorization, tagging, and priority levels to memory items
31 |
32 | 4. **Add Model Selection UI** - Create a better UI for selecting and switching between models.
33 | - Priority: MEDIUM
34 | - Status: Not started
35 | - Notes: Should show available models and allow easy switching
36 |
37 | ## Low Priority Tasks
38 |
39 | 1. **Implement Parallel Task Execution** - Allow multiple non-dependent tasks to be executed in parallel.
40 | - Priority: LOW
41 | - Status: Not started
42 | - Notes: Will require significant refactoring of execution logic
43 |
44 | 2. **Add Export/Import Functionality** - Allow plans to be exported and imported in different formats.
45 | - Priority: LOW
46 | - Status: Not started
47 | - Notes: Consider supporting JSON, YAML, and Markdown formats
48 |
49 | 3. **Add Vim-like Interface** - Implement vim-like navigation and editing in the interface.
50 | - Priority: LOW
51 | - Status: Mostly complete
52 | - Notes: Implemented mode switching (normal/insert), history navigation with j/k, cursor movement (0, $), and text manipulation commands (dd, x). Added configuration option to enable/disable.
53 |
54 | 4. **Add Textual UI Integration** - Consider integrating with Textual for a more advanced TUI.
55 | - Priority: LOW
56 | - Status: Not started
57 | - Notes: Would provide better layout and interaction capabilities
58 |
59 | 5. **Refactor Code for Maintainability** - Improve code organization and reduce complexity.
60 | - Priority: MEDIUM
61 | - Status: In progress
62 | - Notes: Added configuration management, improved input handling, and enhanced Vim interface
63 |
64 | 6. **Add Command Aliases** - Allow users to define custom aliases for common commands.
65 | - Priority: LOW
66 | - Status: Not started
67 | - Notes: Would improve user experience for frequent users
68 |
69 | 7. **Implement Undo Functionality** - Allow undoing actions and edits.
70 | - Priority: LOW
71 | - Status: Not started
72 | - Notes: Would require tracking action history and implementing reverse operations
73 |
74 | ## Completed Tasks
75 |
76 | 1. **Multi-step Execution** - Allow the agent to run for multiple steps with context preservation.
77 | - Priority: HIGH
78 | - Status: Completed
79 | - Notes: Implemented ability to continue execution with command output context
80 |
81 | 2. **Standardize XML Input Schema** - Create a consistent schema for input messages to the model.
82 | - Priority: HIGH
83 | - Status: Completed
84 | - Notes: Created input_schema.py with standardized format for all model interactions
85 |
86 | 3. **XML-Formatted Prompts** - Make all prompts to the model fully XML-formatted.
87 | - Priority: HIGH
88 | - Status: Completed
89 | - Notes: Restructured all prompts to use consistent XML formatting for better model understanding
90 |
91 | 2. **File Editing Functionality** - Add ability to edit files using search/replace.
92 | - Priority: HIGH
93 | - Status: Completed
94 | - Notes: Implemented file editing with search/replace functionality
95 |
96 | 3. **Add System Information Display** - Show relevant system information in the interface.
97 | - Priority: MEDIUM
98 | - Status: Completed
99 | - Notes: Added platform, Python version, and shell information to the welcome screen
100 |
101 | 4. **Add File Editing Confirmation** - Add confirmation for each file edit before execution.
102 | - Priority: HIGH
103 | - Status: Completed
104 | - Notes: Added confirmation dialog showing diff before applying changes
105 |
106 | 4. **Generate Output Without Clearing Terminal** - Ensure output preserves terminal history.
107 | - Priority: HIGH
108 | - Status: Completed
109 | - Notes: Removed terminal clearing functionality to preserve all previous output
110 |
111 | 5. **Fix System Info Display** - Ensure system information is properly displayed without errors.
112 | - Priority: HIGH
113 | - Status: Completed
114 | - Notes: Added error handling for system information retrieval
115 |
116 | 6. **Improve Multiline Input Handling** - Better handling of pasted multiline content.
117 | - Priority: MEDIUM
118 | - Status: Completed
119 | - Notes: Added dedicated paste mode and improved multiline detection
120 |
121 | 7. **Add Task Dependency Tracking** - Improve tracking of task dependencies and status updates.
122 | - Priority: HIGH
123 | - Status: Completed
124 | - Notes: Enhanced dependency resolution and automatic status updates
125 |
126 | 8. **Add Structured XML Input Format** - Allow users to send structured XML input to the agent.
127 | - Priority: HIGH
128 | - Status: Completed
129 | - Notes: Users can now send XML-formatted messages that match the response format
130 |
131 | 9. **Add Persistent Memory** - Give the agent ability to maintain and update persistent memory.
132 | - Priority: HIGH
133 | - Status: Completed
134 | - Notes: Agent can now store and update information across sessions
135 |
136 | 10. **Add Execution Status Tracking** - Allow the agent to indicate if a task is complete or needs user input.
137 | - Priority: MEDIUM
138 | - Status: Completed
139 | - Notes: Added execution_status XML tag to indicate completion status and user input needs
140 |
141 | 11. **Print Full Model Messages** - Show the complete messages being sent to the model.
142 | - Priority: MEDIUM
143 | - Status: Completed
144 | - Notes: Added display of full prompts for better debugging and transparency
145 |
146 | 12. **Preserve Terminal History** - Ensure terminal history is preserved when scrolling up.
147 | - Priority: MEDIUM
148 | - Status: Completed
149 | - Notes: Added newlines to preserve history when generating new output
150 |
--------------------------------------------------------------------------------
/src/interface/interface.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Make sure this file is executable with: chmod +x interface.py
3 |
4 | import sys
5 | import threading
6 | import shutil
7 | from typing import List, Dict, Any, Tuple, Optional
8 | import xml.etree.ElementTree as ET
9 | from rich.console import Console
10 | from rich.prompt import Prompt
11 |
12 | # Import other interface modules
13 | from src.interface.commands import process_command
14 | from src.interface.display import display_welcome, get_system_info
15 | from src.interface.actions import (
16 | execute_action,
17 | execute_file_edit,
18 | execute_shell_command,
19 | )
20 | from src.interface.input import process_user_input, save_chat_history
21 | from src.agent.core import Agent
22 |
23 |
24 | class AgentInterface:
25 | def __init__(self):
26 | self.console = Console()
27 | from src.config import load_config
28 |
29 | config = load_config()
30 | model_name = config.get("default_model", "openrouter/deepseek/deepseek-r1")
31 | self.agent = Agent(model_name=model_name)
32 | self.current_plan = None
33 | self.model_aliases = {
34 | "flash": "openrouter/google/gemini-2.0-flash-001",
35 | "r1": "deepseek/deepseek-reasoner",
36 | "claude": "openrouter/anthropic/claude-3.7-sonnet",
37 | }
38 | self.chat_history = []
39 | self.history_file = "chat_history.json"
40 | self.system_info = get_system_info()
41 | self.multiline_input_buffer = []
42 | self.multiline_input_mode = False
43 | self.load_chat_history()
44 |
45 | def load_chat_history(self):
46 | """Load chat history from file if it exists"""
47 | try:
48 | if os.path.exists(self.history_file):
49 | with open(self.history_file, "r") as f:
50 | self.chat_history = json.load(f)
51 | self.console.print(
52 | f"[dim]Loaded {len(self.chat_history)} previous messages[/dim]"
53 | )
54 | except Exception as e:
55 | self.console.print(f"[dim]Could not load chat history: {e}[/dim]")
56 | self.chat_history = []
57 |
58 | def save_chat_history(self):
59 | """Save chat history to file"""
60 | from src.interface.input import save_chat_history
61 |
62 | save_chat_history(self.chat_history, self.history_file)
63 |
64 | # These methods have been moved to the input module
65 |
66 | def _get_terminal_height(self) -> int:
67 | """Get the terminal height for proper screen clearing"""
68 | try:
69 | import shutil
70 |
71 | terminal_size = shutil.get_terminal_size()
72 | return terminal_size.lines
73 | except Exception:
74 | # Fallback to a reasonable default if we can't get the terminal size
75 | return 40
76 |
77 | def run_command(self, command: List[str], is_slash_command: bool = True):
78 | """Run a command and handle the result"""
79 | process_command(
80 | self.agent,
81 | command,
82 | self.chat_history,
83 | self.history_file,
84 | self.console,
85 | self.multiline_input_mode,
86 | self.multiline_input_buffer,
87 | )
88 |
89 | def chat_with_model(self, message: str):
90 | """Send a message directly to the model and handle the response"""
91 | # Initialize agent if not already done
92 | if not self.agent.repository_info:
93 | with self.console.status("[bold blue]Initializing agent...[/bold blue]"):
94 | self.agent.initialize()
95 |
96 | # Use the process_user_input function from input module
97 | process_user_input(
98 | self.agent, message, self.chat_history, self.history_file, self.console
99 | )
100 |
101 | def run_interactive(self):
102 | """Run the interface in interactive mode"""
103 | display_welcome(self.console, self.system_info)
104 |
105 | while True:
106 | try:
107 | # Handle multiline input mode
108 | if self.multiline_input_mode:
109 | user_input = Prompt.ask("\n[bold yellow]paste>[/bold yellow]")
110 |
111 | # Check for end of multiline input
112 | if user_input.lower() in ["/end", "/done", "/finish"]:
113 | self.multiline_input_mode = False
114 |
115 | # Process the collected input
116 | if self.multiline_input_buffer:
117 | full_input = "\n".join(self.multiline_input_buffer)
118 | self.console.print(
119 | f"[dim]Processing {len(self.multiline_input_buffer)} lines of input...[/dim]"
120 | )
121 | self.chat_with_model(full_input)
122 | self.multiline_input_buffer = []
123 | else:
124 | self.console.print("[yellow]No input to process[/yellow]")
125 | else:
126 | # Add to buffer
127 | self.multiline_input_buffer.append(user_input)
128 | self.console.print(
129 | f"[dim]Line {len(self.multiline_input_buffer)} added[/dim]"
130 | )
131 | else:
132 | # Normal single-line input mode
133 | user_input = Prompt.ask("\n[bold blue]>[/bold blue]")
134 |
135 | if user_input.lower() in [
136 | "exit",
137 | "quit",
138 | "q",
139 | "/exit",
140 | "/quit",
141 | "/q",
142 | ]:
143 | self.console.print("[bold blue]Exiting...[/bold blue]")
144 | sys.exit(0)
145 |
146 | # Check if this is a paste command
147 | if user_input.lower() == "/paste":
148 | self.multiline_input_mode = True
149 | self.multiline_input_buffer = []
150 | self.console.print(
151 | "[bold yellow]Entering multiline paste mode. Type /end when finished.[/bold yellow]"
152 | )
153 | continue
154 |
155 | # Check if this is a slash command
156 | if user_input.startswith("/"):
157 | # Remove the slash and split into command parts
158 | command = user_input[1:].strip().split()
159 | self.run_command(command)
160 | else:
161 | # Check if this might be a multiline paste
162 | if "\n" in user_input:
163 | lines = user_input.split("\n")
164 | self.console.print(
165 | f"[dim]Detected multiline paste with {len(lines)} lines[/dim]"
166 | )
167 | self.chat_with_model(user_input)
168 | else:
169 | # Treat as direct chat with the model
170 | self.chat_with_model(user_input)
171 |
172 | except KeyboardInterrupt:
173 | if self.multiline_input_mode:
174 | self.console.print(
175 | "\n[bold yellow]Cancelling multiline input[/bold yellow]"
176 | )
177 | self.multiline_input_mode = False
178 | self.multiline_input_buffer = []
179 | else:
180 | self.console.print("\n[bold yellow]Exiting...[/bold yellow]")
181 | sys.exit(0)
182 | except EOFError: # Handle Ctrl+D
183 | self.console.print("\n[bold yellow]Exiting...[/bold yellow]")
184 | sys.exit(0)
185 | except Exception as e:
186 | self.console.print(f"[bold red]Error:[/bold red] {e}")
187 |
188 |
189 | def main():
190 | """Main function to run the agent interface"""
191 | interface = AgentInterface()
192 | interface.run_interactive()
193 |
194 |
195 | if __name__ == "__main__":
196 | main()
197 |
--------------------------------------------------------------------------------
/src/agent/execution.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """Task execution functionality."""
3 |
4 | import xml.etree.ElementTree as ET
5 | from ..utils.xml_tools import extract_xml_from_response, format_xml_response
6 | from .plan import check_dependencies, apply_plan_updates
7 |
8 |
9 | def execute_task(agent, task_id: str) -> str:
10 | """
11 | Execute a specific task from the plan.
12 |
13 | Args:
14 | agent: The agent instance
15 | task_id: The ID of the task to execute
16 |
17 | Returns:
18 | Formatted XML response with execution results
19 | """
20 | if not agent.plan_tree:
21 | return format_xml_response({"error": "No plan exists"})
22 |
23 | try:
24 | # Parse the plan tree
25 | parser = ET.XMLParser(resolve_entities=False)
26 | root = ET.fromstring(agent.plan_tree, parser=parser)
27 |
28 | # Find the task with the given ID
29 | task_element = root.find(f".//task[@id='{task_id}']")
30 | if task_element is None:
31 | return format_xml_response({"error": f"Task {task_id} not found"})
32 |
33 | # Get task details
34 | description = task_element.get("description", "")
35 | current_status = task_element.get("status", "pending")
36 |
37 | # Check if task is already completed
38 | if current_status == "completed":
39 | return format_xml_response(
40 | {
41 | "warning": f"Task {task_id} is already marked as completed",
42 | "task": {
43 | "id": task_id,
44 | "description": description,
45 | "status": current_status,
46 | },
47 | }
48 | )
49 |
50 | # Check dependencies
51 | deps_met, missing_deps = check_dependencies(agent, task_id)
52 | if not deps_met:
53 | return format_xml_response(
54 | {
55 | "error": "Dependencies not met",
56 | "task": {"id": task_id, "description": description},
57 | "missing_dependencies": missing_deps,
58 | }
59 | )
60 |
61 | # Update task status to in-progress
62 | task_element.set("status", "in-progress")
63 | task_element.set("progress", "10") # Start with 10% progress
64 | agent.plan_tree = ET.tostring(root, encoding="unicode")
65 |
66 | print(f"Executing task {task_id}: {description}")
67 | print("Status updated to: in-progress (10%)")
68 |
69 | # Get parent task information for context
70 | parent_info = ""
71 | for potential_parent in root.findall(".//task"):
72 | for child in potential_parent.findall("./task"):
73 | if child.get("id") == task_id:
74 | parent_id = potential_parent.get("id")
75 | parent_desc = potential_parent.get("description")
76 | parent_info = f"This task is part of: {parent_id} - {parent_desc}"
77 | break
78 | if parent_info:
79 | break
80 |
81 | # Generate actions for this task
82 | prompt = f"""
83 | I need to execute the following task:
84 |
85 | TASK ID: {task_id}
86 | DESCRIPTION: {description}
87 | {parent_info}
88 |
89 | REPOSITORY INFORMATION:
90 | {agent.repository_info}
91 |
92 | CURRENT PLAN:
93 | {agent.plan_tree}
94 |
95 | Generate the necessary actions to complete this task. The actions should be in XML format:
96 |
97 |
98 |
99 | # Python code here
100 |
101 |
102 |
103 | def old_function():
104 | def new_function():
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | Your response text here
114 |
115 |
116 |
117 |
118 | # Python code here
119 |
120 |
121 |
122 | def old_function():
123 | def new_function():
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | def old_function():
133 | def new_function():
134 |
135 |
136 |
137 |
138 |
139 | echo "Hello World"
140 | rm -rf some_directory
141 |
142 |
143 |
144 |
145 |
146 | Old information to replace
147 | Updated information
148 |
149 | New information to remember
150 |
151 |
152 |
153 |
154 | Status message explaining what's done or what's needed
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | Think step by step about what needs to be done to complete this task.
166 | Focus on creating actions that are specific, concrete, and directly implement the task.
167 | """
168 |
169 | # Update progress to 30% - planning phase
170 | task_element.set("progress", "30")
171 | agent.plan_tree = ET.tostring(root, encoding="unicode")
172 | print("Progress updated to: 30% (planning phase)")
173 |
174 | response = agent.stream_reasoning(prompt)
175 |
176 | # Update progress to 50% - actions generated
177 | task_element.set("progress", "50")
178 | agent.plan_tree = ET.tostring(root, encoding="unicode")
179 | print("Progress updated to: 50% (actions generated)")
180 |
181 | # Extract actions XML from the response
182 | actions_xml = extract_xml_from_response(response, "actions")
183 | plan_update_xml = extract_xml_from_response(response, "plan_update")
184 |
185 | # Apply plan updates if present
186 | if plan_update_xml:
187 | apply_plan_updates(agent, plan_update_xml)
188 |
189 | if actions_xml:
190 | # Update progress to 70% - ready for execution
191 | task_element.set("progress", "70")
192 | agent.plan_tree = ET.tostring(root, encoding="unicode")
193 | print("Progress updated to: 70% (ready for execution)")
194 |
195 | return format_xml_response(
196 | {
197 | "task": {
198 | "id": task_id,
199 | "description": description,
200 | "progress": "70",
201 | },
202 | "actions": actions_xml,
203 | "plan_update": plan_update_xml if plan_update_xml else None,
204 | }
205 | )
206 |
207 | # Update task status to failed
208 | task_element.set("status", "failed")
209 | task_element.set("notes", "Failed to generate actions")
210 | task_element.set("progress", "0")
211 | agent.plan_tree = ET.tostring(root, encoding="unicode")
212 | print(f"Task {task_id} failed: Could not generate actions")
213 |
214 | return format_xml_response(
215 | {
216 | "error": "Failed to generate actions for task",
217 | "task": {
218 | "id": task_id,
219 | "description": description,
220 | "status": "failed",
221 | },
222 | }
223 | )
224 |
225 | except Exception as e:
226 | return format_xml_response({"error": f"Error executing task: {str(e)}"})
227 |
228 |
229 | if __name__ == "__main__":
230 | # Simple test when run directly
231 | print("Task execution module - run through the agent interface")
232 |
--------------------------------------------------------------------------------
/prompt_cycle:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Configuration
4 | MODEL="r1" # Default model
5 | EDITOR_MODEL="" # Editor model (if different from main model)
6 | PROMPTS_FILE="prompts.txt" # File containing prompts to cycle through
7 | ITERATIONS=1000
8 | SLEEP_TIME=1 # Sleep time between iterations in seconds
9 | GLOBAL_PROMPT="" # Global prompt to prepend to all messages
10 |
11 | # Check if rich-cli is installed
12 | if ! command -v rich &> /dev/null; then
13 | echo "Installing rich-cli for better formatting..."
14 | pip install rich-cli
15 | fi
16 |
17 | # Default prompts
18 | DEFAULT_PROMPTS=(
19 | "improve code structure and organization"
20 | "add more error handling and edge cases"
21 | "optimize performance where possible"
22 | "improve documentation and comments"
23 | "refactor for better readability"
24 | "add unit tests for critical functions"
25 | "implement additional features"
26 | "fix potential bugs and issues"
27 | )
28 |
29 | # Create default prompts file if it doesn't exist
30 | if [ ! -f "$PROMPTS_FILE" ]; then
31 | echo "Creating default prompts file: $PROMPTS_FILE"
32 | printf "%s\n" "${DEFAULT_PROMPTS[@]}" > "$PROMPTS_FILE"
33 | fi
34 |
35 | # Initial check if prompts file exists and is not empty
36 | if [ ! -s "$PROMPTS_FILE" ]; then
37 | echo "Error: Prompts file is empty or doesn't exist: $PROMPTS_FILE"
38 | exit 1
39 | fi
40 |
41 | # Initial count of prompts
42 | PROMPT_COUNT=$(wc -l < "$PROMPTS_FILE")
43 | echo "Found $PROMPT_COUNT prompts in $PROMPTS_FILE"
44 | echo "Note: Changes to $PROMPTS_FILE will be detected automatically on each iteration"
45 |
46 | # Function to display usage information
47 | usage() {
48 | echo "Usage: $0 [options] [file1 [file2 ...]]"
49 | echo "Options:"
50 | echo " -m, --model MODEL Set the model (default: $MODEL)"
51 | echo " -e, --editor-model MODEL Set a specific editor model (optional)"
52 | echo " -p, --prompts FILE Set the prompts file (default: $PROMPTS_FILE)"
53 | echo " -i, --iterations NUM Set number of iterations (default: $ITERATIONS)"
54 | echo " -s, --sleep SECONDS Set sleep time between iterations (default: $SLEEP_TIME)"
55 | echo " -n, --no-files Run without specifying files (architect mode can add files)"
56 | echo " -g, --global-prompt TEXT Set a global prompt to prepend to all messages"
57 | echo " -h, --help Display this help message"
58 | exit 1
59 | }
60 |
61 | # Parse command line arguments
62 | FILE_PATTERNS=()
63 | NO_FILES=false
64 | READ_FILES=()
65 | while [[ $# -gt 0 ]]; do
66 | case $1 in
67 | -m|--model)
68 | MODEL="$2"
69 | shift 2
70 | ;;
71 | -e|--editor-model)
72 | EDITOR_MODEL="$2"
73 | shift 2
74 | ;;
75 | -p|--prompts)
76 | PROMPTS_FILE="$2"
77 | # Create default prompts file if specified file doesn't exist
78 | if [ ! -f "$PROMPTS_FILE" ]; then
79 | echo "Creating specified prompts file: $PROMPTS_FILE"
80 | printf "%s\n" "${DEFAULT_PROMPTS[@]}" > "$PROMPTS_FILE"
81 | fi
82 | shift 2
83 | ;;
84 | -g|--global-prompt)
85 | GLOBAL_PROMPT="$2"
86 | shift 2
87 | ;;
88 | -n|--no-files)
89 | NO_FILES=true
90 | shift
91 | ;;
92 | -r|--read)
93 | if [ -f "$2" ]; then
94 | READ_FILES+=("$2")
95 | else
96 | echo "Warning: Read file '$2' does not exist and will be ignored."
97 | fi
98 | shift 2
99 | ;;
100 | -i|--iterations)
101 | ITERATIONS="$2"
102 | shift 2
103 | ;;
104 | -s|--sleep)
105 | SLEEP_TIME="$2"
106 | shift 2
107 | ;;
108 | -h|--help)
109 | usage
110 | ;;
111 | -*)
112 | echo "Unknown option: $1"
113 | usage
114 | ;;
115 | *)
116 | FILE_PATTERNS+=("$1")
117 | shift
118 | ;;
119 | esac
120 | done
121 |
122 | # Default read files if none specified
123 | if [ ${#READ_FILES[@]} -eq 0 ]; then
124 | for DEFAULT_READ in "plex.md" "context.txt" "spec.md"; do
125 | if [ -f "$DEFAULT_READ" ]; then
126 | READ_FILES+=("$DEFAULT_READ")
127 | fi
128 | done
129 | fi
130 |
131 | # Check if at least one file pattern is provided or --no-files flag is set
132 | if [ ${#FILE_PATTERNS[@]} -eq 0 ] && [ "$NO_FILES" = false ]; then
133 | echo "Error: No files specified. Use --no-files flag if you want to run without specifying files."
134 | usage
135 | fi
136 |
137 | # Initial check for file patterns
138 | if [ ${#FILE_PATTERNS[@]} -gt 0 ] && [ "$NO_FILES" = false ]; then
139 | # Check if at least one pattern matches something
140 | FOUND_FILES=false
141 | for PATTERN in "${FILE_PATTERNS[@]}"; do
142 | if compgen -G "$PATTERN" > /dev/null; then
143 | FOUND_FILES=true
144 | break
145 | fi
146 | done
147 |
148 | if [ "$FOUND_FILES" = false ]; then
149 | echo "Warning: None of the specified file patterns match any files."
150 | echo "Files will be included if they appear later during execution."
151 | fi
152 | fi
153 |
154 | # Function to handle script interruption
155 | cleanup() {
156 | echo -e "\nScript interrupted. Exiting gracefully..."
157 | exit 0
158 | }
159 |
160 | # Set up trap for CTRL+C
161 | trap cleanup SIGINT SIGTERM
162 |
163 | # Main loop
164 | for i in $(seq 1 $ITERATIONS); do
165 | # Get valid prompts (non-empty lines)
166 | mapfile -t VALID_PROMPTS < <(grep -v '^\s*$' "$PROMPTS_FILE")
167 | PROMPT_COUNT=${#VALID_PROMPTS[@]}
168 |
169 | if [ "$PROMPT_COUNT" -eq 0 ]; then
170 | echo "Warning: No valid prompts found in $PROMPTS_FILE. Using default prompt."
171 | CURRENT_PROMPT="improve code"
172 | else
173 | # Calculate which prompt to use (random start then cycle)
174 | PROMPT_INDEX=$(( (RANDOM + i - 1) % PROMPT_COUNT ))
175 | CURRENT_PROMPT="${VALID_PROMPTS[$PROMPT_INDEX]}"
176 | fi
177 |
178 | # Combine global prompt with current prompt if global prompt is set
179 | FULL_PROMPT="$CURRENT_PROMPT"
180 | if [ -n "$GLOBAL_PROMPT" ]; then
181 | FULL_PROMPT="$GLOBAL_PROMPT $CURRENT_PROMPT"
182 | fi
183 |
184 | # Display the current prompt with rich formatting
185 | echo -e "\n"
186 | if command -v rich &> /dev/null; then
187 | rich --print "[bold blue]Iteration $i - $(date)[/bold blue]"
188 | rich --print "[bold green]====================================================[/bold green]"
189 | if [ -n "$GLOBAL_PROMPT" ]; then
190 | rich --print "[bold magenta]GLOBAL:[/bold magenta] [white]$GLOBAL_PROMPT[/white]"
191 | fi
192 | rich --print "[bold yellow]PROMPT:[/bold yellow] [bold white]$CURRENT_PROMPT[/bold white]"
193 | rich --print "[bold green]====================================================[/bold green]"
194 | else
195 | echo "Iteration $i - $(date)"
196 | echo "===================================================="
197 | if [ -n "$GLOBAL_PROMPT" ]; then
198 | echo "GLOBAL: $GLOBAL_PROMPT"
199 | fi
200 | echo "PROMPT: $CURRENT_PROMPT"
201 | echo "===================================================="
202 | fi
203 |
204 | # Build read arguments
205 | READ_ARGS=""
206 | for READ_FILE in "${READ_FILES[@]}"; do
207 | READ_ARGS="$READ_ARGS --read \"$READ_FILE\""
208 | done
209 |
210 | # Build read arguments
211 | READ_ARGS=()
212 | for READ_FILE in "${READ_FILES[@]}"; do
213 | READ_ARGS+=(--read "$READ_FILE")
214 | done
215 |
216 | # Build the base command
217 | AIDER_CMD=(aider --model "$MODEL" --subtree-only "${READ_ARGS[@]}"
218 | --yes-always --no-show-model-warnings
219 | --weak-model 'openrouter/google/gemini-2.0-flash-001')
220 |
221 | # Add editor model if specified
222 | if [ -n "$EDITOR_MODEL" ]; then
223 | AIDER_CMD+=(--editor-model "$EDITOR_MODEL")
224 | fi
225 |
226 | # Add message
227 | AIDER_CMD+=(--message "$FULL_PROMPT")
228 |
229 | # Add files if needed - resolve globs on each iteration
230 | if [ "$NO_FILES" = false ]; then
231 | FILES=()
232 | for PATTERN in "${FILE_PATTERNS[@]}"; do
233 | # Use compgen to expand globs
234 | while IFS= read -r FILE; do
235 | if [ -f "$FILE" ]; then
236 | FILES+=("$FILE")
237 | fi
238 | done < <(compgen -G "$PATTERN" 2>/dev/null || echo "")
239 | done
240 |
241 | if [ ${#FILES[@]} -gt 0 ]; then
242 | AIDER_CMD+=("${FILES[@]}")
243 |
244 | # Display the files being processed
245 | if command -v rich &> /dev/null; then
246 | rich --print "[cyan]Processing files:[/cyan] [white]${FILES[*]}[/white]"
247 | else
248 | echo "Processing files: ${FILES[*]}"
249 | fi
250 | else
251 | echo "Warning: No files match the specified patterns at this iteration."
252 | fi
253 | fi
254 |
255 | # Execute the command
256 | "${AIDER_CMD[@]}"
257 |
258 | if command -v rich &> /dev/null; then
259 | rich --print "[dim]Sleeping for $SLEEP_TIME seconds...[/dim]"
260 | else
261 | echo "Sleeping for $SLEEP_TIME seconds..."
262 | fi
263 | sleep $SLEEP_TIME
264 | done
265 |
--------------------------------------------------------------------------------
/src/agent/task.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """Task execution functionality."""
3 |
4 | import json
5 | import xml.etree.ElementTree as ET
6 | import json
7 | import xml.etree.ElementTree as ET
8 | from src.utils.xml_tools import extract_xml_from_response, format_xml_response
9 |
10 |
11 | def execute_task(agent, task_id: str) -> str:
12 | """
13 | Execute a specific task from the plan.
14 |
15 | Args:
16 | agent: The agent instance
17 | task_id: The ID of the task to execute
18 |
19 | Returns:
20 | Formatted XML response with execution results
21 | """
22 | if not hasattr(agent, 'plan_tree') or not agent.plan_tree:
23 | return format_xml_response({"error": "No plan exists"})
24 |
25 | try:
26 | # Parse the plan tree
27 | root = ET.fromstring(agent.plan_tree)
28 |
29 | # Find the task with the given ID
30 | task_element = root.find(f".//task[@id='{task_id}']")
31 | if task_element is None:
32 | return format_xml_response({"error": f"Task {task_id} not found"})
33 |
34 | # Return basic task info
35 | return format_xml_response({
36 | "task": {
37 | "id": task_id,
38 | "description": task_element.get("description", ""),
39 | "status": task_element.get("status", "pending")
40 | }
41 | })
42 |
43 | except Exception as e:
44 | return format_xml_response({"error": f"Error executing task: {str(e)}"})
45 | if task_element is None:
46 | return format_xml_response({"error": f"Task {task_id} not found"})
47 |
48 | # Get task details
49 | description = task_element.get("description", "")
50 | current_status = task_element.get("status", "pending")
51 |
52 | # Check if task is already completed
53 | if current_status == "completed":
54 | return format_xml_response(
55 | {
56 | "warning": f"Task {task_id} is already marked as completed",
57 | "task": {
58 | "id": task_id,
59 | "description": description,
60 | "status": current_status,
61 | },
62 | }
63 | )
64 |
65 | # Check dependencies
66 | from src.agent.plan import check_dependencies
67 |
68 | deps_met, missing_deps = check_dependencies(agent, task_id)
69 | if not deps_met:
70 | return format_xml_response(
71 | {
72 | "error": "Dependencies not met",
73 | "task": {"id": task_id, "description": description},
74 | "missing_dependencies": missing_deps,
75 | }
76 | )
77 |
78 | # Update task status to in-progress
79 | task_element.set("status", "in-progress")
80 | task_element.set("progress", "10") # Start with 10% progress
81 | agent.plan_tree = ET.tostring(root, encoding="unicode")
82 |
83 | print(f"Executing task {task_id}: {description}")
84 | print("Status updated to: in-progress (10%)")
85 |
86 | # Get parent task information for context
87 | parent_info = ""
88 | for potential_parent in root.findall(".//task"):
89 | for child in potential_parent.findall("./task"):
90 | if child.get("id") == task_id:
91 | parent_id = potential_parent.get("id")
92 | parent_desc = potential_parent.get("description")
93 | parent_info = f"This task is part of: {parent_id} - {parent_desc}"
94 | break
95 | if parent_info:
96 | break
97 |
98 | # Generate actions for this task
99 | prompt = f"""
100 | I need to execute the following task:
101 |
102 | TASK ID: {task_id}
103 | DESCRIPTION: {description}
104 | {parent_info}
105 |
106 | REPOSITORY INFORMATION:
107 | {json.dumps(agent.repository_info, indent=2)}
108 |
109 | CURRENT PLAN:
110 | {agent.plan_tree}
111 |
112 | Generate the necessary actions to complete this task. The actions should be in XML format:
113 |
114 |
115 |
116 | # Python code here
117 |
118 |
119 |
120 | def old_function():
121 | def new_function():
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | Your response text here
131 |
132 |
133 |
134 |
135 | # Python code here
136 |
137 |
138 |
139 | def old_function():
140 | def new_function():
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 | def old_function():
150 | def new_function():
151 |
152 |
153 |
154 |
155 |
156 | echo "Hello World"
157 | rm -rf some_directory
158 |
159 |
160 |
161 |
162 |
163 | Old information to replace
164 | Updated information
165 |
166 | New information to remember
167 |
168 |
169 |
170 |
171 | Status message explaining what's done or what's needed
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | Think step by step about what needs to be done to complete this task.
183 | Focus on creating actions that are specific, concrete, and directly implement the task.
184 | """
185 |
186 | # Update progress to 30% - planning phase
187 | task_element.set("progress", "30")
188 | agent.plan_tree = ET.tostring(root, encoding="unicode")
189 | print("Progress updated to: 30% (planning phase)")
190 |
191 | response = agent.stream_reasoning(prompt)
192 |
193 | # Update progress to 50% - actions generated
194 | task_element.set("progress", "50")
195 | agent.plan_tree = ET.tostring(root, encoding="unicode")
196 | print("Progress updated to: 50% (actions generated)")
197 |
198 | # Extract actions XML from the response
199 | actions_xml = extract_xml_from_response(response, "actions")
200 | plan_update_xml = extract_xml_from_response(response, "plan_update")
201 |
202 | # Apply plan updates if present
203 | if plan_update_xml:
204 | from src.agent.plan import apply_plan_updates
205 |
206 | apply_plan_updates(agent, plan_update_xml)
207 |
208 | if not actions_xml:
209 | # Update task status to failed
210 | task_element.set("status", "failed")
211 | task_element.set("notes", "Failed to generate actions")
212 | task_element.set("progress", "0")
213 | agent.plan_tree = ET.tostring(root, encoding="unicode")
214 | print(f"Task {task_id} failed: Could not generate actions")
215 |
216 | # Generate dopamine reward for failure
217 | if hasattr(agent, "dopamine_reward"):
218 | dopamine = agent.dopamine_reward.generate_reward(30)
219 | else:
220 | from src.utils.feedback import DopamineReward
221 |
222 | agent.dopamine_reward = DopamineReward(agent.console)
223 | dopamine = agent.dopamine_reward.generate_reward(30)
224 |
225 | return format_xml_response(
226 | {
227 | "error": "Failed to generate actions for task",
228 | "task": {
229 | "id": task_id,
230 | "description": description,
231 | "status": "failed",
232 | },
233 | "dopamine": dopamine,
234 | }
235 | )
236 |
237 | # Update progress to 70% - ready for execution
238 | task_element.set("progress", "70")
239 | agent.plan_tree = ET.tostring(root, encoding="unicode")
240 | print("Progress updated to: 70% (ready for execution)")
241 |
242 | # Generate dopamine reward for successful action generation
243 | if hasattr(agent, "dopamine_reward"):
244 | dopamine = agent.dopamine_reward.generate_reward(75)
245 | else:
246 | from utils.feedback import DopamineReward
247 |
248 | agent.dopamine_reward = DopamineReward(agent.console)
249 | dopamine = agent.dopamine_reward.generate_reward(75)
250 |
251 | return format_xml_response(
252 | {
253 | "task": {
254 | "id": task_id,
255 | "description": description,
256 | "progress": "70",
257 | },
258 | "actions": actions_xml,
259 | "plan_update": plan_update_xml if plan_update_xml else None,
260 | "dopamine": dopamine,
261 | }
262 | )
263 |
264 | except Exception as e:
265 | return format_xml_response({"error": f"Error executing task: {str(e)}"})
266 |
--------------------------------------------------------------------------------
/src/agent/plan.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """Plan generation and management functionality."""
3 |
4 | import json
5 | import xml.etree.ElementTree as ET
6 | from typing import Dict, Any, List, Tuple, Optional
7 |
8 | from ..utils.xml_tools import extract_xml_from_response, format_xml_response
9 |
10 |
11 | def generate_plan(agent, spec: str, formatted_message: str = None) -> str:
12 | """
13 | Generate a plan tree based on the specification.
14 |
15 | Args:
16 | agent: The agent instance
17 | spec: The specification text
18 |
19 | Returns:
20 | Formatted XML response containing the plan
21 | """
22 | # If no formatted message is provided, create one
23 | if not formatted_message:
24 | # Get system information for the prompt
25 | from src.interface.display import get_system_info
26 |
27 | system_info = get_system_info()
28 |
29 | # Import the input schema formatter
30 | from src.utils.input_schema import format_input_message
31 |
32 | # Format the message with XML tags using the schema
33 | formatted_message = format_input_message(
34 | message=f"Generate a plan based on the following specification:\n\n{spec}",
35 | system_info=system_info,
36 | )
37 |
38 | prompt = f"""
39 |
40 |
41 | Based on the following specification, create a hierarchical plan as an XML tree.
42 | Think step by step about the dependencies between tasks and how to break down the problem effectively.
43 |
44 |
45 |
46 | {spec}
47 |
48 |
49 | {json.dumps(agent.repository_info, indent=2)}
50 |
51 |
52 | Each task should have:
53 | - A unique id
54 | - A clear description
55 | - A status (pending, in-progress, completed, failed)
56 | - A complexity estimate (low, medium, high)
57 | - Dependencies (depends_on attribute with comma-separated task IDs)
58 | - Progress indicator (0-100)
59 | - Subtasks where appropriate
60 |
61 | Example structure:
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | """
77 |
78 | response = agent.stream_reasoning(prompt)
79 |
80 | # Extract XML from the response
81 | xml_content = extract_xml_from_response(response, "plan")
82 | if xml_content:
83 | agent.plan_tree = xml_content
84 | return format_xml_response({"plan": xml_content})
85 | else:
86 | return format_xml_response({"error": "Failed to generate plan"})
87 |
88 |
89 | def update_plan(
90 | agent,
91 | task_id: str,
92 | new_status: str,
93 | notes: Optional[str] = None,
94 | progress: Optional[str] = None,
95 | ) -> str:
96 | """
97 | Update the status of a task in the plan.
98 |
99 | Args:
100 | agent: The agent instance
101 | task_id: The ID of the task to update
102 | new_status: The new status for the task
103 | notes: Optional notes to add to the task
104 | progress: Optional progress value (0-100)
105 |
106 | Returns:
107 | Formatted XML response with the updated plan
108 | """
109 | if not agent.plan_tree:
110 | return format_xml_response({"error": "No plan exists"})
111 |
112 | try:
113 | # Parse the plan tree
114 | parser = ET.XMLParser(resolve_entities=False)
115 | root = ET.fromstring(agent.plan_tree, parser=parser)
116 |
117 | # Find the task with the given ID
118 | task_found = False
119 | for task in root.findall(".//task[@id='{}']".format(task_id)):
120 | task.set("status", new_status)
121 | if notes:
122 | task.set("notes", notes)
123 | if progress and progress.isdigit() and 0 <= int(progress) <= 100:
124 | task.set("progress", progress)
125 | task_found = True
126 |
127 | if not task_found:
128 | return format_xml_response({"error": f"Task {task_id} not found"})
129 |
130 | # Update the plan tree
131 | agent.plan_tree = ET.tostring(root, encoding="unicode")
132 |
133 | return format_xml_response(
134 | {
135 | "plan": agent.plan_tree,
136 | "status": f"Updated task {task_id} to {new_status}",
137 | }
138 | )
139 |
140 | except Exception as e:
141 | return format_xml_response({"error": f"Error updating plan: {str(e)}"})
142 |
143 |
144 | def check_dependencies(agent, task_id: str) -> Tuple[bool, List[str]]:
145 | """
146 | Check if all dependencies for a task are completed.
147 |
148 | Args:
149 | agent: The agent instance
150 | task_id: The ID of the task to check
151 |
152 | Returns:
153 | Tuple of (dependencies_met, list_of_missing_dependencies)
154 | """
155 | if not agent.plan_tree:
156 | return False, ["No plan exists"]
157 |
158 | try:
159 | # Parse the plan tree
160 | root = ET.fromstring(agent.plan_tree)
161 |
162 | # Find the task with the given ID
163 | task_element = root.find(f".//task[@id='{task_id}']")
164 | if task_element is None:
165 | return False, [f"Task {task_id} not found"]
166 |
167 | # Get dependencies
168 | depends_on = task_element.get("depends_on", "")
169 | if not depends_on:
170 | return True, [] # No dependencies
171 |
172 | # Check each dependency
173 | dependency_ids = [dep.strip() for dep in depends_on.split(",") if dep.strip()]
174 | incomplete_deps = []
175 |
176 | for dep_id in dependency_ids:
177 | dep_element = root.find(f".//task[@id='{dep_id}']")
178 | if dep_element is None:
179 | incomplete_deps.append(f"Dependency {dep_id} not found")
180 | continue
181 |
182 | status = dep_element.get("status", "")
183 | if status != "completed":
184 | desc = dep_element.get("description", "")
185 | incomplete_deps.append(
186 | f"Dependency {dep_id} ({desc}) is not completed (status: {status})"
187 | )
188 |
189 | return len(incomplete_deps) == 0, incomplete_deps
190 |
191 | except Exception as e:
192 | return False, [f"Error checking dependencies: {str(e)}"]
193 |
194 |
195 | def apply_plan_updates(agent, plan_update_xml: str) -> None:
196 | """
197 | Apply updates to the plan tree based on the plan_update XML.
198 |
199 | Args:
200 | agent: The agent instance
201 | plan_update_xml: XML string containing plan updates
202 | """
203 | if not agent.plan_tree:
204 | return
205 |
206 | try:
207 | # Parse the plan tree and updates
208 | plan_root = ET.fromstring(agent.plan_tree)
209 | parser = ET.XMLParser(resolve_entities=False)
210 | updates_root = ET.fromstring(plan_update_xml, parser=parser)
211 |
212 | # Track changes for reporting
213 | changes = []
214 |
215 | # Process add_task elements
216 | for add_task in updates_root.findall("./add_task"):
217 | parent_id = add_task.get("parent_id")
218 |
219 | # Find the parent task
220 | parent = plan_root.find(f".//task[@id='{parent_id}']")
221 | if parent is not None:
222 | # Create a new task element
223 | new_task = ET.Element("task")
224 |
225 | # Copy all attributes from add_task to new_task
226 | for attr, value in add_task.attrib.items():
227 | if attr != "parent_id": # Skip the parent_id attribute
228 | new_task.set(attr, value)
229 |
230 | # Add the new task to the parent
231 | parent.append(new_task)
232 | changes.append(
233 | f"Added new task {new_task.get('id')}: {new_task.get('description')}"
234 | )
235 |
236 | # Process modify_task elements
237 | for modify_task in updates_root.findall("./modify_task"):
238 | task_id = modify_task.get("id")
239 |
240 | # Find the task to modify
241 | task = plan_root.find(f".//task[@id='{task_id}']")
242 | if task is not None:
243 | old_desc = task.get("description", "")
244 | # Update attributes
245 | for attr, value in modify_task.attrib.items():
246 | if attr != "id": # Skip the id attribute
247 | task.set(attr, value)
248 | new_desc = task.get("description", "")
249 | if old_desc != new_desc:
250 | changes.append(f"Modified task {task_id}: {old_desc} -> {new_desc}")
251 | else:
252 | changes.append(f"Updated attributes for task {task_id}")
253 |
254 | # Process remove_task elements
255 | for remove_task in updates_root.findall("./remove_task"):
256 | task_id = remove_task.get("id")
257 |
258 | # Find the task to remove
259 | task = plan_root.find(f".//task[@id='{task_id}']")
260 | if task is not None:
261 | desc = task.get("description", "")
262 | # ElementTree in Python doesn't have getparent() method
263 | # We need to find the parent manually
264 | for potential_parent in plan_root.findall(".//task"):
265 | for child in potential_parent.findall("./task"):
266 | if child.get("id") == task_id:
267 | potential_parent.remove(child)
268 | changes.append(f"Removed task {task_id}: {desc}")
269 | break
270 |
271 | # Update the plan tree
272 | agent.plan_tree = ET.tostring(plan_root, encoding="unicode")
273 |
274 | # Report changes
275 | if changes:
276 | print("\nPlan has been updated by the agent:")
277 | for change in changes:
278 | print(f"- {change}")
279 |
280 | except Exception as e:
281 | print(f"Error applying plan updates: {e}")
282 |
283 |
284 | if __name__ == "__main__":
285 | # Simple test when run directly
286 | print("Plan management module - run through the agent interface")
287 |
--------------------------------------------------------------------------------
/src/interface/input.py:
--------------------------------------------------------------------------------
1 | """
2 | Input handling for the agent interface.
3 | """
4 |
5 | import os
6 | import datetime
7 | import json
8 | import xml.etree.ElementTree as ET
9 | from typing import List, Dict, Any, Optional, Callable
10 | from rich.console import Console
11 |
12 | from src.interface.display import get_system_info
13 | from src.interface.chat import process_chat_response
14 |
15 |
16 | def process_user_input(
17 | agent,
18 | user_input: str,
19 | chat_history: List[Dict[str, Any]],
20 | history_file: str,
21 | console: Console,
22 | ):
23 | """Process user input and send to the model."""
24 |
25 | # Add user message to history
26 | timestamp = datetime.datetime.now().isoformat()
27 | chat_history.append({"role": "user", "content": user_input, "timestamp": timestamp})
28 | save_chat_history(chat_history, history_file)
29 |
30 | # Format history for the prompt
31 | formatted_history = _format_history_for_prompt(chat_history)
32 |
33 | # Get persistent memory
34 | memory_content = _load_persistent_memory()
35 |
36 | # Get system information
37 | system_info = get_system_info()
38 |
39 | # Import the input schema formatter
40 | from src.utils.input_schema import format_input_message
41 |
42 | # Format the message with XML tags using the schema
43 | formatted_input = format_input_message(
44 | message=user_input,
45 | system_info=system_info,
46 | memory=memory_content,
47 | history=formatted_history,
48 | )
49 |
50 | # Construct a prompt that instructs the model to respond in XML format
51 | from src.interface.chat import process_chat_message
52 |
53 | prompt = process_chat_message(
54 | formatted_input,
55 | formatted_history,
56 | memory_content,
57 | system_info,
58 | getattr(agent, "config", {}),
59 | )
60 |
61 | try:
62 | # Set a callback to handle streaming in the interface
63 | def stream_callback(content, is_reasoning=False):
64 | if is_reasoning:
65 | # Use yellow color for reasoning tokens
66 | console.print(f"[yellow]{content}[/yellow]", end="")
67 | else:
68 | # Use rich for normal content
69 | console.print(content, end="", highlight=False)
70 |
71 | # Pass the callback to the agent
72 | agent.stream_callback = stream_callback
73 | response = agent.stream_reasoning(prompt)
74 |
75 | # Process the response
76 | process_chat_response(
77 | agent,
78 | console,
79 | response,
80 | chat_history,
81 | _update_persistent_memory,
82 | _get_terminal_height,
83 | _load_persistent_memory,
84 | _format_history_for_prompt,
85 | lambda: save_chat_history(chat_history, history_file),
86 | )
87 | except KeyboardInterrupt:
88 | console.print("\n[bold yellow]Operation cancelled by user[/bold yellow]")
89 |
90 |
91 | def save_chat_history(chat_history: List[Dict[str, Any]], history_file: str):
92 | """Save chat history to file."""
93 | try:
94 | # Ensure directory exists
95 | os.makedirs(os.path.dirname(history_file), exist_ok=True)
96 |
97 | with open(history_file, "w") as f:
98 | json.dump(chat_history, f, indent=2)
99 | except Exception as e:
100 | print(f"Could not save chat history: {e}")
101 |
102 |
103 | def _format_history_for_prompt(chat_history: List[Dict[str, Any]]) -> str:
104 | """Format chat history for inclusion in the prompt."""
105 | # Limit history to last 10 messages to avoid context overflow
106 | recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history
107 |
108 | formatted_history = []
109 | for msg in recent_history:
110 | role = msg["role"]
111 | content = msg["content"]
112 | timestamp = msg.get("timestamp", "")
113 |
114 | # Format as XML
115 | entry = f''
116 |
117 | # For assistant messages, try to extract just the message part to keep history cleaner
118 | if role == "assistant":
119 | from src.utils.xml_tools import extract_xml_from_response
120 |
121 | message_xml = extract_xml_from_response(content, "message")
122 | if message_xml:
123 | try:
124 | root = ET.fromstring(message_xml)
125 | message_text = root.text if root.text else ""
126 | entry += f"{message_text}"
127 | except ET.ParseError:
128 | entry += f"{content}"
129 | else:
130 | entry += f"{content}"
131 | else:
132 | entry += f"{content}"
133 |
134 | entry += ""
135 | formatted_history.append(entry)
136 |
137 | return "\n".join(formatted_history)
138 |
139 |
140 | def _load_persistent_memory() -> str:
141 | """Load memory from file."""
142 | memory_file = "agent_memory.xml"
143 | try:
144 | if os.path.exists(memory_file):
145 | with open(memory_file, "r") as f:
146 | return f.read()
147 | else:
148 | # Create default memory structure - simple and flexible
149 | default_memory = (
150 | "\n \n"
151 | )
152 | with open(memory_file, "w") as f:
153 | f.write(default_memory)
154 | return default_memory
155 | except Exception as e:
156 | print(f"Could not load memory: {e}")
157 | return ""
158 |
159 |
160 | def _update_persistent_memory(memory_updates_xml):
161 | """Update memory based on model's instructions."""
162 | if not memory_updates_xml:
163 | return
164 |
165 | try:
166 | memory_file = "agent_memory.xml"
167 | current_memory = _load_persistent_memory()
168 |
169 | # Parse the updates
170 | updates_root = ET.fromstring(memory_updates_xml)
171 |
172 | # Parse current memory
173 | try:
174 | memory_root = ET.fromstring(current_memory)
175 | except ET.ParseError:
176 | # If parsing fails, create a new memory structure
177 | memory_root = ET.Element("memory")
178 |
179 | # Process edits - simple search/replace approach
180 | for edit in updates_root.findall("./edit"):
181 | search_elem = edit.find("search")
182 | replace_elem = edit.find("replace")
183 |
184 | if search_elem is not None and replace_elem is not None:
185 | search_text = search_elem.text if search_elem.text else ""
186 | replace_text = replace_elem.text if replace_elem.text else ""
187 |
188 | # Convert memory to string for search/replace
189 | memory_str = ET.tostring(memory_root, encoding="unicode")
190 |
191 | if search_text in memory_str:
192 | # Replace the text
193 | memory_str = memory_str.replace(search_text, replace_text)
194 |
195 | # Parse the updated memory
196 | memory_root = ET.fromstring(memory_str)
197 |
198 | # Process additions - just add text directly to memory
199 | for append in updates_root.findall("./append"):
200 | append_text = append.text if append.text else ""
201 | if append_text:
202 | # Append to existing memory text
203 | if memory_root.text is None:
204 | memory_root.text = append_text
205 | else:
206 | memory_root.text += "\n" + append_text
207 |
208 | # Save the updated memory
209 | from src.utils.xml_tools import pretty_format_xml
210 |
211 | updated_memory = pretty_format_xml(ET.tostring(memory_root, encoding="unicode"))
212 | with open(memory_file, "w") as f:
213 | f.write(updated_memory)
214 |
215 | print("Memory updated")
216 |
217 | except Exception as e:
218 | print(f"Error updating memory: {e}")
219 |
220 |
221 | def _get_terminal_height() -> int:
222 | """Get the terminal height for proper screen clearing."""
223 | try:
224 | import shutil
225 |
226 | terminal_size = shutil.get_terminal_size()
227 | return terminal_size.lines
228 | except Exception:
229 | # Fallback to a reasonable default if we can't get the terminal size
230 | return 40
231 |
232 |
233 | def _format_history_for_prompt(chat_history: List[Dict[str, Any]]) -> str:
234 | """
235 | Format chat history for inclusion in the prompt.
236 |
237 | Args:
238 | chat_history: List of chat history entries
239 |
240 | Returns:
241 | Formatted history string
242 | """
243 | formatted_history = []
244 |
245 | # Get the last few messages (up to 10)
246 | recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history
247 |
248 | for entry in recent_history:
249 | role = entry.get("role", "unknown")
250 | content = entry.get("content", "")
251 | formatted_history.append(f'{content}')
252 |
253 | return "\n".join(formatted_history)
254 |
255 |
256 | def save_chat_history(chat_history: List[Dict[str, Any]], history_file: str):
257 | """
258 | Save chat history to file.
259 |
260 | Args:
261 | chat_history: List of chat history entries
262 | history_file: Path to the history file
263 | """
264 | try:
265 | # Ensure directory exists
266 | os.makedirs(os.path.dirname(history_file), exist_ok=True)
267 |
268 | with open(history_file, "w") as f:
269 | import json
270 |
271 | json.dump(chat_history, f, indent=2)
272 | except Exception as e:
273 | print(f"Could not save chat history: {e}")
274 |
275 |
276 | from src.utils.helpers import load_persistent_memory
277 |
278 | def _update_persistent_memory(memory_updates_xml):
279 | """
280 | Update the persistent memory with the provided updates.
281 |
282 | Args:
283 | memory_updates_xml: XML string containing memory updates
284 | """
285 | try:
286 | # Parse the memory updates
287 | updates_root = ET.fromstring(memory_updates_xml)
288 |
289 | # Load the current memory
290 | memory_content = _load_persistent_memory()
291 | memory_root = ET.fromstring(memory_content)
292 |
293 | # Process edits
294 | for edit in updates_root.findall("./edit"):
295 | search = edit.find("./search")
296 | replace = edit.find("./replace")
297 |
298 | if search is not None and replace is not None:
299 | search_text = search.text if search.text else ""
300 | replace_text = replace.text if replace.text else ""
301 |
302 | # Convert memory to string for search/replace
303 | memory_str = ET.tostring(memory_root, encoding="unicode")
304 | memory_str = memory_str.replace(search_text, replace_text)
305 |
306 | # Parse back to XML
307 | memory_root = ET.fromstring(memory_str)
308 |
309 | # Process appends
310 | for append in updates_root.findall("./append"):
311 | append_text = append.text if append.text else ""
312 |
313 | # Create a temporary root to parse the append text
314 | try:
315 | # Try to parse as XML first
316 | append_elem = ET.fromstring(f"{append_text}")
317 | for child in append_elem:
318 | memory_root.append(child)
319 | except ET.ParseError:
320 | # If not valid XML, add as text node to a new element
321 | new_elem = ET.SubElement(memory_root, "entry")
322 | new_elem.text = append_text
323 | new_elem.set("timestamp", datetime.datetime.now().isoformat())
324 |
325 | # Save the updated memory
326 | with open("agent_memory.xml", "w") as f:
327 | f.write(ET.tostring(memory_root, encoding="unicode"))
328 |
329 | except ET.ParseError as e:
330 | print(f"Could not parse memory updates XML: {e}")
331 | except Exception as e:
332 | print(f"Error updating memory: {e}")
333 |
334 |
335 |
--------------------------------------------------------------------------------
/src/agent_main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os
4 | import sys
5 | import json
6 | from typing import Dict, List, Optional, Any, Tuple
7 | import litellm
8 | from rich.console import Console
9 |
10 | # Import refactored modules
11 | from src.agent.repository import analyze_repository
12 | from src.agent.plan import (
13 | generate_plan,
14 | update_plan,
15 | check_dependencies,
16 | apply_plan_updates,
17 | )
18 | from src.agent.task import execute_task
19 | from src.utils.xml_operations import (
20 | extract_xml_from_response,
21 | format_xml_response,
22 | pretty_format_xml,
23 | )
24 | from src.utils.xml_tools import extract_xml_from_response as extract_xml_alt
25 | from src.utils.feedback import DopamineReward
26 |
27 | from src.agent.core import Agent
28 | from typing import List, Optional
29 | import sys
30 | import litellm
31 | from rich.console import Console
32 |
33 |
34 | def main():
35 | """Stream the reasoning process from the model and return the final response"""
36 | messages = [{"role": "user", "content": prompt}]
37 |
38 | # Print the full message being sent to the model
39 | print("\n=== Message Sent to Model ===\n")
40 | print(f"Model: {self.model_name}")
41 | print(prompt)
42 | print("\n=== End Message ===\n")
43 |
44 | # Get terminal height and add that many newlines to preserve history
45 | terminal_height = self._get_terminal_height()
46 | print("\n" * terminal_height)
47 |
48 | if not self.config["stream_reasoning"]:
49 | # Non-streaming mode
50 | try:
51 | response = litellm.completion(
52 | model=self.model_name,
53 | messages=messages,
54 | timeout=60, # Add timeout to prevent hanging
55 | )
56 | return response.choices[0].message.content
57 | except Exception as e:
58 | print(f"Error in non-streaming mode: {e}")
59 | return f"Error: {str(e)}"
60 |
61 | # Streaming mode
62 | full_response = ""
63 | reasoning_output = ""
64 |
65 | try:
66 | # Add timeout to prevent hanging
67 | response = litellm.completion(
68 | model=self.model_name, messages=messages, stream=True, timeout=60
69 | )
70 |
71 | # Track if we're in the reasoning phase
72 | reasoning_phase = True
73 |
74 | for chunk in response:
75 | # Handle regular content
76 | if hasattr(chunk.choices[0], "delta") and hasattr(
77 | chunk.choices[0].delta, "content"
78 | ):
79 | content = chunk.choices[0].delta.content
80 | if content:
81 | # We've transitioned to regular content
82 | reasoning_phase = False
83 |
84 | # Clean content of control characters
85 | clean_content = content.replace("\r", "").replace("\b", "")
86 |
87 | if self.stream_callback:
88 | self.stream_callback(clean_content, is_reasoning=False)
89 | else:
90 | # Print without any special formatting
91 | print(clean_content, end="", flush=True)
92 | full_response += clean_content
93 |
94 | # Handle reasoning content separately (for deepseek models)
95 | if hasattr(chunk.choices[0], "delta") and hasattr(
96 | chunk.choices[0].delta, "reasoning_content"
97 | ):
98 | reasoning = chunk.choices[0].delta.reasoning_content
99 | if reasoning:
100 | # Clean up control chars and handle newlines
101 | clean_reasoning = reasoning.replace("\r", "").replace("\b", "")
102 |
103 | # Use callback if available, otherwise use console
104 | if self.stream_callback:
105 | self.stream_callback(clean_reasoning, is_reasoning=True)
106 | else:
107 | # Use yellow color for reasoning
108 | self.console.print(
109 | f"[yellow]{clean_reasoning}[/yellow]",
110 | end="",
111 | highlight=False,
112 | )
113 | reasoning_output += clean_reasoning
114 |
115 | print("\n")
116 |
117 | # Save reasoning to a file for reference
118 | if reasoning_output:
119 | try:
120 | with open("last_reasoning.txt", "w") as f:
121 | f.write(reasoning_output)
122 | except Exception as e:
123 | print(f"Warning: Could not save reasoning to file: {e}")
124 |
125 | return full_response
126 |
127 | except KeyboardInterrupt:
128 | print("\n\nOperation cancelled by user")
129 | return full_response
130 | except Exception as e:
131 | print(f"\nError during streaming: {e}")
132 | return full_response or f"Error: {str(e)}"
133 |
134 |
135 | # These methods are now imported from utils.xml_operations
136 |
137 |
138 | def main():
139 | """Main function to handle command line arguments and run the agent"""
140 | # Check for model argument
141 | model_name = "openrouter/deepseek/deepseek-r1" # Default model
142 |
143 | # Look for --model or -m flag
144 | for i, arg in enumerate(sys.argv):
145 | if arg in ["--model", "-m"] and i + 1 < len(sys.argv):
146 | model_name = sys.argv[i + 1]
147 | # Remove these arguments
148 | sys.argv.pop(i)
149 | sys.argv.pop(i)
150 | break
151 |
152 | agent = Agent(model_name)
153 |
154 | if len(sys.argv) < 2:
155 | print("Usage: ./agent [--model MODEL_NAME] [arguments]")
156 | print("Commands:")
157 | print(" init - Initialize the agent")
158 | print(
159 | " plan [spec_file] - Generate a plan from specification (default: spec.md)"
160 | )
161 | print(" display - Display the current plan")
162 | print(
163 | " update [--notes=text] [--progress=0-100] - Update task status"
164 | )
165 | print(" execute - Execute a specific task")
166 | print(" review - Review code against specifications")
167 | print("\nOptions:")
168 | print(
169 | " --model, -m MODEL_NAME - Specify the model to use (default: openrouter/deepseek/deepseek-r1)"
170 | )
171 | sys.exit(1)
172 |
173 | command = sys.argv[1]
174 |
175 | if command == "init":
176 | agent.initialize()
177 | print("Agent initialized successfully")
178 |
179 | elif command == "plan":
180 | # Use spec.md by default if no file specified
181 | spec_file = sys.argv[2] if len(sys.argv) > 2 else "spec.md"
182 | print(f"Using specification file: {spec_file}")
183 | try:
184 | with open(spec_file, "r") as f:
185 | spec = f.read()
186 |
187 | print(f"Using model: {agent.model_name}")
188 | agent.initialize()
189 |
190 | try:
191 | result = agent.generate_plan(spec)
192 | print(result)
193 |
194 | # Save the plan to a file
195 | with open("agent_plan.xml", "w") as f:
196 | f.write(result)
197 | print("Plan saved to agent_plan.xml")
198 | except KeyboardInterrupt:
199 | print("\nOperation cancelled by user")
200 | sys.exit(1)
201 |
202 | except FileNotFoundError:
203 | print(f"Error: Specification file '{spec_file}' not found")
204 | sys.exit(1)
205 |
206 | elif command == "display":
207 | # Load the plan from file
208 | try:
209 | with open("agent_plan.xml", "r") as f:
210 | xml_content = f.read()
211 | agent.plan_tree = agent.extract_xml_from_response(xml_content, "plan")
212 |
213 | result = agent.display_plan_tree()
214 | print(result)
215 |
216 | except FileNotFoundError:
217 | print("Error: No plan file found. Generate a plan first.")
218 | sys.exit(1)
219 |
220 | elif command == "update":
221 | if len(sys.argv) < 4:
222 | print("Error: Missing task_id or status")
223 | sys.exit(1)
224 |
225 | task_id = sys.argv[2]
226 | status = sys.argv[3]
227 |
228 | # Check for progress and notes flags
229 | progress = None
230 | notes = None
231 |
232 | for i, arg in enumerate(sys.argv[4:], 4):
233 | if arg.startswith("--progress="):
234 | progress = arg.split("=")[1]
235 | elif arg.startswith("--notes="):
236 | notes = arg.split("=")[1]
237 | elif i == 4 and not arg.startswith("--"):
238 | # For backward compatibility, treat the fourth argument as notes
239 | notes = arg
240 |
241 | # Load the plan from file
242 | try:
243 | with open("agent_plan.xml", "r") as f:
244 | xml_content = f.read()
245 | agent.plan_tree = agent.extract_xml_from_response(xml_content, "plan")
246 |
247 | result = agent.update_plan(task_id, status, notes, progress)
248 | print(result)
249 |
250 | # Save the updated plan
251 | with open("agent_plan.xml", "w") as f:
252 | f.write(result)
253 |
254 | except FileNotFoundError:
255 | print("Error: No plan file found. Generate a plan first.")
256 | sys.exit(1)
257 |
258 | elif command == "execute":
259 | if len(sys.argv) < 3:
260 | print("Error: Missing task_id")
261 | sys.exit(1)
262 |
263 | task_id = sys.argv[2]
264 |
265 | # Load the plan from file
266 | try:
267 | with open("agent_plan.xml", "r") as f:
268 | xml_content = f.read()
269 | agent.plan_tree = agent.extract_xml_from_response(xml_content, "plan")
270 |
271 | try:
272 | result = agent.execute_task(task_id)
273 | print(result)
274 |
275 | # Save the actions to a file
276 | with open(f"agent_actions_{task_id}.xml", "w") as f:
277 | f.write(result)
278 | print(f"Actions saved to agent_actions_{task_id}.xml")
279 |
280 | # Save the updated plan
281 | with open("agent_plan.xml", "w") as f:
282 | f.write(agent.format_xml_response({"plan": agent.plan_tree}))
283 | except KeyboardInterrupt:
284 | print("\nOperation cancelled by user")
285 | sys.exit(1)
286 |
287 | except FileNotFoundError:
288 | print("Error: No plan file found. Generate a plan first.")
289 | sys.exit(1)
290 |
291 | elif command == "interactive":
292 | print(f"Starting interactive mode with model: {agent.model_name}")
293 | agent.initialize()
294 |
295 | while True:
296 | try:
297 | user_input = input("\n> ")
298 | if user_input.lower() in ["exit", "quit", "q"]:
299 | break
300 |
301 | # Simple command processing
302 | if user_input.startswith("/"):
303 | parts = user_input[1:].split()
304 | cmd = parts[0] if parts else ""
305 |
306 | if cmd == "plan" and len(parts) > 1:
307 | spec_file = parts[1]
308 | try:
309 | with open(spec_file, "r") as f:
310 | spec = f.read()
311 | result = agent.generate_plan(spec)
312 | print(result)
313 | except FileNotFoundError:
314 | print(f"Error: File '{spec_file}' not found")
315 |
316 | elif cmd == "display":
317 | result = agent.display_plan_tree()
318 | print(result)
319 |
320 | elif cmd == "execute" and len(parts) > 1:
321 | task_id = parts[1]
322 | result = agent.execute_task(task_id)
323 | print(result)
324 |
325 | elif cmd == "help":
326 | print("Available commands:")
327 | print(
328 | " /plan - Generate a plan from specification"
329 | )
330 | print(" /display - Display the current plan")
331 | print(" /execute - Execute a specific task")
332 | print(" /help - Show this help")
333 | print(" exit, quit, q - Exit interactive mode")
334 |
335 | else:
336 | print("Unknown command. Type /help for available commands.")
337 |
338 | # Treat as a prompt to the model
339 | else:
340 | response = agent.stream_reasoning(user_input)
341 | # No need to print response as it's already streamed
342 |
343 | except KeyboardInterrupt:
344 | print("\nUse 'exit' or 'q' to quit")
345 | except Exception as e:
346 | print(f"Error: {e}")
347 |
348 | else:
349 | print(f"Error: Unknown command '{command}'")
350 | sys.exit(1)
351 |
352 |
353 | if __name__ == "__main__":
354 | main()
355 |
--------------------------------------------------------------------------------
/src/utils/input_schema.py:
--------------------------------------------------------------------------------
1 | """
2 | XML schema definitions for input messages to the model.
3 | """
4 |
5 | from typing import Dict, Any, Optional, List
6 |
7 | INPUT_SCHEMA = """
8 |
9 |
10 |
11 |
12 |
13 | Operating system and version
14 | Python version
15 | User's shell
16 | Current date
17 |
18 | User's timezone
19 | Current working directory
20 |
21 |
22 |
23 | User's input text
24 |
25 |
26 |
27 | Previously executed command
28 |
29 | Command exit code
30 | Time taken to execute the command
31 |
32 |
33 |
34 | Current plan XML
35 |
36 |
37 |
38 | List of files in repository
39 | List of git branches
40 | Current git branch
41 | Git status output
42 |
43 |
44 |
45 | Persistent memory content
46 |
47 |
48 |
49 | Previous user message
50 | Previous assistant response
51 |
52 |
53 |
54 |
55 | Type of error that occurred
56 | Error message
57 | Stack trace if available
58 |
59 |
60 |
61 | """
62 |
63 | RESPONSE_SCHEMA = """
64 | Response text
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | """
75 |
76 |
77 | def get_input_schema() -> str:
78 | """
79 | Get the XML schema for input messages.
80 |
81 | Returns:
82 | XML schema string
83 | """
84 | return INPUT_SCHEMA
85 |
86 |
87 | def get_response_schema() -> str:
88 | """
89 | Get the XML schema for response messages.
90 |
91 | Returns:
92 | XML schema string
93 | """
94 | return RESPONSE_SCHEMA
95 |
96 |
97 | def get_schema() -> str:
98 | """
99 | Get the XML schema for response messages.
100 |
101 | Returns:
102 | XML schema string
103 | """
104 | return RESPONSE_SCHEMA # Kept for backward compatibility
105 |
106 |
107 | def format_input_message(
108 | message: str,
109 | system_info: Dict[str, str],
110 | execution_context: Optional[Dict[str, Any]] = None,
111 | plan: Optional[str] = None,
112 | repository_info: Optional[Dict[str, Any]] = None,
113 | memory: Optional[str] = None,
114 | history: Optional[str] = None,
115 | error_context: Optional[Dict[str, str]] = None,
116 | ) -> str:
117 | """
118 | Format an input message according to the XML schema.
119 |
120 | Args:
121 | message: User message
122 | system_info: System information dictionary
123 | execution_context: Optional execution context from previous commands
124 | plan: Optional plan XML
125 | repository_info: Optional repository information
126 | memory: Optional persistent memory content
127 | history: Optional conversation history
128 | error_context: Optional error information
129 |
130 | Returns:
131 | Formatted XML input message
132 | """
133 | # Start with basic structure
134 | xml_parts = [
135 | "",
136 | " ",
137 | ]
138 |
139 | # Add system info
140 | for key, value in system_info.items():
141 | xml_parts.append(f" <{key}>{value}{key}>")
142 |
143 | xml_parts.append(" ")
144 | xml_parts.append(f" {message}")
145 |
146 | # Add execution context if provided
147 | if execution_context:
148 | xml_parts.append(" ")
149 | xml_parts.append(
150 | f" {execution_context.get('command', '')}"
151 | )
152 | xml_parts.append(f" ")
153 | xml_parts.append(
154 | f" {execution_context.get('exit_code', 0)}"
155 | )
156 | if "execution_time" in execution_context:
157 | xml_parts.append(
158 | f" {execution_context['execution_time']}"
159 | )
160 | xml_parts.append(" ")
161 |
162 | # Add plan if provided
163 | if plan:
164 | xml_parts.append(f" {plan}")
165 |
166 | # Add repository info if provided
167 | if repository_info:
168 | xml_parts.append(" ")
169 | if "files" in repository_info:
170 | xml_parts.append(f" {repository_info['files']}")
171 | if "branches" in repository_info:
172 | xml_parts.append(f" {repository_info['branches']}")
173 | if "current_branch" in repository_info:
174 | xml_parts.append(
175 | f" {repository_info['current_branch']}"
176 | )
177 | if "status" in repository_info:
178 | xml_parts.append(f" {repository_info['status']}")
179 | xml_parts.append(" ")
180 |
181 | # Add memory if provided
182 | if memory:
183 | xml_parts.append(f" {memory}")
184 |
185 | # Add history if provided
186 | if history:
187 | xml_parts.append(" ")
188 | xml_parts.append(f" {history}")
189 | xml_parts.append(" ")
190 |
191 | # Add error context if provided
192 | if error_context:
193 | xml_parts.append(" ")
194 | if "error_type" in error_context:
195 | xml_parts.append(
196 | f" {error_context['error_type']}"
197 | )
198 | if "error_message" in error_context:
199 | xml_parts.append(
200 | f" {error_context['error_message']}"
201 | )
202 | if "traceback" in error_context:
203 | xml_parts.append(f" {error_context['traceback']}")
204 | xml_parts.append(" ")
205 |
206 | xml_parts.append("")
207 |
208 | return "\n".join(xml_parts)
209 |
210 |
211 | def escape_xml_content(content: str) -> str:
212 | """
213 | Escape special characters in XML content.
214 |
215 | Args:
216 | content: The string to escape
217 |
218 | Returns:
219 | Escaped string safe for XML inclusion
220 | """
221 | if not content:
222 | return ""
223 |
224 | # Replace special characters with their XML entities
225 | replacements = {
226 | "&": "&",
227 | "<": "<",
228 | ">": ">",
229 | '"': """,
230 | "'": "'",
231 | }
232 |
233 | for char, entity in replacements.items():
234 | content = content.replace(char, entity)
235 |
236 | return content
237 |
238 |
239 | def format_response_message(
240 | message: Optional[str] = None,
241 | actions: Optional[List[Dict[str, Any]]] = None,
242 | file_edits: Optional[List[Dict[str, Any]]] = None,
243 | shell_commands: Optional[List[Dict[str, str]]] = None,
244 | memory_updates: Optional[Dict[str, Any]] = None,
245 | plan_updates: Optional[List[Dict[str, Any]]] = None,
246 | execution_status: Optional[Dict[str, Any]] = None,
247 | error: Optional[Dict[str, str]] = None,
248 | ) -> str:
249 | """
250 | Format a response message according to the XML schema.
251 |
252 | Args:
253 | message: Optional message to the user
254 | actions: Optional list of actions to execute
255 | file_edits: Optional list of file edits
256 | shell_commands: Optional list of shell commands
257 | memory_updates: Optional memory updates
258 | plan_updates: Optional plan updates
259 | execution_status: Optional execution status
260 | error: Optional error information
261 |
262 | Returns:
263 | Formatted XML response message
264 | """
265 | xml_parts = [""]
266 |
267 | # Add message if provided
268 | if message:
269 | xml_parts.append(f" {escape_xml_content(message)}")
270 |
271 | # Add actions if provided
272 | if actions and len(actions) > 0:
273 | xml_parts.append(" ")
274 | for action in actions:
275 | action_type = action.get("type", "")
276 | if action_type == "create_file":
277 | xml_parts.append(
278 | f" "
279 | )
280 | xml_parts.append(
281 | f" {escape_xml_content(action.get('content', ''))}"
282 | )
283 | xml_parts.append(" ")
284 | elif action_type == "modify_file":
285 | xml_parts.append(
286 | f" "
287 | )
288 | for change in action.get("changes", []):
289 | xml_parts.append(" ")
290 | xml_parts.append(
291 | f" {escape_xml_content(change.get('original', ''))}"
292 | )
293 | xml_parts.append(
294 | f" {escape_xml_content(change.get('new', ''))}"
295 | )
296 | xml_parts.append(" ")
297 | xml_parts.append(" ")
298 | elif action_type == "run_command":
299 | xml_parts.append(
300 | f" "
301 | )
302 | xml_parts.append(" ")
303 | xml_parts.append(" ")
304 |
305 | # Add file edits if provided
306 | if file_edits and len(file_edits) > 0:
307 | xml_parts.append(" ")
308 | for edit in file_edits:
309 | xml_parts.append(f" ")
310 | xml_parts.append(
311 | f" {escape_xml_content(edit.get('search', ''))}"
312 | )
313 | xml_parts.append(
314 | f" {escape_xml_content(edit.get('replace', ''))}"
315 | )
316 | xml_parts.append(" ")
317 | xml_parts.append(" ")
318 |
319 | # Add shell commands if provided
320 | if shell_commands and len(shell_commands) > 0:
321 | xml_parts.append(" ")
322 | for cmd in shell_commands:
323 | safe = cmd.get("safe_to_autorun", False)
324 | xml_parts.append(
325 | f" {escape_xml_content(cmd.get('command', ''))}"
326 | )
327 | xml_parts.append(" ")
328 |
329 | # Add memory updates if provided
330 | if memory_updates:
331 | xml_parts.append(" ")
332 | if "edits" in memory_updates:
333 | for edit in memory_updates["edits"]:
334 | xml_parts.append(" ")
335 | xml_parts.append(
336 | f" {escape_xml_content(edit.get('search', ''))}"
337 | )
338 | xml_parts.append(
339 | f" {escape_xml_content(edit.get('replace', ''))}"
340 | )
341 | xml_parts.append(" ")
342 | if "append" in memory_updates:
343 | xml_parts.append(
344 | f" {escape_xml_content(memory_updates['append'])}"
345 | )
346 | xml_parts.append(" ")
347 |
348 | # Add plan updates if provided
349 | if plan_updates and len(plan_updates) > 0:
350 | xml_parts.append(" ")
351 | for task in plan_updates:
352 | if "id" in task and "status" in task:
353 | xml_parts.append(
354 | f" {escape_xml_content(task.get('description', ''))}"
355 | )
356 | elif "id" in task and task.get("is_new", False):
357 | depends = task.get("depends_on", "")
358 | xml_parts.append(
359 | f" {escape_xml_content(task.get('description', ''))}"
360 | )
361 | xml_parts.append(" ")
362 |
363 | # Add execution status if provided
364 | if execution_status:
365 | complete = execution_status.get("complete", False)
366 | needs_input = execution_status.get("needs_user_input", False)
367 | xml_parts.append(
368 | f' '
369 | )
370 | if "message" in execution_status:
371 | xml_parts.append(
372 | f" {escape_xml_content(execution_status['message'])}"
373 | )
374 | if "progress" in execution_status:
375 | percent = execution_status.get("progress", {}).get("percent", 0)
376 | progress_text = escape_xml_content(
377 | execution_status.get("progress", {}).get("text", "")
378 | )
379 | xml_parts.append(
380 | f' '
381 | )
382 | xml_parts.append(" ")
383 |
384 | # Add error if provided
385 | if error:
386 | xml_parts.append(" ")
387 | if "type" in error:
388 | xml_parts.append(f" {escape_xml_content(error['type'])}")
389 | if "message" in error:
390 | xml_parts.append(
391 | f" {escape_xml_content(error['message'])}"
392 | )
393 | if "suggestion" in error:
394 | xml_parts.append(
395 | f" {escape_xml_content(error['suggestion'])}"
396 | )
397 | xml_parts.append(" ")
398 |
399 | xml_parts.append("")
400 |
401 | return "\n".join(xml_parts)
402 |
--------------------------------------------------------------------------------