aloop 0.1.0__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.
Potentially problematic release.
This version of aloop might be problematic. Click here for more details.
- agent/__init__.py +0 -0
- agent/agent.py +182 -0
- agent/base.py +406 -0
- agent/context.py +126 -0
- agent/todo.py +149 -0
- agent/tool_executor.py +54 -0
- agent/verification.py +135 -0
- aloop-0.1.0.dist-info/METADATA +246 -0
- aloop-0.1.0.dist-info/RECORD +62 -0
- aloop-0.1.0.dist-info/WHEEL +5 -0
- aloop-0.1.0.dist-info/entry_points.txt +2 -0
- aloop-0.1.0.dist-info/licenses/LICENSE +21 -0
- aloop-0.1.0.dist-info/top_level.txt +9 -0
- cli.py +19 -0
- config.py +146 -0
- interactive.py +865 -0
- llm/__init__.py +51 -0
- llm/base.py +26 -0
- llm/compat.py +226 -0
- llm/content_utils.py +309 -0
- llm/litellm_adapter.py +450 -0
- llm/message_types.py +245 -0
- llm/model_manager.py +265 -0
- llm/retry.py +95 -0
- main.py +246 -0
- memory/__init__.py +20 -0
- memory/compressor.py +554 -0
- memory/manager.py +538 -0
- memory/serialization.py +82 -0
- memory/short_term.py +88 -0
- memory/token_tracker.py +203 -0
- memory/types.py +51 -0
- tools/__init__.py +6 -0
- tools/advanced_file_ops.py +557 -0
- tools/base.py +51 -0
- tools/calculator.py +50 -0
- tools/code_navigator.py +975 -0
- tools/explore.py +254 -0
- tools/file_ops.py +150 -0
- tools/git_tools.py +791 -0
- tools/notify.py +69 -0
- tools/parallel_execute.py +420 -0
- tools/session_manager.py +205 -0
- tools/shell.py +147 -0
- tools/shell_background.py +470 -0
- tools/smart_edit.py +491 -0
- tools/todo.py +130 -0
- tools/web_fetch.py +673 -0
- tools/web_search.py +61 -0
- utils/__init__.py +15 -0
- utils/logger.py +105 -0
- utils/model_pricing.py +49 -0
- utils/runtime.py +75 -0
- utils/terminal_ui.py +422 -0
- utils/tui/__init__.py +39 -0
- utils/tui/command_registry.py +49 -0
- utils/tui/components.py +306 -0
- utils/tui/input_handler.py +393 -0
- utils/tui/model_ui.py +204 -0
- utils/tui/progress.py +292 -0
- utils/tui/status_bar.py +178 -0
- 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
|
+
}
|
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)
|