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
agentforge/__init__.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""AgentForge — open-source plug-and-play framework for production AI agents.
|
|
2
|
+
|
|
3
|
+
This package is the default runtime. It ships:
|
|
4
|
+
|
|
5
|
+
- The `Agent` orchestrator (locked constructor surface per feat-001).
|
|
6
|
+
- `InMemoryStore` — process-local default `MemoryStore` so a fresh
|
|
7
|
+
agent has persistence wired without external infra.
|
|
8
|
+
- The configuration loader (`load_config`).
|
|
9
|
+
- The reasoning-strategy infrastructure: `RuntimeContext`,
|
|
10
|
+
`StrategyBase`, `get_runtime`, `ReActLoop` (feat-002).
|
|
11
|
+
|
|
12
|
+
For provider clients, persistence drivers, MCP, observability backends,
|
|
13
|
+
and safety modules, install the corresponding `agentforge-<X>` packages
|
|
14
|
+
or use the `agentforge[<extra>]` install (per ADR-0003).
|
|
15
|
+
|
|
16
|
+
See the project docs at `docs/README.md` (in the design workspace) and
|
|
17
|
+
the per-feature specs under `docs/features/`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from agentforge_core import FallbackChain
|
|
23
|
+
|
|
24
|
+
# feat-018: importing `agentforge.guardrails` here triggers the
|
|
25
|
+
# module-side `@register(...)` decorators on `PromptInjectionBasic`,
|
|
26
|
+
# `PIIRedactBasic`, `CapabilityCheck`, and `Allowlist` so they're
|
|
27
|
+
# resolvable by name from `agentforge.yaml` without an explicit
|
|
28
|
+
# import in the consumer.
|
|
29
|
+
import agentforge.guardrails # noqa: F401
|
|
30
|
+
from agentforge._tools import tool
|
|
31
|
+
from agentforge.agent import Agent
|
|
32
|
+
from agentforge.auth import EnvBearerAuth
|
|
33
|
+
from agentforge.config import AgentForgeConfig, load_config
|
|
34
|
+
from agentforge.findings import (
|
|
35
|
+
MultiSpanFinding,
|
|
36
|
+
NarrativeFinding,
|
|
37
|
+
Patch,
|
|
38
|
+
PatchFinding,
|
|
39
|
+
SimpleFinding,
|
|
40
|
+
Span,
|
|
41
|
+
)
|
|
42
|
+
from agentforge.memory import InMemoryGraphStore, InMemoryStore, InMemoryVectorStore
|
|
43
|
+
from agentforge.pipeline import (
|
|
44
|
+
Pipeline,
|
|
45
|
+
PipelineFailure,
|
|
46
|
+
PipelineFindingsTool,
|
|
47
|
+
PipelineResult,
|
|
48
|
+
Task,
|
|
49
|
+
)
|
|
50
|
+
from agentforge.renderers import (
|
|
51
|
+
MarkdownRenderer,
|
|
52
|
+
MissingRendererError,
|
|
53
|
+
PatchApplierRenderer,
|
|
54
|
+
RendererRegistry,
|
|
55
|
+
ScorecardRenderer,
|
|
56
|
+
SpanTableRenderer,
|
|
57
|
+
)
|
|
58
|
+
from agentforge.resolver_register import register_task
|
|
59
|
+
from agentforge.retrieval import Retriever
|
|
60
|
+
from agentforge.runtime import RUNTIME_KEY, RuntimeContext
|
|
61
|
+
from agentforge.strategies import (
|
|
62
|
+
MultiAgentSupervisor,
|
|
63
|
+
Plan,
|
|
64
|
+
PlanExecuteLoop,
|
|
65
|
+
PlanStep,
|
|
66
|
+
ReActLoop,
|
|
67
|
+
StrategyBase,
|
|
68
|
+
TreeOfThoughts,
|
|
69
|
+
get_runtime,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
__version__ = "0.2.1"
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"RUNTIME_KEY",
|
|
76
|
+
"Agent",
|
|
77
|
+
"AgentForgeConfig",
|
|
78
|
+
"EnvBearerAuth",
|
|
79
|
+
"FallbackChain",
|
|
80
|
+
"InMemoryGraphStore",
|
|
81
|
+
"InMemoryStore",
|
|
82
|
+
"InMemoryVectorStore",
|
|
83
|
+
"MarkdownRenderer",
|
|
84
|
+
"MissingRendererError",
|
|
85
|
+
"MultiAgentSupervisor",
|
|
86
|
+
"MultiSpanFinding",
|
|
87
|
+
"NarrativeFinding",
|
|
88
|
+
"Patch",
|
|
89
|
+
"PatchApplierRenderer",
|
|
90
|
+
"PatchFinding",
|
|
91
|
+
"Pipeline",
|
|
92
|
+
"PipelineFailure",
|
|
93
|
+
"PipelineFindingsTool",
|
|
94
|
+
"PipelineResult",
|
|
95
|
+
"Plan",
|
|
96
|
+
"PlanExecuteLoop",
|
|
97
|
+
"PlanStep",
|
|
98
|
+
"ReActLoop",
|
|
99
|
+
"RendererRegistry",
|
|
100
|
+
"Retriever",
|
|
101
|
+
"RuntimeContext",
|
|
102
|
+
"ScorecardRenderer",
|
|
103
|
+
"SimpleFinding",
|
|
104
|
+
"Span",
|
|
105
|
+
"SpanTableRenderer",
|
|
106
|
+
"StrategyBase",
|
|
107
|
+
"Task",
|
|
108
|
+
"TreeOfThoughts",
|
|
109
|
+
"__version__",
|
|
110
|
+
"get_runtime",
|
|
111
|
+
"load_config",
|
|
112
|
+
"register_task",
|
|
113
|
+
"tool",
|
|
114
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Local test helpers.
|
|
2
|
+
|
|
3
|
+
Private package (underscore prefix) — these helpers exist only to
|
|
4
|
+
support feat-002's tests (and other early features) until feat-016
|
|
5
|
+
ships the full public testing API at `agentforge.testing`.
|
|
6
|
+
|
|
7
|
+
Helpers here:
|
|
8
|
+
|
|
9
|
+
- `FakeLLMClient` — minimal scripted-response `LLMClient`. Replaced
|
|
10
|
+
by feat-016's `MockLLMClient` (richer: recording / replay /
|
|
11
|
+
capability simulation).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from agentforge._testing.fake_llm import FakeLLMClient, echo_response
|
|
17
|
+
from agentforge._testing.fake_tool import FakeTool
|
|
18
|
+
|
|
19
|
+
__all__ = ["FakeLLMClient", "FakeTool", "echo_response"]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""`FakeLLMClient` — minimal scripted-response `LLMClient` for unit tests.
|
|
2
|
+
|
|
3
|
+
Used by feat-002's strategy tests. Replaced by feat-016's full
|
|
4
|
+
`MockLLMClient` (which supports recording / replay / capability
|
|
5
|
+
simulation) when that lands; until then, this class is enough to
|
|
6
|
+
drive the four reasoning strategies through their unit and
|
|
7
|
+
integration tests.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
|
|
11
|
+
fake = FakeLLMClient(
|
|
12
|
+
responses=[
|
|
13
|
+
LLMResponse(content="thought 1", stop_reason="tool_use",
|
|
14
|
+
tool_calls=(ToolCall(id="t1", name="search",
|
|
15
|
+
arguments={"q": "x"}),),
|
|
16
|
+
usage=TokenUsage(input_tokens=10, output_tokens=5),
|
|
17
|
+
cost_usd=0.001, model="m", provider="p"),
|
|
18
|
+
LLMResponse(content="final answer", stop_reason="end_turn",
|
|
19
|
+
usage=TokenUsage(input_tokens=12, output_tokens=8),
|
|
20
|
+
cost_usd=0.002, model="m", provider="p"),
|
|
21
|
+
],
|
|
22
|
+
)
|
|
23
|
+
# When the strategy under test calls fake.call(...) twice,
|
|
24
|
+
# it gets the two scripted responses in order.
|
|
25
|
+
|
|
26
|
+
Responses can also be callables (called with the call's args) for
|
|
27
|
+
dynamic scripting:
|
|
28
|
+
|
|
29
|
+
FakeLLMClient(responses=[lambda system, messages, tools=None: ...])
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from collections.abc import Callable
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from agentforge_core.contracts.llm import LLMClient
|
|
38
|
+
from agentforge_core.values.messages import LLMResponse, Message, TokenUsage, ToolSpec
|
|
39
|
+
|
|
40
|
+
ResponseSpec = LLMResponse | Callable[..., LLMResponse]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FakeLLMClient(LLMClient):
|
|
44
|
+
"""Scripted-response `LLMClient` for tests.
|
|
45
|
+
|
|
46
|
+
Constructor takes a list of `LLMResponse` instances OR callables
|
|
47
|
+
that build a response from the call args. Each `.call()` returns
|
|
48
|
+
the next item in the list; raises if the list is exhausted.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
responses: list[ResponseSpec] | None = None,
|
|
54
|
+
*,
|
|
55
|
+
capabilities: set[str] | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
self._responses: list[ResponseSpec] = list(responses or [])
|
|
58
|
+
self._call_count: int = 0
|
|
59
|
+
self._captured: list[tuple[str, list[Message], list[ToolSpec] | None]] = []
|
|
60
|
+
self._capabilities: set[str] = set(capabilities or ())
|
|
61
|
+
self._closed: bool = False
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def call_count(self) -> int:
|
|
65
|
+
"""Number of `.call()` invocations so far."""
|
|
66
|
+
return self._call_count
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def captured(
|
|
70
|
+
self,
|
|
71
|
+
) -> list[tuple[str, list[Message], list[ToolSpec] | None]]:
|
|
72
|
+
"""Every `(system, messages, tools)` triple seen by `.call()`,
|
|
73
|
+
in order. Useful for asserting the strategy sent the expected
|
|
74
|
+
messages."""
|
|
75
|
+
return list(self._captured)
|
|
76
|
+
|
|
77
|
+
async def call(
|
|
78
|
+
self,
|
|
79
|
+
system: str,
|
|
80
|
+
messages: list[Message],
|
|
81
|
+
tools: list[ToolSpec] | None = None,
|
|
82
|
+
) -> LLMResponse:
|
|
83
|
+
if self._closed:
|
|
84
|
+
raise RuntimeError("FakeLLMClient.close() was already called")
|
|
85
|
+
if self._call_count >= len(self._responses):
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
f"FakeLLMClient exhausted after {self._call_count} calls; "
|
|
88
|
+
f"add more scripted responses or check the strategy's loop."
|
|
89
|
+
)
|
|
90
|
+
spec = self._responses[self._call_count]
|
|
91
|
+
self._captured.append((system, list(messages), tools))
|
|
92
|
+
self._call_count += 1
|
|
93
|
+
if callable(spec):
|
|
94
|
+
return spec(system=system, messages=messages, tools=tools)
|
|
95
|
+
return spec
|
|
96
|
+
|
|
97
|
+
async def close(self) -> None:
|
|
98
|
+
self._closed = True
|
|
99
|
+
|
|
100
|
+
def capabilities(self) -> set[str]:
|
|
101
|
+
return set(self._capabilities)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def echo_response(
|
|
105
|
+
*,
|
|
106
|
+
content: str = "ok",
|
|
107
|
+
stop_reason: str = "end_turn",
|
|
108
|
+
cost_usd: float = 0.0,
|
|
109
|
+
input_tokens: int = 1,
|
|
110
|
+
output_tokens: int = 1,
|
|
111
|
+
**_: Any,
|
|
112
|
+
) -> LLMResponse:
|
|
113
|
+
"""Convenience builder for an `LLMResponse` with sensible defaults.
|
|
114
|
+
|
|
115
|
+
Used by tests that only care about a single LLM call's output
|
|
116
|
+
shape and don't want to hand-write the full `LLMResponse`
|
|
117
|
+
construction every time.
|
|
118
|
+
"""
|
|
119
|
+
return LLMResponse(
|
|
120
|
+
content=content,
|
|
121
|
+
stop_reason=stop_reason, # type: ignore[arg-type]
|
|
122
|
+
usage=TokenUsage(input_tokens=input_tokens, output_tokens=output_tokens),
|
|
123
|
+
cost_usd=cost_usd,
|
|
124
|
+
model="fake",
|
|
125
|
+
provider="fake",
|
|
126
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""`FakeTool` — minimal scripted-response `Tool` for unit tests
|
|
2
|
+
(feat-004 chunk 5).
|
|
3
|
+
|
|
4
|
+
Replaces any tool with a stub during tests. Two construction forms:
|
|
5
|
+
|
|
6
|
+
from agentforge._testing import FakeTool
|
|
7
|
+
|
|
8
|
+
# 1. Static return value
|
|
9
|
+
web_search = FakeTool.fake("web_search", "stub result")
|
|
10
|
+
|
|
11
|
+
# 2. Callable that computes the response from the call args
|
|
12
|
+
web_search = FakeTool.fake(
|
|
13
|
+
"web_search",
|
|
14
|
+
lambda **kwargs: f"results for {kwargs['query']!r}",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
The fake honours the same locked `Tool` ABC: it has a `name`,
|
|
18
|
+
`description`, `input_schema` (a permissive `dict`-shaped model that
|
|
19
|
+
accepts any kwargs), and a `run(**kwargs)` method. `Agent(tools=
|
|
20
|
+
[fake, ...])` works without other changes.
|
|
21
|
+
|
|
22
|
+
Replaced by feat-016's richer testing API; this is the minimum
|
|
23
|
+
surface to support feat-004 / feat-002 tests today.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
from collections.abc import Awaitable, Callable
|
|
30
|
+
from typing import Any, ClassVar
|
|
31
|
+
|
|
32
|
+
from agentforge_core.contracts.tool import Tool
|
|
33
|
+
from pydantic import BaseModel, ConfigDict
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _PermissiveInput(BaseModel):
|
|
37
|
+
"""Input schema for `FakeTool` — accepts any kwargs.
|
|
38
|
+
|
|
39
|
+
Real `Tool` implementations declare a strict Pydantic model so
|
|
40
|
+
bad LLM tool-calls are rejected at the dispatch boundary; the
|
|
41
|
+
fake intentionally relaxes this so test code can pass arbitrary
|
|
42
|
+
kwargs without first defining a schema.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
model_config = ConfigDict(extra="allow")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_FakeFn = Callable[..., Any] | Callable[..., Awaitable[Any]]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FakeTool(Tool):
|
|
52
|
+
"""Test-only `Tool` that returns scripted responses.
|
|
53
|
+
|
|
54
|
+
Construct via `FakeTool.fake(name, response_or_fn)` rather than
|
|
55
|
+
the bare class so the per-instance `name` / `description` work
|
|
56
|
+
without subclassing.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
name: ClassVar[str] = "fake"
|
|
60
|
+
description: ClassVar[str] = "Test-only stub tool."
|
|
61
|
+
input_schema: ClassVar[type[BaseModel]] = _PermissiveInput
|
|
62
|
+
capabilities: ClassVar[frozenset[str]] = frozenset()
|
|
63
|
+
calls: list[dict[str, Any]]
|
|
64
|
+
"""Per-instance recorded `run` invocation kwargs. Populated by
|
|
65
|
+
`fake()`-built instances; bare-class fallback keeps it empty."""
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def fake(
|
|
69
|
+
cls,
|
|
70
|
+
name: str,
|
|
71
|
+
response: Any | _FakeFn,
|
|
72
|
+
*,
|
|
73
|
+
description: str | None = None,
|
|
74
|
+
capabilities: frozenset[str] | set[str] = frozenset(),
|
|
75
|
+
) -> FakeTool:
|
|
76
|
+
"""Build a fake tool with the given name and response.
|
|
77
|
+
|
|
78
|
+
`response` can be:
|
|
79
|
+
- A static value (returned as-is from every `run` call)
|
|
80
|
+
- A sync callable: `fn(**kwargs) -> Any`
|
|
81
|
+
- An async callable: `async fn(**kwargs) -> Any`
|
|
82
|
+
|
|
83
|
+
Records every call in `self.calls` for assertions.
|
|
84
|
+
"""
|
|
85
|
+
# Synthesize a class so `name` / `description` / `capabilities`
|
|
86
|
+
# become per-fake. type() avoids subclass-scope dance from the
|
|
87
|
+
# @tool decorator.
|
|
88
|
+
is_async = _is_async_callable(response)
|
|
89
|
+
is_callable = callable(response) and not isinstance(response, type)
|
|
90
|
+
|
|
91
|
+
async def _run(self: FakeTool, **kwargs: Any) -> Any:
|
|
92
|
+
self.calls.append(dict(kwargs))
|
|
93
|
+
if is_callable:
|
|
94
|
+
if is_async:
|
|
95
|
+
return await response(**kwargs)
|
|
96
|
+
return response(**kwargs)
|
|
97
|
+
return response
|
|
98
|
+
|
|
99
|
+
cls_namespace: dict[str, Any] = {
|
|
100
|
+
"name": name,
|
|
101
|
+
"description": description or f"Fake {name} tool.",
|
|
102
|
+
"input_schema": _PermissiveInput,
|
|
103
|
+
"capabilities": frozenset(capabilities),
|
|
104
|
+
"run": _run,
|
|
105
|
+
"calls": [],
|
|
106
|
+
}
|
|
107
|
+
synthesized = type(f"Fake{name.title()}Tool", (cls,), cls_namespace)
|
|
108
|
+
instance: FakeTool = synthesized()
|
|
109
|
+
return instance
|
|
110
|
+
|
|
111
|
+
async def run(self, **kwargs: Any) -> Any: # noqa: ARG002 — bare-class fallback ignores kwargs
|
|
112
|
+
"""Default `run` body — overridden by `fake()` instances; the
|
|
113
|
+
bare-class fallback returns the empty string."""
|
|
114
|
+
return ""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_async_callable(obj: Any) -> bool:
|
|
118
|
+
"""True for `async def` functions and partials wrapping them."""
|
|
119
|
+
return asyncio.iscoroutinefunction(obj)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
__all__ = ["FakeTool"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Internal tooling module (feat-004).
|
|
2
|
+
|
|
3
|
+
The `@tool` decorator and the four shipped default tools
|
|
4
|
+
(`calculator`, `file_read`, `web_search`, `shell`) live under this
|
|
5
|
+
underscore-prefixed package; the public surface is re-exported from
|
|
6
|
+
`agentforge` (`from agentforge import tool`) and from
|
|
7
|
+
`agentforge.tools` (`from agentforge.tools import calculator`).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from agentforge._tools.decorator import tool
|
|
13
|
+
|
|
14
|
+
__all__ = ["tool"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""`calculator` — arithmetic expression tool (feat-004).
|
|
2
|
+
|
|
3
|
+
Evaluates pure-arithmetic expressions via Python's AST module
|
|
4
|
+
(`ast.parse` + recursive walker). **Does not use `eval()`** — only a
|
|
5
|
+
closed set of node types is allowed, so the tool can't be tricked
|
|
6
|
+
into running arbitrary Python.
|
|
7
|
+
|
|
8
|
+
Supported:
|
|
9
|
+
- Numeric literals (int, float)
|
|
10
|
+
- Binary ops: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
11
|
+
- Unary ops: `+`, `-`
|
|
12
|
+
- Parenthesisation
|
|
13
|
+
|
|
14
|
+
Rejected (raises `ValueError`):
|
|
15
|
+
- Names (variables), attribute access, function calls
|
|
16
|
+
- Subscripts, list / dict / set literals
|
|
17
|
+
- Comprehensions, lambdas, walrus, anything statement-y
|
|
18
|
+
|
|
19
|
+
Capabilities: empty (pure computation, no side effects).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import ast
|
|
25
|
+
import operator
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
|
|
28
|
+
from agentforge._tools.decorator import tool
|
|
29
|
+
|
|
30
|
+
_Number = int | float
|
|
31
|
+
_BinaryFn = Callable[[_Number, _Number], _Number]
|
|
32
|
+
_UnaryFn = Callable[[_Number], _Number]
|
|
33
|
+
|
|
34
|
+
_BINARY_OPS: dict[type[ast.operator], _BinaryFn] = {
|
|
35
|
+
ast.Add: operator.add,
|
|
36
|
+
ast.Sub: operator.sub,
|
|
37
|
+
ast.Mult: operator.mul,
|
|
38
|
+
ast.Div: operator.truediv,
|
|
39
|
+
ast.FloorDiv: operator.floordiv,
|
|
40
|
+
ast.Mod: operator.mod,
|
|
41
|
+
ast.Pow: operator.pow,
|
|
42
|
+
}
|
|
43
|
+
_UNARY_OPS: dict[type[ast.unaryop], _UnaryFn] = {
|
|
44
|
+
ast.UAdd: operator.pos,
|
|
45
|
+
ast.USub: operator.neg,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _evaluate(node: ast.AST) -> _Number:
|
|
50
|
+
"""Walk an AST node, evaluating only the closed set of allowed
|
|
51
|
+
arithmetic node types. Raise `ValueError` on anything else."""
|
|
52
|
+
if isinstance(node, ast.Expression):
|
|
53
|
+
return _evaluate(node.body)
|
|
54
|
+
if isinstance(node, ast.Constant):
|
|
55
|
+
if isinstance(node.value, (int, float)) and not isinstance(node.value, bool):
|
|
56
|
+
return node.value
|
|
57
|
+
msg = f"calculator: literal {node.value!r} is not a number"
|
|
58
|
+
raise ValueError(msg)
|
|
59
|
+
if isinstance(node, ast.BinOp):
|
|
60
|
+
bop_type = type(node.op)
|
|
61
|
+
bop_fn = _BINARY_OPS.get(bop_type)
|
|
62
|
+
if bop_fn is None:
|
|
63
|
+
msg = f"calculator: binary operator {bop_type.__name__!r} not allowed"
|
|
64
|
+
raise ValueError(msg)
|
|
65
|
+
return bop_fn(_evaluate(node.left), _evaluate(node.right))
|
|
66
|
+
if isinstance(node, ast.UnaryOp):
|
|
67
|
+
uop_type = type(node.op)
|
|
68
|
+
uop_fn = _UNARY_OPS.get(uop_type)
|
|
69
|
+
if uop_fn is None:
|
|
70
|
+
msg = f"calculator: unary operator {uop_type.__name__!r} not allowed"
|
|
71
|
+
raise ValueError(msg)
|
|
72
|
+
return uop_fn(_evaluate(node.operand))
|
|
73
|
+
msg = f"calculator: AST node {type(node).__name__!r} not allowed"
|
|
74
|
+
raise ValueError(msg)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@tool
|
|
78
|
+
def calculator(expression: str) -> float:
|
|
79
|
+
"""Evaluate an arithmetic expression and return the result.
|
|
80
|
+
|
|
81
|
+
Supports `+`, `-`, `*`, `/`, `//`, `%`, `**` and parentheses.
|
|
82
|
+
Variables, function calls, and any non-arithmetic syntax are
|
|
83
|
+
rejected — this is a calculator, not a Python interpreter.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
expression: The arithmetic expression to evaluate, e.g.
|
|
87
|
+
`"(1 + 2) * 3"` or `"2 ** 10"`.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The numeric result as a float (int values are coerced to
|
|
91
|
+
float for a uniform return type).
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
tree = ast.parse(expression, mode="eval")
|
|
95
|
+
except SyntaxError as exc:
|
|
96
|
+
msg = f"calculator: cannot parse expression {expression!r}: {exc.msg}"
|
|
97
|
+
raise ValueError(msg) from exc
|
|
98
|
+
result = _evaluate(tree)
|
|
99
|
+
return float(result)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
__all__ = ["calculator"]
|