aloop 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. agent/__init__.py +0 -0
  2. agent/agent.py +182 -0
  3. agent/base.py +406 -0
  4. agent/context.py +126 -0
  5. agent/prompts/__init__.py +1 -0
  6. agent/todo.py +149 -0
  7. agent/tool_executor.py +54 -0
  8. agent/verification.py +135 -0
  9. aloop-0.1.1.dist-info/METADATA +252 -0
  10. aloop-0.1.1.dist-info/RECORD +66 -0
  11. aloop-0.1.1.dist-info/WHEEL +5 -0
  12. aloop-0.1.1.dist-info/entry_points.txt +2 -0
  13. aloop-0.1.1.dist-info/licenses/LICENSE +21 -0
  14. aloop-0.1.1.dist-info/top_level.txt +9 -0
  15. cli.py +19 -0
  16. config.py +146 -0
  17. interactive.py +865 -0
  18. llm/__init__.py +51 -0
  19. llm/base.py +26 -0
  20. llm/compat.py +226 -0
  21. llm/content_utils.py +309 -0
  22. llm/litellm_adapter.py +450 -0
  23. llm/message_types.py +245 -0
  24. llm/model_manager.py +265 -0
  25. llm/retry.py +95 -0
  26. main.py +246 -0
  27. memory/__init__.py +20 -0
  28. memory/compressor.py +554 -0
  29. memory/manager.py +538 -0
  30. memory/serialization.py +82 -0
  31. memory/short_term.py +88 -0
  32. memory/store/__init__.py +6 -0
  33. memory/store/memory_store.py +100 -0
  34. memory/store/yaml_file_memory_store.py +414 -0
  35. memory/token_tracker.py +203 -0
  36. memory/types.py +51 -0
  37. tools/__init__.py +6 -0
  38. tools/advanced_file_ops.py +557 -0
  39. tools/base.py +51 -0
  40. tools/calculator.py +50 -0
  41. tools/code_navigator.py +975 -0
  42. tools/explore.py +254 -0
  43. tools/file_ops.py +150 -0
  44. tools/git_tools.py +791 -0
  45. tools/notify.py +69 -0
  46. tools/parallel_execute.py +420 -0
  47. tools/session_manager.py +205 -0
  48. tools/shell.py +147 -0
  49. tools/shell_background.py +470 -0
  50. tools/smart_edit.py +491 -0
  51. tools/todo.py +130 -0
  52. tools/web_fetch.py +673 -0
  53. tools/web_search.py +61 -0
  54. utils/__init__.py +15 -0
  55. utils/logger.py +105 -0
  56. utils/model_pricing.py +49 -0
  57. utils/runtime.py +75 -0
  58. utils/terminal_ui.py +422 -0
  59. utils/tui/__init__.py +39 -0
  60. utils/tui/command_registry.py +49 -0
  61. utils/tui/components.py +306 -0
  62. utils/tui/input_handler.py +393 -0
  63. utils/tui/model_ui.py +204 -0
  64. utils/tui/progress.py +292 -0
  65. utils/tui/status_bar.py +178 -0
  66. utils/tui/theme.py +165 -0
