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.
@@ -0,0 +1,49 @@
1
+ """AbstractAgent - Agent implementations using AbstractRuntime and AbstractCore."""
2
+
3
+ from .agents import (
4
+ BaseAgent,
5
+ ReactAgent,
6
+ create_react_workflow,
7
+ create_react_agent,
8
+ CodeActAgent,
9
+ create_codeact_workflow,
10
+ create_codeact_agent,
11
+ )
12
+ from .tools import (
13
+ ALL_TOOLS,
14
+ list_files,
15
+ read_file,
16
+ search_files,
17
+ execute_command,
18
+ write_file,
19
+ edit_file,
20
+ web_search,
21
+ fetch_url,
22
+ execute_python,
23
+ self_improve,
24
+ )
25
+
26
+ __all__ = [
27
+ # Base class for custom agents
28
+ "BaseAgent",
29
+ # ReAct agent
30
+ "ReactAgent",
31
+ "create_react_workflow",
32
+ "create_react_agent",
33
+ # CodeAct agent
34
+ "CodeActAgent",
35
+ "create_codeact_workflow",
36
+ "create_codeact_agent",
37
+ # Tools
38
+ "ALL_TOOLS",
39
+ "list_files",
40
+ "read_file",
41
+ "search_files",
42
+ "execute_command",
43
+ "write_file",
44
+ "edit_file",
45
+ "web_search",
46
+ "fetch_url",
47
+ "execute_python",
48
+ "self_improve",
49
+ ]
@@ -0,0 +1,6 @@
1
+ """Runtime adapters for agent logic."""
2
+
3
+ from .codeact_runtime import create_codeact_workflow
4
+ from .react_runtime import create_react_workflow
5
+
6
+ __all__ = ["create_react_workflow", "create_codeact_workflow"]
@@ -0,0 +1,397 @@
1
+ """AbstractRuntime adapter for CodeAct agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from typing import Any, Callable, Dict, List, Optional
8
+
9
+ from abstractcore.tools import ToolCall
10
+ from abstractruntime import Effect, EffectType, RunState, StepPlan, WorkflowSpec
11
+ from abstractruntime.core.vars import ensure_limits, ensure_namespaces
12
+
13
+ from ..logic.codeact import CodeActLogic
14
+
15
+
16
+ def _new_message(
17
+ ctx: Any,
18
+ *,
19
+ role: str,
20
+ content: str,
21
+ metadata: Optional[Dict[str, Any]] = None,
22
+ ) -> Dict[str, Any]:
23
+ timestamp: Optional[str] = None
24
+ now_iso = getattr(ctx, "now_iso", None)
25
+ if callable(now_iso):
26
+ timestamp = str(now_iso())
27
+ if not timestamp:
28
+ from datetime import datetime, timezone
29
+
30
+ timestamp = datetime.now(timezone.utc).isoformat()
31
+
32
+ return {
33
+ "role": role,
34
+ "content": content,
35
+ "timestamp": timestamp,
36
+ "metadata": metadata or {},
37
+ }
38
+
39
+
40
+ def ensure_codeact_vars(run: RunState) -> tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
41
+ """Ensure namespaced vars exist and migrate legacy flat keys in-place.
42
+
43
+ Returns:
44
+ Tuple of (context, scratchpad, runtime_ns, temp, limits) dicts.
45
+ """
46
+ ensure_namespaces(run.vars)
47
+ limits = ensure_limits(run.vars)
48
+ context = run.vars["context"]
49
+ scratchpad = run.vars["scratchpad"]
50
+ runtime_ns = run.vars["_runtime"]
51
+ temp = run.vars["_temp"]
52
+
53
+ if "task" in run.vars and "task" not in context:
54
+ context["task"] = run.vars.pop("task")
55
+ if "messages" in run.vars and "messages" not in context:
56
+ context["messages"] = run.vars.pop("messages")
57
+ if "iteration" in run.vars and "iteration" not in scratchpad:
58
+ scratchpad["iteration"] = run.vars.pop("iteration")
59
+ if "max_iterations" in run.vars and "max_iterations" not in scratchpad:
60
+ scratchpad["max_iterations"] = run.vars.pop("max_iterations")
61
+ if "_inbox" in run.vars and "inbox" not in runtime_ns:
62
+ runtime_ns["inbox"] = run.vars.pop("_inbox")
63
+
64
+ for key in ("llm_response", "tool_results", "pending_tool_calls", "user_response", "final_answer", "pending_code"):
65
+ if key in run.vars and key not in temp:
66
+ temp[key] = run.vars.pop(key)
67
+
68
+ if not isinstance(context.get("messages"), list):
69
+ context["messages"] = []
70
+ if not isinstance(runtime_ns.get("inbox"), list):
71
+ runtime_ns["inbox"] = []
72
+
73
+ iteration = scratchpad.get("iteration")
74
+ if not isinstance(iteration, int):
75
+ try:
76
+ scratchpad["iteration"] = int(iteration or 0)
77
+ except (TypeError, ValueError):
78
+ scratchpad["iteration"] = 0
79
+
80
+ max_iterations = scratchpad.get("max_iterations")
81
+ if max_iterations is None:
82
+ scratchpad["max_iterations"] = 25
83
+ elif not isinstance(max_iterations, int):
84
+ try:
85
+ scratchpad["max_iterations"] = int(max_iterations)
86
+ except (TypeError, ValueError):
87
+ scratchpad["max_iterations"] = 25
88
+
89
+ if scratchpad["max_iterations"] < 1:
90
+ scratchpad["max_iterations"] = 1
91
+
92
+ return context, scratchpad, runtime_ns, temp, limits
93
+
94
+
95
+ def _compute_toolset_id(tool_specs: List[Dict[str, Any]]) -> str:
96
+ normalized = sorted((dict(s) for s in tool_specs), key=lambda s: str(s.get("name", "")))
97
+ payload = json.dumps(normalized, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
98
+ digest = hashlib.sha256(payload).hexdigest()
99
+ return f"ts_{digest}"
100
+
101
+
102
+ def create_codeact_workflow(
103
+ *,
104
+ logic: CodeActLogic,
105
+ on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
106
+ ) -> WorkflowSpec:
107
+ def emit(step: str, data: Dict[str, Any]) -> None:
108
+ if on_step:
109
+ on_step(step, data)
110
+
111
+ tool_defs = logic.tools
112
+ tool_specs = [t.to_dict() for t in tool_defs]
113
+ toolset_id = _compute_toolset_id(tool_specs)
114
+
115
+ def init_node(run: RunState, ctx) -> StepPlan:
116
+ context, scratchpad, runtime_ns, _, limits = ensure_codeact_vars(run)
117
+ scratchpad["iteration"] = 0
118
+ limits["current_iteration"] = 0
119
+
120
+ task = str(context.get("task", "") or "")
121
+ context["task"] = task
122
+ messages = context["messages"]
123
+ if task and (not messages or messages[-1].get("role") != "user" or messages[-1].get("content") != task):
124
+ messages.append(_new_message(ctx, role="user", content=task))
125
+
126
+ runtime_ns.setdefault("tool_specs", tool_specs)
127
+ runtime_ns.setdefault("toolset_id", toolset_id)
128
+ runtime_ns.setdefault("inbox", [])
129
+
130
+ emit("init", {"task": task})
131
+ return StepPlan(node_id="init", next_node="reason")
132
+
133
+ def reason_node(run: RunState, ctx) -> StepPlan:
134
+ context, scratchpad, runtime_ns, _, limits = ensure_codeact_vars(run)
135
+
136
+ # Read from _limits (canonical) with fallback to scratchpad (backward compat)
137
+ if "current_iteration" in limits:
138
+ iteration = int(limits.get("current_iteration", 0) or 0)
139
+ max_iterations = int(limits.get("max_iterations", 25) or 25)
140
+ else:
141
+ # Backward compatibility: use scratchpad
142
+ iteration = int(scratchpad.get("iteration", 0) or 0)
143
+ max_iterations = int(scratchpad.get("max_iterations") or 25)
144
+
145
+ if max_iterations < 1:
146
+ max_iterations = 1
147
+
148
+ if iteration >= max_iterations:
149
+ return StepPlan(node_id="reason", next_node="max_iterations")
150
+
151
+ # Update both for transition period
152
+ scratchpad["iteration"] = iteration + 1
153
+ limits["current_iteration"] = iteration + 1
154
+
155
+ inbox = runtime_ns.get("inbox", [])
156
+ guidance = ""
157
+ if isinstance(inbox, list) and inbox:
158
+ inbox_messages = [str(m.get("content", "") or "") for m in inbox if isinstance(m, dict)]
159
+ guidance = " | ".join([m for m in inbox_messages if m])
160
+ runtime_ns["inbox"] = []
161
+
162
+ req = logic.build_request(
163
+ task=str(context.get("task", "") or ""),
164
+ messages=list(context.get("messages") or []),
165
+ guidance=guidance,
166
+ iteration=iteration + 1,
167
+ max_iterations=max_iterations,
168
+ vars=run.vars, # Pass vars for _limits access
169
+ )
170
+
171
+ emit("reason", {"iteration": iteration + 1, "max_iterations": max_iterations, "has_guidance": bool(guidance)})
172
+
173
+ payload = {"prompt": req.prompt, "tools": [t.to_dict() for t in req.tools]}
174
+ if req.max_tokens is not None:
175
+ payload["params"] = {"max_tokens": req.max_tokens}
176
+
177
+ return StepPlan(
178
+ node_id="reason",
179
+ effect=Effect(
180
+ type=EffectType.LLM_CALL,
181
+ payload=payload,
182
+ result_key="_temp.llm_response",
183
+ ),
184
+ next_node="parse",
185
+ )
186
+
187
+ def parse_node(run: RunState, ctx) -> StepPlan:
188
+ context, _, _, temp, _ = ensure_codeact_vars(run)
189
+ response = temp.get("llm_response", {})
190
+ content, tool_calls = logic.parse_response(response)
191
+
192
+ if content:
193
+ context["messages"].append(_new_message(ctx, role="assistant", content=content))
194
+
195
+ temp.pop("llm_response", None)
196
+ emit("parse", {"has_tool_calls": bool(tool_calls), "content_preview": (content[:100] if content else "(no content)")})
197
+
198
+ if tool_calls:
199
+ temp["pending_tool_calls"] = [tc.__dict__ for tc in tool_calls]
200
+ return StepPlan(node_id="parse", next_node="act")
201
+
202
+ code = logic.extract_code(content)
203
+ if code:
204
+ temp["pending_code"] = code
205
+ return StepPlan(node_id="parse", next_node="execute_code")
206
+
207
+ temp["final_answer"] = content
208
+ return StepPlan(node_id="parse", next_node="done")
209
+
210
+ def act_node(run: RunState, ctx) -> StepPlan:
211
+ _, _, _, temp, _ = ensure_codeact_vars(run)
212
+ tool_calls = temp.get("pending_tool_calls", [])
213
+ if not isinstance(tool_calls, list):
214
+ tool_calls = []
215
+
216
+ if not tool_calls:
217
+ return StepPlan(node_id="act", next_node="reason")
218
+
219
+ # Handle ask_user specially with ASK_USER effect.
220
+ for i, tc in enumerate(tool_calls):
221
+ if not isinstance(tc, dict):
222
+ continue
223
+ if tc.get("name") != "ask_user":
224
+ continue
225
+ args = tc.get("arguments") or {}
226
+ question = str(args.get("question") or "Please provide input:")
227
+ choices = args.get("choices")
228
+ choices = list(choices) if isinstance(choices, list) else None
229
+
230
+ temp["pending_tool_calls"] = tool_calls[i + 1 :]
231
+ emit("ask_user", {"question": question, "choices": choices or []})
232
+ return StepPlan(
233
+ node_id="act",
234
+ effect=Effect(
235
+ type=EffectType.ASK_USER,
236
+ payload={"prompt": question, "choices": choices, "allow_free_text": True},
237
+ result_key="_temp.user_response",
238
+ ),
239
+ next_node="handle_user_response",
240
+ )
241
+
242
+ for tc in tool_calls:
243
+ if isinstance(tc, dict):
244
+ emit("act", {"tool": tc.get("name", ""), "args": tc.get("arguments", {})})
245
+
246
+ formatted_calls: List[Dict[str, Any]] = []
247
+ for tc in tool_calls:
248
+ if isinstance(tc, dict):
249
+ formatted_calls.append(
250
+ {"name": tc.get("name", ""), "arguments": tc.get("arguments", {}), "call_id": tc.get("call_id", "1")}
251
+ )
252
+ elif isinstance(tc, ToolCall):
253
+ formatted_calls.append(
254
+ {"name": tc.name, "arguments": tc.arguments, "call_id": tc.call_id or "1"}
255
+ )
256
+
257
+ return StepPlan(
258
+ node_id="act",
259
+ effect=Effect(
260
+ type=EffectType.TOOL_CALLS,
261
+ payload={"tool_calls": formatted_calls},
262
+ result_key="_temp.tool_results",
263
+ ),
264
+ next_node="observe",
265
+ )
266
+
267
+ def execute_code_node(run: RunState, ctx) -> StepPlan:
268
+ _, _, _, temp, _ = ensure_codeact_vars(run)
269
+ code = temp.get("pending_code")
270
+ if not isinstance(code, str) or not code.strip():
271
+ return StepPlan(node_id="execute_code", next_node="reason")
272
+
273
+ temp.pop("pending_code", None)
274
+ emit("act", {"tool": "execute_python", "args": {"code": "(inline)", "timeout_s": 10.0}})
275
+
276
+ return StepPlan(
277
+ node_id="execute_code",
278
+ effect=Effect(
279
+ type=EffectType.TOOL_CALLS,
280
+ payload={
281
+ "tool_calls": [
282
+ {
283
+ "name": "execute_python",
284
+ "arguments": {"code": code, "timeout_s": 10.0},
285
+ "call_id": "code",
286
+ }
287
+ ]
288
+ },
289
+ result_key="_temp.tool_results",
290
+ ),
291
+ next_node="observe",
292
+ )
293
+
294
+ def observe_node(run: RunState, ctx) -> StepPlan:
295
+ context, _, _, temp, _ = ensure_codeact_vars(run)
296
+ tool_results = temp.get("tool_results", {})
297
+ if not isinstance(tool_results, dict):
298
+ tool_results = {}
299
+
300
+ results = tool_results.get("results", [])
301
+ if not isinstance(results, list):
302
+ results = []
303
+
304
+ for r in results:
305
+ if not isinstance(r, dict):
306
+ continue
307
+ name = str(r.get("name", "tool") or "tool")
308
+ success = bool(r.get("success"))
309
+ output = r.get("output", "")
310
+ error = r.get("error", "")
311
+ rendered = logic.format_observation(
312
+ name=name,
313
+ output=(output if success else (error or output)),
314
+ success=success,
315
+ )
316
+ emit("observe", {"tool": name, "result": rendered[:150]})
317
+ context["messages"].append(
318
+ _new_message(
319
+ ctx,
320
+ role="tool",
321
+ content=rendered,
322
+ metadata={"name": name, "call_id": r.get("call_id"), "success": success},
323
+ )
324
+ )
325
+
326
+ temp.pop("tool_results", None)
327
+ temp["pending_tool_calls"] = []
328
+ return StepPlan(node_id="observe", next_node="reason")
329
+
330
+ def handle_user_response_node(run: RunState, ctx) -> StepPlan:
331
+ context, _, _, temp, _ = ensure_codeact_vars(run)
332
+ user_response = temp.get("user_response", {})
333
+ if not isinstance(user_response, dict):
334
+ user_response = {}
335
+ response_text = str(user_response.get("response", "") or "")
336
+ emit("user_response", {"response": response_text})
337
+
338
+ context["messages"].append(_new_message(ctx, role="user", content=f"[User response]: {response_text}"))
339
+ temp.pop("user_response", None)
340
+
341
+ if temp.get("pending_tool_calls"):
342
+ return StepPlan(node_id="handle_user_response", next_node="act")
343
+ return StepPlan(node_id="handle_user_response", next_node="reason")
344
+
345
+ def done_node(run: RunState, ctx) -> StepPlan:
346
+ context, scratchpad, _, temp, limits = ensure_codeact_vars(run)
347
+ answer = str(temp.get("final_answer") or "No answer provided")
348
+ emit("done", {"answer": answer})
349
+
350
+ # Prefer _limits.current_iteration, fall back to scratchpad
351
+ iterations = int(limits.get("current_iteration", 0) or scratchpad.get("iteration", 0) or 0)
352
+
353
+ return StepPlan(
354
+ node_id="done",
355
+ complete_output={
356
+ "answer": answer,
357
+ "iterations": iterations,
358
+ "messages": list(context.get("messages") or []),
359
+ },
360
+ )
361
+
362
+ def max_iterations_node(run: RunState, ctx) -> StepPlan:
363
+ context, scratchpad, _, _, limits = ensure_codeact_vars(run)
364
+
365
+ # Prefer _limits, fall back to scratchpad
366
+ max_iterations = int(limits.get("max_iterations", 0) or scratchpad.get("max_iterations", 25) or 25)
367
+ if max_iterations < 1:
368
+ max_iterations = 1
369
+ emit("max_iterations", {"iterations": max_iterations})
370
+
371
+ messages = list(context.get("messages") or [])
372
+ last_content = messages[-1]["content"] if messages else "Max iterations reached"
373
+ return StepPlan(
374
+ node_id="max_iterations",
375
+ complete_output={
376
+ "answer": last_content,
377
+ "iterations": max_iterations,
378
+ "messages": messages,
379
+ },
380
+ )
381
+
382
+ return WorkflowSpec(
383
+ workflow_id="codeact_agent",
384
+ entry_node="init",
385
+ nodes={
386
+ "init": init_node,
387
+ "reason": reason_node,
388
+ "parse": parse_node,
389
+ "act": act_node,
390
+ "execute_code": execute_code_node,
391
+ "observe": observe_node,
392
+ "handle_user_response": handle_user_response_node,
393
+ "done": done_node,
394
+ "max_iterations": max_iterations_node,
395
+ },
396
+ )
397
+