swarph-mesh 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.
- swarph_mesh-0.1.0/LICENSE +21 -0
- swarph_mesh-0.1.0/PKG-INFO +107 -0
- swarph_mesh-0.1.0/README.md +74 -0
- swarph_mesh-0.1.0/pyproject.toml +57 -0
- swarph_mesh-0.1.0/setup.cfg +4 -0
- swarph_mesh-0.1.0/src/swarph_mesh/__init__.py +95 -0
- swarph_mesh-0.1.0/src/swarph_mesh/adapters/__init__.py +56 -0
- swarph_mesh-0.1.0/src/swarph_mesh/adapters/gemini.py +195 -0
- swarph_mesh-0.1.0/src/swarph_mesh/attribution.py +141 -0
- swarph_mesh-0.1.0/src/swarph_mesh/exceptions.py +22 -0
- swarph_mesh-0.1.0/src/swarph_mesh/hooks.py +107 -0
- swarph_mesh-0.1.0/src/swarph_mesh/json_harness.py +118 -0
- swarph_mesh-0.1.0/src/swarph_mesh/swarph_call.py +207 -0
- swarph_mesh-0.1.0/src/swarph_mesh/types.py +101 -0
- swarph_mesh-0.1.0/src/swarph_mesh.egg-info/PKG-INFO +107 -0
- swarph_mesh-0.1.0/src/swarph_mesh.egg-info/SOURCES.txt +21 -0
- swarph_mesh-0.1.0/src/swarph_mesh.egg-info/dependency_links.txt +1 -0
- swarph_mesh-0.1.0/src/swarph_mesh.egg-info/requires.txt +7 -0
- swarph_mesh-0.1.0/src/swarph_mesh.egg-info/top_level.txt +1 -0
- swarph_mesh-0.1.0/tests/test_gemini_adapter.py +216 -0
- swarph_mesh-0.1.0/tests/test_smoke_gemini.py +83 -0
- swarph_mesh-0.1.0/tests/test_swarph_call.py +380 -0
- swarph_mesh-0.1.0/tests/test_types.py +172 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pierre Samson and Claude Opus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swarph-mesh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Model-agnostic Python substrate for the swarph-mesh ecosystem. v0.1.0 ships SwarphCall + GeminiAdapter (Phase 1 substrate per PLAN.md §13).
|
|
5
|
+
Author: Pierre Samson, Claude Opus
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/darw007d/swarph-mesh
|
|
8
|
+
Project-URL: Source, https://github.com/darw007d/swarph-mesh
|
|
9
|
+
Project-URL: CLI, https://github.com/darw007d/swarph-cli
|
|
10
|
+
Project-URL: Plugin, https://github.com/darw007d/swarph-meshlm
|
|
11
|
+
Project-URL: Spec, https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md
|
|
12
|
+
Keywords: swarph,llm,mesh,cli,multi-llm,gemini,claude,deepseek
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: swarph-shared>=0.2.0
|
|
27
|
+
Requires-Dist: pydantic>=2.0
|
|
28
|
+
Requires-Dist: langgraph-genai-bridge>=0.1.5
|
|
29
|
+
Requires-Dist: langchain-core>=0.3
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# swarph-mesh
|
|
35
|
+
|
|
36
|
+
Model-agnostic Python substrate for the swarph-mesh ecosystem. Pure library, no CLI.
|
|
37
|
+
|
|
38
|
+
Designed to fill the gap left by existing tools (`aichat`, `mods`, Simon Willison's `llm`, `gemini-cli`, `claude-cli`): none expose mesh-gateway participation, per-caller attribution, structured-output discipline, or the cooperative-protocol patterns the swarph encodes.
|
|
39
|
+
|
|
40
|
+
This is one of three repos in the v0.3.x architecture:
|
|
41
|
+
|
|
42
|
+
| Repo | Role |
|
|
43
|
+
|---|---|
|
|
44
|
+
| [`swarph-mesh`](https://github.com/darw007d/swarph-mesh) | This package — typed Protocol + adapters + SwarphCall + MeshClient |
|
|
45
|
+
| [`swarph-cli`](https://github.com/darw007d/swarph-cli) | The `swarph` binary. Thin client over `swarph-mesh` |
|
|
46
|
+
| [`swarph-meshlm`](https://github.com/darw007d/swarph-meshlm) | Simon Willison `llm` plugin. Same primitives wired into `llm`'s plugin host |
|
|
47
|
+
|
|
48
|
+
All three sit on top of [`swarph-shared`](https://github.com/darw007d/swarph-shared) which provides the cross-billing-path attribution + subprocess env scrubbing + JSON-mode harness + peer-name registry primitives.
|
|
49
|
+
|
|
50
|
+
## Status
|
|
51
|
+
|
|
52
|
+
**v0.1.0 — Phase 1 substrate.** Live `SwarphCall(provider="gemini")` works end-to-end against real Gemini API per PLAN.md §13 falsifiability gate.
|
|
53
|
+
|
|
54
|
+
Public surface:
|
|
55
|
+
|
|
56
|
+
- `LLMAdapter` Protocol (runtime-checkable) + `ChatMessage` + `LLMResponse`
|
|
57
|
+
- `SwarphCall` — public entry point with caller-convention validation, hooks, attribution
|
|
58
|
+
- `GeminiAdapter` — wraps `langgraph-genai-bridge` (Flex tier, context caching)
|
|
59
|
+
- JSON-mode harness — retry-once with [USER]-turn feedback (per swarph-shared invariant)
|
|
60
|
+
- Attribution: `FileAttributionWriter` default; `set_default_writer()` for production TSDB consumers
|
|
61
|
+
|
|
62
|
+
Tests: **43/43 passing** (42 offline + 1 live smoke gated on `GEMINI_API_KEY`).
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from swarph_mesh import SwarphCall, ChatMessage
|
|
66
|
+
|
|
67
|
+
result = await SwarphCall(
|
|
68
|
+
provider="gemini",
|
|
69
|
+
caller="orchestrator.boss",
|
|
70
|
+
).chat(
|
|
71
|
+
messages=[ChatMessage(role="user", content="hi")],
|
|
72
|
+
)
|
|
73
|
+
print(result.text, result.cost_usd, result.input_tokens)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Spec
|
|
77
|
+
|
|
78
|
+
The canonical PLAN with sequencing, falsifiability gates, and design rationale lives at:
|
|
79
|
+
|
|
80
|
+
→ [hedge-fund-mcp / research/swarph_cli/PLAN.md](https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md)
|
|
81
|
+
|
|
82
|
+
## Phase rollout
|
|
83
|
+
|
|
84
|
+
| Phase | Scope |
|
|
85
|
+
|---|---|
|
|
86
|
+
| **0** (v0.0.1) | Typed substrate — Protocol + dataclasses + exceptions |
|
|
87
|
+
| **1** (v0.1.0 — this release) | Gemini adapter + `SwarphCall` surface + caller convention import + JSON-mode harness + attribution hook |
|
|
88
|
+
| **3** | `MeshClient` — replaces hand-rolled curl in `lab_loop_drain.py` etc. |
|
|
89
|
+
| **4** | DeepSeek + Claude (subscription) + OpenAI adapters |
|
|
90
|
+
| **5.5** | `swarph onboard` + `swarph ratify` (lives in `swarph-cli`, depends on this) |
|
|
91
|
+
| **5.7** | `swarph daemon` + REPL drain coroutine (lives in `swarph-cli`) |
|
|
92
|
+
| **6** | PyPI publish |
|
|
93
|
+
| **7** | `swarph-meshlm` plugin (separate repo, this dep) |
|
|
94
|
+
|
|
95
|
+
## Install (dev)
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git clone https://github.com/darw007d/swarph-mesh
|
|
99
|
+
cd swarph-mesh
|
|
100
|
+
python -m venv venv && source venv/bin/activate
|
|
101
|
+
pip install -e ".[dev]"
|
|
102
|
+
pytest
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT. Pierre Samson + Claude Opus, 2026.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# swarph-mesh
|
|
2
|
+
|
|
3
|
+
Model-agnostic Python substrate for the swarph-mesh ecosystem. Pure library, no CLI.
|
|
4
|
+
|
|
5
|
+
Designed to fill the gap left by existing tools (`aichat`, `mods`, Simon Willison's `llm`, `gemini-cli`, `claude-cli`): none expose mesh-gateway participation, per-caller attribution, structured-output discipline, or the cooperative-protocol patterns the swarph encodes.
|
|
6
|
+
|
|
7
|
+
This is one of three repos in the v0.3.x architecture:
|
|
8
|
+
|
|
9
|
+
| Repo | Role |
|
|
10
|
+
|---|---|
|
|
11
|
+
| [`swarph-mesh`](https://github.com/darw007d/swarph-mesh) | This package — typed Protocol + adapters + SwarphCall + MeshClient |
|
|
12
|
+
| [`swarph-cli`](https://github.com/darw007d/swarph-cli) | The `swarph` binary. Thin client over `swarph-mesh` |
|
|
13
|
+
| [`swarph-meshlm`](https://github.com/darw007d/swarph-meshlm) | Simon Willison `llm` plugin. Same primitives wired into `llm`'s plugin host |
|
|
14
|
+
|
|
15
|
+
All three sit on top of [`swarph-shared`](https://github.com/darw007d/swarph-shared) which provides the cross-billing-path attribution + subprocess env scrubbing + JSON-mode harness + peer-name registry primitives.
|
|
16
|
+
|
|
17
|
+
## Status
|
|
18
|
+
|
|
19
|
+
**v0.1.0 — Phase 1 substrate.** Live `SwarphCall(provider="gemini")` works end-to-end against real Gemini API per PLAN.md §13 falsifiability gate.
|
|
20
|
+
|
|
21
|
+
Public surface:
|
|
22
|
+
|
|
23
|
+
- `LLMAdapter` Protocol (runtime-checkable) + `ChatMessage` + `LLMResponse`
|
|
24
|
+
- `SwarphCall` — public entry point with caller-convention validation, hooks, attribution
|
|
25
|
+
- `GeminiAdapter` — wraps `langgraph-genai-bridge` (Flex tier, context caching)
|
|
26
|
+
- JSON-mode harness — retry-once with [USER]-turn feedback (per swarph-shared invariant)
|
|
27
|
+
- Attribution: `FileAttributionWriter` default; `set_default_writer()` for production TSDB consumers
|
|
28
|
+
|
|
29
|
+
Tests: **43/43 passing** (42 offline + 1 live smoke gated on `GEMINI_API_KEY`).
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from swarph_mesh import SwarphCall, ChatMessage
|
|
33
|
+
|
|
34
|
+
result = await SwarphCall(
|
|
35
|
+
provider="gemini",
|
|
36
|
+
caller="orchestrator.boss",
|
|
37
|
+
).chat(
|
|
38
|
+
messages=[ChatMessage(role="user", content="hi")],
|
|
39
|
+
)
|
|
40
|
+
print(result.text, result.cost_usd, result.input_tokens)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Spec
|
|
44
|
+
|
|
45
|
+
The canonical PLAN with sequencing, falsifiability gates, and design rationale lives at:
|
|
46
|
+
|
|
47
|
+
→ [hedge-fund-mcp / research/swarph_cli/PLAN.md](https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md)
|
|
48
|
+
|
|
49
|
+
## Phase rollout
|
|
50
|
+
|
|
51
|
+
| Phase | Scope |
|
|
52
|
+
|---|---|
|
|
53
|
+
| **0** (v0.0.1) | Typed substrate — Protocol + dataclasses + exceptions |
|
|
54
|
+
| **1** (v0.1.0 — this release) | Gemini adapter + `SwarphCall` surface + caller convention import + JSON-mode harness + attribution hook |
|
|
55
|
+
| **3** | `MeshClient` — replaces hand-rolled curl in `lab_loop_drain.py` etc. |
|
|
56
|
+
| **4** | DeepSeek + Claude (subscription) + OpenAI adapters |
|
|
57
|
+
| **5.5** | `swarph onboard` + `swarph ratify` (lives in `swarph-cli`, depends on this) |
|
|
58
|
+
| **5.7** | `swarph daemon` + REPL drain coroutine (lives in `swarph-cli`) |
|
|
59
|
+
| **6** | PyPI publish |
|
|
60
|
+
| **7** | `swarph-meshlm` plugin (separate repo, this dep) |
|
|
61
|
+
|
|
62
|
+
## Install (dev)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/darw007d/swarph-mesh
|
|
66
|
+
cd swarph-mesh
|
|
67
|
+
python -m venv venv && source venv/bin/activate
|
|
68
|
+
pip install -e ".[dev]"
|
|
69
|
+
pytest
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT. Pierre Samson + Claude Opus, 2026.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "swarph-mesh"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Model-agnostic Python substrate for the swarph-mesh ecosystem. v0.1.0 ships SwarphCall + GeminiAdapter (Phase 1 substrate per PLAN.md §13)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Pierre Samson" },
|
|
14
|
+
{ name = "Claude Opus" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["swarph", "llm", "mesh", "cli", "multi-llm", "gemini", "claude", "deepseek"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"swarph-shared>=0.2.0",
|
|
31
|
+
"pydantic>=2.0",
|
|
32
|
+
# Phase 1 — Gemini adapter wraps the bridge (PLAN.md §3 ship order #1)
|
|
33
|
+
"langgraph-genai-bridge>=0.1.5",
|
|
34
|
+
"langchain-core>=0.3",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/darw007d/swarph-mesh"
|
|
39
|
+
Source = "https://github.com/darw007d/swarph-mesh"
|
|
40
|
+
CLI = "https://github.com/darw007d/swarph-cli"
|
|
41
|
+
Plugin = "https://github.com/darw007d/swarph-meshlm"
|
|
42
|
+
Spec = "https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md"
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
dev = ["pytest>=7.0"]
|
|
46
|
+
|
|
47
|
+
# NOTE: this repo is the substrate Python package only — pure library.
|
|
48
|
+
# The `swarph` CLI binary lives in `darw007d/swarph-cli`; the Simon
|
|
49
|
+
# Willison `llm` plugin in `darw007d/swarph-meshlm`. All three import
|
|
50
|
+
# from this package as the bottom layer.
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
where = ["src"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
addopts = "-v --tb=short"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""swarph-mesh — model-agnostic Python substrate for the swarph-mesh ecosystem.
|
|
2
|
+
|
|
3
|
+
The substrate gap the existing CLIs (``aichat`` / ``mods`` / Simon Willison's
|
|
4
|
+
``llm`` / ``gemini-cli`` / ``claude-cli``) leave open: none expose
|
|
5
|
+
mesh-gateway participation, per-caller attribution, structured-output
|
|
6
|
+
discipline, or the cooperative-protocol patterns the swarph encodes.
|
|
7
|
+
|
|
8
|
+
This package fills it as a pure Python library. Three repos make up
|
|
9
|
+
the v0.3.x architecture:
|
|
10
|
+
|
|
11
|
+
* ``swarph-mesh`` (this package) — typed Protocol + adapters + SwarphCall.
|
|
12
|
+
Pure library, no CLI.
|
|
13
|
+
* ``swarph-cli`` (separate repo) — the ``swarph`` binary. Thin client
|
|
14
|
+
on top of ``swarph-mesh``.
|
|
15
|
+
* ``swarph-meshlm`` (separate) — Simon Willison ``llm`` plugin.
|
|
16
|
+
|
|
17
|
+
See the canonical PLAN at:
|
|
18
|
+
``https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md``
|
|
19
|
+
|
|
20
|
+
v0.1.0 — Phase 1 substrate. Ships:
|
|
21
|
+
|
|
22
|
+
* :class:`LLMAdapter` Protocol + ``ChatMessage`` + ``LLMResponse`` (from v0.0.1)
|
|
23
|
+
* :class:`SwarphCall` public surface — caller-validated, hook-wired entry-point
|
|
24
|
+
* :class:`GeminiAdapter` — wraps ``langgraph-genai-bridge`` (Flex tier, caching)
|
|
25
|
+
* JSON-mode harness — retry-once with [USER]-turn feedback (per PR #125 invariant)
|
|
26
|
+
* Attribution hooks + writers (FileAttributionWriter default;
|
|
27
|
+
``set_default_writer`` for production TSDB consumers)
|
|
28
|
+
|
|
29
|
+
Future phases (per PLAN.md §13):
|
|
30
|
+
3 MeshClient — mesh-gateway HTTP wrapper
|
|
31
|
+
4 DeepSeek + Claude (subscription) + OpenAI + Grok adapters
|
|
32
|
+
2.5+ ``swarph import`` (lives in swarph-cli)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
# Public types
|
|
38
|
+
from swarph_mesh.exceptions import (
|
|
39
|
+
AdapterError,
|
|
40
|
+
SwarphMeshError,
|
|
41
|
+
UnknownProvider,
|
|
42
|
+
)
|
|
43
|
+
from swarph_mesh.types import (
|
|
44
|
+
ChatMessage,
|
|
45
|
+
LLMAdapter,
|
|
46
|
+
LLMResponse,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Phase 1 surfaces
|
|
50
|
+
from swarph_mesh.swarph_call import SwarphCall
|
|
51
|
+
from swarph_mesh.adapters import get_adapter, register_adapter
|
|
52
|
+
from swarph_mesh.attribution import (
|
|
53
|
+
AttributionEvent,
|
|
54
|
+
AttributionWriter,
|
|
55
|
+
FileAttributionWriter,
|
|
56
|
+
NullAttributionWriter,
|
|
57
|
+
get_default_writer,
|
|
58
|
+
set_default_writer,
|
|
59
|
+
)
|
|
60
|
+
from swarph_mesh.hooks import (
|
|
61
|
+
CallContext,
|
|
62
|
+
HookSet,
|
|
63
|
+
attribution_post_call,
|
|
64
|
+
default_hooks,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
__version__ = "0.1.0"
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"__version__",
|
|
71
|
+
# types
|
|
72
|
+
"ChatMessage",
|
|
73
|
+
"LLMResponse",
|
|
74
|
+
"LLMAdapter",
|
|
75
|
+
# exceptions
|
|
76
|
+
"SwarphMeshError",
|
|
77
|
+
"AdapterError",
|
|
78
|
+
"UnknownProvider",
|
|
79
|
+
# SwarphCall public surface
|
|
80
|
+
"SwarphCall",
|
|
81
|
+
"get_adapter",
|
|
82
|
+
"register_adapter",
|
|
83
|
+
# attribution
|
|
84
|
+
"AttributionEvent",
|
|
85
|
+
"AttributionWriter",
|
|
86
|
+
"FileAttributionWriter",
|
|
87
|
+
"NullAttributionWriter",
|
|
88
|
+
"get_default_writer",
|
|
89
|
+
"set_default_writer",
|
|
90
|
+
# hooks
|
|
91
|
+
"CallContext",
|
|
92
|
+
"HookSet",
|
|
93
|
+
"attribution_post_call",
|
|
94
|
+
"default_hooks",
|
|
95
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Provider adapters — registry + dispatch.
|
|
2
|
+
|
|
3
|
+
Phase 1 ships only the Gemini adapter (PLAN.md §3 ship order).
|
|
4
|
+
Subsequent phases add DeepSeek / Claude / OpenAI / Grok by adding
|
|
5
|
+
modules here + registering them in :func:`get_adapter`.
|
|
6
|
+
|
|
7
|
+
Adapters are singletons per provider — instantiated on first
|
|
8
|
+
request, reused for the rest of the process. This matches the
|
|
9
|
+
"adapter registry" shape from PLAN.md §4.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from swarph_mesh.exceptions import UnknownProvider
|
|
17
|
+
from swarph_mesh.types import LLMAdapter
|
|
18
|
+
|
|
19
|
+
# Registry of instantiated singletons (one per provider name)
|
|
20
|
+
_REGISTRY: dict[str, LLMAdapter] = {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_adapter(provider: str, *, api_key: Optional[str] = None) -> LLMAdapter:
|
|
24
|
+
"""Return the adapter for ``provider``, instantiating on first request.
|
|
25
|
+
|
|
26
|
+
Phase 1: only ``"gemini"`` is registered. Other providers raise
|
|
27
|
+
:class:`UnknownProvider`. Phase 4+ adds DeepSeek, Claude, OpenAI, Grok.
|
|
28
|
+
"""
|
|
29
|
+
if provider in _REGISTRY:
|
|
30
|
+
return _REGISTRY[provider]
|
|
31
|
+
|
|
32
|
+
if provider == "gemini":
|
|
33
|
+
from swarph_mesh.adapters.gemini import GeminiAdapter
|
|
34
|
+
|
|
35
|
+
adapter = GeminiAdapter(api_key=api_key)
|
|
36
|
+
_REGISTRY[provider] = adapter
|
|
37
|
+
return adapter
|
|
38
|
+
|
|
39
|
+
raise UnknownProvider(
|
|
40
|
+
f"no adapter registered for provider {provider!r}. "
|
|
41
|
+
"Phase 1 ships gemini only; DeepSeek/Claude/OpenAI/Grok ship in Phase 4+."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def register_adapter(provider: str, adapter: LLMAdapter) -> None:
|
|
46
|
+
"""Programmatic adapter registration. Test fixtures use this to
|
|
47
|
+
inject mocks; production consumers normally don't need it."""
|
|
48
|
+
_REGISTRY[provider] = adapter
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def reset_registry() -> None:
|
|
52
|
+
"""Test-only: clear the registry. Not part of the public API."""
|
|
53
|
+
_REGISTRY.clear()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ["get_adapter", "register_adapter", "reset_registry"]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Gemini adapter — wraps ``langgraph-genai-bridge`` per PLAN.md §3.
|
|
2
|
+
|
|
3
|
+
Why the bridge and not raw ``google.genai``: the bridge ships
|
|
4
|
+
production-tested Flex tier handling, context caching (Pro tier),
|
|
5
|
+
and usage-metadata extraction in a clean abstraction. Re-implementing
|
|
6
|
+
those features in this adapter would duplicate ~200 LOC for no
|
|
7
|
+
substrate benefit. The bridge is a stable v0.1.5 PyPI package that
|
|
8
|
+
both lab and droplet have run in production for weeks.
|
|
9
|
+
|
|
10
|
+
Cost calculation uses Google's published per-Mtok pricing as of
|
|
11
|
+
2026-04-29; update the ``PRICING`` table when Google revises rates.
|
|
12
|
+
Flex tier (``flex=True``) gets a 50% rebate per Google's announced
|
|
13
|
+
pricing — applied after the base cost computation.
|
|
14
|
+
|
|
15
|
+
The adapter exposes the swarph_mesh :class:`LLMAdapter` Protocol
|
|
16
|
+
shape: async ``chat`` + ``stream`` + sync ``cost_per_token``.
|
|
17
|
+
``stream`` is a v0.2.0 stretch (PLAN.md doesn't gate Phase 1 on
|
|
18
|
+
streaming; the bridge supports it but we keep the surface simple
|
|
19
|
+
for now and raise ``NotImplementedError``).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import os
|
|
26
|
+
import time
|
|
27
|
+
from typing import AsyncIterator, Optional
|
|
28
|
+
|
|
29
|
+
from swarph_mesh.exceptions import AdapterError
|
|
30
|
+
from swarph_mesh.types import ChatMessage, LLMResponse
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Gemini per-Mtok pricing (USD), 2026-04-29 baseline.
|
|
34
|
+
# Flex tier applies a 50% rebate after base computation.
|
|
35
|
+
PRICING: dict[str, tuple[float, float]] = {
|
|
36
|
+
# model_id: (input_per_mtok, output_per_mtok)
|
|
37
|
+
"gemini-2.5-flash": (0.075, 0.30),
|
|
38
|
+
"gemini-2.5-flash-lite": (0.019, 0.075),
|
|
39
|
+
"gemini-2.5-pro": (1.25, 5.00),
|
|
40
|
+
"gemini-2.5-pro-preview": (1.25, 5.00),
|
|
41
|
+
# Default fallback used when an unknown model id is requested
|
|
42
|
+
"_default": (0.075, 0.30),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _to_langchain_messages(messages: list[ChatMessage]) -> list:
|
|
47
|
+
"""Convert our ChatMessage list to LangChain BaseMessage list.
|
|
48
|
+
|
|
49
|
+
The bridge ``invoke()`` method takes LangChain messages; we
|
|
50
|
+
keep our package's user-facing surface as plain dataclasses
|
|
51
|
+
(no LangChain dep at the public API).
|
|
52
|
+
"""
|
|
53
|
+
from langchain_core.messages import (
|
|
54
|
+
AIMessage,
|
|
55
|
+
HumanMessage,
|
|
56
|
+
SystemMessage,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
out: list = []
|
|
60
|
+
for m in messages:
|
|
61
|
+
if m.role == "user":
|
|
62
|
+
out.append(HumanMessage(content=m.content))
|
|
63
|
+
elif m.role == "assistant":
|
|
64
|
+
out.append(AIMessage(content=m.content))
|
|
65
|
+
elif m.role == "system":
|
|
66
|
+
out.append(SystemMessage(content=m.content))
|
|
67
|
+
else:
|
|
68
|
+
# Unknown role — let the bridge surface the error
|
|
69
|
+
out.append(HumanMessage(content=f"[{m.role}] {m.content}"))
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _compute_cost(
|
|
74
|
+
model: str, input_tokens: int, output_tokens: int, flex: bool
|
|
75
|
+
) -> float:
|
|
76
|
+
"""Per-Mtok cost using ``PRICING``. Flex applies 50% rebate."""
|
|
77
|
+
in_per_mtok, out_per_mtok = PRICING.get(model, PRICING["_default"])
|
|
78
|
+
cost = (input_tokens / 1_000_000.0) * in_per_mtok + (
|
|
79
|
+
output_tokens / 1_000_000.0
|
|
80
|
+
) * out_per_mtok
|
|
81
|
+
if flex:
|
|
82
|
+
cost *= 0.5
|
|
83
|
+
return cost
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class GeminiAdapter:
|
|
87
|
+
"""``LLMAdapter`` implementation backed by ``langgraph-genai-bridge``."""
|
|
88
|
+
|
|
89
|
+
name = "gemini"
|
|
90
|
+
default_model = "gemini-2.5-flash"
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
api_key: Optional[str] = None,
|
|
95
|
+
flex: bool = True,
|
|
96
|
+
):
|
|
97
|
+
"""``api_key`` falls back to ``GEMINI_API_KEY`` env. ``flex``
|
|
98
|
+
defaults to True per OMEGA's standard production config —
|
|
99
|
+
Flex tier costs 50% less for latency-tolerant workloads."""
|
|
100
|
+
self._api_key = api_key or os.environ.get("GEMINI_API_KEY")
|
|
101
|
+
self._flex = flex
|
|
102
|
+
# Per-model bridge instances are cached so we don't re-init
|
|
103
|
+
# the SDK client for every call. Keyed by (model, flex).
|
|
104
|
+
self._bridges: dict[tuple[str, bool], object] = {}
|
|
105
|
+
|
|
106
|
+
def _get_bridge(self, model: str, flex: bool) -> object:
|
|
107
|
+
from langgraph_genai_bridge import GenAIBridge
|
|
108
|
+
|
|
109
|
+
key = (model, flex)
|
|
110
|
+
if key in self._bridges:
|
|
111
|
+
return self._bridges[key]
|
|
112
|
+
if not self._api_key:
|
|
113
|
+
raise AdapterError(
|
|
114
|
+
"GeminiAdapter requires GEMINI_API_KEY env or api_key kwarg"
|
|
115
|
+
)
|
|
116
|
+
b = GenAIBridge(api_key=self._api_key, model=model, flex=flex)
|
|
117
|
+
self._bridges[key] = b
|
|
118
|
+
return b
|
|
119
|
+
|
|
120
|
+
async def chat(
|
|
121
|
+
self,
|
|
122
|
+
messages: list[ChatMessage],
|
|
123
|
+
model: str,
|
|
124
|
+
system_prompt: Optional[str] = None,
|
|
125
|
+
json_schema: Optional[dict] = None,
|
|
126
|
+
temperature: float = 0.7,
|
|
127
|
+
max_tokens: Optional[int] = None,
|
|
128
|
+
) -> LLMResponse:
|
|
129
|
+
"""Single multi-turn completion. Calls the bridge via
|
|
130
|
+
``asyncio.to_thread`` so the sync bridge fits in async code."""
|
|
131
|
+
# json_schema enforcement is the JSON harness's job — this
|
|
132
|
+
# method just returns text. The harness handles the parse +
|
|
133
|
+
# retry orchestration.
|
|
134
|
+
del json_schema # not used directly here
|
|
135
|
+
|
|
136
|
+
lc_messages = _to_langchain_messages(messages)
|
|
137
|
+
bridge = self._get_bridge(model, self._flex)
|
|
138
|
+
|
|
139
|
+
start = time.monotonic()
|
|
140
|
+
try:
|
|
141
|
+
ai_message = await asyncio.to_thread(
|
|
142
|
+
bridge.invoke,
|
|
143
|
+
lc_messages,
|
|
144
|
+
system_prompt,
|
|
145
|
+
)
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
duration_s = time.monotonic() - start
|
|
148
|
+
raise AdapterError(
|
|
149
|
+
f"GeminiAdapter.chat failed for model {model!r}: {exc}"
|
|
150
|
+
) from exc
|
|
151
|
+
duration_s = time.monotonic() - start
|
|
152
|
+
|
|
153
|
+
# Extract usage from the AIMessage
|
|
154
|
+
usage = getattr(ai_message, "usage_metadata", None) or {}
|
|
155
|
+
input_tokens = int(usage.get("input_tokens", 0))
|
|
156
|
+
output_tokens = int(usage.get("output_tokens", 0))
|
|
157
|
+
cached_tokens_dict = usage.get("input_token_details", {}) or {}
|
|
158
|
+
cached_tokens = int(cached_tokens_dict.get("cache_read", 0))
|
|
159
|
+
|
|
160
|
+
cost = _compute_cost(model, input_tokens, output_tokens, self._flex)
|
|
161
|
+
|
|
162
|
+
return LLMResponse(
|
|
163
|
+
text=ai_message.content if isinstance(ai_message.content, str) else str(ai_message.content),
|
|
164
|
+
input_tokens=input_tokens,
|
|
165
|
+
output_tokens=output_tokens,
|
|
166
|
+
cost_usd=cost,
|
|
167
|
+
duration_s=duration_s,
|
|
168
|
+
cached=cached_tokens > 0,
|
|
169
|
+
raw_response={
|
|
170
|
+
"cached_tokens": cached_tokens,
|
|
171
|
+
"model": model,
|
|
172
|
+
"flex": self._flex,
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
async def stream(
|
|
177
|
+
self,
|
|
178
|
+
messages: list[ChatMessage],
|
|
179
|
+
model: str,
|
|
180
|
+
**kwargs,
|
|
181
|
+
) -> AsyncIterator[str]:
|
|
182
|
+
"""Token-by-token streaming. Phase 1 v0.1.0 raises
|
|
183
|
+
NotImplementedError; bridge supports streaming, this adapter
|
|
184
|
+
will wire it up in v0.2.0."""
|
|
185
|
+
raise NotImplementedError(
|
|
186
|
+
"GeminiAdapter.stream is v0.2.0 stretch; use chat() for now."
|
|
187
|
+
)
|
|
188
|
+
# Unreachable, but keeps the AsyncIterator return type valid for
|
|
189
|
+
# static analysis.
|
|
190
|
+
yield "" # pragma: no cover
|
|
191
|
+
|
|
192
|
+
def cost_per_token(self, model: str) -> tuple[float, float]:
|
|
193
|
+
"""Return (input_per_mtok, output_per_mtok) USD for ``model``.
|
|
194
|
+
Flex tier rebate is applied at call time, not in this lookup."""
|
|
195
|
+
return PRICING.get(model, PRICING["_default"])
|