agent/context.py ADDED
@@ -0,0 +1,126 @@
1
+ """Context injection module for providing environment information to agents."""
2
+
3
+ import asyncio
4
+ import os
5
+ import platform
6
+ from datetime import datetime
7
+ from typing import Any, Dict
8
+
9
+
10
+ def get_working_directory() -> str:
11
+ """Get the current working directory.
12
+
13
+ Returns:
14
+ Absolute path of current working directory
15
+ """
16
+ return os.getcwd()
17
+
18
+
19
+ def get_platform_info() -> Dict[str, str]:
20
+ """Get platform and system information.
21
+
22
+ Returns:
23
+ Dictionary with platform details
24
+ """
25
+ return {
26
+ "system": platform.system(), # Linux, Darwin, Windows
27
+ "platform": os.name, # posix, nt
28
+ "python_version": platform.python_version(),
29
+ }
30
+
31
+
32
+ async def _run_git_command(args: list[str], timeout: float = 2.0) -> tuple[int, str, str]:
33
+ process = await asyncio.create_subprocess_exec(
34
+ *args,
35
+ stdout=asyncio.subprocess.PIPE,
36
+ stderr=asyncio.subprocess.PIPE,
37
+ )
38
+ try:
39
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
40
+ except asyncio.TimeoutError:
41
+ process.kill()
42
+ await process.communicate()
43
+ raise
44
+ returncode = process.returncode if process.returncode is not None else 0
45
+ return (
46
+ returncode,
47
+ stdout.decode(errors="ignore").strip(),
48
+ stderr.decode(errors="ignore").strip(),
49
+ )
50
+
51
+
52
+ async def get_git_status() -> Dict[str, Any]:
53
+ """Get git repository information if available.
54
+
55
+ Returns:
56
+ Dictionary with git information or is_repo=False
57
+ """
58
+ try:
59
+ returncode, _, _ = await _run_git_command(["git", "rev-parse", "--git-dir"], timeout=2)
60
+ if returncode != 0:
61
+ return {"is_repo": False}
62
+
63
+ _, branch, _ = await _run_git_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], 2)
64
+ _, status, _ = await _run_git_command(["git", "status", "--short"], 2)
65
+ _, recent_commits, _ = await _run_git_command(["git", "log", "-5", "--oneline"], 2)
66
+
67
+ try:
68
+ _, remote_head, _ = await _run_git_command(
69
+ ["git", "symbolic-ref", "refs/remotes/origin/HEAD"], 2
70
+ )
71
+ main_branch = remote_head.split("/")[-1] if remote_head else "main"
72
+ except asyncio.TimeoutError:
73
+ main_branch = "main"
74
+
75
+ return {
76
+ "is_repo": True,
77
+ "branch": branch,
78
+ "main_branch": main_branch,
79
+ "status": status if status else "Clean working directory",
80
+ "recent_commits": recent_commits,
81
+ "has_uncommitted_changes": bool(status),
82
+ }
83
+
84
+ except (asyncio.TimeoutError, FileNotFoundError):
85
+ return {"is_repo": False}
86
+
87
+
88
+ async def format_context_prompt() -> str:
89
+ """Format environment context as a prompt section.
90
+
91
+ Returns:
92
+ Formatted context string with XML tags
93
+
94
+ Note:
95
+ Git information is intentionally excluded to save tokens.
96
+ Agents can use git_status, git_log, and other git tools
97
+ to get up-to-date repository information when needed.
98
+ """
99
+ cwd = get_working_directory()
100
+ platform_info = get_platform_info()
101
+ today = datetime.now().strftime("%Y-%m-%d")
102
+
103
+ lines = [
104
+ "<environment>",
105
+ f"Working directory: {cwd}",
106
+ f"Platform: {platform_info['system']}",
107
+ f"Today's date: {today}",
108
+ "</environment>\n",
109
+ ]
110
+
111
+ return "\n".join(lines)
112
+
113
+
114
+ async def get_context_dict() -> Dict[str, Any]:
115
+ """Get context information as a dictionary.
116
+
117
+ Returns:
118
+ Dictionary with all context information
119
+ """
120
+ return {
121
+ "working_directory": get_working_directory(),
122
+ "platform": get_platform_info(),
123
+ "git": await get_git_status(),
124
+ "date": datetime.now().strftime("%Y-%m-%d"),
125
+ "timestamp": datetime.now().isoformat(),
126
+ }
@@ -0,0 +1 @@
1
+ """Prompts for agent operations."""
agent/todo.py ADDED
@@ -0,0 +1,149 @@
1
+ """Todo list management for agents to track complex multi-step tasks."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Dict, List
6
+
7
+
8
+ class TodoStatus(Enum):
9
+ """Status of a todo item."""
10
+
11
+ PENDING = "pending"
12
+ IN_PROGRESS = "in_progress"
13
+ COMPLETED = "completed"
14
+
15
+
16
+ @dataclass
17
+ class TodoItem:
18
+ """A single todo item."""
19
+
20
+ content: str # Imperative form: "Fix authentication bug"
21
+ activeForm: str # Present continuous form: "Fixing authentication bug"
22
+ status: TodoStatus
23
+
24
+ def to_dict(self) -> dict:
25
+ """Convert to dictionary for serialization."""
26
+ return {"content": self.content, "activeForm": self.activeForm, "status": self.status.value}
27
+
28
+
29
+ class TodoList:
30
+ """Manages a list of todo items for an agent."""
31
+
32
+ def __init__(self) -> None:
33
+ self._items: List[TodoItem] = []
34
+
35
+ def add(self, content: str, activeForm: str) -> str:
36
+ """Add a new todo item.
37
+
38
+ Args:
39
+ content: Imperative form (e.g., "Read CSV file")
40
+ activeForm: Present continuous form (e.g., "Reading CSV file")
41
+
42
+ Returns:
43
+ Success message with item index
44
+ """
45
+ if not content or not activeForm:
46
+ return "Error: Both content and activeForm are required"
47
+
48
+ item = TodoItem(content=content, activeForm=activeForm, status=TodoStatus.PENDING)
49
+ self._items.append(item)
50
+ return f"Added todo #{len(self._items)}: {content}"
51
+
52
+ def update_status(self, index: int, status: str) -> str:
53
+ """Update the status of a todo item.
54
+
55
+ Args:
56
+ index: 1-indexed position of the todo item
57
+ status: New status (pending, in_progress, or completed)
58
+
59
+ Returns:
60
+ Success or error message
61
+ """
62
+ if index < 1 or index > len(self._items):
63
+ return f"Error: Invalid index {index}. Valid range: 1-{len(self._items)}"
64
+
65
+ try:
66
+ new_status = TodoStatus(status)
67
+ except ValueError:
68
+ return f"Error: Invalid status '{status}'. Must be: pending, in_progress, or completed"
69
+
70
+ # Check the ONE in_progress rule
71
+ if new_status == TodoStatus.IN_PROGRESS:
72
+ in_progress_count = sum(
73
+ 1 for item in self._items if item.status == TodoStatus.IN_PROGRESS
74
+ )
75
+ if in_progress_count > 0:
76
+ in_progress_items = [
77
+ i + 1
78
+ for i, item in enumerate(self._items)
79
+ if item.status == TodoStatus.IN_PROGRESS
80
+ ]
81
+ return f"Error: Task #{in_progress_items[0]} is already in_progress. Complete it first before starting another task."
82
+
83
+ item = self._items[index - 1]
84
+ old_status = item.status.value
85
+ item.status = new_status
86
+
87
+ return f"Updated todo #{index} status: {old_status} → {status}"
88
+
89
+ def get_current(self) -> List[TodoItem]:
90
+ """Get all current todo items."""
91
+ return self._items.copy()
92
+
93
+ def remove(self, index: int) -> str:
94
+ """Remove a todo item.
95
+
96
+ Args:
97
+ index: 1-indexed position of the todo item
98
+
99
+ Returns:
100
+ Success or error message
101
+ """
102
+ if index < 1 or index > len(self._items):
103
+ return f"Error: Invalid index {index}. Valid range: 1-{len(self._items)}"
104
+
105
+ item = self._items.pop(index - 1)
106
+ return f"Removed todo: {item.content}"
107
+
108
+ def format_list(self) -> str:
109
+ """Format the todo list for display."""
110
+ if not self._items:
111
+ return "No todos in the list"
112
+
113
+ lines = ["Current Todo List:"]
114
+ for i, item in enumerate(self._items, 1):
115
+ status_symbol = {
116
+ TodoStatus.PENDING: "⏳",
117
+ TodoStatus.IN_PROGRESS: "🔄",
118
+ TodoStatus.COMPLETED: "✅",
119
+ }[item.status]
120
+
121
+ status_text = item.activeForm if item.status == TodoStatus.IN_PROGRESS else item.content
122
+ lines.append(f"{i}. {status_symbol} [{item.status.value}] {status_text}")
123
+
124
+ # Summary
125
+ pending = sum(1 for item in self._items if item.status == TodoStatus.PENDING)
126
+ in_progress = sum(1 for item in self._items if item.status == TodoStatus.IN_PROGRESS)
127
+ completed = sum(1 for item in self._items if item.status == TodoStatus.COMPLETED)
128
+
129
+ lines.append(
130
+ f"\nSummary: {completed} completed, {in_progress} in progress, {pending} pending"
131
+ )
132
+
133
+ return "\n".join(lines)
134
+
135
+ def get_summary(self) -> Dict[str, int]:
136
+ """Get summary statistics."""
137
+ return {
138
+ "total": len(self._items),
139
+ "pending": sum(1 for item in self._items if item.status == TodoStatus.PENDING),
140
+ "in_progress": sum(1 for item in self._items if item.status == TodoStatus.IN_PROGRESS),
141
+ "completed": sum(1 for item in self._items if item.status == TodoStatus.COMPLETED),
142
+ }
143
+
144
+ def clear_completed(self) -> str:
145
+ """Remove all completed items."""
146
+ before_count = len(self._items)
147
+ self._items = [item for item in self._items if item.status != TodoStatus.COMPLETED]
148
+ removed = before_count - len(self._items)
149
+ return f"Removed {removed} completed todo(s)"
agent/tool_executor.py ADDED
@@ -0,0 +1,54 @@
1
+ """Tool execution engine for managing and executing tools."""
2
+
3
+ import asyncio
4
+ from typing import Any, Dict, List
5
+
6
+ from config import Config
7
+ from tools.base import BaseTool
8
+
9
+
10
+ class ToolExecutor:
11
+ """Executes tools called by the LLM."""
12
+
13
+ def __init__(self, tools: List[BaseTool]):
14
+ """Initialize with a list of tools."""
15
+ self.tools = {tool.name: tool for tool in tools}
16
+
17
+ async def execute_tool_call(self, tool_name: str, tool_input: Dict[str, Any]) -> str:
18
+ """Execute a single tool call and return result."""
19
+ if tool_name not in self.tools:
20
+ return f"Error: Tool '{tool_name}' not found"
21
+
22
+ try:
23
+ timeout = Config.TOOL_TIMEOUT
24
+ if "timeout" in tool_input and tool_input["timeout"] is not None:
25
+ try:
26
+ timeout = float(tool_input["timeout"])
27
+ except (TypeError, ValueError):
28
+ timeout = Config.TOOL_TIMEOUT
29
+
30
+ if timeout is not None and timeout > 0:
31
+ async with asyncio.timeout(timeout):
32
+ result = await self.tools[tool_name].execute(**tool_input)
33
+ else:
34
+ result = await self.tools[tool_name].execute(**tool_input)
35
+ return str(result)
36
+ except asyncio.CancelledError:
37
+ # Re-raise CancelledError to allow proper cleanup
38
+ raise
39
+ except TimeoutError:
40
+ return f"Error: Tool '{tool_name}' timed out after {timeout}s"
41
+ except Exception as e:
42
+ return f"Error executing {tool_name}: {str(e)}"
43
+
44
+ def get_tool_schemas(self) -> List[Dict[str, Any]]:
45
+ """Get Anthropic-formatted schemas for all tools."""
46
+ return [tool.to_anthropic_schema() for tool in self.tools.values()]
47
+
48
+ def add_tool(self, tool: BaseTool):
49
+ """Add a tool to the executor.
50
+
51
+ Args:
52
+ tool: Tool instance to add
53
+ """
54
+ self.tools[tool.name] = tool
agent/verification.py ADDED
@@ -0,0 +1,135 @@
1
+ """Verification interface and default LLM verifier for the Ralph Loop.
2
+
3
+ The verifier judges whether the agent's final answer truly satisfies the
4
+ original task. If not, feedback is returned so the outer loop can re-enter
5
+ the inner ReAct loop with corrective guidance.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
12
+
13
+ from llm import LLMMessage
14
+ from utils import get_logger
15
+ from utils.tui.progress import AsyncSpinner
16
+
17
+ if TYPE_CHECKING:
18
+ from llm import LiteLLMAdapter
19
+ from utils.tui.terminal_ui import TerminalUI
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class VerificationResult:
26
+ """Result of a verification check."""
27
+
28
+ complete: bool
29
+ reason: str
30
+
31
+
32
+ @runtime_checkable
33
+ class Verifier(Protocol):
34
+ """Protocol for task-completion verifiers."""
35
+
36
+ async def verify(
37
+ self,
38
+ task: str,
39
+ result: str,
40
+ iteration: int,
41
+ previous_results: list[VerificationResult],
42
+ ) -> VerificationResult:
43
+ """Judge whether *result* satisfies *task*.
44
+
45
+ Args:
46
+ task: The original user task description.
47
+ result: The agent's final answer from the inner loop.
48
+ iteration: Current outer-loop iteration (1-indexed).
49
+ previous_results: Verification results from earlier iterations.
50
+
51
+ Returns:
52
+ VerificationResult indicating completion status and reasoning.
53
+ """
54
+ ... # pragma: no cover
55
+
56
+
57
+ _VERIFICATION_PROMPT = """\
58
+ You are a strict verification assistant. Your job is to determine whether an \
59
+ AI agent's answer fully and correctly completes the user's original task.
60
+
61
+ <task>
62
+ {task}
63
+ </task>
64
+
65
+ <agent_answer>
66
+ {result}
67
+ </agent_answer>
68
+
69
+ {previous_context}
70
+
71
+ <judgment_rules>
72
+ 1. If the task is a ONE-TIME request (e.g. "calculate 1+1", "summarize this file"), \
73
+ judge whether the answer is correct and complete.
74
+
75
+ 2. If the task requires MULTIPLE steps and only some were done, respond INCOMPLETE \
76
+ with specific feedback on what remains.
77
+ </judgment_rules>
78
+
79
+ Respond with EXACTLY one of:
80
+ - COMPLETE: <brief reason why the task is satisfied>
81
+ - INCOMPLETE: <specific feedback on what is missing or wrong>
82
+
83
+ Do NOT restate the answer. Only judge it."""
84
+
85
+
86
+ class LLMVerifier:
87
+ """Default verifier that uses a lightweight LLM call (no tools)."""
88
+
89
+ def __init__(self, llm: LiteLLMAdapter, terminal_ui: TerminalUI | None = None):
90
+ self.llm = llm
91
+ self._tui = terminal_ui
92
+
93
+ async def verify(
94
+ self,
95
+ task: str,
96
+ result: str,
97
+ iteration: int,
98
+ previous_results: list[VerificationResult],
99
+ ) -> VerificationResult:
100
+ previous_context = ""
101
+ if previous_results:
102
+ lines = []
103
+ for i, pr in enumerate(previous_results, 1):
104
+ status = "complete" if pr.complete else "incomplete"
105
+ lines.append(f" Attempt {i}: {status} — {pr.reason}")
106
+ previous_context = "Previous verification attempts:\n" + "\n".join(lines)
107
+
108
+ prompt = _VERIFICATION_PROMPT.format(
109
+ task=task,
110
+ result=result[:4000], # Truncate to avoid excessive tokens
111
+ previous_context=previous_context,
112
+ )
113
+
114
+ messages = [
115
+ LLMMessage(role="system", content="You are a task-completion verifier."),
116
+ LLMMessage(role="user", content=prompt),
117
+ ]
118
+
119
+ console = self._tui.console if self._tui else None
120
+ if console:
121
+ async with AsyncSpinner(console, "Verifying completion..."):
122
+ response = await self.llm.call_async(messages=messages, tools=None, max_tokens=512)
123
+ else:
124
+ response = await self.llm.call_async(messages=messages, tools=None, max_tokens=512)
125
+
126
+ text = (response.content or "").strip()
127
+ logger.debug(f"Verification response (iter {iteration}): {text}")
128
+
129
+ upper = text.upper()
130
+ if upper.startswith("COMPLETE"):
131
+ reason = text.split(":", 1)[1].strip() if ":" in text else text
132
+ return VerificationResult(complete=True, reason=reason)
133
+ else:
134
+ reason = text.split(":", 1)[1].strip() if ":" in text else text
135
+ return VerificationResult(complete=False, reason=reason)