agentforge-py 0.2.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.
- agentforge/__init__.py +114 -0
- agentforge/_testing/__init__.py +19 -0
- agentforge/_testing/fake_llm.py +126 -0
- agentforge/_testing/fake_tool.py +122 -0
- agentforge/_tools/__init__.py +14 -0
- agentforge/_tools/calculator.py +102 -0
- agentforge/_tools/decorator.py +300 -0
- agentforge/_tools/file_read.py +112 -0
- agentforge/_tools/shell.py +134 -0
- agentforge/_tools/web_search.py +207 -0
- agentforge/agent.py +817 -0
- agentforge/auth.py +42 -0
- agentforge/cli/__init__.py +18 -0
- agentforge/cli/_build.py +323 -0
- agentforge/cli/_scaffold_state.py +250 -0
- agentforge/cli/_shared_scaffold.py +174 -0
- agentforge/cli/config_cmd.py +174 -0
- agentforge/cli/db_cmd.py +262 -0
- agentforge/cli/debug_cmd.py +168 -0
- agentforge/cli/docs_cmd.py +217 -0
- agentforge/cli/eval_cmd.py +181 -0
- agentforge/cli/health_cmd.py +139 -0
- agentforge/cli/list_modules.py +85 -0
- agentforge/cli/main.py +81 -0
- agentforge/cli/manifest_apply.py +368 -0
- agentforge/cli/module_cmd.py +247 -0
- agentforge/cli/new_cmd.py +171 -0
- agentforge/cli/run_cmd.py +234 -0
- agentforge/cli/upgrade_cmd.py +230 -0
- agentforge/config/__init__.py +45 -0
- agentforge/eval/__init__.py +18 -0
- agentforge/eval/consistency.py +107 -0
- agentforge/eval/coverage.py +100 -0
- agentforge/eval/format_compliance.py +107 -0
- agentforge/eval/regression.py +143 -0
- agentforge/findings.py +166 -0
- agentforge/guardrails/__init__.py +32 -0
- agentforge/guardrails/allowlist.py +49 -0
- agentforge/guardrails/capability_check.py +58 -0
- agentforge/guardrails/engine.py +289 -0
- agentforge/guardrails/pii_redact_basic.py +61 -0
- agentforge/guardrails/prompt_injection_basic.py +90 -0
- agentforge/memory/__init__.py +16 -0
- agentforge/memory/in_memory.py +130 -0
- agentforge/memory/in_memory_graph.py +262 -0
- agentforge/memory/in_memory_vector.py +167 -0
- agentforge/pipeline/__init__.py +26 -0
- agentforge/pipeline/engine.py +189 -0
- agentforge/pipeline/errors.py +19 -0
- agentforge/pipeline/tool.py +93 -0
- agentforge/py.typed +0 -0
- agentforge/recording.py +189 -0
- agentforge/renderers/__init__.py +28 -0
- agentforge/renderers/_defaults.py +32 -0
- agentforge/renderers/markdown.py +44 -0
- agentforge/renderers/patch_applier.py +46 -0
- agentforge/renderers/registry.py +108 -0
- agentforge/renderers/scorecard.py +59 -0
- agentforge/renderers/span_table.py +71 -0
- agentforge/replay.py +260 -0
- agentforge/resolver_register.py +41 -0
- agentforge/retrieval.py +410 -0
- agentforge/runtime.py +63 -0
- agentforge/strategies/__init__.py +27 -0
- agentforge/strategies/_base.py +280 -0
- agentforge/strategies/_plan.py +93 -0
- agentforge/strategies/multi_agent.py +541 -0
- agentforge/strategies/plan_execute.py +506 -0
- agentforge/strategies/react.py +237 -0
- agentforge/strategies/tot.py +472 -0
- agentforge/templates/_shared/.cursorrules +12 -0
- agentforge/templates/_shared/.github/copilot-instructions.md +13 -0
- agentforge/templates/_shared/.gitkeep +0 -0
- agentforge/templates/_shared/AGENTS.md.tmpl +123 -0
- agentforge/templates/_shared/CLAUDE.md +13 -0
- agentforge/templates/_shared/docs/runbooks/01-set-up-new-agent.md.tmpl +67 -0
- agentforge/templates/_shared/docs/runbooks/02-add-a-tool.md +67 -0
- agentforge/templates/_shared/docs/runbooks/03-add-a-pipeline-task.md +69 -0
- agentforge/templates/_shared/docs/runbooks/04-pick-reasoning-strategy.md +67 -0
- agentforge/templates/_shared/docs/runbooks/05-write-prompts.md +75 -0
- agentforge/templates/_shared/docs/runbooks/06-test-your-agent.md +75 -0
- agentforge/templates/_shared/docs/runbooks/07-debug-a-run.md +70 -0
- agentforge/templates/_shared/docs/runbooks/08-add-memory.md +75 -0
- agentforge/templates/_shared/docs/runbooks/09-add-mcp.md +78 -0
- agentforge/templates/_shared/docs/runbooks/10-add-evaluators.md +76 -0
- agentforge/templates/_shared/docs/runbooks/11-add-safety-guardrails.md +83 -0
- agentforge/templates/_shared/docs/runbooks/12-add-observability.md +77 -0
- agentforge/templates/_shared/docs/runbooks/13-configure-multi-provider.md +91 -0
- agentforge/templates/_shared/docs/runbooks/14-deploy-your-agent.md +70 -0
- agentforge/templates/_shared/docs/runbooks/15-upgrade-your-agent.md +67 -0
- agentforge/templates/_shared/docs/runbooks/16-configuration-reference.md +81 -0
- agentforge/templates/_shared/docs/runbooks/17-add-reranker.md +78 -0
- agentforge/templates/_shared/docs/runbooks/18-add-hybrid-search.md +78 -0
- agentforge/templates/_shared/docs/runbooks/19-add-graphrag.md +83 -0
- agentforge/templates/_shared/docs/runbooks/20-apply-schema-migrations.md +92 -0
- agentforge/templates/_shared/docs/runbooks/21-use-streaming-guardrails.md +82 -0
- agentforge/templates/_shared/docs/runbooks/README.md.tmpl +68 -0
- agentforge/templates/code-reviewer/.env.example +8 -0
- agentforge/templates/code-reviewer/.gitignore +7 -0
- agentforge/templates/code-reviewer/README.md +12 -0
- agentforge/templates/code-reviewer/agentforge.yaml +23 -0
- agentforge/templates/code-reviewer/copier.yml +34 -0
- agentforge/templates/code-reviewer/pyproject.toml +18 -0
- agentforge/templates/code-reviewer/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/code-reviewer/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
- agentforge/templates/docs-qa/.env.example +8 -0
- agentforge/templates/docs-qa/.gitignore +7 -0
- agentforge/templates/docs-qa/README.md +14 -0
- agentforge/templates/docs-qa/agentforge.yaml +19 -0
- agentforge/templates/docs-qa/copier.yml +31 -0
- agentforge/templates/docs-qa/pyproject.toml +18 -0
- agentforge/templates/docs-qa/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/docs-qa/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
- agentforge/templates/minimal/.env.example +11 -0
- agentforge/templates/minimal/.gitignore +10 -0
- agentforge/templates/minimal/README.md +28 -0
- agentforge/templates/minimal/agentforge.yaml +10 -0
- agentforge/templates/minimal/copier.yml +52 -0
- agentforge/templates/minimal/pyproject.toml +18 -0
- agentforge/templates/minimal/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/minimal/src/{{project_slug.replace('-', '_')}}/main.py +34 -0
- agentforge/templates/patch-bot/.env.example +8 -0
- agentforge/templates/patch-bot/.gitignore +7 -0
- agentforge/templates/patch-bot/README.md +13 -0
- agentforge/templates/patch-bot/agentforge.yaml +15 -0
- agentforge/templates/patch-bot/copier.yml +31 -0
- agentforge/templates/patch-bot/pyproject.toml +18 -0
- agentforge/templates/patch-bot/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/patch-bot/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
- agentforge/templates/research/.env.example +8 -0
- agentforge/templates/research/.gitignore +7 -0
- agentforge/templates/research/README.md +14 -0
- agentforge/templates/research/agentforge.yaml +17 -0
- agentforge/templates/research/copier.yml +31 -0
- agentforge/templates/research/pyproject.toml +18 -0
- agentforge/templates/research/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/research/src/{{project_slug.replace('-', '_')}}/main.py +31 -0
- agentforge/templates/triage/.env.example +8 -0
- agentforge/templates/triage/.gitignore +7 -0
- agentforge/templates/triage/README.md +14 -0
- agentforge/templates/triage/agentforge.yaml +25 -0
- agentforge/templates/triage/copier.yml +31 -0
- agentforge/templates/triage/pyproject.toml +18 -0
- agentforge/templates/triage/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/triage/src/{{project_slug.replace('-', '_')}}/main.py +30 -0
- agentforge/testing/__init__.py +69 -0
- agentforge/testing/conformance.py +40 -0
- agentforge/testing/factory.py +89 -0
- agentforge/testing/fixtures.py +42 -0
- agentforge/testing/llm.py +235 -0
- agentforge/testing/recording.py +177 -0
- agentforge/tools/__init__.py +41 -0
- agentforge_py-0.2.1.dist-info/METADATA +158 -0
- agentforge_py-0.2.1.dist-info/RECORD +157 -0
- agentforge_py-0.2.1.dist-info/WHEEL +4 -0
- agentforge_py-0.2.1.dist-info/entry_points.txt +2 -0
- agentforge_py-0.2.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""`ReActLoop` — modern reasoning + acting loop.
|
|
2
|
+
|
|
3
|
+
Per feat-002 §4.3: a single LLM call per iteration that may return
|
|
4
|
+
zero or more *structured* tool calls. The loop terminates when the
|
|
5
|
+
LLM returns a response with no tool calls (modern Anthropic / OpenAI
|
|
6
|
+
tool-calling pattern — `stop_reason="end_turn"` with empty
|
|
7
|
+
`tool_calls`). No special "finish" tool is needed.
|
|
8
|
+
|
|
9
|
+
Step shape: each iteration produces one `think` step (the LLM's
|
|
10
|
+
content), one `act` step per tool call, and one `observe` step per
|
|
11
|
+
tool result. Tool-execution errors are surfaced to the LLM as
|
|
12
|
+
observations (counted toward `error_streak_limit`).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import AsyncIterator
|
|
18
|
+
|
|
19
|
+
from agentforge_core.contracts.tool import Tool
|
|
20
|
+
from agentforge_core.observability.tracing import get_tracer
|
|
21
|
+
from agentforge_core.values.chat import StreamingEvent
|
|
22
|
+
from agentforge_core.values.messages import Message
|
|
23
|
+
from agentforge_core.values.state import AgentState
|
|
24
|
+
|
|
25
|
+
from agentforge.resolver_register import register_strategy
|
|
26
|
+
from agentforge.strategies._base import StrategyBase, _events_for_new_steps, get_runtime
|
|
27
|
+
|
|
28
|
+
DEFAULT_SYSTEM_PROMPT = (
|
|
29
|
+
"You are a helpful AI assistant. You can use tools when they help "
|
|
30
|
+
"answer the user's question. Provide a final answer without calling "
|
|
31
|
+
"tools when you have everything you need."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@register_strategy("react")
|
|
36
|
+
class ReActLoop(StrategyBase):
|
|
37
|
+
"""Think → act → observe loop with structured tool calls.
|
|
38
|
+
|
|
39
|
+
Per feat-002 §4.2 the constructor surface is locked at v0.1:
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
max_iterations: Per-loop iteration cap. If `None`, the
|
|
43
|
+
strategy uses `BudgetPolicy.max_iterations` from the
|
|
44
|
+
runtime context. The override applies only to this
|
|
45
|
+
run; the agent's configured budget caps are preserved.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, *, max_iterations: int | None = None) -> None:
|
|
49
|
+
self._max_iterations_override: int | None = max_iterations
|
|
50
|
+
|
|
51
|
+
async def run(self, state: AgentState) -> AgentState:
|
|
52
|
+
runtime = get_runtime(state)
|
|
53
|
+
if self._max_iterations_override is not None:
|
|
54
|
+
runtime.budget.max_iterations = self._max_iterations_override
|
|
55
|
+
|
|
56
|
+
system_prompt = runtime.system_prompt or DEFAULT_SYSTEM_PROMPT
|
|
57
|
+
tool_specs = [tool.to_spec() for tool in runtime.tools] if runtime.tools else None
|
|
58
|
+
messages: list[Message] = [Message(role="user", content=state.task)]
|
|
59
|
+
iteration = 0
|
|
60
|
+
tracer = get_tracer()
|
|
61
|
+
|
|
62
|
+
while True:
|
|
63
|
+
with tracer.start_as_current_span(
|
|
64
|
+
"strategy.iteration",
|
|
65
|
+
attributes={
|
|
66
|
+
"agentforge.iteration": iteration,
|
|
67
|
+
"agentforge.strategy": "react",
|
|
68
|
+
},
|
|
69
|
+
):
|
|
70
|
+
self._check_guardrails(state)
|
|
71
|
+
|
|
72
|
+
response = await self._call_llm(
|
|
73
|
+
state,
|
|
74
|
+
iteration=iteration,
|
|
75
|
+
system=system_prompt,
|
|
76
|
+
messages=messages,
|
|
77
|
+
tools=tool_specs,
|
|
78
|
+
kind="think",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Modern termination: no tool calls means the LLM is done.
|
|
82
|
+
if not response.tool_calls:
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
# Record the assistant's turn for the next iteration's context.
|
|
86
|
+
messages.append(Message(role="assistant", content=response.content))
|
|
87
|
+
|
|
88
|
+
# Dispatch every tool call the LLM emitted.
|
|
89
|
+
for tool_call in response.tool_calls:
|
|
90
|
+
self._record_step(
|
|
91
|
+
state,
|
|
92
|
+
iteration=iteration,
|
|
93
|
+
kind="act",
|
|
94
|
+
content={
|
|
95
|
+
"tool": tool_call.name,
|
|
96
|
+
"arguments": tool_call.arguments,
|
|
97
|
+
},
|
|
98
|
+
tool_call=tool_call,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
tool = _find_tool(runtime.tools, tool_call.name)
|
|
102
|
+
observation = await self._dispatch_tool(
|
|
103
|
+
tool, tool_call.name, dict(tool_call.arguments)
|
|
104
|
+
)
|
|
105
|
+
if observation.startswith("Error:"):
|
|
106
|
+
runtime.budget.record_error()
|
|
107
|
+
else:
|
|
108
|
+
runtime.budget.record_success()
|
|
109
|
+
|
|
110
|
+
self._record_step(
|
|
111
|
+
state,
|
|
112
|
+
iteration=iteration,
|
|
113
|
+
kind="observe",
|
|
114
|
+
content=observation,
|
|
115
|
+
tool_call=tool_call,
|
|
116
|
+
)
|
|
117
|
+
messages.append(
|
|
118
|
+
Message(
|
|
119
|
+
role="tool",
|
|
120
|
+
content=observation,
|
|
121
|
+
tool_call_id=tool_call.id,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
iteration += 1
|
|
126
|
+
|
|
127
|
+
return state
|
|
128
|
+
|
|
129
|
+
async def stream(self, state: AgentState) -> AsyncIterator[StreamingEvent]:
|
|
130
|
+
"""Per-iteration streaming override (feat-002 v0.3 polish).
|
|
131
|
+
|
|
132
|
+
Mirrors :meth:`run` but yields a ``step`` `StreamingEvent`
|
|
133
|
+
each time a step is appended to ``state.steps``. The terminal
|
|
134
|
+
``done`` event is yielded so ``Agent.stream`` can swallow it
|
|
135
|
+
and emit its own canonical one carrying the full RunResult
|
|
136
|
+
shape.
|
|
137
|
+
|
|
138
|
+
Strategies that bypass `Agent.stream()` (e.g. unit tests) see
|
|
139
|
+
the strategy-level done with ``run_id`` + ``cost_usd``.
|
|
140
|
+
"""
|
|
141
|
+
runtime = get_runtime(state)
|
|
142
|
+
if self._max_iterations_override is not None:
|
|
143
|
+
runtime.budget.max_iterations = self._max_iterations_override
|
|
144
|
+
|
|
145
|
+
system_prompt = runtime.system_prompt or DEFAULT_SYSTEM_PROMPT
|
|
146
|
+
tool_specs = [tool.to_spec() for tool in runtime.tools] if runtime.tools else None
|
|
147
|
+
messages: list[Message] = [Message(role="user", content=state.task)]
|
|
148
|
+
iteration = 0
|
|
149
|
+
before = len(state.steps)
|
|
150
|
+
tracer = get_tracer()
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
with tracer.start_as_current_span(
|
|
154
|
+
"strategy.iteration",
|
|
155
|
+
attributes={
|
|
156
|
+
"agentforge.iteration": iteration,
|
|
157
|
+
"agentforge.strategy": "react",
|
|
158
|
+
},
|
|
159
|
+
):
|
|
160
|
+
self._check_guardrails(state)
|
|
161
|
+
|
|
162
|
+
response = await self._call_llm(
|
|
163
|
+
state,
|
|
164
|
+
iteration=iteration,
|
|
165
|
+
system=system_prompt,
|
|
166
|
+
messages=messages,
|
|
167
|
+
tools=tool_specs,
|
|
168
|
+
kind="think",
|
|
169
|
+
)
|
|
170
|
+
for ev in _events_for_new_steps(state.steps, before):
|
|
171
|
+
yield ev
|
|
172
|
+
before = len(state.steps)
|
|
173
|
+
|
|
174
|
+
if not response.tool_calls:
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
messages.append(Message(role="assistant", content=response.content))
|
|
178
|
+
|
|
179
|
+
for tool_call in response.tool_calls:
|
|
180
|
+
self._record_step(
|
|
181
|
+
state,
|
|
182
|
+
iteration=iteration,
|
|
183
|
+
kind="act",
|
|
184
|
+
content={
|
|
185
|
+
"tool": tool_call.name,
|
|
186
|
+
"arguments": tool_call.arguments,
|
|
187
|
+
},
|
|
188
|
+
tool_call=tool_call,
|
|
189
|
+
)
|
|
190
|
+
for ev in _events_for_new_steps(state.steps, before):
|
|
191
|
+
yield ev
|
|
192
|
+
before = len(state.steps)
|
|
193
|
+
|
|
194
|
+
tool = _find_tool(runtime.tools, tool_call.name)
|
|
195
|
+
observation = await self._dispatch_tool(
|
|
196
|
+
tool, tool_call.name, dict(tool_call.arguments)
|
|
197
|
+
)
|
|
198
|
+
if observation.startswith("Error:"):
|
|
199
|
+
runtime.budget.record_error()
|
|
200
|
+
else:
|
|
201
|
+
runtime.budget.record_success()
|
|
202
|
+
|
|
203
|
+
self._record_step(
|
|
204
|
+
state,
|
|
205
|
+
iteration=iteration,
|
|
206
|
+
kind="observe",
|
|
207
|
+
content=observation,
|
|
208
|
+
tool_call=tool_call,
|
|
209
|
+
)
|
|
210
|
+
for ev in _events_for_new_steps(state.steps, before):
|
|
211
|
+
yield ev
|
|
212
|
+
before = len(state.steps)
|
|
213
|
+
|
|
214
|
+
messages.append(
|
|
215
|
+
Message(
|
|
216
|
+
role="tool",
|
|
217
|
+
content=observation,
|
|
218
|
+
tool_call_id=tool_call.id,
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
iteration += 1
|
|
223
|
+
|
|
224
|
+
yield StreamingEvent(
|
|
225
|
+
kind="done",
|
|
226
|
+
content={
|
|
227
|
+
"run_id": state.run_id,
|
|
228
|
+
"cost_usd": float(runtime.budget.spent_usd),
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _find_tool(tools: tuple[Tool, ...], name: str) -> Tool | None:
|
|
234
|
+
for tool in tools:
|
|
235
|
+
if type(tool).name == name:
|
|
236
|
+
return tool
|
|
237
|
+
return None
|