meander-agent 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.
- meander_agent-0.1.0/.gitattributes +2 -0
- meander_agent-0.1.0/.github/workflows/meander-agent-tests.yml +25 -0
- meander_agent-0.1.0/.github/workflows/publish-pypi.yml +69 -0
- meander_agent-0.1.0/.gitignore +12 -0
- meander_agent-0.1.0/PKG-INFO +229 -0
- meander_agent-0.1.0/README.md +212 -0
- meander_agent-0.1.0/pixi.lock +1559 -0
- meander_agent-0.1.0/pyproject.toml +47 -0
- meander_agent-0.1.0/src/meander_agent/__init__.py +47 -0
- meander_agent-0.1.0/src/meander_agent/claude.py +101 -0
- meander_agent-0.1.0/src/meander_agent/core.py +379 -0
- meander_agent-0.1.0/src/meander_agent/emitter.py +297 -0
- meander_agent-0.1.0/src/meander_agent/openai.py +175 -0
- meander_agent-0.1.0/src/meander_agent/transport.py +85 -0
- meander_agent-0.1.0/tests/test_claude.py +193 -0
- meander_agent-0.1.0/tests/test_core.py +255 -0
- meander_agent-0.1.0/tests/test_emitter.py +318 -0
- meander_agent-0.1.0/tests/test_openai.py +192 -0
- meander_agent-0.1.0/tests/test_transport.py +103 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: meander-agent tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
name: Test
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
|
+
with:
|
|
17
|
+
persist-credentials: false
|
|
18
|
+
|
|
19
|
+
- uses: prefix-dev/setup-pixi@v0.9.5
|
|
20
|
+
with:
|
|
21
|
+
manifest-path: pyproject.toml
|
|
22
|
+
cache: true
|
|
23
|
+
|
|
24
|
+
- name: Run tests
|
|
25
|
+
run: pixi run test
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v[0-9]+.[0-9]+.[0-9]+"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
uses: ./.github/workflows/meander-agent-tests.yml
|
|
11
|
+
|
|
12
|
+
build:
|
|
13
|
+
name: Build dist
|
|
14
|
+
needs: test
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
with:
|
|
19
|
+
persist-credentials: false
|
|
20
|
+
|
|
21
|
+
- uses: prefix-dev/setup-pixi@v0.9.5
|
|
22
|
+
with:
|
|
23
|
+
manifest-path: pyproject.toml
|
|
24
|
+
cache: true
|
|
25
|
+
|
|
26
|
+
- name: Verify tag matches pyproject version
|
|
27
|
+
run: |
|
|
28
|
+
python - <<'PY'
|
|
29
|
+
import os
|
|
30
|
+
import tomllib
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
tag = os.environ["GITHUB_REF_NAME"]
|
|
34
|
+
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
35
|
+
expected = f"v{version}"
|
|
36
|
+
|
|
37
|
+
if tag != expected:
|
|
38
|
+
raise SystemExit(f"tag {tag!r} does not match pyproject version {version!r}; expected {expected!r}")
|
|
39
|
+
PY
|
|
40
|
+
|
|
41
|
+
- name: Build distributions
|
|
42
|
+
run: |
|
|
43
|
+
rm -rf dist
|
|
44
|
+
pixi run build
|
|
45
|
+
pixi run check-dist
|
|
46
|
+
|
|
47
|
+
- uses: actions/upload-artifact@v6
|
|
48
|
+
with:
|
|
49
|
+
name: python-package-distributions
|
|
50
|
+
path: dist/
|
|
51
|
+
|
|
52
|
+
publish:
|
|
53
|
+
name: Publish to PyPI
|
|
54
|
+
needs: build
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
environment:
|
|
57
|
+
name: pypi
|
|
58
|
+
url: https://pypi.org/p/meander-agent
|
|
59
|
+
permissions:
|
|
60
|
+
id-token: write
|
|
61
|
+
steps:
|
|
62
|
+
- name: Download all the dists
|
|
63
|
+
uses: actions/download-artifact@v6
|
|
64
|
+
with:
|
|
65
|
+
name: python-package-distributions
|
|
66
|
+
path: dist/
|
|
67
|
+
|
|
68
|
+
- name: Publish dist to PyPI
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meander-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Client-side meander.plan SDK: transport setup + a form-checked plan onto a real OTel span, with thin Claude/OpenAI bindings. Imports meander NOT.
|
|
5
|
+
Author-email: Raphael Feikert <r.feikert@symbolic-intelligence.de>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.30
|
|
8
|
+
Requires-Dist: opentelemetry-sdk<2,>=1.30
|
|
9
|
+
Provides-Extra: claude
|
|
10
|
+
Requires-Dist: claude-agent-sdk<0.3,>=0.2; extra == 'claude'
|
|
11
|
+
Provides-Extra: openai
|
|
12
|
+
Requires-Dist: openai-agents<0.18,>=0.17; extra == 'openai'
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest-randomly>=3; extra == 'test'
|
|
15
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# meander-agent
|
|
19
|
+
|
|
20
|
+
Kundenseitiges SDK für die **Plan-Sende-Seite** von meander. Dein Agent hält
|
|
21
|
+
fest, was er getan hat, als nachvollziehbare Behauptung mit Herkunft, und stellt
|
|
22
|
+
GENAU EINE Frage an eine Regel deiner Ontologie. Das Paket baut daraus ein
|
|
23
|
+
`meander.plan`, prüft dessen FORM und hängt es an einen echten OpenTelemetry-Span,
|
|
24
|
+
den dein OTLP-Export zu meander trägt. Kein meander-Import, keine Ontologie im
|
|
25
|
+
Client.
|
|
26
|
+
|
|
27
|
+
## Das Modell in einem Absatz
|
|
28
|
+
|
|
29
|
+
Ein `meander.plan` ist eine **Behauptung plus eine Frage**, keine Entscheidung:
|
|
30
|
+
|
|
31
|
+
- **facts** — was der Agent beobachtet/eingeschätzt hat. Jeder Fakt trägt SEINE
|
|
32
|
+
Herkunft (`origin.kind`: `tool` | `api` | `human` | `agent`). Ein Tool-Ergebnis
|
|
33
|
+
ist `tool`, die eigene Einschätzung des Agenten ist `agent`.
|
|
34
|
+
- **relations** — behauptete Beziehungen zwischen Entities (optional).
|
|
35
|
+
- **question** — GENAU EINE Frage an eine bestehende Derivation (Regel) deiner
|
|
36
|
+
Welt, z.B. „gilt hier `requires_review`?". Der Agent **entscheidet nicht**; ob
|
|
37
|
+
die Regel greift, bestimmt meander serverseitig, und ein Mensch prüft.
|
|
38
|
+
|
|
39
|
+
Der Client prüft nur die **FORM** (Pflichtfelder, Typen, genau eine Frage,
|
|
40
|
+
`origin`-Vokabular, `plan_version == 1`). Ob die Entities/Properties/Derivations
|
|
41
|
+
wirklich existieren, weiß nur der Server (er kennt deine Ontologie). Darum
|
|
42
|
+
importiert dieses Paket meander NICHT und lädt keine Ontologie.
|
|
43
|
+
|
|
44
|
+
**Garantien (nie stilles Nichtstun):** ein Fehlerlauf hängt NIE ein Attribut an;
|
|
45
|
+
ein nicht mehr beschreibbarer Span verweigert das Attribut laut
|
|
46
|
+
(`AttachResult(ok=False, span_not_recording)`); ein formfehlerhafter Plan wird mit
|
|
47
|
+
Feldpfad gemeldet, statt halb geschrieben zu werden.
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
pip install meander-agent # Transport + Emitter (schlank, ohne LLM-SDK)
|
|
53
|
+
pip install "meander-agent[claude]" # + Claude-Agent-SDK-Binding
|
|
54
|
+
pip install "meander-agent[openai]" # + OpenAI-Agents-SDK-Binding
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 1. Dein Vokabular deklarieren
|
|
58
|
+
|
|
59
|
+
Du gibst dem Agenten die Namen deiner Welt (er soll nichts erfinden). Je Entity
|
|
60
|
+
ihre identity-Felder und Properties, je Relation ihre Endpunkte, plus die
|
|
61
|
+
Derivation-IDs, die er befragen darf:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
vocabulary = {
|
|
65
|
+
"entities": {
|
|
66
|
+
"Order": {"identity": ["order_id"], "properties": ["amount", "risk"]},
|
|
67
|
+
"Case": {"identity": ["case_id"], "properties": []},
|
|
68
|
+
"Outcome": {"identity": ["outcome_id"], "properties": []},
|
|
69
|
+
},
|
|
70
|
+
"relations": {
|
|
71
|
+
"concerns": {"from": "Case", "to": "Order"},
|
|
72
|
+
},
|
|
73
|
+
"derivation_ids": ["requires_review.high_value"],
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Daraus erzeugt das Paket den Prompt-Baustein (nennt dem Modell nur diese Namen)
|
|
78
|
+
und das JSON-Schema der strukturierten Ausgabe (erlaubt nur diese Namen).
|
|
79
|
+
|
|
80
|
+
## 2. Transport einrichten
|
|
81
|
+
|
|
82
|
+
Ohne eigenes OTel genügt ein Aufruf. `endpoint` ist die volle OTLP-Traces-URL
|
|
83
|
+
deiner Source, `source_key` das Bearer-Token dieser Source:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from meander_agent import init_meander
|
|
87
|
+
|
|
88
|
+
client = init_meander(
|
|
89
|
+
endpoint="https://<host>/api/sources/<source_id>/v1/traces",
|
|
90
|
+
source_key="<bearer-token>",
|
|
91
|
+
)
|
|
92
|
+
# client.tracer -> der verdrahtete OTel-Tracer
|
|
93
|
+
# client.shutdown() / client.force_flush() -> Export abschließen
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`init_meander` setzt KEINEN globalen Provider; der Client hält seinen eigenen.
|
|
97
|
+
Hast du bereits ein OTel-Setup, lass `init_meander` weg und übergib deinen Tracer
|
|
98
|
+
direkt (siehe Abschnitt 5).
|
|
99
|
+
|
|
100
|
+
## 3. Einen Agenten-Run fahren (Claude)
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from meander_agent.claude import run_with_plan
|
|
104
|
+
|
|
105
|
+
result = run_with_plan(
|
|
106
|
+
"Bearbeite Bestellung ORD-42. Rufe die üblichen Tools auf, behaupte die "
|
|
107
|
+
"gewonnenen Fakten mit ihrer Herkunft und stelle GENAU EINE Frage an die "
|
|
108
|
+
"Derivation requires_review.high_value. Triff KEINE Entscheidung.",
|
|
109
|
+
vocabulary=vocabulary,
|
|
110
|
+
tracer=client.tracer,
|
|
111
|
+
)
|
|
112
|
+
print("angehängt" if result.attached else f"kein Plan gesetzt: {result.error_state}")
|
|
113
|
+
client.shutdown() # Span exportieren
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Das Binding öffnet den Root-Span, fährt das Modell mit strukturierter Ausgabe,
|
|
117
|
+
sperrt bei Fehler/Abbruch und hängt bei Erfolg den geprüften Plan an. Es gibt ein
|
|
118
|
+
`RunResult` zurück (siehe Abschnitt 4). Braucht das `[claude]`-Extra und einen
|
|
119
|
+
`ANTHROPIC_API_KEY`.
|
|
120
|
+
|
|
121
|
+
## 3b. Derselbe Run über OpenAI
|
|
122
|
+
|
|
123
|
+
Identischer Aufruf, anderes Binding (Extra `[openai]`, `OPENAI_API_KEY`):
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from meander_agent.openai import run_with_plan
|
|
127
|
+
|
|
128
|
+
result = run_with_plan(task, vocabulary=vocabulary, tracer=client.tracer)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Beide Bindings gibt es auch async (`run_with_plan_async`).
|
|
132
|
+
|
|
133
|
+
## 4. Was du zurückbekommst
|
|
134
|
+
|
|
135
|
+
`RunResult`:
|
|
136
|
+
|
|
137
|
+
- `attached: bool` — wurde `meander.plan` an den Span gehängt?
|
|
138
|
+
- `plan: dict | None` — der angehängte Plan (bei Erfolg).
|
|
139
|
+
- `shape_errors: list[ShapeError]` — Formfehler mit `path` / `code` / `message`,
|
|
140
|
+
falls die Ausgabe die FORM-Prüfung nicht bestand.
|
|
141
|
+
- `error_state` — `None` bei Erfolg; sonst der Fehlerausgang des Laufs
|
|
142
|
+
(SDK-Fehler, Abbruch, Typ-Mismatch). Ist er gesetzt, wird NIE angehängt.
|
|
143
|
+
|
|
144
|
+
Kein `meander.plan` heißt also immer: entweder ein Fehlerlauf (`error_state`) oder
|
|
145
|
+
eine formfehlerhafte Ausgabe (`shape_errors`) — beides steht im Ergebnis, nichts
|
|
146
|
+
verschwindet still.
|
|
147
|
+
|
|
148
|
+
## 5. Eigenes Tracing / eigenes SDK (der Kern)
|
|
149
|
+
|
|
150
|
+
Willst du dein eigenes SDK anbinden (oder deinen eigenen Tracer nutzen), fährst du
|
|
151
|
+
den SDK-neutralen Kontextmanager selbst. Er liefert dir den Baustein und das
|
|
152
|
+
Schema und übernimmt Parsen, Formprüfung, Sperren und Anhängen:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from meander_agent import meander_run
|
|
156
|
+
|
|
157
|
+
with meander_run(vocabulary=vocabulary, tracer=my_tracer) as run:
|
|
158
|
+
# run.prompt_fragment : Format + erlaubtes Vokabular -> als Instruktion ans SDK
|
|
159
|
+
# run.output_schema : JSON-Schema, falls dein SDK strukturierte Ausgabe kann
|
|
160
|
+
output, error = call_your_llm(task, instructions=run.prompt_fragment,
|
|
161
|
+
schema=run.output_schema)
|
|
162
|
+
# output: das strukturierte Ergebnis (dict) ODER ein reiner JSON-String.
|
|
163
|
+
# error_state: None bei Erfolg, sonst ein beliebiges Detail (=> Sperre).
|
|
164
|
+
result = run.finalize(output, error_state=error)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Der Kern übergibt nie selbst Prompts ans SDK und liest nie SDK-Resultate — das ist
|
|
168
|
+
Sache deines Bindings. Genauso arbeiten die mitgelieferten Claude-/OpenAI-Bindings.
|
|
169
|
+
|
|
170
|
+
## 6. Deterministischer Plan ohne LLM
|
|
171
|
+
|
|
172
|
+
Baust du den Plan selbst (Tests, regelbasierte Agenten, auth-loser Pfad), nutzt du
|
|
173
|
+
den Emitter direkt:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from meander_agent import attach_plan, validate_plan_shape
|
|
177
|
+
|
|
178
|
+
plan = {
|
|
179
|
+
"plan_version": 1,
|
|
180
|
+
"facts": [
|
|
181
|
+
{"entity": "Order", "identity": {"order_id": "ORD-42"},
|
|
182
|
+
"property": "amount", "value": 900,
|
|
183
|
+
"origin": {"kind": "tool", "ref": "lookup_order"}},
|
|
184
|
+
],
|
|
185
|
+
"relations": [
|
|
186
|
+
{"relation": "concerns",
|
|
187
|
+
"from": {"entity": "Case", "identity": {"case_id": "C-1"}},
|
|
188
|
+
"to": {"entity": "Order", "identity": {"order_id": "ORD-42"}},
|
|
189
|
+
"origin": {"kind": "agent"}},
|
|
190
|
+
],
|
|
191
|
+
"question": {
|
|
192
|
+
"derivation_id": "requires_review.high_value",
|
|
193
|
+
"subject": {"entity": "Case", "identity": {"case_id": "C-1"}},
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
errors = validate_plan_shape(plan) # reine FORM-Prüfung (leer = ok)
|
|
198
|
+
with client.tracer.start_as_current_span("agent.run") as span:
|
|
199
|
+
res = attach_plan(span, plan) # hängt bei Formgültigkeit an
|
|
200
|
+
if not res.ok:
|
|
201
|
+
print("nicht angehängt:", [e.as_dict() for e in res.errors])
|
|
202
|
+
client.shutdown()
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Öffentliche Fläche
|
|
206
|
+
|
|
207
|
+
| Symbol | Zweck |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `init_meander(endpoint, source_key) -> MeanderClient` | Transport (OTel + OTLP) einrichten |
|
|
210
|
+
| `MeanderClient.tracer / .run(vocabulary=…) / .force_flush() / .shutdown()` | Tracer, Kern-Zucker, Export |
|
|
211
|
+
| `meander_agent.claude.run_with_plan[_async](task, *, vocabulary, tracer)` | Claude-Binding |
|
|
212
|
+
| `meander_agent.openai.run_with_plan[_async](task, *, vocabulary, tracer)` | OpenAI-Binding |
|
|
213
|
+
| `meander_run(vocabulary, tracer) -> Run` | SDK-neutraler Kern-Kontextmanager |
|
|
214
|
+
| `Run.prompt_fragment / .output_schema / .finalize(output, error_state)` | Bausteine + Verarbeitung |
|
|
215
|
+
| `attach_plan(span, plan) -> AttachResult` | FORM-Prüfung + Anhang |
|
|
216
|
+
| `validate_plan_shape(plan) -> list[ShapeError]` | reine FORM-Prüfung |
|
|
217
|
+
| `RunResult`, `AttachResult`, `ShapeError` | Ergebnis-/Fehlertypen |
|
|
218
|
+
| `PLAN_ATTRIBUTE_KEY`, `ORIGIN_KINDS`, `PLAN_VERSION` | Client-Konstanten |
|
|
219
|
+
|
|
220
|
+
## Entwicklung
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
pixi run test # zufällige Reihenfolge (pytest-randomly)
|
|
224
|
+
pixi run -- pytest -p no:randomly # feste Reihenfolge
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Die credential-gated Live-LLM-Tests (Claude/OpenAI) laufen dort, wo die Keys
|
|
228
|
+
gesetzt sind, und werden sonst übersprungen; die deterministische Suite läuft
|
|
229
|
+
immer und ohne Mocks.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# meander-agent
|
|
2
|
+
|
|
3
|
+
Kundenseitiges SDK für die **Plan-Sende-Seite** von meander. Dein Agent hält
|
|
4
|
+
fest, was er getan hat, als nachvollziehbare Behauptung mit Herkunft, und stellt
|
|
5
|
+
GENAU EINE Frage an eine Regel deiner Ontologie. Das Paket baut daraus ein
|
|
6
|
+
`meander.plan`, prüft dessen FORM und hängt es an einen echten OpenTelemetry-Span,
|
|
7
|
+
den dein OTLP-Export zu meander trägt. Kein meander-Import, keine Ontologie im
|
|
8
|
+
Client.
|
|
9
|
+
|
|
10
|
+
## Das Modell in einem Absatz
|
|
11
|
+
|
|
12
|
+
Ein `meander.plan` ist eine **Behauptung plus eine Frage**, keine Entscheidung:
|
|
13
|
+
|
|
14
|
+
- **facts** — was der Agent beobachtet/eingeschätzt hat. Jeder Fakt trägt SEINE
|
|
15
|
+
Herkunft (`origin.kind`: `tool` | `api` | `human` | `agent`). Ein Tool-Ergebnis
|
|
16
|
+
ist `tool`, die eigene Einschätzung des Agenten ist `agent`.
|
|
17
|
+
- **relations** — behauptete Beziehungen zwischen Entities (optional).
|
|
18
|
+
- **question** — GENAU EINE Frage an eine bestehende Derivation (Regel) deiner
|
|
19
|
+
Welt, z.B. „gilt hier `requires_review`?". Der Agent **entscheidet nicht**; ob
|
|
20
|
+
die Regel greift, bestimmt meander serverseitig, und ein Mensch prüft.
|
|
21
|
+
|
|
22
|
+
Der Client prüft nur die **FORM** (Pflichtfelder, Typen, genau eine Frage,
|
|
23
|
+
`origin`-Vokabular, `plan_version == 1`). Ob die Entities/Properties/Derivations
|
|
24
|
+
wirklich existieren, weiß nur der Server (er kennt deine Ontologie). Darum
|
|
25
|
+
importiert dieses Paket meander NICHT und lädt keine Ontologie.
|
|
26
|
+
|
|
27
|
+
**Garantien (nie stilles Nichtstun):** ein Fehlerlauf hängt NIE ein Attribut an;
|
|
28
|
+
ein nicht mehr beschreibbarer Span verweigert das Attribut laut
|
|
29
|
+
(`AttachResult(ok=False, span_not_recording)`); ein formfehlerhafter Plan wird mit
|
|
30
|
+
Feldpfad gemeldet, statt halb geschrieben zu werden.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pip install meander-agent # Transport + Emitter (schlank, ohne LLM-SDK)
|
|
36
|
+
pip install "meander-agent[claude]" # + Claude-Agent-SDK-Binding
|
|
37
|
+
pip install "meander-agent[openai]" # + OpenAI-Agents-SDK-Binding
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 1. Dein Vokabular deklarieren
|
|
41
|
+
|
|
42
|
+
Du gibst dem Agenten die Namen deiner Welt (er soll nichts erfinden). Je Entity
|
|
43
|
+
ihre identity-Felder und Properties, je Relation ihre Endpunkte, plus die
|
|
44
|
+
Derivation-IDs, die er befragen darf:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
vocabulary = {
|
|
48
|
+
"entities": {
|
|
49
|
+
"Order": {"identity": ["order_id"], "properties": ["amount", "risk"]},
|
|
50
|
+
"Case": {"identity": ["case_id"], "properties": []},
|
|
51
|
+
"Outcome": {"identity": ["outcome_id"], "properties": []},
|
|
52
|
+
},
|
|
53
|
+
"relations": {
|
|
54
|
+
"concerns": {"from": "Case", "to": "Order"},
|
|
55
|
+
},
|
|
56
|
+
"derivation_ids": ["requires_review.high_value"],
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Daraus erzeugt das Paket den Prompt-Baustein (nennt dem Modell nur diese Namen)
|
|
61
|
+
und das JSON-Schema der strukturierten Ausgabe (erlaubt nur diese Namen).
|
|
62
|
+
|
|
63
|
+
## 2. Transport einrichten
|
|
64
|
+
|
|
65
|
+
Ohne eigenes OTel genügt ein Aufruf. `endpoint` ist die volle OTLP-Traces-URL
|
|
66
|
+
deiner Source, `source_key` das Bearer-Token dieser Source:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from meander_agent import init_meander
|
|
70
|
+
|
|
71
|
+
client = init_meander(
|
|
72
|
+
endpoint="https://<host>/api/sources/<source_id>/v1/traces",
|
|
73
|
+
source_key="<bearer-token>",
|
|
74
|
+
)
|
|
75
|
+
# client.tracer -> der verdrahtete OTel-Tracer
|
|
76
|
+
# client.shutdown() / client.force_flush() -> Export abschließen
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`init_meander` setzt KEINEN globalen Provider; der Client hält seinen eigenen.
|
|
80
|
+
Hast du bereits ein OTel-Setup, lass `init_meander` weg und übergib deinen Tracer
|
|
81
|
+
direkt (siehe Abschnitt 5).
|
|
82
|
+
|
|
83
|
+
## 3. Einen Agenten-Run fahren (Claude)
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from meander_agent.claude import run_with_plan
|
|
87
|
+
|
|
88
|
+
result = run_with_plan(
|
|
89
|
+
"Bearbeite Bestellung ORD-42. Rufe die üblichen Tools auf, behaupte die "
|
|
90
|
+
"gewonnenen Fakten mit ihrer Herkunft und stelle GENAU EINE Frage an die "
|
|
91
|
+
"Derivation requires_review.high_value. Triff KEINE Entscheidung.",
|
|
92
|
+
vocabulary=vocabulary,
|
|
93
|
+
tracer=client.tracer,
|
|
94
|
+
)
|
|
95
|
+
print("angehängt" if result.attached else f"kein Plan gesetzt: {result.error_state}")
|
|
96
|
+
client.shutdown() # Span exportieren
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Das Binding öffnet den Root-Span, fährt das Modell mit strukturierter Ausgabe,
|
|
100
|
+
sperrt bei Fehler/Abbruch und hängt bei Erfolg den geprüften Plan an. Es gibt ein
|
|
101
|
+
`RunResult` zurück (siehe Abschnitt 4). Braucht das `[claude]`-Extra und einen
|
|
102
|
+
`ANTHROPIC_API_KEY`.
|
|
103
|
+
|
|
104
|
+
## 3b. Derselbe Run über OpenAI
|
|
105
|
+
|
|
106
|
+
Identischer Aufruf, anderes Binding (Extra `[openai]`, `OPENAI_API_KEY`):
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from meander_agent.openai import run_with_plan
|
|
110
|
+
|
|
111
|
+
result = run_with_plan(task, vocabulary=vocabulary, tracer=client.tracer)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Beide Bindings gibt es auch async (`run_with_plan_async`).
|
|
115
|
+
|
|
116
|
+
## 4. Was du zurückbekommst
|
|
117
|
+
|
|
118
|
+
`RunResult`:
|
|
119
|
+
|
|
120
|
+
- `attached: bool` — wurde `meander.plan` an den Span gehängt?
|
|
121
|
+
- `plan: dict | None` — der angehängte Plan (bei Erfolg).
|
|
122
|
+
- `shape_errors: list[ShapeError]` — Formfehler mit `path` / `code` / `message`,
|
|
123
|
+
falls die Ausgabe die FORM-Prüfung nicht bestand.
|
|
124
|
+
- `error_state` — `None` bei Erfolg; sonst der Fehlerausgang des Laufs
|
|
125
|
+
(SDK-Fehler, Abbruch, Typ-Mismatch). Ist er gesetzt, wird NIE angehängt.
|
|
126
|
+
|
|
127
|
+
Kein `meander.plan` heißt also immer: entweder ein Fehlerlauf (`error_state`) oder
|
|
128
|
+
eine formfehlerhafte Ausgabe (`shape_errors`) — beides steht im Ergebnis, nichts
|
|
129
|
+
verschwindet still.
|
|
130
|
+
|
|
131
|
+
## 5. Eigenes Tracing / eigenes SDK (der Kern)
|
|
132
|
+
|
|
133
|
+
Willst du dein eigenes SDK anbinden (oder deinen eigenen Tracer nutzen), fährst du
|
|
134
|
+
den SDK-neutralen Kontextmanager selbst. Er liefert dir den Baustein und das
|
|
135
|
+
Schema und übernimmt Parsen, Formprüfung, Sperren und Anhängen:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from meander_agent import meander_run
|
|
139
|
+
|
|
140
|
+
with meander_run(vocabulary=vocabulary, tracer=my_tracer) as run:
|
|
141
|
+
# run.prompt_fragment : Format + erlaubtes Vokabular -> als Instruktion ans SDK
|
|
142
|
+
# run.output_schema : JSON-Schema, falls dein SDK strukturierte Ausgabe kann
|
|
143
|
+
output, error = call_your_llm(task, instructions=run.prompt_fragment,
|
|
144
|
+
schema=run.output_schema)
|
|
145
|
+
# output: das strukturierte Ergebnis (dict) ODER ein reiner JSON-String.
|
|
146
|
+
# error_state: None bei Erfolg, sonst ein beliebiges Detail (=> Sperre).
|
|
147
|
+
result = run.finalize(output, error_state=error)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Der Kern übergibt nie selbst Prompts ans SDK und liest nie SDK-Resultate — das ist
|
|
151
|
+
Sache deines Bindings. Genauso arbeiten die mitgelieferten Claude-/OpenAI-Bindings.
|
|
152
|
+
|
|
153
|
+
## 6. Deterministischer Plan ohne LLM
|
|
154
|
+
|
|
155
|
+
Baust du den Plan selbst (Tests, regelbasierte Agenten, auth-loser Pfad), nutzt du
|
|
156
|
+
den Emitter direkt:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from meander_agent import attach_plan, validate_plan_shape
|
|
160
|
+
|
|
161
|
+
plan = {
|
|
162
|
+
"plan_version": 1,
|
|
163
|
+
"facts": [
|
|
164
|
+
{"entity": "Order", "identity": {"order_id": "ORD-42"},
|
|
165
|
+
"property": "amount", "value": 900,
|
|
166
|
+
"origin": {"kind": "tool", "ref": "lookup_order"}},
|
|
167
|
+
],
|
|
168
|
+
"relations": [
|
|
169
|
+
{"relation": "concerns",
|
|
170
|
+
"from": {"entity": "Case", "identity": {"case_id": "C-1"}},
|
|
171
|
+
"to": {"entity": "Order", "identity": {"order_id": "ORD-42"}},
|
|
172
|
+
"origin": {"kind": "agent"}},
|
|
173
|
+
],
|
|
174
|
+
"question": {
|
|
175
|
+
"derivation_id": "requires_review.high_value",
|
|
176
|
+
"subject": {"entity": "Case", "identity": {"case_id": "C-1"}},
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
errors = validate_plan_shape(plan) # reine FORM-Prüfung (leer = ok)
|
|
181
|
+
with client.tracer.start_as_current_span("agent.run") as span:
|
|
182
|
+
res = attach_plan(span, plan) # hängt bei Formgültigkeit an
|
|
183
|
+
if not res.ok:
|
|
184
|
+
print("nicht angehängt:", [e.as_dict() for e in res.errors])
|
|
185
|
+
client.shutdown()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Öffentliche Fläche
|
|
189
|
+
|
|
190
|
+
| Symbol | Zweck |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `init_meander(endpoint, source_key) -> MeanderClient` | Transport (OTel + OTLP) einrichten |
|
|
193
|
+
| `MeanderClient.tracer / .run(vocabulary=…) / .force_flush() / .shutdown()` | Tracer, Kern-Zucker, Export |
|
|
194
|
+
| `meander_agent.claude.run_with_plan[_async](task, *, vocabulary, tracer)` | Claude-Binding |
|
|
195
|
+
| `meander_agent.openai.run_with_plan[_async](task, *, vocabulary, tracer)` | OpenAI-Binding |
|
|
196
|
+
| `meander_run(vocabulary, tracer) -> Run` | SDK-neutraler Kern-Kontextmanager |
|
|
197
|
+
| `Run.prompt_fragment / .output_schema / .finalize(output, error_state)` | Bausteine + Verarbeitung |
|
|
198
|
+
| `attach_plan(span, plan) -> AttachResult` | FORM-Prüfung + Anhang |
|
|
199
|
+
| `validate_plan_shape(plan) -> list[ShapeError]` | reine FORM-Prüfung |
|
|
200
|
+
| `RunResult`, `AttachResult`, `ShapeError` | Ergebnis-/Fehlertypen |
|
|
201
|
+
| `PLAN_ATTRIBUTE_KEY`, `ORIGIN_KINDS`, `PLAN_VERSION` | Client-Konstanten |
|
|
202
|
+
|
|
203
|
+
## Entwicklung
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
pixi run test # zufällige Reihenfolge (pytest-randomly)
|
|
207
|
+
pixi run -- pytest -p no:randomly # feste Reihenfolge
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Die credential-gated Live-LLM-Tests (Claude/OpenAI) laufen dort, wo die Keys
|
|
211
|
+
gesetzt sind, und werden sonst übersprungen; die deterministische Suite läuft
|
|
212
|
+
immer und ohne Mocks.
|