framework-agents 0.1.0__tar.gz
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.
- framework_agents-0.1.0/.gitignore +47 -0
- framework_agents-0.1.0/PKG-INFO +118 -0
- framework_agents-0.1.0/README.md +87 -0
- framework_agents-0.1.0/examples/01-streaming-tools.py +49 -0
- framework_agents-0.1.0/examples/02-orchestration.py +27 -0
- framework_agents-0.1.0/examples/03-swarm.py +25 -0
- framework_agents-0.1.0/examples/04-workflows.py +47 -0
- framework_agents-0.1.0/examples/05-memory-sessions.py +32 -0
- framework_agents-0.1.0/examples/06-hitl-resume.py +64 -0
- framework_agents-0.1.0/examples/07-guardrails-budgets.py +53 -0
- framework_agents-0.1.0/examples/08-semantic-graph.py +38 -0
- framework_agents-0.1.0/examples/09-a2a-remote.py +49 -0
- framework_agents-0.1.0/examples/10-middleware-suite.py +47 -0
- framework_agents-0.1.0/examples/11-rag-retrieval.py +24 -0
- framework_agents-0.1.0/examples/12-providers.py +32 -0
- framework_agents-0.1.0/examples/13-serve-framework.py +59 -0
- framework_agents-0.1.0/examples/README.md +22 -0
- framework_agents-0.1.0/pyproject.toml +53 -0
- framework_agents-0.1.0/src/framework_agents/__init__.py +345 -0
- framework_agents-0.1.0/src/framework_agents/a2a_client.py +360 -0
- framework_agents-0.1.0/src/framework_agents/a2a_server.py +495 -0
- framework_agents-0.1.0/src/framework_agents/agent.py +920 -0
- framework_agents-0.1.0/src/framework_agents/checkpoint.py +231 -0
- framework_agents-0.1.0/src/framework_agents/config.py +200 -0
- framework_agents-0.1.0/src/framework_agents/conversation.py +249 -0
- framework_agents-0.1.0/src/framework_agents/errors.py +47 -0
- framework_agents-0.1.0/src/framework_agents/evals.py +127 -0
- framework_agents-0.1.0/src/framework_agents/mcp_client.py +477 -0
- framework_agents-0.1.0/src/framework_agents/memory.py +103 -0
- framework_agents-0.1.0/src/framework_agents/memory_compaction.py +63 -0
- framework_agents-0.1.0/src/framework_agents/middleware.py +1302 -0
- framework_agents-0.1.0/src/framework_agents/models/__init__.py +100 -0
- framework_agents-0.1.0/src/framework_agents/models/anthropic.py +339 -0
- framework_agents-0.1.0/src/framework_agents/models/capabilities.py +45 -0
- framework_agents-0.1.0/src/framework_agents/models/ollama.py +47 -0
- framework_agents-0.1.0/src/framework_agents/models/openai_compatible.py +409 -0
- framework_agents-0.1.0/src/framework_agents/observability.py +84 -0
- framework_agents-0.1.0/src/framework_agents/ontology_review.py +321 -0
- framework_agents-0.1.0/src/framework_agents/orchestration.py +169 -0
- framework_agents-0.1.0/src/framework_agents/pricing.py +63 -0
- framework_agents-0.1.0/src/framework_agents/prompts.py +132 -0
- framework_agents-0.1.0/src/framework_agents/rag.py +123 -0
- framework_agents-0.1.0/src/framework_agents/remote_sparql.py +213 -0
- framework_agents-0.1.0/src/framework_agents/runtime.py +88 -0
- framework_agents-0.1.0/src/framework_agents/sandbox.py +327 -0
- framework_agents-0.1.0/src/framework_agents/semantic.py +1614 -0
- framework_agents-0.1.0/src/framework_agents/serve.py +436 -0
- framework_agents-0.1.0/src/framework_agents/sessions.py +249 -0
- framework_agents-0.1.0/src/framework_agents/skills.py +213 -0
- framework_agents-0.1.0/src/framework_agents/state.py +89 -0
- framework_agents-0.1.0/src/framework_agents/streaming.py +74 -0
- framework_agents-0.1.0/src/framework_agents/swarm.py +318 -0
- framework_agents-0.1.0/src/framework_agents/tools.py +114 -0
- framework_agents-0.1.0/src/framework_agents/tracing.py +131 -0
- framework_agents-0.1.0/src/framework_agents/types.py +262 -0
- framework_agents-0.1.0/src/framework_agents/workflows.py +130 -0
- framework_agents-0.1.0/tests/conftest.py +8 -0
- framework_agents-0.1.0/tests/test_agent_live_together.py +117 -0
- framework_agents-0.1.0/tests/test_anthropic_offline.py +194 -0
- framework_agents-0.1.0/tests/test_conversation_sessions.py +448 -0
- framework_agents-0.1.0/tests/test_core_parity.py +273 -0
- framework_agents-0.1.0/tests/test_cross_language.py +114 -0
- framework_agents-0.1.0/tests/test_gaps.py +722 -0
- framework_agents-0.1.0/tests/test_live_providers.py +177 -0
- framework_agents-0.1.0/tests/test_middleware.py +646 -0
- framework_agents-0.1.0/tests/test_middleware_extra.py +559 -0
- framework_agents-0.1.0/tests/test_multi_and_evals.py +705 -0
- framework_agents-0.1.0/tests/test_review_fixes.py +1165 -0
- framework_agents-0.1.0/tests/test_semantic_parity.py +860 -0
- framework_agents-0.1.0/tests/test_serve.py +338 -0
- framework_agents-0.1.0/tests/test_servers_and_clients.py +767 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# deps
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnpm-store/
|
|
4
|
+
.turbo/
|
|
5
|
+
|
|
6
|
+
# build output
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.tsbuildinfo
|
|
10
|
+
|
|
11
|
+
# test / coverage
|
|
12
|
+
coverage/
|
|
13
|
+
.nyc_output/
|
|
14
|
+
|
|
15
|
+
# env
|
|
16
|
+
.env
|
|
17
|
+
.env.*
|
|
18
|
+
!.env.example
|
|
19
|
+
|
|
20
|
+
# OS / editor
|
|
21
|
+
.DS_Store
|
|
22
|
+
*.log
|
|
23
|
+
.vscode/*
|
|
24
|
+
!.vscode/extensions.json
|
|
25
|
+
.idea/
|
|
26
|
+
|
|
27
|
+
# python (future package)
|
|
28
|
+
__pycache__/
|
|
29
|
+
*.pyc
|
|
30
|
+
.venv/
|
|
31
|
+
.uv/
|
|
32
|
+
|
|
33
|
+
# docs build
|
|
34
|
+
site/
|
|
35
|
+
.venv-docs/
|
|
36
|
+
|
|
37
|
+
# python package build
|
|
38
|
+
packages/python/dist/
|
|
39
|
+
packages/python/build/
|
|
40
|
+
*.egg-info/
|
|
41
|
+
uv.lock
|
|
42
|
+
|
|
43
|
+
# npm tarballs
|
|
44
|
+
*.tgz
|
|
45
|
+
|
|
46
|
+
# doc scripts / temp
|
|
47
|
+
claude.md
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: framework-agents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python agent framework: ReAct loop, tools, memory, streaming, budgets, middleware, multi-agent, semantic graph, A2A serving - parity with the TypeScript package
|
|
5
|
+
Project-URL: Homepage, https://github.com/fab679/framework
|
|
6
|
+
Project-URL: Documentation, https://fab679.github.io/framework/
|
|
7
|
+
Project-URL: Repository, https://github.com/fab679/framework
|
|
8
|
+
Project-URL: Changelog, https://github.com/fab679/framework/releases
|
|
9
|
+
Author: Framework Agents
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: a2a,agents,ai,asyncio,llm,mcp,semantic-web,streaming
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: pydantic>=2.7
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: anyio>=4; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Python package — TS parity port (`framework_agents`)
|
|
33
|
+
|
|
34
|
+
The TypeScript package (`packages/typescript`) is the reference implementation.
|
|
35
|
+
This port targets API parity with the **fixed** TS semantics — every behavior
|
|
36
|
+
that was a real bug during the TS line (and is regression-pinned there) is
|
|
37
|
+
encoded here from day one:
|
|
38
|
+
|
|
39
|
+
| Pinned semantic | Why |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `reasoning_tokens` is a SUBSET of `output_tokens` | never double-billed in budgets/cost |
|
|
42
|
+
| `cost_usd` only when a price is known | unknown models → `None`, never a fake $0 |
|
|
43
|
+
| `save()` APPENDS; the loop does clear-then-save | history never duplicates across runs |
|
|
44
|
+
| Structured `'auto'` resolves by capability | unresolved auto never throws STRUCTURED_NO_MATCH |
|
|
45
|
+
| `finish_reason=length` + zero content → loud `MODEL_ERROR` | a thinking model burning its whole budget is not a silent empty answer |
|
|
46
|
+
| Tool parameter schemas ride the wire | models see real argument shapes |
|
|
47
|
+
| HITL decisions: exact key THEN bare tool name | resume converges even when models regenerate arg text |
|
|
48
|
+
| Local `ollama:` pricing wins over name collisions | never bill cloud prices for free local models |
|
|
49
|
+
| Storage keys/threadIds reject traversal (`..`) | no writes outside the root |
|
|
50
|
+
| Remote SPARQL: descriptive UA + 429/5xx retry honoring Retry-After | public endpoints reject and rate-limit otherwise |
|
|
51
|
+
|
|
52
|
+
No personal or brand names appear anywhere in this package.
|
|
53
|
+
|
|
54
|
+
## Port status (mirroring the TS module map)
|
|
55
|
+
|
|
56
|
+
| TS module | Python | State |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| types (messages/blocks/usage) | `types.py` | ✅ |
|
|
59
|
+
| errors + retryable verdicts | `errors.py` | ✅ |
|
|
60
|
+
| tools (define_tool/registry/schemas) | `tools.py` | ✅ |
|
|
61
|
+
| memory (+compaction) | `memory.py`, `memory_compaction.py` | ✅ (MEMORY_CORRUPT loud, atomic unique-tmp writes) |
|
|
62
|
+
| conversation managers | `conversation.py` | ✅ (order-preserving pin; model or fn summarizer) |
|
|
63
|
+
| sessions + snapshots | `sessions.py` | ✅ (validated keys, TS-tested preset semantics) |
|
|
64
|
+
| checkpoint + file locks | `checkpoint.py` | ✅ (O_EXCL + stale reclaim, threadId validation) |
|
|
65
|
+
| runtime (abort/timeout) | `runtime.py` | ✅ |
|
|
66
|
+
| models (mock + OpenAI-compatible + Anthropic + Ollama) | `models/` | ✅ (Together+OpenAI field unions, native Anthropic Messages API w/ thinking budgets + SSE machine, vision capability fail-fast, stream usage, truncation guard) |
|
|
67
|
+
| state (AgentState + invocation) | `state.py`, `agent.py` | ✅ (validated JSON KV, custom-prototype rejection, snapshots/session restore, pause/resume, edit/branch/timelines, v1 reasoning blocks, `with_tools`) |
|
|
68
|
+
| pricing | `pricing.py` | ✅ |
|
|
69
|
+
| config + factory | `config.py` | ✅ (together/openai/ollama/custom) |
|
|
70
|
+
| agent loop (budgets/structured/streaming) | `agent.py` | ✅ (calls stream via drain; budget price refusal; synthesized final answer) |
|
|
71
|
+
| middleware (hooks, HITL, retries, limits, policy, todos, pii) | `middleware.py` | ✅ |
|
|
72
|
+
| orchestration | `orchestration.py` | ✅ (sequential/parallel/router; routed result semantics) |
|
|
73
|
+
| workflows | `workflows.py` | ✅ (dedup'd indegree, retries, propagation) |
|
|
74
|
+
| swarm | `swarm.py` | ✅ (handoff tool + stream events + loop detection default) |
|
|
75
|
+
| evals | `evals.py` | ✅ (JSONL + pass-rate reporter) |
|
|
76
|
+
| skills + TF-IDF routing | `skills.py` | ✅ ($mention word-boundary wins) |
|
|
77
|
+
| prompts (+A/B) | `prompts.py` | ✅ (CRLF/BOM tolerance) |
|
|
78
|
+
| rag (tf-idf retriever + tool) | `rag.py` | ✅ |
|
|
79
|
+
| semantic (store/SHACL/rules/graph memory/linking/JSON-LD/evolution) | `semantic.py`, `ontology_review.py` | ✅ (version graphs under `urn:framework:ontology:*`) |
|
|
80
|
+
| remote SPARQL | `remote_sparql.py` | ✅ (UA + Retry-After retry) |
|
|
81
|
+
| A2A clients (SSE + JSON-RPC) | `a2a_client.py` | ✅ (envelope + contextId chain + retry) |
|
|
82
|
+
| A2A server (contexts/LRU/disconnect/SSE) | `a2a_server.py` | ✅ (stdio-of-asyncio raw HTTP, body caps, CORS) |
|
|
83
|
+
| MCP clients + bridging | `mcp_client.py` | ✅ (stdio child lifecycle, no-id notification, per-tool degrade) |
|
|
84
|
+
| observability + tracing sinks | `observability.py`, `tracing.py` | ✅ |
|
|
85
|
+
| sandbox (JS + shell) | `sandbox.py` | ✅ (caveat: exec timeouts abandon a thread, no hard kill) |
|
|
86
|
+
| serve (framework.json + CLI) | `serve.py` | ✅ (lazy model init, argparse CLI, per-context isolated agents) |
|
|
87
|
+
| React SDK | — | n/a (React-specific; browser clients use A2A/SSE) |
|
|
88
|
+
|
|
89
|
+
**Batch-2 middleware** (`model_fallback`, `summarization`, `context_editing` +
|
|
90
|
+
`ClearToolUsesEdit`, `llm_tool_selector`, `tool_emulator`, `filesystem`,
|
|
91
|
+
`create_sub_agent`, `pii` tool-results) is ported with deviations pinned in
|
|
92
|
+
`middleware.py`'s docstring (feedback-strings instead of raises on escape
|
|
93
|
+
misses; `older_than_steps` interpreted as a step gate).
|
|
94
|
+
|
|
95
|
+
## Running
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python3 -m venv .venv
|
|
99
|
+
.venv/bin/pip install -e ".[dev]"
|
|
100
|
+
.venv/bin/python -m pytest tests -q # offline parity suite (233 tests)
|
|
101
|
+
E2E_MODEL=1 .venv/bin/python -m pytest tests -q -m live
|
|
102
|
+
# live Together parity runs; OpenAI + Anthropic suites skip cleanly and go
|
|
103
|
+
# live as soon as OPENAI_API_KEY / ANTHROPIC_API_KEY appear in framework/.env
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Known deviations (documented in module docstrings)
|
|
107
|
+
|
|
108
|
+
- `Agent.stream()` yields a `done` event and stores the result (generators
|
|
109
|
+
can't return values); run() drains — wire contracts identical.
|
|
110
|
+
- Async file IO runs via `asyncio.to_thread` in filesystem tools; storage
|
|
111
|
+
classes use blocking IO inside async methods (single-process locality).
|
|
112
|
+
- Sandbox JS cannot hard-interrupt a thread mid-statement (node:vm can) —
|
|
113
|
+
the timeout surfaces per contract, the abandoned thread dies at exit.
|
|
114
|
+
- Semantic version graphs use `urn:framework:ontology:<version>` (the TS
|
|
115
|
+
default `urn:fabisch:*` embeds a personal name; both are configurable).
|
|
116
|
+
- Cross-language conformance: the Python A2A client speaks the exact wire
|
|
117
|
+
contract of the TS A2A server (envelope, SSE frames, [DONE]) and vice
|
|
118
|
+
versa — see `tests/test_servers_and_clients.py`.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Python package — TS parity port (`framework_agents`)
|
|
2
|
+
|
|
3
|
+
The TypeScript package (`packages/typescript`) is the reference implementation.
|
|
4
|
+
This port targets API parity with the **fixed** TS semantics — every behavior
|
|
5
|
+
that was a real bug during the TS line (and is regression-pinned there) is
|
|
6
|
+
encoded here from day one:
|
|
7
|
+
|
|
8
|
+
| Pinned semantic | Why |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `reasoning_tokens` is a SUBSET of `output_tokens` | never double-billed in budgets/cost |
|
|
11
|
+
| `cost_usd` only when a price is known | unknown models → `None`, never a fake $0 |
|
|
12
|
+
| `save()` APPENDS; the loop does clear-then-save | history never duplicates across runs |
|
|
13
|
+
| Structured `'auto'` resolves by capability | unresolved auto never throws STRUCTURED_NO_MATCH |
|
|
14
|
+
| `finish_reason=length` + zero content → loud `MODEL_ERROR` | a thinking model burning its whole budget is not a silent empty answer |
|
|
15
|
+
| Tool parameter schemas ride the wire | models see real argument shapes |
|
|
16
|
+
| HITL decisions: exact key THEN bare tool name | resume converges even when models regenerate arg text |
|
|
17
|
+
| Local `ollama:` pricing wins over name collisions | never bill cloud prices for free local models |
|
|
18
|
+
| Storage keys/threadIds reject traversal (`..`) | no writes outside the root |
|
|
19
|
+
| Remote SPARQL: descriptive UA + 429/5xx retry honoring Retry-After | public endpoints reject and rate-limit otherwise |
|
|
20
|
+
|
|
21
|
+
No personal or brand names appear anywhere in this package.
|
|
22
|
+
|
|
23
|
+
## Port status (mirroring the TS module map)
|
|
24
|
+
|
|
25
|
+
| TS module | Python | State |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| types (messages/blocks/usage) | `types.py` | ✅ |
|
|
28
|
+
| errors + retryable verdicts | `errors.py` | ✅ |
|
|
29
|
+
| tools (define_tool/registry/schemas) | `tools.py` | ✅ |
|
|
30
|
+
| memory (+compaction) | `memory.py`, `memory_compaction.py` | ✅ (MEMORY_CORRUPT loud, atomic unique-tmp writes) |
|
|
31
|
+
| conversation managers | `conversation.py` | ✅ (order-preserving pin; model or fn summarizer) |
|
|
32
|
+
| sessions + snapshots | `sessions.py` | ✅ (validated keys, TS-tested preset semantics) |
|
|
33
|
+
| checkpoint + file locks | `checkpoint.py` | ✅ (O_EXCL + stale reclaim, threadId validation) |
|
|
34
|
+
| runtime (abort/timeout) | `runtime.py` | ✅ |
|
|
35
|
+
| models (mock + OpenAI-compatible + Anthropic + Ollama) | `models/` | ✅ (Together+OpenAI field unions, native Anthropic Messages API w/ thinking budgets + SSE machine, vision capability fail-fast, stream usage, truncation guard) |
|
|
36
|
+
| state (AgentState + invocation) | `state.py`, `agent.py` | ✅ (validated JSON KV, custom-prototype rejection, snapshots/session restore, pause/resume, edit/branch/timelines, v1 reasoning blocks, `with_tools`) |
|
|
37
|
+
| pricing | `pricing.py` | ✅ |
|
|
38
|
+
| config + factory | `config.py` | ✅ (together/openai/ollama/custom) |
|
|
39
|
+
| agent loop (budgets/structured/streaming) | `agent.py` | ✅ (calls stream via drain; budget price refusal; synthesized final answer) |
|
|
40
|
+
| middleware (hooks, HITL, retries, limits, policy, todos, pii) | `middleware.py` | ✅ |
|
|
41
|
+
| orchestration | `orchestration.py` | ✅ (sequential/parallel/router; routed result semantics) |
|
|
42
|
+
| workflows | `workflows.py` | ✅ (dedup'd indegree, retries, propagation) |
|
|
43
|
+
| swarm | `swarm.py` | ✅ (handoff tool + stream events + loop detection default) |
|
|
44
|
+
| evals | `evals.py` | ✅ (JSONL + pass-rate reporter) |
|
|
45
|
+
| skills + TF-IDF routing | `skills.py` | ✅ ($mention word-boundary wins) |
|
|
46
|
+
| prompts (+A/B) | `prompts.py` | ✅ (CRLF/BOM tolerance) |
|
|
47
|
+
| rag (tf-idf retriever + tool) | `rag.py` | ✅ |
|
|
48
|
+
| semantic (store/SHACL/rules/graph memory/linking/JSON-LD/evolution) | `semantic.py`, `ontology_review.py` | ✅ (version graphs under `urn:framework:ontology:*`) |
|
|
49
|
+
| remote SPARQL | `remote_sparql.py` | ✅ (UA + Retry-After retry) |
|
|
50
|
+
| A2A clients (SSE + JSON-RPC) | `a2a_client.py` | ✅ (envelope + contextId chain + retry) |
|
|
51
|
+
| A2A server (contexts/LRU/disconnect/SSE) | `a2a_server.py` | ✅ (stdio-of-asyncio raw HTTP, body caps, CORS) |
|
|
52
|
+
| MCP clients + bridging | `mcp_client.py` | ✅ (stdio child lifecycle, no-id notification, per-tool degrade) |
|
|
53
|
+
| observability + tracing sinks | `observability.py`, `tracing.py` | ✅ |
|
|
54
|
+
| sandbox (JS + shell) | `sandbox.py` | ✅ (caveat: exec timeouts abandon a thread, no hard kill) |
|
|
55
|
+
| serve (framework.json + CLI) | `serve.py` | ✅ (lazy model init, argparse CLI, per-context isolated agents) |
|
|
56
|
+
| React SDK | — | n/a (React-specific; browser clients use A2A/SSE) |
|
|
57
|
+
|
|
58
|
+
**Batch-2 middleware** (`model_fallback`, `summarization`, `context_editing` +
|
|
59
|
+
`ClearToolUsesEdit`, `llm_tool_selector`, `tool_emulator`, `filesystem`,
|
|
60
|
+
`create_sub_agent`, `pii` tool-results) is ported with deviations pinned in
|
|
61
|
+
`middleware.py`'s docstring (feedback-strings instead of raises on escape
|
|
62
|
+
misses; `older_than_steps` interpreted as a step gate).
|
|
63
|
+
|
|
64
|
+
## Running
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python3 -m venv .venv
|
|
68
|
+
.venv/bin/pip install -e ".[dev]"
|
|
69
|
+
.venv/bin/python -m pytest tests -q # offline parity suite (233 tests)
|
|
70
|
+
E2E_MODEL=1 .venv/bin/python -m pytest tests -q -m live
|
|
71
|
+
# live Together parity runs; OpenAI + Anthropic suites skip cleanly and go
|
|
72
|
+
# live as soon as OPENAI_API_KEY / ANTHROPIC_API_KEY appear in framework/.env
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Known deviations (documented in module docstrings)
|
|
76
|
+
|
|
77
|
+
- `Agent.stream()` yields a `done` event and stores the result (generators
|
|
78
|
+
can't return values); run() drains — wire contracts identical.
|
|
79
|
+
- Async file IO runs via `asyncio.to_thread` in filesystem tools; storage
|
|
80
|
+
classes use blocking IO inside async methods (single-process locality).
|
|
81
|
+
- Sandbox JS cannot hard-interrupt a thread mid-statement (node:vm can) —
|
|
82
|
+
the timeout surfaces per contract, the abandoned thread dies at exit.
|
|
83
|
+
- Semantic version graphs use `urn:framework:ontology:<version>` (the TS
|
|
84
|
+
default `urn:fabisch:*` embeds a personal name; both are configurable).
|
|
85
|
+
- Cross-language conformance: the Python A2A client speaks the exact wire
|
|
86
|
+
contract of the TS A2A server (envelope, SSE frames, [DONE]) and vice
|
|
87
|
+
versa — see `tests/test_servers_and_clients.py`.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""LIVE: streaming — real token/thinking deltas through Agent.stream(), and
|
|
2
|
+
tool calls streamed live (tool_call event before execution, tool_result when
|
|
3
|
+
it lands)."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from framework_agents import Agent, AgentOptions, define_tool, load_config
|
|
10
|
+
from framework_agents.config import init_chat_model
|
|
11
|
+
from framework_agents.middleware import model_retry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Count(BaseModel):
|
|
15
|
+
count: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def main() -> None:
|
|
19
|
+
cfg = load_config()
|
|
20
|
+
model = init_chat_model(f"together:{cfg.together_model}", max_tokens=2500)
|
|
21
|
+
|
|
22
|
+
async def digits(args: Count, ctx) -> str:
|
|
23
|
+
await asyncio.sleep(0.7) # the 700ms proves tool_result lands mid-run
|
|
24
|
+
return "3.14159"
|
|
25
|
+
|
|
26
|
+
agent = Agent(AgentOptions(
|
|
27
|
+
model=model,
|
|
28
|
+
max_steps=4,
|
|
29
|
+
middleware=[model_retry(max_retries=3, initial_delay_ms=500)],
|
|
30
|
+
tools=[define_tool(name="get_pi_digits", description="Returns digits of pi.", schema=Count, execute=digits)],
|
|
31
|
+
))
|
|
32
|
+
|
|
33
|
+
kinds: dict[str, int] = {}
|
|
34
|
+
text = ""
|
|
35
|
+
async for event in agent.stream("Use the get_pi_digits tool with count 5, then reply with exactly the digits it returned."):
|
|
36
|
+
kinds[event["type"]] = kinds.get(event["type"], 0) + 1
|
|
37
|
+
if event["type"] in ("token", "thinking"):
|
|
38
|
+
pass # deltas stream live through hooks; batched per call here
|
|
39
|
+
if event["type"] == "done":
|
|
40
|
+
text = event["text"]
|
|
41
|
+
print("EVENT KINDS:", kinds)
|
|
42
|
+
print("TEXT:", text.strip())
|
|
43
|
+
assert agent._last_result is not None
|
|
44
|
+
print("USAGE:", agent._last_result.usage)
|
|
45
|
+
ok = kinds.get("tool_call") and kinds.get("tool_result") and "3.14159" in text
|
|
46
|
+
print("PASS: tools streamed live" if ok else "NOTE: inspect trace (model variance)")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""LIVE: orchestration — sequential chain, parallel fan-out, model router."""
|
|
2
|
+
import asyncio
|
|
3
|
+
from framework_agents import Agent, AgentOptions, load_config
|
|
4
|
+
from framework_agents.orchestration import OrchestrationPlan, run_orchestrated
|
|
5
|
+
|
|
6
|
+
cfg = load_config()
|
|
7
|
+
model = lambda: __import__("framework_agents").init_chat_model(f"together:{cfg.together_model}", max_tokens=8000)
|
|
8
|
+
|
|
9
|
+
async def main():
|
|
10
|
+
retry = [__import__("framework_agents.middleware", fromlist=["model_retry"]).model_retry(max_retries=3, initial_delay_ms=500)]
|
|
11
|
+
mk = lambda name, sys, desc="": Agent(AgentOptions(model=model(), name=name, description=desc, system_prompt=sys, middleware=retry, max_steps=2))
|
|
12
|
+
seq = await run_orchestrated(OrchestrationPlan(strategy="sequential", agents=[
|
|
13
|
+
mk("math", "Answer with just the number."), mk("halver", "Answer with just half of the number you are given."),
|
|
14
|
+
]), "What is 100 + 24?")
|
|
15
|
+
print("SEQUENTIAL:", seq.text.strip(), "(expect 62)")
|
|
16
|
+
par = await run_orchestrated(OrchestrationPlan(strategy="parallel", concurrency=2, agents=[
|
|
17
|
+
mk("a", 'Reply with exactly: AAA'), mk("b", 'Reply with exactly: BBB'), mk("c", 'Reply with exactly: CCC'),
|
|
18
|
+
]), "Respond as instructed.")
|
|
19
|
+
print("PARALLEL:", [r.text.strip() for r in par.results])
|
|
20
|
+
router = await run_orchestrated(OrchestrationPlan(strategy="router", agents=[
|
|
21
|
+
mk("poet", "Reply with a one-line poem.", "writes poetry"),
|
|
22
|
+
mk("calculator", "Answer with just the number.", "does arithmetic"),
|
|
23
|
+
]), "What is 12 times 12?")
|
|
24
|
+
print("ROUTER:", router.routed_to, router.results[0].text.strip(), "(expect 1 / 144)")
|
|
25
|
+
print("PASS" if "62" in seq.text and router.routed_to == 1 and "144" in router.results[0].text else "NOTE: model variance")
|
|
26
|
+
|
|
27
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""LIVE: swarm — autonomous handoffs between live members."""
|
|
2
|
+
import asyncio
|
|
3
|
+
from framework_agents import Agent, AgentOptions, load_config
|
|
4
|
+
from framework_agents import Swarm
|
|
5
|
+
|
|
6
|
+
cfg = load_config()
|
|
7
|
+
model = lambda: __import__("framework_agents").init_chat_model(f"together:{cfg.together_model}", max_tokens=3000)
|
|
8
|
+
|
|
9
|
+
async def main():
|
|
10
|
+
from framework_agents.middleware import model_retry
|
|
11
|
+
retry = [model_retry(max_retries=3, initial_delay_ms=500)]
|
|
12
|
+
researcher = Agent(AgentOptions(model=model(), name="researcher", description="Gathers key facts",
|
|
13
|
+
system_prompt="Produce ONE short fact, then call handoff_to_agent to the writer carrying it. Never answer the user directly.", middleware=retry))
|
|
14
|
+
writer = Agent(AgentOptions(model=model(), name="writer", description="Writes the final answer",
|
|
15
|
+
system_prompt="Write the final answer in one sentence from the handoff message. Do not call handoff_to_agent.",))
|
|
16
|
+
swarm = Swarm(agents=[researcher, writer], entry_point="researcher", max_handoffs=2, max_iterations=3, loop_detection_window=4)
|
|
17
|
+
events = []
|
|
18
|
+
async for e in swarm.stream("State the boiling point of water in one sentence."):
|
|
19
|
+
events.append(e.get("type") if e.get("type") != "handoff" else "handoff->" + str(e.get("to")))
|
|
20
|
+
result = researcher and writer # noqa - events carry the trace
|
|
21
|
+
print("EVENTS:", events)
|
|
22
|
+
ok = any(str(x).startswith("handoff") for x in events)
|
|
23
|
+
print("PASS: live handoff" if ok else "NOTE: inspect (model variance)")
|
|
24
|
+
|
|
25
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""LIVE: workflow DAG — levels, dedup'd deps, real node retry."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from framework_agents import Agent, AgentOptions, load_config, init_chat_model
|
|
6
|
+
from framework_agents.middleware import model_retry
|
|
7
|
+
from framework_agents.workflows import WorkflowNode, run_workflow
|
|
8
|
+
|
|
9
|
+
cfg = load_config()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def main():
|
|
13
|
+
retry = [model_retry(max_retries=3, initial_delay_ms=500)]
|
|
14
|
+
facts_agent = Agent(AgentOptions(
|
|
15
|
+
model=init_chat_model(f"together:{cfg.together_model}", max_tokens=2000),
|
|
16
|
+
system_prompt="Answer with exactly: water boils",
|
|
17
|
+
middleware=retry, max_steps=2,
|
|
18
|
+
))
|
|
19
|
+
calls = {"flaky": 0}
|
|
20
|
+
|
|
21
|
+
async def flaky(ctx, invocation_state=None):
|
|
22
|
+
calls["flaky"] += 1
|
|
23
|
+
if calls["flaky"] == 1:
|
|
24
|
+
raise RuntimeError("transient live failure")
|
|
25
|
+
return "recovered on retry"
|
|
26
|
+
|
|
27
|
+
async def facts(ctx, invocation_state=None):
|
|
28
|
+
return await facts_agent.run("Say your line.")
|
|
29
|
+
|
|
30
|
+
async def summary(ctx, invocation_state=None):
|
|
31
|
+
return {
|
|
32
|
+
"heard": getattr(ctx.get("facts"), "text", None),
|
|
33
|
+
"flaky": ctx.get("flaky"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
out = await run_workflow([
|
|
37
|
+
WorkflowNode(id="facts", run=facts),
|
|
38
|
+
WorkflowNode(id="flaky", run=flaky, retry={"attempts": 2, "backoff_ms": 100}),
|
|
39
|
+
WorkflowNode(id="summary", depends_on=["facts", "flaky"], run=summary),
|
|
40
|
+
])
|
|
41
|
+
heard = (getattr(out.get("facts"), "text", "") or "").strip()
|
|
42
|
+
print("FACTS:", heard, "| FLAKY calls:", calls["flaky"])
|
|
43
|
+
print("SUMMARY:", {k: (str(v)[:30] if v else v) for k, v in (out.get("summary") or {}).items()})
|
|
44
|
+
print("PASS: DAG + real retry" if "water" in heard and calls["flaky"] == 2 else "NOTE: inspect")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""LIVE: memory + sessions — multi-turn recall + restore across a 'restart'."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
from framework_agents import Agent, AgentOptions, FileMemory, InMemoryMemory, LocalFileStorage, SessionManager, load_config, init_chat_model
|
|
6
|
+
from framework_agents.middleware import model_retry
|
|
7
|
+
|
|
8
|
+
cfg = load_config()
|
|
9
|
+
|
|
10
|
+
async def main():
|
|
11
|
+
retry = [model_retry(max_retries=3, initial_delay_ms=500)]
|
|
12
|
+
model = lambda: init_chat_model(f"together:{cfg.together_model}", max_tokens=2500)
|
|
13
|
+
with tempfile.TemporaryDirectory() as d:
|
|
14
|
+
# ALSO the pause/resume question, live: pause between turns, resume continues.
|
|
15
|
+
from framework_agents import FileCheckpointer
|
|
16
|
+
agent = Agent(AgentOptions(model=model(), memory=FileMemory(d), middleware=retry))
|
|
17
|
+
await agent.run("My codename is Falcon. Just reply ok.")
|
|
18
|
+
paused = await agent.pause("conv-1", FileCheckpointer(d))
|
|
19
|
+
print("PAUSED:", paused)
|
|
20
|
+
agent2 = Agent(AgentOptions(model=model(), memory=FileMemory(d), middleware=retry))
|
|
21
|
+
resumed = await agent2.resume("conv-1", None, FileCheckpointer(d))
|
|
22
|
+
print("RESUMED (no new input):", resumed.text.strip()[:60])
|
|
23
|
+
# Real multi-agent "restart": a brand-new instance with a session store.
|
|
24
|
+
sessions = SessionManager(storage=LocalFileStorage(d + "/sessions"), session_id="demo")
|
|
25
|
+
a1 = Agent(AgentOptions(model=model(), session_manager=sessions, middleware=retry))
|
|
26
|
+
await a1.run("Remember: the launch word is ZULU-7. Just reply ok.")
|
|
27
|
+
a2 = Agent(AgentOptions(model=model(), session_manager=SessionManager(storage=LocalFileStorage(d + "/sessions"), session_id="demo"), middleware=retry))
|
|
28
|
+
recall = await a2.run("What is the launch word? Reply with just the word.")
|
|
29
|
+
print("RESTORED RECALL:", recall.text.strip(), "(expect ZULU-7)")
|
|
30
|
+
print("PASS: memory + pause/resume + session restore" if "zulu" in recall.text.lower() else "NOTE: variance")
|
|
31
|
+
|
|
32
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""LIVE: human-in-the-loop — suspend on a guarded tool, checkpoint, resume
|
|
2
|
+
with the TOOL-NAME WILDCARD decision (models re-issue args on resume).
|
|
3
|
+
|
|
4
|
+
NOTE: `except ... as first` DELETES the name at the end of the except block
|
|
5
|
+
(a Python gotcha) — stash it before leaving the block.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from framework_agents import Agent, AgentOptions, FileCheckpointer, define_tool, init_chat_model, load_config
|
|
14
|
+
from framework_agents.middleware import HitlInterrupt, human_in_the_loop, model_retry
|
|
15
|
+
|
|
16
|
+
cfg = load_config()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Publish(BaseModel):
|
|
20
|
+
title: str
|
|
21
|
+
word: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def main():
|
|
25
|
+
cp = FileCheckpointer(tempfile.mkdtemp())
|
|
26
|
+
model = lambda: init_chat_model(f"together:{cfg.together_model}", max_tokens=1200)
|
|
27
|
+
|
|
28
|
+
async def publish(args: Publish, ctx) -> str:
|
|
29
|
+
return f'published "{args.title}"'
|
|
30
|
+
|
|
31
|
+
def mk():
|
|
32
|
+
return Agent(AgentOptions(
|
|
33
|
+
model=model(), max_steps=6, tools=[
|
|
34
|
+
define_tool(
|
|
35
|
+
name="publish",
|
|
36
|
+
description="Publishes the report. ALWAYS call this tool before finishing.",
|
|
37
|
+
schema=Publish,
|
|
38
|
+
execute=publish,
|
|
39
|
+
),
|
|
40
|
+
],
|
|
41
|
+
middleware=[model_retry(max_retries=2, initial_delay_ms=300), human_in_the_loop(interrupt_on={"publish": True})],
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
suspended: dict = {}
|
|
45
|
+
try:
|
|
46
|
+
await mk().run('Write a one-sentence report about the word "falcon" and publish it.',
|
|
47
|
+
checkpointer=cp, thread_id="t1")
|
|
48
|
+
print("NOTE: model finished without calling publish — model variance, rerun")
|
|
49
|
+
return
|
|
50
|
+
except HitlInterrupt as first:
|
|
51
|
+
# Stash now: the `as` binding is deleted when the except block ends.
|
|
52
|
+
suspended = {"tool": first.tool, "args": first.args}
|
|
53
|
+
|
|
54
|
+
print("SUSPENDED:", suspended)
|
|
55
|
+
# Human decision: WILDCARD bare tool name (models regenerate arg text; an
|
|
56
|
+
# exact-key-only approval would never converge after resume).
|
|
57
|
+
resumed = await mk().resume("t1", {suspended["tool"]: "approve"}, checkpointer=cp)
|
|
58
|
+
published = next((c.output for c in resumed.tool_calls if c.name == "publish"), None)
|
|
59
|
+
print("PUBLISHED:", str(published)[:60])
|
|
60
|
+
print("RESUMED FINAL:", resumed.text.strip()[:60])
|
|
61
|
+
print("PASS: suspend -> checkpoint -> wildcard-approve -> resume" if published else "NOTE: inspect")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""LIVE: guardrails + budgets — PII redaction, policy deny, real cost caps."""
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from framework_agents import Agent, AgentOptions, TokenBudgets, define_tool, AgentError, init_chat_model, load_config
|
|
7
|
+
from framework_agents.middleware import model_retry, pii_redact, policy_filter
|
|
8
|
+
|
|
9
|
+
cfg = load_config()
|
|
10
|
+
model = lambda: init_chat_model(f"together:{cfg.together_model}", max_tokens=2500)
|
|
11
|
+
|
|
12
|
+
async def main():
|
|
13
|
+
pii_agent = Agent(AgentOptions(
|
|
14
|
+
model=model(),
|
|
15
|
+
middleware=[model_retry(max_retries=2, initial_delay_ms=300), pii_redact("email", strategy="redact", apply_to_output=True)],
|
|
16
|
+
))
|
|
17
|
+
pii = await pii_agent.run("Reply exactly with: my email is foo.bar@corp.example")
|
|
18
|
+
print("PII REDACTED:", "foo.bar@corp.example" not in pii.text)
|
|
19
|
+
print("PII TEXT:", pii.text.strip())
|
|
20
|
+
|
|
21
|
+
class NoArgs(BaseModel):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
executed = []
|
|
25
|
+
|
|
26
|
+
async def rm_exec(a, c):
|
|
27
|
+
executed.append("rm_rf")
|
|
28
|
+
return "deleted"
|
|
29
|
+
|
|
30
|
+
async def safe_exec(a, c):
|
|
31
|
+
executed.append("noop")
|
|
32
|
+
return "did nothing"
|
|
33
|
+
|
|
34
|
+
rm_rf = define_tool(name="rm_rf", description="Deletes everything.", schema=NoArgs, execute=rm_exec)
|
|
35
|
+
safe = define_tool(name="safe", description="Does nothing.", schema=NoArgs, execute=safe_exec)
|
|
36
|
+
policy = Agent(AgentOptions(model=model(), max_steps=4, tools=[rm_rf, safe],
|
|
37
|
+
middleware=[model_retry(max_retries=2, initial_delay_ms=300), policy_filter(deny=["rm_"], exit_behavior="continue")]))
|
|
38
|
+
await policy.run('Call the rm_rf tool; if refused call noop instead, then say "done".')
|
|
39
|
+
print("POLICY:", "rm_rf never executed" if "rm" not in executed else "FAIL — rm executed!", "| calls:", executed)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
await Agent(AgentOptions(model=model(), budgets=TokenBudgets(max_tokens=1))).run("Say anything")
|
|
43
|
+
print("BUDGET: NOTE - no cap hit")
|
|
44
|
+
except AgentError as e:
|
|
45
|
+
print("BUDGET CODE:", e.code, "(cap enforced on real usage)")
|
|
46
|
+
|
|
47
|
+
ok_pii = "foo.bar@corp.example" not in pii.text
|
|
48
|
+
ok_policy = "rm" not in executed
|
|
49
|
+
print("PASS: guardrails live" if ok_pii and ok_policy else "NOTE: inspect — pii redacted: %s, rm blocked: %s" % (ok_pii, ok_policy))
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
|
|
53
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""LIVE: semantic graph — a live model commits validated facts, the ontology
|
|
2
|
+
self-reports gaps, SHACL is induced from ITS OWN data and adopted."""
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from framework_agents import Agent, AgentOptions, InMemoryGraphStore, define_tool, init_chat_model, load_config
|
|
6
|
+
from framework_agents.middleware import model_retry
|
|
7
|
+
from framework_agents.semantic import graph_add_tool, claim_check_tool, sparql_select_tool
|
|
8
|
+
from framework_agents.ontology_review import propose_shapes, adopt_shapes, review_ontology, review_ontology_tool, propose_shapes_tool
|
|
9
|
+
|
|
10
|
+
cfg = load_config()
|
|
11
|
+
|
|
12
|
+
async def main():
|
|
13
|
+
store = InMemoryGraphStore()
|
|
14
|
+
model = lambda: init_chat_model(f"together:{cfg.together_model}", max_tokens=1500)
|
|
15
|
+
writer = Agent(AgentOptions(
|
|
16
|
+
model=model(), max_steps=8,
|
|
17
|
+
middleware=[model_retry(max_retries=2, initial_delay_ms=300)],
|
|
18
|
+
tools=[graph_add_tool(store), review_ontology_tool(store), propose_shapes_tool(store, min_instances=1)],
|
|
19
|
+
))
|
|
20
|
+
run = await writer.run(
|
|
21
|
+
'Call graph_add once with these 6 quads (source "user:demo", keep quotes): '
|
|
22
|
+
"1. ex:GlobalCorp rdf:type ex:Company ; 2. ex:GlobalCorp ex:name \"Global Corp\" ; 3. ex:GlobalCorp ex:headcount 42 ; "
|
|
23
|
+
"4. ex:Acme rdf:type ex:Company ; 5. ex:Acme ex:name \"Acme\" ; 6. ex:Acme ex:headcount 10 . Reply with just: stored"
|
|
24
|
+
)
|
|
25
|
+
print("WRITE:", run.text.strip()[:40])
|
|
26
|
+
review = await review_ontology(store)
|
|
27
|
+
undeclared = list(review.undeclared) if review.undeclared else []
|
|
28
|
+
print("UNDECLARED (what to add next):", [getattr(t, "term", t.get("term") if isinstance(t, dict) else t) for t in undeclared])
|
|
29
|
+
shapes = await propose_shapes(store, min_instances=1)
|
|
30
|
+
adoption = await adopt_shapes(store, shapes)
|
|
31
|
+
print("SHACL INDUCED:", bool(shapes), "| adopted:", len(adoption.adopted), "rejected:", len(adoption.rejected))
|
|
32
|
+
reader = Agent(AgentOptions(model=model(), max_steps=4, tools=[claim_check_tool(store), sparql_select_tool(store)]))
|
|
33
|
+
answer = await reader.run('How many employees does GlobalCorp have? Ground it with claim_check or sparql_select first.')
|
|
34
|
+
print("GROUNDED:", answer.text.strip()[:80])
|
|
35
|
+
ok = "42" in answer.text and adopted
|
|
36
|
+
print("PASS: graph write + review + SHACL adoption + grounded answer" if ok else "NOTE: model variance — inspect")
|
|
37
|
+
|
|
38
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""LIVE: A2A over the wire — the Python server hosts a live agent; the Python
|
|
2
|
+
client streams two turns through one context, and a2a_as_tool lets ANOTHER
|
|
3
|
+
agent call it as a tool."""
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from framework_agents import A2AServer, Agent, AgentOptions, a2a_as_tool, init_chat_model, load_config
|
|
7
|
+
from framework_agents.a2a_client import AgentClient
|
|
8
|
+
from framework_agents.middleware import model_retry
|
|
9
|
+
|
|
10
|
+
cfg = load_config()
|
|
11
|
+
|
|
12
|
+
async def main():
|
|
13
|
+
model = lambda: init_chat_model(f"together:{cfg.together_model}", max_tokens=1000)
|
|
14
|
+
server = A2AServer(
|
|
15
|
+
card={"name": "specialist", "description": "Knows capitals"},
|
|
16
|
+
agent_factory=lambda context_id: Agent(AgentOptions(
|
|
17
|
+
model=model(),
|
|
18
|
+
system_prompt="Answer capital-city questions in one word.",
|
|
19
|
+
middleware=[model_retry(max_retries=3, initial_delay_ms=400)],
|
|
20
|
+
)),
|
|
21
|
+
)
|
|
22
|
+
port = await server.start()
|
|
23
|
+
url = f"http://127.0.0.1:{port}"
|
|
24
|
+
print("A2A server up:", url)
|
|
25
|
+
try:
|
|
26
|
+
client = AgentClient(url)
|
|
27
|
+
context: list[str] = []
|
|
28
|
+
async def turn(msg: str) -> str:
|
|
29
|
+
text = ""
|
|
30
|
+
async for e in client.stream(msg, context[0] if context else None, None, on_context=context.append):
|
|
31
|
+
if e["type"] == "done":
|
|
32
|
+
text = e["text"]
|
|
33
|
+
return text
|
|
34
|
+
t1 = await turn("The capital of France? One word.")
|
|
35
|
+
t2 = await turn("And of Japan? One word.")
|
|
36
|
+
print("TURN 1:", t1.strip(), "| TURN 2:", t2.strip(), "| shared contextId:", bool(context))
|
|
37
|
+
|
|
38
|
+
from framework_agents.a2a_client import A2AClient
|
|
39
|
+
remote = a2a_as_tool(A2AClient(url), name="ask_specialist", description="Ask the remote capitals specialist.")
|
|
40
|
+
orchestrator = Agent(AgentOptions(model=model(), max_steps=4, middleware=[model_retry(max_retries=3, initial_delay_ms=400)], tools=[remote]))
|
|
41
|
+
res = await orchestrator.run("Use the ask_specialist tool to ask for the capital of Italy, then repeat it.")
|
|
42
|
+
print("REMOTE-AS-TOOL:", res.text.strip()[:60])
|
|
43
|
+
ok = "paris" in t1.lower() and res.text.strip()
|
|
44
|
+
print("PASS: A2A live (stream x2 + remote-as-tool)" if ok else "NOTE: model variance")
|
|
45
|
+
finally:
|
|
46
|
+
await server.stop()
|
|
47
|
+
print("stopped")
|
|
48
|
+
|
|
49
|
+
asyncio.run(main())
|