abstractagent 0.2.0__py3-none-any.whl → 0.3.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/repl.py CHANGED
@@ -1,457 +1,34 @@
1
- """Async REPL for the ReAct agent.
1
+ """Deprecated CLI entrypoint for AbstractAgent.
2
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()
3
+ The interactive REPL and UX components were extracted into **AbstractCode** to
4
+ avoid mixing UI concerns with agent patterns.
54
5
 
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
6
+ Use:
7
+ abstractcode --agent react --provider <provider> --model <model>
8
+ """
311
9
 
312
- {_c('During execution:', Colors.BOLD)}
313
- Ctrl+C Interrupt (preserves state)
10
+ from __future__ import annotations
314
11
 
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)
12
+ import argparse
13
+ import os
14
+ import sys
330
15
 
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
16
 
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}")
17
+ def main(argv: list[str] | None = None) -> int:
18
+ parser = argparse.ArgumentParser(
19
+ prog="react-agent",
20
+ description="Deprecated: the interactive REPL moved to AbstractCode.",
21
+ )
22
+ parser.add_argument("--provider", default=os.getenv("ABSTRACTCODE_PROVIDER", "ollama"))
23
+ parser.add_argument("--model", default=os.getenv("ABSTRACTCODE_MODEL", "qwen3:4b-instruct-2507-q4_K_M"))
24
+ args = parser.parse_args(list(argv) if argv is not None else None)
438
25
 
26
+ print("The AbstractAgent interactive REPL has moved to AbstractCode.\n")
27
+ print("Run:")
28
+ print(f" abstractcode --agent react --provider {args.provider} --model {args.model}")
29
+ return 0
439
30
 
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
31
 
32
+ if __name__ == "__main__": # pragma: no cover
33
+ raise SystemExit(main(sys.argv[1:]))
455
34
 
456
- if __name__ == "__main__":
457
- main()
@@ -0,0 +1,5 @@
1
+ """Utility scripts for AbstractAgent.
2
+
3
+ These are intended for local evaluation and debugging (not required for library use).
4
+ """
5
+