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.
Files changed (157) hide show
  1. agentforge/__init__.py +114 -0
  2. agentforge/_testing/__init__.py +19 -0
  3. agentforge/_testing/fake_llm.py +126 -0
  4. agentforge/_testing/fake_tool.py +122 -0
  5. agentforge/_tools/__init__.py +14 -0
  6. agentforge/_tools/calculator.py +102 -0
  7. agentforge/_tools/decorator.py +300 -0
  8. agentforge/_tools/file_read.py +112 -0
  9. agentforge/_tools/shell.py +134 -0
  10. agentforge/_tools/web_search.py +207 -0
  11. agentforge/agent.py +817 -0
  12. agentforge/auth.py +42 -0
  13. agentforge/cli/__init__.py +18 -0
  14. agentforge/cli/_build.py +323 -0
  15. agentforge/cli/_scaffold_state.py +250 -0
  16. agentforge/cli/_shared_scaffold.py +174 -0
  17. agentforge/cli/config_cmd.py +174 -0
  18. agentforge/cli/db_cmd.py +262 -0
  19. agentforge/cli/debug_cmd.py +168 -0
  20. agentforge/cli/docs_cmd.py +217 -0
  21. agentforge/cli/eval_cmd.py +181 -0
  22. agentforge/cli/health_cmd.py +139 -0
  23. agentforge/cli/list_modules.py +85 -0
  24. agentforge/cli/main.py +81 -0
  25. agentforge/cli/manifest_apply.py +368 -0
  26. agentforge/cli/module_cmd.py +247 -0
  27. agentforge/cli/new_cmd.py +171 -0
  28. agentforge/cli/run_cmd.py +234 -0
  29. agentforge/cli/upgrade_cmd.py +230 -0
  30. agentforge/config/__init__.py +45 -0
  31. agentforge/eval/__init__.py +18 -0
  32. agentforge/eval/consistency.py +107 -0
  33. agentforge/eval/coverage.py +100 -0
  34. agentforge/eval/format_compliance.py +107 -0
  35. agentforge/eval/regression.py +143 -0
  36. agentforge/findings.py +166 -0
  37. agentforge/guardrails/__init__.py +32 -0
  38. agentforge/guardrails/allowlist.py +49 -0
  39. agentforge/guardrails/capability_check.py +58 -0
  40. agentforge/guardrails/engine.py +289 -0
  41. agentforge/guardrails/pii_redact_basic.py +61 -0
  42. agentforge/guardrails/prompt_injection_basic.py +90 -0
  43. agentforge/memory/__init__.py +16 -0
  44. agentforge/memory/in_memory.py +130 -0
  45. agentforge/memory/in_memory_graph.py +262 -0
  46. agentforge/memory/in_memory_vector.py +167 -0
  47. agentforge/pipeline/__init__.py +26 -0
  48. agentforge/pipeline/engine.py +189 -0
  49. agentforge/pipeline/errors.py +19 -0
  50. agentforge/pipeline/tool.py +93 -0
  51. agentforge/py.typed +0 -0
  52. agentforge/recording.py +189 -0
  53. agentforge/renderers/__init__.py +28 -0
  54. agentforge/renderers/_defaults.py +32 -0
  55. agentforge/renderers/markdown.py +44 -0
  56. agentforge/renderers/patch_applier.py +46 -0
  57. agentforge/renderers/registry.py +108 -0
  58. agentforge/renderers/scorecard.py +59 -0
  59. agentforge/renderers/span_table.py +71 -0
  60. agentforge/replay.py +260 -0
  61. agentforge/resolver_register.py +41 -0
  62. agentforge/retrieval.py +410 -0
  63. agentforge/runtime.py +63 -0
  64. agentforge/strategies/__init__.py +27 -0
  65. agentforge/strategies/_base.py +280 -0
  66. agentforge/strategies/_plan.py +93 -0
  67. agentforge/strategies/multi_agent.py +541 -0
  68. agentforge/strategies/plan_execute.py +506 -0
  69. agentforge/strategies/react.py +237 -0
  70. agentforge/strategies/tot.py +472 -0
  71. agentforge/templates/_shared/.cursorrules +12 -0
  72. agentforge/templates/_shared/.github/copilot-instructions.md +13 -0
  73. agentforge/templates/_shared/.gitkeep +0 -0
  74. agentforge/templates/_shared/AGENTS.md.tmpl +123 -0
  75. agentforge/templates/_shared/CLAUDE.md +13 -0
  76. agentforge/templates/_shared/docs/runbooks/01-set-up-new-agent.md.tmpl +67 -0
  77. agentforge/templates/_shared/docs/runbooks/02-add-a-tool.md +67 -0
  78. agentforge/templates/_shared/docs/runbooks/03-add-a-pipeline-task.md +69 -0
  79. agentforge/templates/_shared/docs/runbooks/04-pick-reasoning-strategy.md +67 -0
  80. agentforge/templates/_shared/docs/runbooks/05-write-prompts.md +75 -0
  81. agentforge/templates/_shared/docs/runbooks/06-test-your-agent.md +75 -0
  82. agentforge/templates/_shared/docs/runbooks/07-debug-a-run.md +70 -0
  83. agentforge/templates/_shared/docs/runbooks/08-add-memory.md +75 -0
  84. agentforge/templates/_shared/docs/runbooks/09-add-mcp.md +78 -0
  85. agentforge/templates/_shared/docs/runbooks/10-add-evaluators.md +76 -0
  86. agentforge/templates/_shared/docs/runbooks/11-add-safety-guardrails.md +83 -0
  87. agentforge/templates/_shared/docs/runbooks/12-add-observability.md +77 -0
  88. agentforge/templates/_shared/docs/runbooks/13-configure-multi-provider.md +91 -0
  89. agentforge/templates/_shared/docs/runbooks/14-deploy-your-agent.md +70 -0
  90. agentforge/templates/_shared/docs/runbooks/15-upgrade-your-agent.md +67 -0
  91. agentforge/templates/_shared/docs/runbooks/16-configuration-reference.md +81 -0
  92. agentforge/templates/_shared/docs/runbooks/17-add-reranker.md +78 -0
  93. agentforge/templates/_shared/docs/runbooks/18-add-hybrid-search.md +78 -0
  94. agentforge/templates/_shared/docs/runbooks/19-add-graphrag.md +83 -0
  95. agentforge/templates/_shared/docs/runbooks/20-apply-schema-migrations.md +92 -0
  96. agentforge/templates/_shared/docs/runbooks/21-use-streaming-guardrails.md +82 -0
  97. agentforge/templates/_shared/docs/runbooks/README.md.tmpl +68 -0
  98. agentforge/templates/code-reviewer/.env.example +8 -0
  99. agentforge/templates/code-reviewer/.gitignore +7 -0
  100. agentforge/templates/code-reviewer/README.md +12 -0
  101. agentforge/templates/code-reviewer/agentforge.yaml +23 -0
  102. agentforge/templates/code-reviewer/copier.yml +34 -0
  103. agentforge/templates/code-reviewer/pyproject.toml +18 -0
  104. agentforge/templates/code-reviewer/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  105. agentforge/templates/code-reviewer/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
  106. agentforge/templates/docs-qa/.env.example +8 -0
  107. agentforge/templates/docs-qa/.gitignore +7 -0
  108. agentforge/templates/docs-qa/README.md +14 -0
  109. agentforge/templates/docs-qa/agentforge.yaml +19 -0
  110. agentforge/templates/docs-qa/copier.yml +31 -0
  111. agentforge/templates/docs-qa/pyproject.toml +18 -0
  112. agentforge/templates/docs-qa/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  113. agentforge/templates/docs-qa/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
  114. agentforge/templates/minimal/.env.example +11 -0
  115. agentforge/templates/minimal/.gitignore +10 -0
  116. agentforge/templates/minimal/README.md +28 -0
  117. agentforge/templates/minimal/agentforge.yaml +10 -0
  118. agentforge/templates/minimal/copier.yml +52 -0
  119. agentforge/templates/minimal/pyproject.toml +18 -0
  120. agentforge/templates/minimal/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  121. agentforge/templates/minimal/src/{{project_slug.replace('-', '_')}}/main.py +34 -0
  122. agentforge/templates/patch-bot/.env.example +8 -0
  123. agentforge/templates/patch-bot/.gitignore +7 -0
  124. agentforge/templates/patch-bot/README.md +13 -0
  125. agentforge/templates/patch-bot/agentforge.yaml +15 -0
  126. agentforge/templates/patch-bot/copier.yml +31 -0
  127. agentforge/templates/patch-bot/pyproject.toml +18 -0
  128. agentforge/templates/patch-bot/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  129. agentforge/templates/patch-bot/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
  130. agentforge/templates/research/.env.example +8 -0
  131. agentforge/templates/research/.gitignore +7 -0
  132. agentforge/templates/research/README.md +14 -0
  133. agentforge/templates/research/agentforge.yaml +17 -0
  134. agentforge/templates/research/copier.yml +31 -0
  135. agentforge/templates/research/pyproject.toml +18 -0
  136. agentforge/templates/research/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  137. agentforge/templates/research/src/{{project_slug.replace('-', '_')}}/main.py +31 -0
  138. agentforge/templates/triage/.env.example +8 -0
  139. agentforge/templates/triage/.gitignore +7 -0
  140. agentforge/templates/triage/README.md +14 -0
  141. agentforge/templates/triage/agentforge.yaml +25 -0
  142. agentforge/templates/triage/copier.yml +31 -0
  143. agentforge/templates/triage/pyproject.toml +18 -0
  144. agentforge/templates/triage/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  145. agentforge/templates/triage/src/{{project_slug.replace('-', '_')}}/main.py +30 -0
  146. agentforge/testing/__init__.py +69 -0
  147. agentforge/testing/conformance.py +40 -0
  148. agentforge/testing/factory.py +89 -0
  149. agentforge/testing/fixtures.py +42 -0
  150. agentforge/testing/llm.py +235 -0
  151. agentforge/testing/recording.py +177 -0
  152. agentforge/tools/__init__.py +41 -0
  153. agentforge_py-0.2.1.dist-info/METADATA +158 -0
  154. agentforge_py-0.2.1.dist-info/RECORD +157 -0
  155. agentforge_py-0.2.1.dist-info/WHEEL +4 -0
  156. agentforge_py-0.2.1.dist-info/entry_points.txt +2 -0
  157. 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