loopgain 0.1.4__tar.gz → 0.1.7__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.
- {loopgain-0.1.4 → loopgain-0.1.7}/PKG-INFO +121 -3
- {loopgain-0.1.4 → loopgain-0.1.7}/README.md +109 -1
- {loopgain-0.1.4 → loopgain-0.1.7}/loopgain/__init__.py +1 -2
- loopgain-0.1.7/loopgain/_version.py +9 -0
- {loopgain-0.1.4 → loopgain-0.1.7}/loopgain/core.py +35 -7
- loopgain-0.1.7/loopgain/integrations/__init__.py +66 -0
- loopgain-0.1.7/loopgain/integrations/autogen.py +156 -0
- loopgain-0.1.7/loopgain/integrations/crewai.py +155 -0
- loopgain-0.1.7/loopgain/integrations/langgraph.py +166 -0
- loopgain-0.1.7/loopgain/telemetry.py +262 -0
- {loopgain-0.1.4 → loopgain-0.1.7}/loopgain.egg-info/PKG-INFO +121 -3
- {loopgain-0.1.4 → loopgain-0.1.7}/loopgain.egg-info/SOURCES.txt +6 -0
- loopgain-0.1.7/loopgain.egg-info/requires.txt +17 -0
- {loopgain-0.1.4 → loopgain-0.1.7}/pyproject.toml +12 -2
- loopgain-0.1.7/tests/test_integrations.py +372 -0
- loopgain-0.1.7/tests/test_telemetry.py +639 -0
- loopgain-0.1.4/loopgain/telemetry.py +0 -148
- loopgain-0.1.4/loopgain.egg-info/requires.txt +0 -3
- loopgain-0.1.4/tests/test_telemetry.py +0 -307
- {loopgain-0.1.4 → loopgain-0.1.7}/LICENSE +0 -0
- {loopgain-0.1.4 → loopgain-0.1.7}/loopgain.egg-info/dependency_links.txt +0 -0
- {loopgain-0.1.4 → loopgain-0.1.7}/loopgain.egg-info/top_level.txt +0 -0
- {loopgain-0.1.4 → loopgain-0.1.7}/setup.cfg +0 -0
- {loopgain-0.1.4 → loopgain-0.1.7}/tests/test_core.py +0 -0
- {loopgain-0.1.4 → loopgain-0.1.7}/tests/test_stress.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loopgain
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Barkhausen stability monitor for AI agent loops. Real-time loop-gain (Aβ) monitoring with five named threshold bands, best-so-far rollback, and ETA prediction.
|
|
5
|
-
Author-email: Dave Fitzsimmons <
|
|
5
|
+
Author-email: Dave Fitzsimmons <hello@loopgain.ai>
|
|
6
6
|
License: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://loopgain.ai
|
|
8
8
|
Project-URL: Repository, https://github.com/loopgain-ai/loopgain
|
|
@@ -24,6 +24,16 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Provides-Extra: test
|
|
26
26
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
27
|
+
Provides-Extra: langgraph
|
|
28
|
+
Requires-Dist: langgraph>=0.2; extra == "langgraph"
|
|
29
|
+
Provides-Extra: crewai
|
|
30
|
+
Requires-Dist: crewai>=0.30; extra == "crewai"
|
|
31
|
+
Provides-Extra: autogen
|
|
32
|
+
Requires-Dist: autogen-agentchat>=0.4; extra == "autogen"
|
|
33
|
+
Provides-Extra: all
|
|
34
|
+
Requires-Dist: langgraph>=0.2; extra == "all"
|
|
35
|
+
Requires-Dist: crewai>=0.30; extra == "all"
|
|
36
|
+
Requires-Dist: autogen-agentchat>=0.4; extra == "all"
|
|
27
37
|
Dynamic: license-file
|
|
28
38
|
|
|
29
39
|
# LoopGain
|
|
@@ -87,7 +97,14 @@ print(result.savings_vs_fixed_cap)
|
|
|
87
97
|
|
|
88
98
|
## How it works
|
|
89
99
|
|
|
90
|
-
LoopGain measures empirical loop gain
|
|
100
|
+
LoopGain measures empirical loop gain at every iteration, then smooths it with an EMA:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Aβ(n) = E(n) / E(n-1)
|
|
104
|
+
Aβ_smooth = EMA(Aβ, w=3)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
It classifies `Aβ_smooth` into five named bands:
|
|
91
108
|
|
|
92
109
|
| `Aβ_smooth` range | State | Action |
|
|
93
110
|
| --- | --- | --- |
|
|
@@ -205,6 +222,107 @@ The hosted endpoint at `telemetry.loopgain.ai` is one acceptable destination. Th
|
|
|
205
222
|
|
|
206
223
|
---
|
|
207
224
|
|
|
225
|
+
## Framework adapters
|
|
226
|
+
|
|
227
|
+
Thin wrappers under `loopgain.integrations` drive each major agent framework's iteration with a `LoopGain` monitor and auto-stamp `framework="<name>"` on telemetry. The frameworks themselves are **optional dependencies** — install the extra you need:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
pip install 'loopgain[langgraph]' # LangGraph
|
|
231
|
+
pip install 'loopgain[crewai]' # CrewAI
|
|
232
|
+
pip install 'loopgain[autogen]' # AutoGen v0.4+
|
|
233
|
+
pip install 'loopgain[all]' # all three
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
All adapters take a `LoopGain` instance plus an `error_fn` you provide — the framework doesn't know what your error signal is, so the adapter doesn't either. `error_fn` returns a non-negative number (or `None` to skip an iteration).
|
|
237
|
+
|
|
238
|
+
### LangGraph
|
|
239
|
+
|
|
240
|
+
Drives `graph.stream(input, stream_mode="updates")`. Each update is one iteration.
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from loopgain import LoopGain
|
|
244
|
+
from loopgain.integrations import LangGraphAdapter
|
|
245
|
+
|
|
246
|
+
graph = build_my_verify_revise_graph().compile()
|
|
247
|
+
lg = LoopGain(target_error=0.1, max_iterations=20)
|
|
248
|
+
|
|
249
|
+
adapter = LangGraphAdapter(
|
|
250
|
+
lg=lg,
|
|
251
|
+
error_fn=lambda update: len(update.get("verifier", {}).get("errors", [])),
|
|
252
|
+
)
|
|
253
|
+
final_state = adapter.run(graph, {"draft": initial})
|
|
254
|
+
|
|
255
|
+
lg.send_telemetry(
|
|
256
|
+
endpoint=os.environ["LOOPGAIN_TELEMETRY_ENDPOINT"],
|
|
257
|
+
token=os.environ["LOOPGAIN_TELEMETRY_TOKEN"],
|
|
258
|
+
workload_id="rag-rewrite",
|
|
259
|
+
framework=adapter.framework_name, # "langgraph", auto-stamped
|
|
260
|
+
)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`adapter.stream(...)` yields each item if you want the full trace; `adapter.arun(...)` / `adapter.astream(...)` are the async counterparts and accept an async `error_fn`.
|
|
264
|
+
|
|
265
|
+
### CrewAI
|
|
266
|
+
|
|
267
|
+
Installs `step_callback` and/or `task_callback` on a Crew. Pick whichever granularity matches your loop — `step_error_fn` for refinement *within* a Task, `task_error_fn` for refinement *across* Tasks.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from crewai import Crew
|
|
271
|
+
from loopgain import LoopGain
|
|
272
|
+
from loopgain.integrations import CrewAIAdapter
|
|
273
|
+
|
|
274
|
+
lg = LoopGain(target_error=0.1, max_iterations=20)
|
|
275
|
+
adapter = CrewAIAdapter(
|
|
276
|
+
lg=lg,
|
|
277
|
+
task_error_fn=lambda task_output: count_failed_checks(task_output.raw),
|
|
278
|
+
)
|
|
279
|
+
crew = Crew(agents=[...], tasks=[...])
|
|
280
|
+
adapter.install(crew)
|
|
281
|
+
result = crew.kickoff()
|
|
282
|
+
adapter.uninstall() # or use `with CrewAIAdapter(...) as a:` context
|
|
283
|
+
|
|
284
|
+
lg.send_telemetry(
|
|
285
|
+
endpoint=...,
|
|
286
|
+
token=...,
|
|
287
|
+
framework=adapter.framework_name, # "crewai"
|
|
288
|
+
)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The adapter chains with any callback you already had installed — your existing instrumentation isn't overwritten.
|
|
292
|
+
|
|
293
|
+
### AutoGen (v0.4+)
|
|
294
|
+
|
|
295
|
+
Wraps `team.run_stream(task=...)`. In a verify-revise rotation, filter to the verifier's messages with `observe_sources={"verifier"}` so only it drives `observe()`.
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from autogen_agentchat.teams import RoundRobinGroupChat
|
|
299
|
+
from loopgain import LoopGain
|
|
300
|
+
from loopgain.integrations import AutoGenAdapter
|
|
301
|
+
|
|
302
|
+
team = RoundRobinGroupChat(participants=[generator, verifier])
|
|
303
|
+
lg = LoopGain(target_error=0.1, max_iterations=20)
|
|
304
|
+
adapter = AutoGenAdapter(
|
|
305
|
+
lg=lg,
|
|
306
|
+
error_fn=lambda msg: parse_verifier_score(msg.content),
|
|
307
|
+
observe_sources={"verifier"},
|
|
308
|
+
)
|
|
309
|
+
result = await adapter.run(team, task="...")
|
|
310
|
+
|
|
311
|
+
lg.send_telemetry(
|
|
312
|
+
endpoint=...,
|
|
313
|
+
token=...,
|
|
314
|
+
framework=adapter.framework_name, # "autogen"
|
|
315
|
+
)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Pass a `cancellation_token` to `adapter.run(...)` and the adapter will cancel it when LoopGain reaches a terminal state (target met, oscillation, divergence). The legacy v0.2 `ConversableAgent.initiate_chat` API is **not** supported — use the v0.4 event-driven runtime.
|
|
319
|
+
|
|
320
|
+
### Custom integrations
|
|
321
|
+
|
|
322
|
+
For frameworks without an adapter, the raw `LoopGain.observe()` API works against any iterable. The adapters are 100-200 lines each — copy one of `loopgain/integrations/{langgraph,crewai,autogen}.py` as a starting point.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
208
326
|
## Status
|
|
209
327
|
|
|
210
328
|
**Initial public release.** Core library shipped (current version: see the PyPI badge at the top). Framework adapters (LangGraph, CrewAI, AutoGen) and the cloud-aggregator dashboard come in v0.2+. The math and the API surface are stable.
|
|
@@ -59,7 +59,14 @@ print(result.savings_vs_fixed_cap)
|
|
|
59
59
|
|
|
60
60
|
## How it works
|
|
61
61
|
|
|
62
|
-
LoopGain measures empirical loop gain
|
|
62
|
+
LoopGain measures empirical loop gain at every iteration, then smooths it with an EMA:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Aβ(n) = E(n) / E(n-1)
|
|
66
|
+
Aβ_smooth = EMA(Aβ, w=3)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
It classifies `Aβ_smooth` into five named bands:
|
|
63
70
|
|
|
64
71
|
| `Aβ_smooth` range | State | Action |
|
|
65
72
|
| --- | --- | --- |
|
|
@@ -177,6 +184,107 @@ The hosted endpoint at `telemetry.loopgain.ai` is one acceptable destination. Th
|
|
|
177
184
|
|
|
178
185
|
---
|
|
179
186
|
|
|
187
|
+
## Framework adapters
|
|
188
|
+
|
|
189
|
+
Thin wrappers under `loopgain.integrations` drive each major agent framework's iteration with a `LoopGain` monitor and auto-stamp `framework="<name>"` on telemetry. The frameworks themselves are **optional dependencies** — install the extra you need:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
pip install 'loopgain[langgraph]' # LangGraph
|
|
193
|
+
pip install 'loopgain[crewai]' # CrewAI
|
|
194
|
+
pip install 'loopgain[autogen]' # AutoGen v0.4+
|
|
195
|
+
pip install 'loopgain[all]' # all three
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
All adapters take a `LoopGain` instance plus an `error_fn` you provide — the framework doesn't know what your error signal is, so the adapter doesn't either. `error_fn` returns a non-negative number (or `None` to skip an iteration).
|
|
199
|
+
|
|
200
|
+
### LangGraph
|
|
201
|
+
|
|
202
|
+
Drives `graph.stream(input, stream_mode="updates")`. Each update is one iteration.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from loopgain import LoopGain
|
|
206
|
+
from loopgain.integrations import LangGraphAdapter
|
|
207
|
+
|
|
208
|
+
graph = build_my_verify_revise_graph().compile()
|
|
209
|
+
lg = LoopGain(target_error=0.1, max_iterations=20)
|
|
210
|
+
|
|
211
|
+
adapter = LangGraphAdapter(
|
|
212
|
+
lg=lg,
|
|
213
|
+
error_fn=lambda update: len(update.get("verifier", {}).get("errors", [])),
|
|
214
|
+
)
|
|
215
|
+
final_state = adapter.run(graph, {"draft": initial})
|
|
216
|
+
|
|
217
|
+
lg.send_telemetry(
|
|
218
|
+
endpoint=os.environ["LOOPGAIN_TELEMETRY_ENDPOINT"],
|
|
219
|
+
token=os.environ["LOOPGAIN_TELEMETRY_TOKEN"],
|
|
220
|
+
workload_id="rag-rewrite",
|
|
221
|
+
framework=adapter.framework_name, # "langgraph", auto-stamped
|
|
222
|
+
)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`adapter.stream(...)` yields each item if you want the full trace; `adapter.arun(...)` / `adapter.astream(...)` are the async counterparts and accept an async `error_fn`.
|
|
226
|
+
|
|
227
|
+
### CrewAI
|
|
228
|
+
|
|
229
|
+
Installs `step_callback` and/or `task_callback` on a Crew. Pick whichever granularity matches your loop — `step_error_fn` for refinement *within* a Task, `task_error_fn` for refinement *across* Tasks.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from crewai import Crew
|
|
233
|
+
from loopgain import LoopGain
|
|
234
|
+
from loopgain.integrations import CrewAIAdapter
|
|
235
|
+
|
|
236
|
+
lg = LoopGain(target_error=0.1, max_iterations=20)
|
|
237
|
+
adapter = CrewAIAdapter(
|
|
238
|
+
lg=lg,
|
|
239
|
+
task_error_fn=lambda task_output: count_failed_checks(task_output.raw),
|
|
240
|
+
)
|
|
241
|
+
crew = Crew(agents=[...], tasks=[...])
|
|
242
|
+
adapter.install(crew)
|
|
243
|
+
result = crew.kickoff()
|
|
244
|
+
adapter.uninstall() # or use `with CrewAIAdapter(...) as a:` context
|
|
245
|
+
|
|
246
|
+
lg.send_telemetry(
|
|
247
|
+
endpoint=...,
|
|
248
|
+
token=...,
|
|
249
|
+
framework=adapter.framework_name, # "crewai"
|
|
250
|
+
)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The adapter chains with any callback you already had installed — your existing instrumentation isn't overwritten.
|
|
254
|
+
|
|
255
|
+
### AutoGen (v0.4+)
|
|
256
|
+
|
|
257
|
+
Wraps `team.run_stream(task=...)`. In a verify-revise rotation, filter to the verifier's messages with `observe_sources={"verifier"}` so only it drives `observe()`.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from autogen_agentchat.teams import RoundRobinGroupChat
|
|
261
|
+
from loopgain import LoopGain
|
|
262
|
+
from loopgain.integrations import AutoGenAdapter
|
|
263
|
+
|
|
264
|
+
team = RoundRobinGroupChat(participants=[generator, verifier])
|
|
265
|
+
lg = LoopGain(target_error=0.1, max_iterations=20)
|
|
266
|
+
adapter = AutoGenAdapter(
|
|
267
|
+
lg=lg,
|
|
268
|
+
error_fn=lambda msg: parse_verifier_score(msg.content),
|
|
269
|
+
observe_sources={"verifier"},
|
|
270
|
+
)
|
|
271
|
+
result = await adapter.run(team, task="...")
|
|
272
|
+
|
|
273
|
+
lg.send_telemetry(
|
|
274
|
+
endpoint=...,
|
|
275
|
+
token=...,
|
|
276
|
+
framework=adapter.framework_name, # "autogen"
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Pass a `cancellation_token` to `adapter.run(...)` and the adapter will cancel it when LoopGain reaches a terminal state (target met, oscillation, divergence). The legacy v0.2 `ConversableAgent.initiate_chat` API is **not** supported — use the v0.4 event-driven runtime.
|
|
281
|
+
|
|
282
|
+
### Custom integrations
|
|
283
|
+
|
|
284
|
+
For frameworks without an adapter, the raw `LoopGain.observe()` API works against any iterable. The adapters are 100-200 lines each — copy one of `loopgain/integrations/{langgraph,crewai,autogen}.py` as a starting point.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
180
288
|
## Status
|
|
181
289
|
|
|
182
290
|
**Initial public release.** Core library shipped (current version: see the PyPI badge at the top). Framework adapters (LangGraph, CrewAI, AutoGen) and the cloud-aggregator dashboard come in v0.2+. The math and the API surface are stable.
|
|
@@ -9,6 +9,7 @@ Public API:
|
|
|
9
9
|
result = lg.result
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
from loopgain._version import __version__
|
|
12
13
|
from loopgain.core import (
|
|
13
14
|
LoopGain,
|
|
14
15
|
LoopGainResult,
|
|
@@ -24,8 +25,6 @@ from loopgain.core import (
|
|
|
24
25
|
)
|
|
25
26
|
from loopgain.telemetry import build_payload as build_telemetry_payload
|
|
26
27
|
|
|
27
|
-
__version__ = "0.1.4"
|
|
28
|
-
|
|
29
28
|
__all__ = [
|
|
30
29
|
"LoopGain",
|
|
31
30
|
"LoopGainResult",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Single source of truth for the package version.
|
|
2
|
+
|
|
3
|
+
Both ``loopgain/__init__.py`` and ``loopgain/telemetry.py`` import
|
|
4
|
+
``__version__`` from here so the value never drifts between
|
|
5
|
+
``__version__`` and the ``library_version`` field on telemetry payloads.
|
|
6
|
+
Update this file (and ``pyproject.toml``) for each release.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.7"
|
|
@@ -398,24 +398,41 @@ class LoopGain:
|
|
|
398
398
|
token: str,
|
|
399
399
|
workload_id: Optional[str] = None,
|
|
400
400
|
timeout: float = 2.0,
|
|
401
|
+
allow_insecure: bool = False,
|
|
402
|
+
framework: Optional[str] = None,
|
|
403
|
+
loop_type: Optional[str] = None,
|
|
404
|
+
team: Optional[str] = None,
|
|
405
|
+
include_per_iteration: bool = True,
|
|
401
406
|
) -> bool:
|
|
402
407
|
"""Send anonymized telemetry to a receiver endpoint.
|
|
403
408
|
|
|
404
409
|
Opt-in. Call once after the loop terminates. Sends only structural
|
|
405
|
-
statistics
|
|
406
|
-
flag, library version, optional opaque
|
|
407
|
-
prompts, completions, error contents, or customer
|
|
408
|
-
the bearer token.
|
|
410
|
+
statistics — Aβ values, error magnitudes, state transitions, gain
|
|
411
|
+
margin, rollback flag, library version, and optional opaque labels.
|
|
412
|
+
Never sends prompts, completions, error contents, or customer
|
|
413
|
+
identity beyond the bearer token.
|
|
409
414
|
|
|
410
415
|
Best-effort: errors are swallowed; never raises. Safe to call from
|
|
411
416
|
within an exception handler or finally block.
|
|
412
417
|
|
|
413
418
|
Args:
|
|
414
|
-
endpoint: Telemetry receiver URL.
|
|
419
|
+
endpoint: Telemetry receiver URL. Must use ``https://``;
|
|
420
|
+
``http://`` is rejected unless ``allow_insecure`` is ``True``.
|
|
415
421
|
token: Bearer token issued by the receiver (rotatable).
|
|
416
422
|
workload_id: Optional opaque label that groups related loops in
|
|
417
423
|
the dashboard. Never used to identify the customer.
|
|
418
424
|
timeout: Per-request timeout in seconds. Default 2.0.
|
|
425
|
+
allow_insecure: If ``True``, permit ``http://`` endpoints (for
|
|
426
|
+
local development). Default ``False``.
|
|
427
|
+
framework: Optional classification — agent framework name
|
|
428
|
+
(``"langgraph"``, ``"crewai"``, etc.). Adapters auto-stamp.
|
|
429
|
+
loop_type: Optional classification — loop pattern name
|
|
430
|
+
(``"verify_revise"``, ``"rag_refine"``, etc.).
|
|
431
|
+
team: Optional classification — team or environment label.
|
|
432
|
+
include_per_iteration: If ``True`` (default), include the
|
|
433
|
+
per-iteration Aβ + error trajectories (capped) so the
|
|
434
|
+
dashboard's Loop Detail scrubber works. Set ``False`` to
|
|
435
|
+
send only aggregate summary stats.
|
|
419
436
|
|
|
420
437
|
Returns:
|
|
421
438
|
``True`` on 2xx response, ``False`` otherwise.
|
|
@@ -429,9 +446,20 @@ class LoopGain:
|
|
|
429
446
|
... endpoint="https://telemetry.loopgain.ai/v1/aggregate",
|
|
430
447
|
... token="your-token-here",
|
|
431
448
|
... workload_id="my-rag-pipeline",
|
|
449
|
+
... framework="langgraph",
|
|
450
|
+
... loop_type="verify_revise",
|
|
432
451
|
... )
|
|
433
452
|
"""
|
|
434
453
|
from loopgain.telemetry import build_payload, send_payload
|
|
435
454
|
|
|
436
|
-
payload = build_payload(
|
|
437
|
-
|
|
455
|
+
payload = build_payload(
|
|
456
|
+
self,
|
|
457
|
+
workload_id=workload_id,
|
|
458
|
+
framework=framework,
|
|
459
|
+
loop_type=loop_type,
|
|
460
|
+
team=team,
|
|
461
|
+
include_per_iteration=include_per_iteration,
|
|
462
|
+
)
|
|
463
|
+
return send_payload(
|
|
464
|
+
endpoint, token, payload, timeout=timeout, allow_insecure=allow_insecure
|
|
465
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Framework integration adapters for LoopGain.
|
|
2
|
+
|
|
3
|
+
Each adapter is a thin wrapper that drives the host framework's iteration,
|
|
4
|
+
calls ``LoopGain.observe()`` on each step with an error magnitude derived
|
|
5
|
+
from a user-provided ``error_fn``, and (optionally) sends telemetry on
|
|
6
|
+
completion with ``framework="<name>"`` auto-stamped.
|
|
7
|
+
|
|
8
|
+
Adapters are isolated submodules so the host frameworks (langgraph, crewai,
|
|
9
|
+
autogen) remain *optional* dependencies. Importing this package does not
|
|
10
|
+
import any framework — each adapter only imports its framework when its
|
|
11
|
+
class is instantiated, and surfaces a clear ``ImportError`` if missing.
|
|
12
|
+
|
|
13
|
+
Install adapter extras::
|
|
14
|
+
|
|
15
|
+
pip install 'loopgain[langgraph]' # LangGraph
|
|
16
|
+
pip install 'loopgain[crewai]' # CrewAI
|
|
17
|
+
pip install 'loopgain[autogen]' # AutoGen v0.4+
|
|
18
|
+
pip install 'loopgain[all]' # all of the above
|
|
19
|
+
|
|
20
|
+
Common pattern::
|
|
21
|
+
|
|
22
|
+
from loopgain import LoopGain
|
|
23
|
+
from loopgain.integrations import LangGraphAdapter # or CrewAIAdapter, AutoGenAdapter
|
|
24
|
+
|
|
25
|
+
lg = LoopGain(target_error=0.1, max_iterations=20)
|
|
26
|
+
adapter = LangGraphAdapter(
|
|
27
|
+
lg=lg,
|
|
28
|
+
error_fn=lambda update: len(update.get("verifier_errors") or []),
|
|
29
|
+
)
|
|
30
|
+
final_state = adapter.run(graph, input_state)
|
|
31
|
+
|
|
32
|
+
# Optional: ship telemetry with framework auto-stamped.
|
|
33
|
+
lg.send_telemetry(
|
|
34
|
+
endpoint="https://telemetry.loopgain.ai/v1/aggregate",
|
|
35
|
+
token=os.environ["LOOPGAIN_TELEMETRY_TOKEN"],
|
|
36
|
+
workload_id="rag-rewrite",
|
|
37
|
+
framework=adapter.framework_name, # "langgraph"
|
|
38
|
+
)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
# Adapters are imported lazily so importing this package does NOT pull in
|
|
44
|
+
# langgraph / crewai / autogen. Each name resolves on first attribute access
|
|
45
|
+
# and surfaces a clear ImportError if its host framework isn't installed.
|
|
46
|
+
__all__ = [
|
|
47
|
+
"LangGraphAdapter",
|
|
48
|
+
"CrewAIAdapter",
|
|
49
|
+
"AutoGenAdapter",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def __getattr__(name: str):
|
|
54
|
+
if name == "LangGraphAdapter":
|
|
55
|
+
from loopgain.integrations.langgraph import LangGraphAdapter
|
|
56
|
+
|
|
57
|
+
return LangGraphAdapter
|
|
58
|
+
if name == "CrewAIAdapter":
|
|
59
|
+
from loopgain.integrations.crewai import CrewAIAdapter
|
|
60
|
+
|
|
61
|
+
return CrewAIAdapter
|
|
62
|
+
if name == "AutoGenAdapter":
|
|
63
|
+
from loopgain.integrations.autogen import AutoGenAdapter
|
|
64
|
+
|
|
65
|
+
return AutoGenAdapter
|
|
66
|
+
raise AttributeError(f"module 'loopgain.integrations' has no attribute {name!r}")
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""AutoGen v0.4+ adapter for LoopGain.
|
|
2
|
+
|
|
3
|
+
AutoGen v0.4 reorganized around an event-driven async runtime: a Team
|
|
4
|
+
(``RoundRobinGroupChat``, ``SocietyOfMindAgent``, ``Swarm``, etc.) exposes
|
|
5
|
+
``run_stream(task=...)`` which yields ``BaseAgentEvent | BaseChatMessage``
|
|
6
|
+
items per message, terminating with a ``TaskResult``.
|
|
7
|
+
|
|
8
|
+
In a verify-revise pattern the Team is typically a 2-agent rotation
|
|
9
|
+
(generator → verifier → generator → ...). The verifier's most recent
|
|
10
|
+
message carries the error signal; the user's ``error_fn`` extracts it
|
|
11
|
+
and the adapter feeds it to LoopGain.
|
|
12
|
+
|
|
13
|
+
Reference: https://microsoft.github.io/autogen/stable/_modules/autogen_agentchat/teams/_group_chat/_base_group_chat.html
|
|
14
|
+
|
|
15
|
+
The adapter does NOT support the legacy v0.2 ``ConversableAgent.initiate_chat``
|
|
16
|
+
API. v0.2 is in maintenance mode upstream; users on the old runtime
|
|
17
|
+
should upgrade or fall back to the raw ``LoopGain.observe()`` loop.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Awaitable, Callable, List, Optional
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from loopgain.core import LoopGain
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
AsyncMessageErrorFn = Callable[[Any], Awaitable[Optional[float]]]
|
|
30
|
+
MessageErrorFn = Callable[[Any], Optional[float]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AutoGenAdapter:
|
|
34
|
+
"""Drive an AutoGen v0.4+ Team with a LoopGain monitor.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
lg: A ``LoopGain`` instance to drive.
|
|
38
|
+
error_fn: Maps one streamed message/event to an error magnitude.
|
|
39
|
+
Return ``None`` to skip (e.g. for non-verifier messages).
|
|
40
|
+
Both sync and async callables are accepted; if async, await
|
|
41
|
+
in the function body — the adapter will detect and handle.
|
|
42
|
+
observe_sources: Optional set of agent ``source`` names to observe.
|
|
43
|
+
If provided, messages from other sources are passed through
|
|
44
|
+
without invoking ``error_fn``. Useful when only the verifier
|
|
45
|
+
agent's messages carry an error signal.
|
|
46
|
+
|
|
47
|
+
Example::
|
|
48
|
+
|
|
49
|
+
from autogen_agentchat.teams import RoundRobinGroupChat
|
|
50
|
+
from loopgain import LoopGain
|
|
51
|
+
from loopgain.integrations import AutoGenAdapter
|
|
52
|
+
|
|
53
|
+
team = RoundRobinGroupChat(participants=[generator, verifier])
|
|
54
|
+
lg = LoopGain(target_error=0.1, max_iterations=20)
|
|
55
|
+
adapter = AutoGenAdapter(
|
|
56
|
+
lg=lg,
|
|
57
|
+
error_fn=lambda msg: parse_verifier_score(msg.content),
|
|
58
|
+
observe_sources={"verifier"},
|
|
59
|
+
)
|
|
60
|
+
result = await adapter.run(team, task="Write a haiku about loops.")
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
framework_name = "autogen"
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
lg: "LoopGain",
|
|
68
|
+
error_fn: MessageErrorFn,
|
|
69
|
+
observe_sources: Optional[set[str]] = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
self.lg = lg
|
|
72
|
+
self.error_fn = error_fn
|
|
73
|
+
self.observe_sources = observe_sources
|
|
74
|
+
|
|
75
|
+
async def run(
|
|
76
|
+
self,
|
|
77
|
+
team: Any,
|
|
78
|
+
task: Any,
|
|
79
|
+
cancellation_token: Optional[Any] = None,
|
|
80
|
+
) -> List[Any]:
|
|
81
|
+
"""Drive ``team.run_stream(task=...)`` to completion, returning
|
|
82
|
+
the full list of yielded messages/events (including the terminal
|
|
83
|
+
``TaskResult``).
|
|
84
|
+
|
|
85
|
+
If LoopGain reaches a terminal state mid-stream, the team is
|
|
86
|
+
cancelled via the supplied ``cancellation_token`` (if one was
|
|
87
|
+
provided) — AutoGen has no way to interrupt a stream from outside
|
|
88
|
+
the cancellation-token mechanism.
|
|
89
|
+
"""
|
|
90
|
+
out: List[Any] = []
|
|
91
|
+
async for item in self.stream(team, task, cancellation_token=cancellation_token):
|
|
92
|
+
out.append(item)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
async def stream(
|
|
96
|
+
self,
|
|
97
|
+
team: Any,
|
|
98
|
+
task: Any,
|
|
99
|
+
cancellation_token: Optional[Any] = None,
|
|
100
|
+
) -> AsyncIterator[Any]:
|
|
101
|
+
"""Yield each message/event from ``team.run_stream`` while driving
|
|
102
|
+
LoopGain. Cancels the team's cancellation_token when LoopGain
|
|
103
|
+
signals a terminal state, then breaks out of the iteration.
|
|
104
|
+
|
|
105
|
+
AutoGen's runtime raises ``asyncio.CancelledError`` from its own
|
|
106
|
+
internal tasks once the token is cancelled. The adapter catches
|
|
107
|
+
that error iff *we* initiated the cancellation, so callers see a
|
|
108
|
+
clean termination instead of an exception they didn't ask for."""
|
|
109
|
+
kwargs: dict[str, Any] = {"task": task}
|
|
110
|
+
if cancellation_token is not None:
|
|
111
|
+
kwargs["cancellation_token"] = cancellation_token
|
|
112
|
+
|
|
113
|
+
we_cancelled = False
|
|
114
|
+
iterator = team.run_stream(**kwargs).__aiter__()
|
|
115
|
+
while True:
|
|
116
|
+
try:
|
|
117
|
+
message = await iterator.__anext__()
|
|
118
|
+
except StopAsyncIteration:
|
|
119
|
+
break
|
|
120
|
+
except asyncio.CancelledError:
|
|
121
|
+
if we_cancelled:
|
|
122
|
+
break
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
yield message
|
|
126
|
+
|
|
127
|
+
# Don't observe the terminal TaskResult — it's a wrapper, not
|
|
128
|
+
# a per-iteration event. Detect by duck-typing on the
|
|
129
|
+
# `messages` + `stop_reason` attributes (AutoGen's TaskResult
|
|
130
|
+
# shape) so we don't have to import the framework.
|
|
131
|
+
if hasattr(message, "messages") and hasattr(message, "stop_reason"):
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Source-filter: skip messages we're not configured to observe.
|
|
135
|
+
if self.observe_sources is not None:
|
|
136
|
+
source = getattr(message, "source", None)
|
|
137
|
+
if source not in self.observe_sources:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
magnitude = self.error_fn(message)
|
|
141
|
+
# Allow the user to write either a sync or async error_fn.
|
|
142
|
+
if hasattr(magnitude, "__await__"):
|
|
143
|
+
magnitude = await magnitude # type: ignore[assignment]
|
|
144
|
+
|
|
145
|
+
if magnitude is not None:
|
|
146
|
+
self.lg.observe(magnitude, output=message)
|
|
147
|
+
|
|
148
|
+
if not self.lg.should_continue() and cancellation_token is not None:
|
|
149
|
+
# Best-effort: AutoGen uses the cancellation token to abort.
|
|
150
|
+
cancel = getattr(cancellation_token, "cancel", None)
|
|
151
|
+
if callable(cancel):
|
|
152
|
+
cancel()
|
|
153
|
+
we_cancelled = True
|
|
154
|
+
# Stop pulling from the iterator. The next __anext__
|
|
155
|
+
# would raise CancelledError once the runtime tears down.
|
|
156
|
+
break
|