abstractagent 0.2.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.
- abstractagent/__init__.py +49 -0
- abstractagent/adapters/__init__.py +6 -0
- abstractagent/adapters/codeact_runtime.py +397 -0
- abstractagent/adapters/react_runtime.py +390 -0
- abstractagent/agents/__init__.py +15 -0
- abstractagent/agents/base.py +421 -0
- abstractagent/agents/codeact.py +194 -0
- abstractagent/agents/react.py +210 -0
- abstractagent/logic/__init__.py +19 -0
- abstractagent/logic/builtins.py +29 -0
- abstractagent/logic/codeact.py +166 -0
- abstractagent/logic/react.py +126 -0
- abstractagent/logic/types.py +30 -0
- abstractagent/repl.py +457 -0
- abstractagent/sandbox/__init__.py +7 -0
- abstractagent/sandbox/interface.py +22 -0
- abstractagent/sandbox/local.py +68 -0
- abstractagent/tools/__init__.py +58 -0
- abstractagent/tools/code_execution.py +45 -0
- abstractagent/tools/self_improve.py +56 -0
- abstractagent/ui/__init__.py +5 -0
- abstractagent/ui/question.py +197 -0
- abstractagent-0.2.0.dist-info/METADATA +134 -0
- abstractagent-0.2.0.dist-info/RECORD +28 -0
- abstractagent-0.2.0.dist-info/WHEEL +5 -0
- abstractagent-0.2.0.dist-info/entry_points.txt +2 -0
- abstractagent-0.2.0.dist-info/licenses/LICENSE +25 -0
- abstractagent-0.2.0.dist-info/top_level.txt +1 -0
abstractagent/repl.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""Async REPL for the ReAct agent.
|
|
2
|
+
|
|
3
|
+
Provides an interactive interface with:
|
|
4
|
+
- Real-time step visibility
|
|
5
|
+
- Pause/resume capability
|
|
6
|
+
- Interactive question handling
|
|
7
|
+
- Run persistence for resume across sessions
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import sys
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
from typing import Dict, Any, Optional
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from abstractcore.tools import ToolDefinition
|
|
19
|
+
from abstractruntime import (
|
|
20
|
+
RunStatus,
|
|
21
|
+
InMemoryRunStore,
|
|
22
|
+
InMemoryLedgerStore,
|
|
23
|
+
JsonFileRunStore,
|
|
24
|
+
JsonlLedgerStore,
|
|
25
|
+
)
|
|
26
|
+
from abstractruntime.integrations.abstractcore import MappingToolExecutor, create_local_runtime
|
|
27
|
+
|
|
28
|
+
from .agents.react import ReactAgent
|
|
29
|
+
from .tools import ALL_TOOLS
|
|
30
|
+
from .ui.question import get_user_response_async, Colors, _c
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AgentREPL:
|
|
34
|
+
"""Interactive REPL for the ReAct agent."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, provider: str, model: str, state_file: Optional[str] = None):
|
|
37
|
+
self.provider = provider
|
|
38
|
+
self.model = model
|
|
39
|
+
self.state_file = state_file
|
|
40
|
+
self.agent: Optional[ReactAgent] = None
|
|
41
|
+
self._interrupted = False
|
|
42
|
+
self._tools = list(ALL_TOOLS)
|
|
43
|
+
|
|
44
|
+
# Setup runtime with persistence. If a state file is provided, use
|
|
45
|
+
# file-backed stores so runs can resume across process restarts.
|
|
46
|
+
if self.state_file:
|
|
47
|
+
base_dir = Path(self.state_file).expanduser().resolve()
|
|
48
|
+
store_dir = base_dir.with_name(base_dir.name + ".d")
|
|
49
|
+
self.run_store = JsonFileRunStore(store_dir)
|
|
50
|
+
self.ledger_store = JsonlLedgerStore(store_dir)
|
|
51
|
+
else:
|
|
52
|
+
self.run_store = InMemoryRunStore()
|
|
53
|
+
self.ledger_store = InMemoryLedgerStore()
|
|
54
|
+
|
|
55
|
+
self.runtime = create_local_runtime(
|
|
56
|
+
provider=provider,
|
|
57
|
+
model=model,
|
|
58
|
+
run_store=self.run_store,
|
|
59
|
+
ledger_store=self.ledger_store,
|
|
60
|
+
tool_executor=MappingToolExecutor.from_tools(self._tools),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.agent = ReactAgent(
|
|
64
|
+
runtime=self.runtime,
|
|
65
|
+
tools=self._tools,
|
|
66
|
+
on_step=self.print_step,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def print_step(self, step: str, data: Dict[str, Any]) -> None:
|
|
70
|
+
"""Print a step to the console with formatting."""
|
|
71
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
72
|
+
ts = _c(f"[{timestamp}]", Colors.DIM)
|
|
73
|
+
|
|
74
|
+
if step == "init":
|
|
75
|
+
task = data.get('task', '')[:60]
|
|
76
|
+
print(f"\n{ts} {_c('Starting:', Colors.CYAN, Colors.BOLD)} {task}")
|
|
77
|
+
elif step == "reason":
|
|
78
|
+
iteration = data.get('iteration', '?')
|
|
79
|
+
print(f"{ts} {_c(f'Thinking (step {iteration})...', Colors.YELLOW)}")
|
|
80
|
+
elif step == "parse":
|
|
81
|
+
has_tools = data.get('has_tool_calls', False)
|
|
82
|
+
if has_tools:
|
|
83
|
+
print(f"{ts} {_c('Decided to use tools', Colors.BLUE)}")
|
|
84
|
+
elif step == "act":
|
|
85
|
+
tool = data.get('tool', 'unknown')
|
|
86
|
+
args = data.get('args', {})
|
|
87
|
+
args_str = json.dumps(args) if args else ""
|
|
88
|
+
if len(args_str) > 50:
|
|
89
|
+
args_str = args_str[:47] + "..."
|
|
90
|
+
print(f"{ts} {_c('Tool:', Colors.GREEN)} {tool}({args_str})")
|
|
91
|
+
elif step == "observe":
|
|
92
|
+
result = data.get('result', '')[:80]
|
|
93
|
+
print(f"{ts} {_c('Result:', Colors.DIM)} {result}")
|
|
94
|
+
elif step == "ask_user":
|
|
95
|
+
print(f"{ts} {_c('Agent has a question...', Colors.MAGENTA, Colors.BOLD)}")
|
|
96
|
+
elif step == "user_response":
|
|
97
|
+
response = data.get('response', '')[:50]
|
|
98
|
+
print(f"{ts} {_c('You answered:', Colors.MAGENTA)} {response}")
|
|
99
|
+
elif step == "done":
|
|
100
|
+
answer = data.get('answer', '')
|
|
101
|
+
print(f"\n{ts} {_c('ANSWER:', Colors.GREEN, Colors.BOLD)}")
|
|
102
|
+
print(_c("─" * 60, Colors.DIM))
|
|
103
|
+
print(answer)
|
|
104
|
+
print(_c("─" * 60, Colors.DIM))
|
|
105
|
+
elif step == "max_iterations":
|
|
106
|
+
print(f"{ts} {_c('Max iterations reached', Colors.YELLOW)}")
|
|
107
|
+
|
|
108
|
+
async def handle_waiting_state(self) -> bool:
|
|
109
|
+
"""Handle agent waiting state (questions).
|
|
110
|
+
|
|
111
|
+
Returns True if handled and should continue, False to stop.
|
|
112
|
+
"""
|
|
113
|
+
if not self.agent or not self.agent.is_waiting():
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
question = self.agent.get_pending_question()
|
|
117
|
+
if not question:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
# Get user response via UI
|
|
121
|
+
response = await get_user_response_async(
|
|
122
|
+
prompt=question.get("prompt", "Please respond:"),
|
|
123
|
+
choices=question.get("choices"),
|
|
124
|
+
allow_free_text=question.get("allow_free_text", True),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if not response:
|
|
128
|
+
print(_c("No response provided. Agent paused.", Colors.YELLOW))
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Resume agent with response
|
|
132
|
+
self.agent.resume(response)
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
async def run_agent_async(self, task: str) -> None:
|
|
136
|
+
"""Run the agent asynchronously with step visibility."""
|
|
137
|
+
self.agent.start(task)
|
|
138
|
+
if self.state_file:
|
|
139
|
+
self.agent.save_state(self.state_file)
|
|
140
|
+
self._interrupted = False
|
|
141
|
+
|
|
142
|
+
print(f"\n{_c('═' * 60, Colors.CYAN)}")
|
|
143
|
+
print(f"{_c('Task:', Colors.BOLD)} {task}")
|
|
144
|
+
print(f"{_c('═' * 60, Colors.CYAN)}")
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
while not self._interrupted:
|
|
148
|
+
state = self.agent.step()
|
|
149
|
+
|
|
150
|
+
if state.status == RunStatus.COMPLETED:
|
|
151
|
+
print(f"\n{_c('═' * 60, Colors.GREEN)}")
|
|
152
|
+
print(f"{_c('Completed', Colors.GREEN, Colors.BOLD)} in {state.output.get('iterations', '?')} steps")
|
|
153
|
+
print(f"{_c('═' * 60, Colors.GREEN)}")
|
|
154
|
+
if self.state_file:
|
|
155
|
+
self.agent.clear_state(self.state_file)
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
elif state.status == RunStatus.WAITING:
|
|
159
|
+
# Handle question
|
|
160
|
+
handled = await self.handle_waiting_state()
|
|
161
|
+
if not handled:
|
|
162
|
+
print(f"\n{_c('Agent paused.', Colors.YELLOW)} Type 'resume' to continue.")
|
|
163
|
+
break
|
|
164
|
+
# After handling, continue the loop to process next step
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
elif state.status == RunStatus.FAILED:
|
|
168
|
+
print(f"\n{_c('Failed:', Colors.YELLOW)} {state.error}")
|
|
169
|
+
if self.state_file:
|
|
170
|
+
self.agent.clear_state(self.state_file)
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
# Small delay for interrupt handling
|
|
174
|
+
await asyncio.sleep(0.01)
|
|
175
|
+
|
|
176
|
+
except asyncio.CancelledError:
|
|
177
|
+
print(f"\n{_c('Interrupted', Colors.YELLOW)}")
|
|
178
|
+
self._interrupted = True
|
|
179
|
+
|
|
180
|
+
def interrupt(self) -> None:
|
|
181
|
+
"""Interrupt the running agent."""
|
|
182
|
+
self._interrupted = True
|
|
183
|
+
print(f"\n{_c('Interrupting...', Colors.YELLOW)} (state preserved)")
|
|
184
|
+
|
|
185
|
+
async def resume_agent(self) -> None:
|
|
186
|
+
"""Resume a paused agent."""
|
|
187
|
+
if not self.agent:
|
|
188
|
+
print("No agent to resume. Start a new task.")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
state = self.agent.get_state()
|
|
192
|
+
if not state:
|
|
193
|
+
print("No active run.")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if state.status == RunStatus.WAITING:
|
|
197
|
+
handled = await self.handle_waiting_state()
|
|
198
|
+
if handled:
|
|
199
|
+
# Continue running after handling
|
|
200
|
+
await self._continue_running()
|
|
201
|
+
elif state.status == RunStatus.RUNNING:
|
|
202
|
+
self._interrupted = False
|
|
203
|
+
await self._continue_running()
|
|
204
|
+
else:
|
|
205
|
+
print(f"Agent is {state.status.value}, cannot resume.")
|
|
206
|
+
|
|
207
|
+
async def _continue_running(self) -> None:
|
|
208
|
+
"""Continue running the agent after resume."""
|
|
209
|
+
try:
|
|
210
|
+
while not self._interrupted:
|
|
211
|
+
state = self.agent.step()
|
|
212
|
+
|
|
213
|
+
if state.status == RunStatus.COMPLETED:
|
|
214
|
+
print(f"\n{_c('═' * 60, Colors.GREEN)}")
|
|
215
|
+
print(f"{_c('Completed', Colors.GREEN, Colors.BOLD)} in {state.output.get('iterations', '?')} steps")
|
|
216
|
+
print(f"{_c('═' * 60, Colors.GREEN)}")
|
|
217
|
+
break
|
|
218
|
+
elif state.status == RunStatus.WAITING:
|
|
219
|
+
handled = await self.handle_waiting_state()
|
|
220
|
+
if not handled:
|
|
221
|
+
print(f"\n{_c('Agent paused.', Colors.YELLOW)} Type 'resume' to continue.")
|
|
222
|
+
break
|
|
223
|
+
continue
|
|
224
|
+
elif state.status == RunStatus.FAILED:
|
|
225
|
+
print(f"\n{_c('Failed:', Colors.YELLOW)} {state.error}")
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
await asyncio.sleep(0.01)
|
|
229
|
+
except asyncio.CancelledError:
|
|
230
|
+
self._interrupted = True
|
|
231
|
+
|
|
232
|
+
def show_status(self) -> None:
|
|
233
|
+
"""Show current agent status."""
|
|
234
|
+
if not self.agent:
|
|
235
|
+
print("No active agent")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
state = self.agent.get_state()
|
|
239
|
+
if not state:
|
|
240
|
+
print("No active run")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
print(f"\n{_c('Agent Status', Colors.CYAN, Colors.BOLD)}")
|
|
244
|
+
print(_c("─" * 40, Colors.DIM))
|
|
245
|
+
print(f" Run ID: {state.run_id[:16]}...")
|
|
246
|
+
print(f" Status: {_c(state.status.value, Colors.GREEN if state.status == RunStatus.COMPLETED else Colors.YELLOW)}")
|
|
247
|
+
print(f" Node: {state.current_node}")
|
|
248
|
+
print(f" Iteration: {state.vars.get('iteration', 0)}")
|
|
249
|
+
|
|
250
|
+
if state.status == RunStatus.WAITING and state.waiting:
|
|
251
|
+
print(f"\n {_c('Waiting for:', Colors.MAGENTA)} {state.waiting.reason.value}")
|
|
252
|
+
if state.waiting.prompt:
|
|
253
|
+
print(f" {_c('Question:', Colors.MAGENTA)} {state.waiting.prompt[:50]}...")
|
|
254
|
+
|
|
255
|
+
print(_c("─" * 40, Colors.DIM))
|
|
256
|
+
|
|
257
|
+
def show_history(self) -> None:
|
|
258
|
+
"""Show agent conversation history."""
|
|
259
|
+
if not self.agent:
|
|
260
|
+
print("No active agent")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
state = self.agent.get_state()
|
|
264
|
+
if not state:
|
|
265
|
+
print("No active run")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
history = state.vars.get('messages', [])
|
|
269
|
+
if not history:
|
|
270
|
+
print("No history yet")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
print(f"\n{_c('Conversation History', Colors.CYAN, Colors.BOLD)}")
|
|
274
|
+
print(_c("─" * 60, Colors.DIM))
|
|
275
|
+
|
|
276
|
+
for i, entry in enumerate(history):
|
|
277
|
+
role = entry.get('role', 'unknown')
|
|
278
|
+
content = entry.get('content', '')
|
|
279
|
+
|
|
280
|
+
if role == "assistant":
|
|
281
|
+
role_color = Colors.GREEN
|
|
282
|
+
elif role == "tool":
|
|
283
|
+
role_color = Colors.BLUE
|
|
284
|
+
elif role == "user":
|
|
285
|
+
role_color = Colors.MAGENTA
|
|
286
|
+
else:
|
|
287
|
+
role_color = Colors.DIM
|
|
288
|
+
|
|
289
|
+
# Truncate long content
|
|
290
|
+
if len(content) > 100:
|
|
291
|
+
content = content[:97] + "..."
|
|
292
|
+
|
|
293
|
+
print(f"[{i+1}] {_c(role, role_color, Colors.BOLD)}: {content}")
|
|
294
|
+
|
|
295
|
+
print(_c("─" * 60, Colors.DIM))
|
|
296
|
+
|
|
297
|
+
def show_help(self) -> None:
|
|
298
|
+
"""Show help message."""
|
|
299
|
+
print(f"""
|
|
300
|
+
{_c('ReAct Agent REPL', Colors.CYAN, Colors.BOLD)}
|
|
301
|
+
{_c('─' * 40, Colors.DIM)}
|
|
302
|
+
|
|
303
|
+
{_c('Commands:', Colors.BOLD)}
|
|
304
|
+
<task> Start agent with a task
|
|
305
|
+
resume Resume a paused/interrupted agent
|
|
306
|
+
status Show current agent status
|
|
307
|
+
history Show conversation history
|
|
308
|
+
tools List available tools
|
|
309
|
+
help Show this help
|
|
310
|
+
quit Exit
|
|
311
|
+
|
|
312
|
+
{_c('During execution:', Colors.BOLD)}
|
|
313
|
+
Ctrl+C Interrupt (preserves state)
|
|
314
|
+
|
|
315
|
+
{_c('Examples:', Colors.DIM)}
|
|
316
|
+
> list the python files in this directory
|
|
317
|
+
> what is in the README.md file?
|
|
318
|
+
> search for TODO comments in the code
|
|
319
|
+
""")
|
|
320
|
+
|
|
321
|
+
def show_tools(self) -> None:
|
|
322
|
+
"""Show available tools."""
|
|
323
|
+
print(f"\n{_c('Available Tools', Colors.CYAN, Colors.BOLD)}")
|
|
324
|
+
print(_c("─" * 50, Colors.DIM))
|
|
325
|
+
|
|
326
|
+
for tool in self._tools:
|
|
327
|
+
tool_def = getattr(tool, "_tool_definition", None)
|
|
328
|
+
if tool_def is None:
|
|
329
|
+
tool_def = ToolDefinition.from_function(tool)
|
|
330
|
+
|
|
331
|
+
params = ", ".join(str(k) for k in (tool_def.parameters or {}).keys())
|
|
332
|
+
print(f" {_c(tool_def.name, Colors.GREEN)}({params})")
|
|
333
|
+
print(f" {_c(tool_def.description, Colors.DIM)}")
|
|
334
|
+
|
|
335
|
+
# Show built-in ask_user
|
|
336
|
+
print(f" {_c('ask_user', Colors.MAGENTA)}(question, choices?)")
|
|
337
|
+
print(f" {_c('Ask the user a question', Colors.DIM)}")
|
|
338
|
+
|
|
339
|
+
print(_c("─" * 50, Colors.DIM))
|
|
340
|
+
|
|
341
|
+
async def repl_loop(self) -> None:
|
|
342
|
+
"""Main REPL loop."""
|
|
343
|
+
# Header
|
|
344
|
+
print(f"""
|
|
345
|
+
{_c('╔' + '═' * 58 + '╗', Colors.CYAN)}
|
|
346
|
+
{_c('║', Colors.CYAN)} {_c('ReAct Agent REPL', Colors.BOLD)} {_c('║', Colors.CYAN)}
|
|
347
|
+
{_c('║', Colors.CYAN)} {_c('║', Colors.CYAN)}
|
|
348
|
+
{_c('║', Colors.CYAN)} Provider: {self.provider:<15} Model: {self.model:<17} {_c('║', Colors.CYAN)}
|
|
349
|
+
{_c('║', Colors.CYAN)} {_c('║', Colors.CYAN)}
|
|
350
|
+
{_c('║', Colors.CYAN)} Type {_c("'help'", Colors.GREEN)} for commands, or enter a task. {_c('║', Colors.CYAN)}
|
|
351
|
+
{_c('╚' + '═' * 58 + '╝', Colors.CYAN)}
|
|
352
|
+
""")
|
|
353
|
+
|
|
354
|
+
self.show_tools()
|
|
355
|
+
|
|
356
|
+
if self.state_file:
|
|
357
|
+
try:
|
|
358
|
+
loaded = self.agent.load_state(self.state_file)
|
|
359
|
+
if loaded is not None:
|
|
360
|
+
print(f"\n{_c('Loaded saved run.', Colors.CYAN)} Type 'status' or 'resume'.")
|
|
361
|
+
except Exception as e:
|
|
362
|
+
print(f"{_c('State load failed:', Colors.YELLOW)} {e}")
|
|
363
|
+
|
|
364
|
+
agent_task: Optional[asyncio.Task] = None
|
|
365
|
+
|
|
366
|
+
while True:
|
|
367
|
+
try:
|
|
368
|
+
# Get input
|
|
369
|
+
if sys.stdin.isatty():
|
|
370
|
+
prompt = f"\n{_c('>', Colors.CYAN, Colors.BOLD)} "
|
|
371
|
+
user_input = await asyncio.get_event_loop().run_in_executor(
|
|
372
|
+
None, lambda: input(prompt).strip()
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
line = sys.stdin.readline()
|
|
376
|
+
if not line:
|
|
377
|
+
break
|
|
378
|
+
user_input = line.strip()
|
|
379
|
+
|
|
380
|
+
if not user_input:
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
# Handle commands
|
|
384
|
+
cmd = user_input.lower()
|
|
385
|
+
|
|
386
|
+
if cmd in ('quit', 'exit', 'q'):
|
|
387
|
+
if agent_task and not agent_task.done():
|
|
388
|
+
agent_task.cancel()
|
|
389
|
+
try:
|
|
390
|
+
await agent_task
|
|
391
|
+
except asyncio.CancelledError:
|
|
392
|
+
pass
|
|
393
|
+
print(_c("Goodbye!", Colors.CYAN))
|
|
394
|
+
break
|
|
395
|
+
|
|
396
|
+
elif cmd == 'help':
|
|
397
|
+
self.show_help()
|
|
398
|
+
|
|
399
|
+
elif cmd == 'tools':
|
|
400
|
+
self.show_tools()
|
|
401
|
+
|
|
402
|
+
elif cmd == 'status':
|
|
403
|
+
self.show_status()
|
|
404
|
+
|
|
405
|
+
elif cmd == 'history':
|
|
406
|
+
self.show_history()
|
|
407
|
+
|
|
408
|
+
elif cmd == 'resume':
|
|
409
|
+
await self.resume_agent()
|
|
410
|
+
|
|
411
|
+
else:
|
|
412
|
+
# Treat as a task
|
|
413
|
+
if agent_task and not agent_task.done():
|
|
414
|
+
print(_c("Agent is running. Use Ctrl+C to interrupt.", Colors.YELLOW))
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
agent_task = asyncio.create_task(self.run_agent_async(user_input))
|
|
418
|
+
try:
|
|
419
|
+
await agent_task
|
|
420
|
+
except asyncio.CancelledError:
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
except KeyboardInterrupt:
|
|
424
|
+
print()
|
|
425
|
+
if agent_task and not agent_task.done():
|
|
426
|
+
self.interrupt()
|
|
427
|
+
agent_task.cancel()
|
|
428
|
+
try:
|
|
429
|
+
await agent_task
|
|
430
|
+
except asyncio.CancelledError:
|
|
431
|
+
pass
|
|
432
|
+
else:
|
|
433
|
+
print(_c("Type 'quit' to exit.", Colors.DIM))
|
|
434
|
+
except EOFError:
|
|
435
|
+
break
|
|
436
|
+
except Exception as e:
|
|
437
|
+
print(f"{_c('Error:', Colors.YELLOW)} {e}")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def main():
|
|
441
|
+
"""Entry point for the REPL."""
|
|
442
|
+
parser = argparse.ArgumentParser(description="ReAct Agent REPL")
|
|
443
|
+
parser.add_argument("--provider", default="ollama", help="LLM provider")
|
|
444
|
+
parser.add_argument("--model", default="qwen3:1.7b-q4_K_M", help="Model name")
|
|
445
|
+
parser.add_argument("--state-file", help="File to persist agent state")
|
|
446
|
+
args = parser.parse_args()
|
|
447
|
+
|
|
448
|
+
repl = AgentREPL(
|
|
449
|
+
provider=args.provider,
|
|
450
|
+
model=args.model,
|
|
451
|
+
state_file=args.state_file,
|
|
452
|
+
)
|
|
453
|
+
asyncio.run(repl.repl_loop())
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
if __name__ == "__main__":
|
|
457
|
+
main()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Sandbox interfaces for CodeAct-style agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional, Protocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ExecutionResult:
|
|
11
|
+
stdout: str
|
|
12
|
+
stderr: str
|
|
13
|
+
exit_code: int
|
|
14
|
+
duration_ms: float
|
|
15
|
+
error: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Sandbox(Protocol):
|
|
19
|
+
def execute(self, code: str, *, timeout_s: float = 10.0) -> ExecutionResult: ...
|
|
20
|
+
|
|
21
|
+
def reset(self) -> None: ...
|
|
22
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Local subprocess sandbox (development-only).
|
|
2
|
+
|
|
3
|
+
This is intentionally minimal: it enforces a timeout and captures stdout/stderr.
|
|
4
|
+
Stronger isolation (docker/e2b/wasm) can be added later.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from .interface import ExecutionResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LocalSandbox:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
cwd: Optional[str] = None,
|
|
23
|
+
python_executable: Optional[str] = None,
|
|
24
|
+
):
|
|
25
|
+
self._cwd = cwd or os.getcwd()
|
|
26
|
+
self._python = python_executable or sys.executable
|
|
27
|
+
|
|
28
|
+
def reset(self) -> None:
|
|
29
|
+
# Stateless sandbox (new subprocess per call).
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def execute(self, code: str, *, timeout_s: float = 10.0) -> ExecutionResult:
|
|
33
|
+
started = time.monotonic()
|
|
34
|
+
try:
|
|
35
|
+
completed = subprocess.run(
|
|
36
|
+
[self._python, "-c", code],
|
|
37
|
+
cwd=self._cwd,
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
timeout=float(timeout_s),
|
|
41
|
+
)
|
|
42
|
+
duration_ms = (time.monotonic() - started) * 1000.0
|
|
43
|
+
return ExecutionResult(
|
|
44
|
+
stdout=completed.stdout or "",
|
|
45
|
+
stderr=completed.stderr or "",
|
|
46
|
+
exit_code=int(completed.returncode),
|
|
47
|
+
duration_ms=duration_ms,
|
|
48
|
+
error=None,
|
|
49
|
+
)
|
|
50
|
+
except subprocess.TimeoutExpired as e:
|
|
51
|
+
duration_ms = (time.monotonic() - started) * 1000.0
|
|
52
|
+
return ExecutionResult(
|
|
53
|
+
stdout=(e.stdout or "") if isinstance(e.stdout, str) else "",
|
|
54
|
+
stderr=(e.stderr or "") if isinstance(e.stderr, str) else "",
|
|
55
|
+
exit_code=124,
|
|
56
|
+
duration_ms=duration_ms,
|
|
57
|
+
error=f"Timeout after {timeout_s}s",
|
|
58
|
+
)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
duration_ms = (time.monotonic() - started) * 1000.0
|
|
61
|
+
return ExecutionResult(
|
|
62
|
+
stdout="",
|
|
63
|
+
stderr="",
|
|
64
|
+
exit_code=1,
|
|
65
|
+
duration_ms=duration_ms,
|
|
66
|
+
error=str(e),
|
|
67
|
+
)
|
|
68
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""AbstractAgent tools.
|
|
2
|
+
|
|
3
|
+
Common tools are imported from AbstractCore (canonical source).
|
|
4
|
+
Agent-specific tools (execute_python, self_improve) are defined locally.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Import common tools from AbstractCore (canonical source)
|
|
8
|
+
from abstractcore.tools.common_tools import (
|
|
9
|
+
list_files,
|
|
10
|
+
read_file,
|
|
11
|
+
search_files,
|
|
12
|
+
write_file,
|
|
13
|
+
edit_file,
|
|
14
|
+
web_search,
|
|
15
|
+
fetch_url,
|
|
16
|
+
execute_command,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Agent-specific tools
|
|
20
|
+
from .code_execution import execute_python
|
|
21
|
+
from .self_improve import self_improve
|
|
22
|
+
|
|
23
|
+
# Default toolset for agents
|
|
24
|
+
ALL_TOOLS = [
|
|
25
|
+
# File operations (from abstractcore)
|
|
26
|
+
list_files,
|
|
27
|
+
read_file,
|
|
28
|
+
search_files,
|
|
29
|
+
write_file,
|
|
30
|
+
edit_file,
|
|
31
|
+
# Web tools (from abstractcore)
|
|
32
|
+
web_search,
|
|
33
|
+
fetch_url,
|
|
34
|
+
# System tools (from abstractcore)
|
|
35
|
+
execute_command,
|
|
36
|
+
# Agent-specific tools
|
|
37
|
+
execute_python,
|
|
38
|
+
self_improve,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# File operations
|
|
43
|
+
"list_files",
|
|
44
|
+
"read_file",
|
|
45
|
+
"search_files",
|
|
46
|
+
"write_file",
|
|
47
|
+
"edit_file",
|
|
48
|
+
# Web tools
|
|
49
|
+
"web_search",
|
|
50
|
+
"fetch_url",
|
|
51
|
+
# System tools
|
|
52
|
+
"execute_command",
|
|
53
|
+
# Agent-specific tools
|
|
54
|
+
"execute_python",
|
|
55
|
+
"self_improve",
|
|
56
|
+
# Collections
|
|
57
|
+
"ALL_TOOLS",
|
|
58
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Code execution tool used by CodeAct agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abstractcore.tools import tool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _truncate(text: str, *, limit: int = 6000) -> str:
|
|
9
|
+
if not text:
|
|
10
|
+
return ""
|
|
11
|
+
if len(text) <= limit:
|
|
12
|
+
return text
|
|
13
|
+
head = text[:4000]
|
|
14
|
+
tail = text[-1500:] if len(text) > 5500 else ""
|
|
15
|
+
return head + f"\n... (truncated, {len(text)} chars total)\n" + tail
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@tool(
|
|
19
|
+
name="execute_python",
|
|
20
|
+
description="Execute a Python snippet in a subprocess sandbox (timeout enforced). Returns stdout/stderr/exit_code.",
|
|
21
|
+
when_to_use="When you need to compute, inspect files, or transform data using Python code",
|
|
22
|
+
)
|
|
23
|
+
def execute_python(code: str, timeout_s: float = 10.0) -> dict:
|
|
24
|
+
"""Execute Python code in a local subprocess.
|
|
25
|
+
|
|
26
|
+
Notes:
|
|
27
|
+
- This is a dev sandbox (timeout only). It is not a hardened security boundary.
|
|
28
|
+
- Use small snippets and print what you need.
|
|
29
|
+
"""
|
|
30
|
+
code = str(code or "")
|
|
31
|
+
if not code.strip():
|
|
32
|
+
raise ValueError("code must be a non-empty string")
|
|
33
|
+
|
|
34
|
+
from ..sandbox.local import LocalSandbox
|
|
35
|
+
|
|
36
|
+
sandbox = LocalSandbox()
|
|
37
|
+
result = sandbox.execute(code, timeout_s=float(timeout_s))
|
|
38
|
+
return {
|
|
39
|
+
"stdout": _truncate(result.stdout),
|
|
40
|
+
"stderr": _truncate(result.stderr),
|
|
41
|
+
"exit_code": int(result.exit_code),
|
|
42
|
+
"duration_ms": float(result.duration_ms),
|
|
43
|
+
"error": result.error,
|
|
44
|
+
}
|
|
45
|
+
|