zespan-autogen 1.2.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.
- zespan_autogen-1.2.0/.gitignore +55 -0
- zespan_autogen-1.2.0/PKG-INFO +86 -0
- zespan_autogen-1.2.0/README.md +69 -0
- zespan_autogen-1.2.0/pyproject.toml +32 -0
- zespan_autogen-1.2.0/tests/test_tracer.py +76 -0
- zespan_autogen-1.2.0/zespan_autogen/__init__.py +17 -0
- zespan_autogen-1.2.0/zespan_autogen/ingest.py +85 -0
- zespan_autogen-1.2.0/zespan_autogen/legacy.py +78 -0
- zespan_autogen-1.2.0/zespan_autogen/model_client.py +109 -0
- zespan_autogen-1.2.0/zespan_autogen/tracer.py +126 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# Dependencies
|
|
4
|
+
node_modules
|
|
5
|
+
.pnp
|
|
6
|
+
.pnp.js
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
langfuse-main/
|
|
11
|
+
langwatch-main/
|
|
12
|
+
.claude
|
|
13
|
+
.gemini
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Testing
|
|
17
|
+
coverage
|
|
18
|
+
|
|
19
|
+
# Turbo
|
|
20
|
+
.turbo
|
|
21
|
+
|
|
22
|
+
# Vercel
|
|
23
|
+
.vercel
|
|
24
|
+
|
|
25
|
+
# Wrangler (Cloudflare dev artifacts — not used in VPS deploy)
|
|
26
|
+
.wrangler/
|
|
27
|
+
|
|
28
|
+
# Build Outputs
|
|
29
|
+
.next/
|
|
30
|
+
.open-next/
|
|
31
|
+
out/
|
|
32
|
+
build
|
|
33
|
+
dist
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Debug
|
|
37
|
+
npm-debug.log*
|
|
38
|
+
yarn-debug.log*
|
|
39
|
+
yarn-error.log*
|
|
40
|
+
|
|
41
|
+
# Misc
|
|
42
|
+
.DS_Store
|
|
43
|
+
*.pem
|
|
44
|
+
|
|
45
|
+
.hypothesis
|
|
46
|
+
.kiro
|
|
47
|
+
.vscode
|
|
48
|
+
|
|
49
|
+
lumiqtrace-landing
|
|
50
|
+
# Production secrets — never commit
|
|
51
|
+
ops/cloudflared/creds.json
|
|
52
|
+
|
|
53
|
+
# Added by code-review-graph
|
|
54
|
+
.code-review-graph/
|
|
55
|
+
.gitnexus
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zespan-autogen
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Zespan observability middleware for Microsoft AutoGen
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: requests>=2.28
|
|
8
|
+
Provides-Extra: agentchat
|
|
9
|
+
Requires-Dist: autogen-agentchat>=0.4; extra == 'agentchat'
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: autogen-agentchat>=0.4; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
14
|
+
Provides-Extra: legacy
|
|
15
|
+
Requires-Dist: pyautogen>=0.2; extra == 'legacy'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# zespan-autogen
|
|
19
|
+
|
|
20
|
+
Observability middleware for [Microsoft AutoGen](https://microsoft.github.io/autogen/) via Zespan.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# For AutoGen v0.4 (autogen-agentchat)
|
|
26
|
+
pip install 'zespan-autogen[agentchat]'
|
|
27
|
+
|
|
28
|
+
# For AutoGen v0.2 (pyautogen / ag2)
|
|
29
|
+
pip install 'zespan-autogen[legacy]'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## AutoGen v0.4 — `run_with_tracing()`
|
|
33
|
+
|
|
34
|
+
Replace `await team.run(task=...)` with `await run_with_tracing(team, task, ...)`:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from zespan_autogen import run_with_tracing
|
|
38
|
+
from autogen_agentchat.teams import RoundRobinGroupChat
|
|
39
|
+
from autogen_agentchat.conditions import MaxMessageTermination
|
|
40
|
+
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
|
41
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
42
|
+
|
|
43
|
+
model = OpenAIChatCompletionClient(model="gpt-4o")
|
|
44
|
+
agent = AssistantAgent("assistant", model_client=model)
|
|
45
|
+
team = RoundRobinGroupChat([agent], termination_condition=MaxMessageTermination(5))
|
|
46
|
+
|
|
47
|
+
trace_id = await run_with_tracing(
|
|
48
|
+
team=team,
|
|
49
|
+
task="Analyze the sales data",
|
|
50
|
+
api_key="zsp_your_key",
|
|
51
|
+
project_id="your_project_id",
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## AutoGen v0.4 — `ZespanModelClient` (per-LLM-call tracing)
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from zespan_autogen import ZespanModelClient
|
|
59
|
+
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
|
60
|
+
|
|
61
|
+
base = OpenAIChatCompletionClient(model="gpt-4o")
|
|
62
|
+
model = ZespanModelClient(base, api_key="zsp_your_key", project_id="proj_id")
|
|
63
|
+
agent = AssistantAgent("assistant", model_client=model)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## AutoGen v0.2 — `register_hooks()`
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from autogen import AssistantAgent, UserProxyAgent
|
|
70
|
+
from zespan_autogen import register_hooks
|
|
71
|
+
|
|
72
|
+
assistant = AssistantAgent("assistant", llm_config={"config_list": [...]})
|
|
73
|
+
user = UserProxyAgent("user", human_input_mode="NEVER")
|
|
74
|
+
|
|
75
|
+
register_hooks(assistant, user, api_key="zsp_your_key", project_id="proj_id")
|
|
76
|
+
user.initiate_chat(assistant, message="Research quantum computing trends")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## What's captured
|
|
80
|
+
|
|
81
|
+
- All agent messages (source, content, token counts)
|
|
82
|
+
- Tool call requests and execution results
|
|
83
|
+
- Agent handoffs and delegation
|
|
84
|
+
- Internal reasoning/thought events (thinking models)
|
|
85
|
+
- Memory query events
|
|
86
|
+
- Raw LLM request/response (via `ZespanModelClient`)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# zespan-autogen
|
|
2
|
+
|
|
3
|
+
Observability middleware for [Microsoft AutoGen](https://microsoft.github.io/autogen/) via Zespan.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# For AutoGen v0.4 (autogen-agentchat)
|
|
9
|
+
pip install 'zespan-autogen[agentchat]'
|
|
10
|
+
|
|
11
|
+
# For AutoGen v0.2 (pyautogen / ag2)
|
|
12
|
+
pip install 'zespan-autogen[legacy]'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## AutoGen v0.4 — `run_with_tracing()`
|
|
16
|
+
|
|
17
|
+
Replace `await team.run(task=...)` with `await run_with_tracing(team, task, ...)`:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from zespan_autogen import run_with_tracing
|
|
21
|
+
from autogen_agentchat.teams import RoundRobinGroupChat
|
|
22
|
+
from autogen_agentchat.conditions import MaxMessageTermination
|
|
23
|
+
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
|
24
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
25
|
+
|
|
26
|
+
model = OpenAIChatCompletionClient(model="gpt-4o")
|
|
27
|
+
agent = AssistantAgent("assistant", model_client=model)
|
|
28
|
+
team = RoundRobinGroupChat([agent], termination_condition=MaxMessageTermination(5))
|
|
29
|
+
|
|
30
|
+
trace_id = await run_with_tracing(
|
|
31
|
+
team=team,
|
|
32
|
+
task="Analyze the sales data",
|
|
33
|
+
api_key="zsp_your_key",
|
|
34
|
+
project_id="your_project_id",
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## AutoGen v0.4 — `ZespanModelClient` (per-LLM-call tracing)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from zespan_autogen import ZespanModelClient
|
|
42
|
+
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
|
43
|
+
|
|
44
|
+
base = OpenAIChatCompletionClient(model="gpt-4o")
|
|
45
|
+
model = ZespanModelClient(base, api_key="zsp_your_key", project_id="proj_id")
|
|
46
|
+
agent = AssistantAgent("assistant", model_client=model)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## AutoGen v0.2 — `register_hooks()`
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from autogen import AssistantAgent, UserProxyAgent
|
|
53
|
+
from zespan_autogen import register_hooks
|
|
54
|
+
|
|
55
|
+
assistant = AssistantAgent("assistant", llm_config={"config_list": [...]})
|
|
56
|
+
user = UserProxyAgent("user", human_input_mode="NEVER")
|
|
57
|
+
|
|
58
|
+
register_hooks(assistant, user, api_key="zsp_your_key", project_id="proj_id")
|
|
59
|
+
user.initiate_chat(assistant, message="Research quantum computing trends")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## What's captured
|
|
63
|
+
|
|
64
|
+
- All agent messages (source, content, token counts)
|
|
65
|
+
- Tool call requests and execution results
|
|
66
|
+
- Agent handoffs and delegation
|
|
67
|
+
- Internal reasoning/thought events (thinking models)
|
|
68
|
+
- Memory query events
|
|
69
|
+
- Raw LLM request/response (via `ZespanModelClient`)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zespan-autogen"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Zespan observability middleware for Microsoft AutoGen"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"requests>=2.28",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
agentchat = ["autogen-agentchat>=0.4"]
|
|
18
|
+
legacy = ["pyautogen>=0.2"]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.0",
|
|
21
|
+
"pytest-asyncio>=0.23",
|
|
22
|
+
"autogen-agentchat>=0.4",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.hatch.version]
|
|
26
|
+
path = "zespan_autogen/__init__.py"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["zespan_autogen"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import asyncio
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def make_stream_events():
|
|
7
|
+
"""Simulate AutoGen v0.4 run_stream() event sequence."""
|
|
8
|
+
from unittest.mock import MagicMock
|
|
9
|
+
|
|
10
|
+
class MockTextMessage:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.source = "assistant"
|
|
13
|
+
self.content = "Hello there"
|
|
14
|
+
self.models_usage = MagicMock(prompt_tokens=10, completion_tokens=5)
|
|
15
|
+
self.created_at = None
|
|
16
|
+
|
|
17
|
+
class MockToolCallReq:
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.source = "assistant"
|
|
20
|
+
self.content = [MagicMock(name="search", arguments='{"q":"test"}', id="tc1")]
|
|
21
|
+
|
|
22
|
+
class MockToolCallExec:
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.source = "assistant"
|
|
25
|
+
self.content = [MagicMock(call_id="tc1", content="result text")]
|
|
26
|
+
|
|
27
|
+
class MockTaskResult:
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.stop_reason = "max_turns"
|
|
30
|
+
self.messages = [MagicMock(), MagicMock()]
|
|
31
|
+
|
|
32
|
+
return [MockToolCallReq(), MockToolCallExec(), MockTextMessage(), MockTaskResult()]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def mock_run_stream(task, **kwargs):
|
|
36
|
+
for event in make_stream_events():
|
|
37
|
+
yield event
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_run_with_tracing_captures_events():
|
|
42
|
+
"""run_with_tracing captures all event types and calls send_event."""
|
|
43
|
+
with patch("zespan_autogen.ingest.send_event") as mock_send:
|
|
44
|
+
from zespan_autogen.tracer import run_with_tracing
|
|
45
|
+
|
|
46
|
+
mock_team = MagicMock()
|
|
47
|
+
mock_team.run_stream = mock_run_stream
|
|
48
|
+
|
|
49
|
+
await run_with_tracing(
|
|
50
|
+
team=mock_team,
|
|
51
|
+
task="test task",
|
|
52
|
+
api_key="zsp_test",
|
|
53
|
+
project_id="proj_1",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
calls = [c[0][0] for c in mock_send.call_args_list]
|
|
57
|
+
assert "tool.call.requested" in calls
|
|
58
|
+
assert "tool.call.executed" in calls
|
|
59
|
+
assert "agent.message" in calls
|
|
60
|
+
assert "conversation.completed" in calls
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.asyncio
|
|
64
|
+
async def test_run_with_tracing_returns_trace_id():
|
|
65
|
+
"""run_with_tracing returns a non-empty trace_id."""
|
|
66
|
+
with patch("zespan_autogen.ingest.send_event"):
|
|
67
|
+
from zespan_autogen.tracer import run_with_tracing
|
|
68
|
+
|
|
69
|
+
mock_team = MagicMock()
|
|
70
|
+
mock_team.run_stream = mock_run_stream
|
|
71
|
+
|
|
72
|
+
trace_id = await run_with_tracing(
|
|
73
|
+
team=mock_team, task="test", api_key="zsp_test", project_id="proj_1"
|
|
74
|
+
)
|
|
75
|
+
assert isinstance(trace_id, str)
|
|
76
|
+
assert len(trace_id) > 0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zespan AutoGen middleware.
|
|
3
|
+
|
|
4
|
+
For AutoGen v0.4 (autogen-agentchat):
|
|
5
|
+
from zespan_autogen import run_with_tracing, ZespanModelClient
|
|
6
|
+
|
|
7
|
+
For AutoGen v0.2 (pyautogen / ag2):
|
|
8
|
+
from zespan_autogen import register_hooks
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "1.2.0"
|
|
12
|
+
|
|
13
|
+
from .tracer import run_with_tracing
|
|
14
|
+
from .model_client import ZespanModelClient
|
|
15
|
+
from .legacy import register_hooks
|
|
16
|
+
|
|
17
|
+
__all__ = ["run_with_tracing", "ZespanModelClient", "register_hooks"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""HTTP ingest client for Zespan."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
import queue
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
_send_queue: queue.Queue[dict | None] = queue.Queue(maxsize=500)
|
|
9
|
+
_worker_started = False
|
|
10
|
+
_worker_lock = threading.Lock()
|
|
11
|
+
|
|
12
|
+
_api_key: str = ""
|
|
13
|
+
_project_id: str = ""
|
|
14
|
+
_endpoint: str = "https://api.zespan.com/v1/ingest"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def configure(api_key: str, project_id: str, endpoint: str | None = None) -> None:
|
|
18
|
+
global _api_key, _project_id, _endpoint
|
|
19
|
+
_api_key = api_key
|
|
20
|
+
_project_id = project_id
|
|
21
|
+
if endpoint:
|
|
22
|
+
_endpoint = endpoint
|
|
23
|
+
_ensure_worker()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ensure_worker() -> None:
|
|
27
|
+
global _worker_started
|
|
28
|
+
with _worker_lock:
|
|
29
|
+
if not _worker_started:
|
|
30
|
+
t = threading.Thread(target=_drain_worker, daemon=True)
|
|
31
|
+
t.start()
|
|
32
|
+
_worker_started = True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _drain_worker() -> None:
|
|
36
|
+
import requests
|
|
37
|
+
|
|
38
|
+
batch: list[dict] = []
|
|
39
|
+
while True:
|
|
40
|
+
try:
|
|
41
|
+
item = _send_queue.get(timeout=2.0)
|
|
42
|
+
if item is None:
|
|
43
|
+
break
|
|
44
|
+
batch.append(item)
|
|
45
|
+
while not _send_queue.empty() and len(batch) < 50:
|
|
46
|
+
try:
|
|
47
|
+
batch.append(_send_queue.get_nowait())
|
|
48
|
+
except queue.Empty:
|
|
49
|
+
break
|
|
50
|
+
if batch:
|
|
51
|
+
try:
|
|
52
|
+
requests.post(
|
|
53
|
+
_endpoint,
|
|
54
|
+
headers={"x-api-key": _api_key},
|
|
55
|
+
json={"events": batch},
|
|
56
|
+
timeout=5,
|
|
57
|
+
)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
batch = []
|
|
61
|
+
except queue.Empty:
|
|
62
|
+
if batch:
|
|
63
|
+
try:
|
|
64
|
+
import requests as req
|
|
65
|
+
req.post(
|
|
66
|
+
_endpoint,
|
|
67
|
+
headers={"x-api-key": _api_key},
|
|
68
|
+
json={"events": batch},
|
|
69
|
+
timeout=5,
|
|
70
|
+
)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
batch = []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def send_event(event_type: str, data: dict[str, Any]) -> None:
|
|
77
|
+
try:
|
|
78
|
+
_send_queue.put_nowait({
|
|
79
|
+
"type": event_type,
|
|
80
|
+
"project_id": _project_id,
|
|
81
|
+
"timestamp": time.time(),
|
|
82
|
+
**data,
|
|
83
|
+
})
|
|
84
|
+
except queue.Full:
|
|
85
|
+
pass
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoGen v0.2 (pyautogen / ag2) hook registration.
|
|
3
|
+
|
|
4
|
+
Adds Zespan message capture hooks to ConversableAgent instances
|
|
5
|
+
without subclassing or modifying agent definitions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from . import ingest as _ingest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_hooks(
|
|
17
|
+
*agents: Any,
|
|
18
|
+
api_key: str,
|
|
19
|
+
project_id: str,
|
|
20
|
+
endpoint: str | None = None,
|
|
21
|
+
) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Register Zespan observability hooks on one or more AutoGen v0.2 agents.
|
|
24
|
+
|
|
25
|
+
Returns a trace_id shared across all registered agents.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
from autogen import AssistantAgent, UserProxyAgent
|
|
29
|
+
from zespan_autogen import register_hooks
|
|
30
|
+
|
|
31
|
+
assistant = AssistantAgent("assistant", llm_config=...)
|
|
32
|
+
user = UserProxyAgent("user", human_input_mode="NEVER")
|
|
33
|
+
|
|
34
|
+
trace_id = register_hooks(
|
|
35
|
+
assistant, user,
|
|
36
|
+
api_key="zsp_your_key",
|
|
37
|
+
project_id="proj_id",
|
|
38
|
+
)
|
|
39
|
+
user.initiate_chat(assistant, message="...")
|
|
40
|
+
"""
|
|
41
|
+
_ingest.configure(api_key, project_id, endpoint)
|
|
42
|
+
trace_id = str(uuid.uuid4())
|
|
43
|
+
|
|
44
|
+
for agent in agents:
|
|
45
|
+
_register_agent_hooks(agent, trace_id)
|
|
46
|
+
|
|
47
|
+
return trace_id
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _register_agent_hooks(agent: Any, trace_id: str) -> None:
|
|
51
|
+
agent_name = getattr(agent, "name", "unknown")
|
|
52
|
+
|
|
53
|
+
def capture_messages(messages: list[dict]) -> list[dict]:
|
|
54
|
+
for msg in messages:
|
|
55
|
+
_ingest.send_event("agent.message", {
|
|
56
|
+
"trace_id": trace_id,
|
|
57
|
+
"agent_name": agent_name,
|
|
58
|
+
"role": msg.get("role"),
|
|
59
|
+
"content": str(msg.get("content", ""))[:2000],
|
|
60
|
+
})
|
|
61
|
+
return messages
|
|
62
|
+
|
|
63
|
+
def capture_outgoing(sender: Any, message: Any, recipient: Any, silent: bool) -> Any:
|
|
64
|
+
_ingest.send_event("agent.send", {
|
|
65
|
+
"trace_id": trace_id,
|
|
66
|
+
"from": getattr(sender, "name", "unknown"),
|
|
67
|
+
"to": getattr(recipient, "name", "unknown"),
|
|
68
|
+
"content": str(message)[:2000] if isinstance(message, str)
|
|
69
|
+
else str(message.get("content", ""))[:2000] if isinstance(message, dict)
|
|
70
|
+
else str(message)[:2000],
|
|
71
|
+
})
|
|
72
|
+
return message
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
agent.register_hook("process_all_messages_before_reply", capture_messages)
|
|
76
|
+
agent.register_hook("process_message_before_send", capture_outgoing)
|
|
77
|
+
except AttributeError:
|
|
78
|
+
pass # agent doesn't support hooks (e.g., GroupChatManager subclass)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ZespanModelClient — wraps OpenAIChatCompletionClient to capture
|
|
3
|
+
raw LLM requests/responses with full token counts and latency.
|
|
4
|
+
|
|
5
|
+
Use this alongside run_with_tracing() for per-LLM-call observability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Any, AsyncGenerator
|
|
13
|
+
|
|
14
|
+
from . import ingest as _ingest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ZespanModelClient:
|
|
18
|
+
"""
|
|
19
|
+
Transparent proxy around any AutoGen v0.4 ChatCompletionClient.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
|
23
|
+
from zespan_autogen import ZespanModelClient
|
|
24
|
+
|
|
25
|
+
base = OpenAIChatCompletionClient(model="gpt-4o")
|
|
26
|
+
model = ZespanModelClient(
|
|
27
|
+
client=base,
|
|
28
|
+
api_key="zsp_your_key",
|
|
29
|
+
project_id="proj_id",
|
|
30
|
+
)
|
|
31
|
+
agent = AssistantAgent("assistant", model_client=model)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
client: Any,
|
|
37
|
+
api_key: str,
|
|
38
|
+
project_id: str,
|
|
39
|
+
endpoint: str | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self._client = client
|
|
42
|
+
_ingest.configure(api_key, project_id, endpoint)
|
|
43
|
+
|
|
44
|
+
async def create(self, messages: Any, *, tools: Any = [], cancellation_token: Any = None, **kwargs: Any) -> Any:
|
|
45
|
+
span_id = str(uuid.uuid4())
|
|
46
|
+
started_at = time.time()
|
|
47
|
+
|
|
48
|
+
_ingest.send_event("llm.call.started", {
|
|
49
|
+
"span_id": span_id,
|
|
50
|
+
"message_count": len(messages) if hasattr(messages, "__len__") else None,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
result = await self._client.create(
|
|
55
|
+
messages, tools=tools, cancellation_token=cancellation_token, **kwargs
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
usage = getattr(result, "usage", None)
|
|
59
|
+
_ingest.send_event("llm.call.completed", {
|
|
60
|
+
"span_id": span_id,
|
|
61
|
+
"finish_reason": getattr(result, "finish_reason", None),
|
|
62
|
+
"prompt_tokens": getattr(usage, "prompt_tokens", None) if usage else None,
|
|
63
|
+
"completion_tokens": getattr(usage, "completion_tokens", None) if usage else None,
|
|
64
|
+
"cached": getattr(result, "cached", None),
|
|
65
|
+
"duration_ms": (time.time() - started_at) * 1000,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
except Exception as err:
|
|
71
|
+
_ingest.send_event("llm.call.failed", {
|
|
72
|
+
"span_id": span_id,
|
|
73
|
+
"error": str(err)[:500],
|
|
74
|
+
"duration_ms": (time.time() - started_at) * 1000,
|
|
75
|
+
})
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
async def create_stream(self, messages: Any, *, tools: Any = [], cancellation_token: Any = None, **kwargs: Any) -> AsyncGenerator[Any, None]:
|
|
79
|
+
span_id = str(uuid.uuid4())
|
|
80
|
+
started_at = time.time()
|
|
81
|
+
|
|
82
|
+
_ingest.send_event("llm.stream.started", {"span_id": span_id})
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
async for chunk in self._client.create_stream(
|
|
86
|
+
messages, tools=tools, cancellation_token=cancellation_token, **kwargs
|
|
87
|
+
):
|
|
88
|
+
if isinstance(chunk, str):
|
|
89
|
+
_ingest.send_event("llm.stream.chunk", {"span_id": span_id})
|
|
90
|
+
else:
|
|
91
|
+
# Final CreateResult chunk
|
|
92
|
+
usage = getattr(chunk, "usage", None)
|
|
93
|
+
_ingest.send_event("llm.stream.completed", {
|
|
94
|
+
"span_id": span_id,
|
|
95
|
+
"prompt_tokens": getattr(usage, "prompt_tokens", None) if usage else None,
|
|
96
|
+
"completion_tokens": getattr(usage, "completion_tokens", None) if usage else None,
|
|
97
|
+
"duration_ms": (time.time() - started_at) * 1000,
|
|
98
|
+
})
|
|
99
|
+
yield chunk
|
|
100
|
+
except Exception as err:
|
|
101
|
+
_ingest.send_event("llm.stream.failed", {
|
|
102
|
+
"span_id": span_id,
|
|
103
|
+
"error": str(err)[:500],
|
|
104
|
+
})
|
|
105
|
+
raise
|
|
106
|
+
|
|
107
|
+
# Delegate all other attributes/methods to the wrapped client
|
|
108
|
+
def __getattr__(self, name: str) -> Any:
|
|
109
|
+
return getattr(self._client, name)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoGen v0.4 (autogen-agentchat) observability via run_stream() iteration.
|
|
3
|
+
|
|
4
|
+
All typed event objects emitted by team.run_stream() are intercepted
|
|
5
|
+
and forwarded to Zespan without modifying agent or team definitions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from . import ingest as _ingest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def run_with_tracing(
|
|
17
|
+
team: Any,
|
|
18
|
+
task: str,
|
|
19
|
+
api_key: str,
|
|
20
|
+
project_id: str,
|
|
21
|
+
endpoint: str | None = None,
|
|
22
|
+
) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Run an AutoGen team with full observability.
|
|
25
|
+
|
|
26
|
+
Wraps team.run_stream(), captures all event types, and returns the trace_id.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
team: Any AutoGen team (RoundRobinGroupChat, SelectorGroupChat, etc.)
|
|
30
|
+
task: The task string to pass to run_stream()
|
|
31
|
+
api_key: Zespan API key
|
|
32
|
+
project_id: Zespan project ID
|
|
33
|
+
endpoint: Optional custom ingest endpoint
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
trace_id string for this conversation
|
|
37
|
+
"""
|
|
38
|
+
_ingest.configure(api_key, project_id, endpoint)
|
|
39
|
+
|
|
40
|
+
# Import message types lazily — don't require autogen-agentchat at import time
|
|
41
|
+
try:
|
|
42
|
+
from autogen_agentchat.messages import (
|
|
43
|
+
TextMessage,
|
|
44
|
+
ToolCallRequestEvent,
|
|
45
|
+
ToolCallExecutionEvent,
|
|
46
|
+
ModelClientStreamingChunkEvent,
|
|
47
|
+
ThoughtEvent,
|
|
48
|
+
MemoryQueryEvent,
|
|
49
|
+
HandoffMessage,
|
|
50
|
+
)
|
|
51
|
+
from autogen_agentchat.base import TaskResult
|
|
52
|
+
from autogen_core import CancellationToken
|
|
53
|
+
except ImportError as e:
|
|
54
|
+
raise ImportError(
|
|
55
|
+
"autogen-agentchat is required: pip install 'zespan-autogen[agentchat]'"
|
|
56
|
+
) from e
|
|
57
|
+
|
|
58
|
+
trace_id = str(uuid.uuid4())
|
|
59
|
+
|
|
60
|
+
_ingest.send_event("conversation.started", {
|
|
61
|
+
"trace_id": trace_id,
|
|
62
|
+
"task": task,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
async for event in team.run_stream(task=task, cancellation_token=CancellationToken()):
|
|
66
|
+
|
|
67
|
+
if isinstance(event, TaskResult):
|
|
68
|
+
_ingest.send_event("conversation.completed", {
|
|
69
|
+
"trace_id": trace_id,
|
|
70
|
+
"stop_reason": getattr(event, "stop_reason", None),
|
|
71
|
+
"message_count": len(getattr(event, "messages", [])),
|
|
72
|
+
})
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
elif isinstance(event, TextMessage):
|
|
76
|
+
usage = getattr(event, "models_usage", None)
|
|
77
|
+
_ingest.send_event("agent.message", {
|
|
78
|
+
"trace_id": trace_id,
|
|
79
|
+
"source": event.source,
|
|
80
|
+
"content": str(event.content)[:2000],
|
|
81
|
+
"prompt_tokens": getattr(usage, "prompt_tokens", None) if usage else None,
|
|
82
|
+
"completion_tokens": getattr(usage, "completion_tokens", None) if usage else None,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
elif isinstance(event, ToolCallRequestEvent):
|
|
86
|
+
for fc in getattr(event, "content", []):
|
|
87
|
+
_ingest.send_event("tool.call.requested", {
|
|
88
|
+
"trace_id": trace_id,
|
|
89
|
+
"source": event.source,
|
|
90
|
+
"tool_name": getattr(fc, "name", None),
|
|
91
|
+
"tool_args": getattr(fc, "arguments", None),
|
|
92
|
+
"call_id": getattr(fc, "id", None),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
elif isinstance(event, ToolCallExecutionEvent):
|
|
96
|
+
for result in getattr(event, "content", []):
|
|
97
|
+
_ingest.send_event("tool.call.executed", {
|
|
98
|
+
"trace_id": trace_id,
|
|
99
|
+
"source": event.source,
|
|
100
|
+
"call_id": getattr(result, "call_id", None),
|
|
101
|
+
"output": str(getattr(result, "content", ""))[:2000],
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
elif isinstance(event, ThoughtEvent):
|
|
105
|
+
_ingest.send_event("agent.thought", {
|
|
106
|
+
"trace_id": trace_id,
|
|
107
|
+
"source": event.source,
|
|
108
|
+
"thought": str(event.content)[:2000],
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
elif isinstance(event, MemoryQueryEvent):
|
|
112
|
+
_ingest.send_event("agent.memory_query", {
|
|
113
|
+
"trace_id": trace_id,
|
|
114
|
+
"source": event.source,
|
|
115
|
+
"results_count": len(getattr(event, "content", [])),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
elif isinstance(event, HandoffMessage):
|
|
119
|
+
_ingest.send_event("agent.handoff", {
|
|
120
|
+
"trace_id": trace_id,
|
|
121
|
+
"source": event.source,
|
|
122
|
+
"target": getattr(event, "target", None),
|
|
123
|
+
"content": str(event.content)[:500],
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
return trace_id
|