computer-agent-py 0.1.1__tar.gz → 0.1.3__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.
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/PKG-INFO +1 -1
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/pyproject.toml +1 -1
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/_proxy/client.py +9 -1
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/_proxy/query.py +33 -7
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/event.py +8 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/agentos.py +20 -3
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/otel.py +157 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/README.md +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/__init__.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/_proxy/__init__.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/policy/__init__.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/policy/authorizer.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/policy/cedar.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/policy/opa.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/policy/types.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/py.typed +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/__init__.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/config.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/middleware/__init__.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/middleware/guardrails.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/middleware/pii.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/pipeline.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/__init__.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/message_archive.py +0 -0
- {computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: computer-agent-py
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Drop-in replacement for claude-agent-sdk that adds a proxied telemetry pipeline (PII redaction + guardrails) with OpenTelemetry and AgentOS sinks.
|
|
5
5
|
Keywords: computeragent,claude-agent-sdk,claude,agent,telemetry,otel,opentelemetry,agentos,pii
|
|
6
6
|
Author: Abhi Bhat
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "computer-agent-py"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "Drop-in replacement for claude-agent-sdk that adds a proxied telemetry pipeline (PII redaction + guardrails) with OpenTelemetry and AgentOS sinks."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Abhi Bhat", email = "abhishek.bhat@lyzr.ai" },
|
|
@@ -20,7 +20,7 @@ import claude_agent_sdk
|
|
|
20
20
|
from claude_agent_sdk.types import AssistantMessage, ResultMessage, TextBlock
|
|
21
21
|
|
|
22
22
|
from ..telemetry import TelemetryEvent, get_pipeline, new_session_id
|
|
23
|
-
from .query import _derive_agent_name
|
|
23
|
+
from .query import _derive_agent_description, _derive_agent_name
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
26
|
from collections.abc import AsyncIterable, AsyncIterator
|
|
@@ -50,6 +50,7 @@ class ClaudeSDKClient:
|
|
|
50
50
|
# placeholder-vs-upstream rationale).
|
|
51
51
|
self._upstream_session_id: str | None = None
|
|
52
52
|
self._agent_name = _derive_agent_name(options)
|
|
53
|
+
self._agent_description = _derive_agent_description(options)
|
|
53
54
|
self._started_at: float | None = None
|
|
54
55
|
self._last_result: ResultMessage | None = None
|
|
55
56
|
self._last_assistant_text: str = ""
|
|
@@ -75,6 +76,7 @@ class ClaudeSDKClient:
|
|
|
75
76
|
TelemetryEvent.session_started(
|
|
76
77
|
session_id=self._session_id,
|
|
77
78
|
agent_name=self._agent_name,
|
|
79
|
+
agent_description=self._agent_description,
|
|
78
80
|
options=self._options,
|
|
79
81
|
prompt=prompt if prompt is not None else "<connect>",
|
|
80
82
|
)
|
|
@@ -122,6 +124,8 @@ class ClaudeSDKClient:
|
|
|
122
124
|
for ev in TelemetryEvent.from_message(
|
|
123
125
|
msg, session_id=self._session_id, agent_name=self._agent_name
|
|
124
126
|
):
|
|
127
|
+
if self._agent_description is not None:
|
|
128
|
+
ev.agent_description = self._agent_description
|
|
125
129
|
await pipeline.emit(ev)
|
|
126
130
|
if isinstance(msg, AssistantMessage):
|
|
127
131
|
for block in getattr(msg, "content", []) or []:
|
|
@@ -145,6 +149,8 @@ class ClaudeSDKClient:
|
|
|
145
149
|
for ev in TelemetryEvent.from_message(
|
|
146
150
|
msg, session_id=self._session_id, agent_name=self._agent_name
|
|
147
151
|
):
|
|
152
|
+
if self._agent_description is not None:
|
|
153
|
+
ev.agent_description = self._agent_description
|
|
148
154
|
await pipeline.emit(ev)
|
|
149
155
|
if isinstance(msg, AssistantMessage):
|
|
150
156
|
for block in getattr(msg, "content", []) or []:
|
|
@@ -203,6 +209,7 @@ class ClaudeSDKClient:
|
|
|
203
209
|
duration_ms=duration_ms,
|
|
204
210
|
last_assistant_text=self._last_assistant_text,
|
|
205
211
|
)
|
|
212
|
+
ev.agent_description = self._agent_description
|
|
206
213
|
if self._upstream_session_id is not None:
|
|
207
214
|
ev.payload["upstream_session_id"] = self._upstream_session_id
|
|
208
215
|
await pipeline.emit(ev)
|
|
@@ -212,6 +219,7 @@ class ClaudeSDKClient:
|
|
|
212
219
|
kind="session_ended",
|
|
213
220
|
session_id=self._session_id,
|
|
214
221
|
agent_name=self._agent_name,
|
|
222
|
+
agent_description=self._agent_description,
|
|
215
223
|
payload={
|
|
216
224
|
"is_error": False,
|
|
217
225
|
"subtype": "no_result",
|
|
@@ -39,6 +39,7 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
39
39
|
pipeline = get_pipeline()
|
|
40
40
|
session_id = new_session_id()
|
|
41
41
|
agent_name = _derive_agent_name(options)
|
|
42
|
+
agent_description = _derive_agent_description(options)
|
|
42
43
|
started_at = time.monotonic()
|
|
43
44
|
last_assistant_text = ""
|
|
44
45
|
|
|
@@ -47,6 +48,7 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
47
48
|
TelemetryEvent.session_started(
|
|
48
49
|
session_id=session_id,
|
|
49
50
|
agent_name=agent_name,
|
|
51
|
+
agent_description=agent_description,
|
|
50
52
|
options=options,
|
|
51
53
|
prompt=prompt,
|
|
52
54
|
)
|
|
@@ -78,6 +80,11 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
78
80
|
for ev in TelemetryEvent.from_message(
|
|
79
81
|
msg, session_id=session_id, agent_name=agent_name
|
|
80
82
|
):
|
|
83
|
+
# Carry the description forward on every event so sinks that
|
|
84
|
+
# group by description (or want to surface it on per-message
|
|
85
|
+
# telemetry) don't have to back-reference session_started.
|
|
86
|
+
if agent_description is not None:
|
|
87
|
+
ev.agent_description = agent_description
|
|
81
88
|
await pipeline.emit(ev)
|
|
82
89
|
|
|
83
90
|
if isinstance(msg, AssistantMessage):
|
|
@@ -100,6 +107,7 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
100
107
|
duration_ms=duration_ms,
|
|
101
108
|
last_assistant_text=last_assistant_text,
|
|
102
109
|
)
|
|
110
|
+
ev.agent_description = agent_description
|
|
103
111
|
if upstream_session_id is not None:
|
|
104
112
|
ev.payload["upstream_session_id"] = upstream_session_id
|
|
105
113
|
await pipeline.emit(ev)
|
|
@@ -110,6 +118,7 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
110
118
|
kind="session_ended",
|
|
111
119
|
session_id=session_id,
|
|
112
120
|
agent_name=agent_name,
|
|
121
|
+
agent_description=agent_description,
|
|
113
122
|
payload={
|
|
114
123
|
"is_error": False,
|
|
115
124
|
"subtype": "no_result",
|
|
@@ -124,14 +133,14 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
124
133
|
except BaseException as exc:
|
|
125
134
|
duration_ms = (time.monotonic() - started_at) * 1000.0
|
|
126
135
|
try:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
duration_ms=duration_ms,
|
|
133
|
-
)
|
|
136
|
+
err_ev = TelemetryEvent.session_ended_error(
|
|
137
|
+
session_id=session_id,
|
|
138
|
+
agent_name=agent_name,
|
|
139
|
+
exc=exc,
|
|
140
|
+
duration_ms=duration_ms,
|
|
134
141
|
)
|
|
142
|
+
err_ev.agent_description = agent_description
|
|
143
|
+
await pipeline.emit(err_ev)
|
|
135
144
|
except Exception: # noqa: BLE001
|
|
136
145
|
logger.debug("error-path telemetry emit failed", exc_info=True)
|
|
137
146
|
raise
|
|
@@ -163,3 +172,20 @@ def _derive_agent_name(options: Any) -> str | None:
|
|
|
163
172
|
if first:
|
|
164
173
|
return first[:80]
|
|
165
174
|
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _derive_agent_description(options: Any) -> str | None:
|
|
178
|
+
"""Pick an agent description from options.
|
|
179
|
+
|
|
180
|
+
Mirrors :func:`_derive_agent_name` — the description is purely opt-in.
|
|
181
|
+
Callers set it on options via ``options.agent_description = "..."`` and
|
|
182
|
+
the proxy threads it through to the telemetry pipeline. Unlike the
|
|
183
|
+
name, there's no implicit fallback to system_prompt: a description is
|
|
184
|
+
a human label, not something we want to synthesize from the prompt.
|
|
185
|
+
"""
|
|
186
|
+
if options is None:
|
|
187
|
+
return None
|
|
188
|
+
desc = getattr(options, "agent_description", None)
|
|
189
|
+
if isinstance(desc, str) and desc:
|
|
190
|
+
return desc
|
|
191
|
+
return None
|
|
@@ -46,6 +46,12 @@ class TelemetryEvent:
|
|
|
46
46
|
session_id: str
|
|
47
47
|
timestamp: datetime = field(default_factory=_utcnow)
|
|
48
48
|
agent_name: str | None = None
|
|
49
|
+
# Human-readable description of the agent. Mirrors `agent_name`'s
|
|
50
|
+
# opt-in shape: derived from ``options.agent_description`` (set via
|
|
51
|
+
# setattr by the caller) and propagated through the same event path.
|
|
52
|
+
# ``None`` when the caller doesn't supply one — sinks fall back to an
|
|
53
|
+
# empty string in the registry doc.
|
|
54
|
+
agent_description: str | None = None
|
|
49
55
|
payload: dict[str, Any] = field(default_factory=dict)
|
|
50
56
|
raw: Any = None
|
|
51
57
|
|
|
@@ -60,6 +66,7 @@ class TelemetryEvent:
|
|
|
60
66
|
*,
|
|
61
67
|
session_id: str,
|
|
62
68
|
agent_name: str | None,
|
|
69
|
+
agent_description: str | None = None,
|
|
63
70
|
options: Any,
|
|
64
71
|
prompt: Any,
|
|
65
72
|
) -> TelemetryEvent:
|
|
@@ -86,6 +93,7 @@ class TelemetryEvent:
|
|
|
86
93
|
kind="session_started",
|
|
87
94
|
session_id=session_id,
|
|
88
95
|
agent_name=agent_name,
|
|
96
|
+
agent_description=agent_description,
|
|
89
97
|
payload=payload,
|
|
90
98
|
raw=options,
|
|
91
99
|
)
|
{computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/agentos.py
RENAMED
|
@@ -151,6 +151,7 @@ class AgentRegistrySink:
|
|
|
151
151
|
|
|
152
152
|
async def _upsert_registry(self, event: TelemetryEvent) -> None:
|
|
153
153
|
name = event.agent_name or _agent_name_from_event(event)
|
|
154
|
+
description = event.agent_description or ""
|
|
154
155
|
now = _utcnow()
|
|
155
156
|
await self._registry.update_one(
|
|
156
157
|
{"_id": name},
|
|
@@ -161,8 +162,12 @@ class AgentRegistrySink:
|
|
|
161
162
|
},
|
|
162
163
|
"$set": {
|
|
163
164
|
"harness": "claude-agent-sdk",
|
|
164
|
-
|
|
165
|
+
# `source.manifest` carries the description too so any
|
|
166
|
+
# downstream consumer that reads the inline manifest
|
|
167
|
+
# gets the same value as the top-level `description`.
|
|
168
|
+
"source": _inline_source_for(name, event.payload, description),
|
|
165
169
|
"model": event.payload.get("model"),
|
|
170
|
+
"description": description,
|
|
166
171
|
"registeredBy": self._registered_by,
|
|
167
172
|
"updatedAt": now,
|
|
168
173
|
"lastSeen": now,
|
|
@@ -359,7 +364,11 @@ DEFAULT_AGENT_INSTRUCTIONS = (
|
|
|
359
364
|
)
|
|
360
365
|
|
|
361
366
|
|
|
362
|
-
def _inline_source_for(
|
|
367
|
+
def _inline_source_for(
|
|
368
|
+
name: str,
|
|
369
|
+
options_payload: dict[str, Any],
|
|
370
|
+
description: str = "",
|
|
371
|
+
) -> dict[str, Any]:
|
|
363
372
|
"""Build a complete inline ``IdentitySource`` the harness can clone into a
|
|
364
373
|
sandbox workdir for live chat.
|
|
365
374
|
|
|
@@ -386,11 +395,16 @@ def _inline_source_for(name: str, options_payload: dict[str, Any]) -> dict[str,
|
|
|
386
395
|
allowed_tools=allowed_tools,
|
|
387
396
|
max_turns=max_turns,
|
|
388
397
|
permission_mode=permission_mode,
|
|
398
|
+
description=description,
|
|
389
399
|
)
|
|
390
400
|
|
|
401
|
+
manifest: dict[str, Any] = {"name": name, "version": "0.1.0"}
|
|
402
|
+
if description:
|
|
403
|
+
manifest["description"] = description
|
|
404
|
+
|
|
391
405
|
return {
|
|
392
406
|
"type": "inline",
|
|
393
|
-
"manifest":
|
|
407
|
+
"manifest": manifest,
|
|
394
408
|
"model": model,
|
|
395
409
|
"files": {
|
|
396
410
|
"agent.yaml": agent_yaml,
|
|
@@ -406,6 +420,7 @@ def _build_agent_yaml(
|
|
|
406
420
|
allowed_tools: list[str],
|
|
407
421
|
max_turns: int | None,
|
|
408
422
|
permission_mode: str,
|
|
423
|
+
description: str = "",
|
|
409
424
|
) -> str:
|
|
410
425
|
"""Render the agent.yaml the harness's inline loader expects.
|
|
411
426
|
|
|
@@ -419,6 +434,8 @@ def _build_agent_yaml(
|
|
|
419
434
|
f"name: {safe_name}",
|
|
420
435
|
"version: 0.1.0",
|
|
421
436
|
]
|
|
437
|
+
if description:
|
|
438
|
+
lines.append(f"description: {_yaml_str(description)}")
|
|
422
439
|
if model:
|
|
423
440
|
lines += [
|
|
424
441
|
"model:",
|
{computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/otel.py
RENAMED
|
@@ -84,6 +84,7 @@ logger = logging.getLogger("computeragent.telemetry")
|
|
|
84
84
|
# constants for these keys move between alpha versions; literals are safer.
|
|
85
85
|
GEN_AI_SYSTEM = "gen_ai.system"
|
|
86
86
|
GEN_AI_AGENT_NAME = "gen_ai.agent.name"
|
|
87
|
+
GEN_AI_AGENT_DESCRIPTION = "gen_ai.agent.description"
|
|
87
88
|
GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id"
|
|
88
89
|
GEN_AI_REQUEST_MODEL = "gen_ai.request.model"
|
|
89
90
|
GEN_AI_OPERATION_NAME = "gen_ai.operation.name"
|
|
@@ -145,6 +146,32 @@ class OtelSink:
|
|
|
145
146
|
self._exporter = exporter or cfg.get("exporter") or _infer_exporter(self._endpoint)
|
|
146
147
|
self._temporality: Temporality = _resolve_temporality(temporality or cfg.get("temporality"))
|
|
147
148
|
|
|
149
|
+
# Content capture — opt-in. When on, the sink writes prompt /
|
|
150
|
+
# completion / tool input / tool output to spans per the GenAI
|
|
151
|
+
# semconv. Middleware (PiiRedactor, GuardrailFilter) has already
|
|
152
|
+
# mutated the payload by this point, so what lands on spans is the
|
|
153
|
+
# redacted projection — never raw content from the agent stream.
|
|
154
|
+
self._capture_content = bool(
|
|
155
|
+
cfg.get("capture_content")
|
|
156
|
+
if "capture_content" in cfg
|
|
157
|
+
else _env_bool("COMPUTERAGENT_CAPTURE_CONTENT", False)
|
|
158
|
+
)
|
|
159
|
+
mode = cfg.get("capture_content_mode") or os.environ.get(
|
|
160
|
+
"COMPUTERAGENT_CAPTURE_CONTENT_MODE", "events"
|
|
161
|
+
)
|
|
162
|
+
if mode not in ("attributes", "events", "both"):
|
|
163
|
+
mode = "events"
|
|
164
|
+
self._capture_mode: str = mode
|
|
165
|
+
# Cap attribute size so a runaway prompt doesn't blow OTLP packet
|
|
166
|
+
# limits. Honors the same env that GuardrailFilter consumes.
|
|
167
|
+
try:
|
|
168
|
+
self._max_attr_length = int(
|
|
169
|
+
cfg.get("max_attribute_length")
|
|
170
|
+
or os.environ.get("COMPUTERAGENT_MAX_ATTRIBUTE_LENGTH", "8192")
|
|
171
|
+
)
|
|
172
|
+
except (TypeError, ValueError):
|
|
173
|
+
self._max_attr_length = 8192
|
|
174
|
+
|
|
148
175
|
self._tracer_provider = tracer_provider or _build_tracer_provider(
|
|
149
176
|
service_name=self._service_name,
|
|
150
177
|
exporter=self._exporter,
|
|
@@ -210,6 +237,44 @@ class OtelSink:
|
|
|
210
237
|
|
|
211
238
|
# ── handlers (one per event kind) --------------------------------------
|
|
212
239
|
|
|
240
|
+
def _emit_content(
|
|
241
|
+
self,
|
|
242
|
+
span: Any,
|
|
243
|
+
role: str,
|
|
244
|
+
content: str,
|
|
245
|
+
attr_prefix: str,
|
|
246
|
+
event_name: str,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Write content to ``span`` per ``capture_content_mode``.
|
|
249
|
+
|
|
250
|
+
* ``attributes`` — ``<attr_prefix>.role`` + ``<attr_prefix>.content``
|
|
251
|
+
per the GenAI semconv 1.27 ``gen_ai.prompt.{i}.*`` / ``gen_ai.completion.{i}.*``
|
|
252
|
+
pattern. Easy to query in NRQL, lossy beyond ~32 KB.
|
|
253
|
+
* ``events`` — ``span.add_event(event_name, {role, content})``.
|
|
254
|
+
Preserves multi-message history without overwriting prior attrs.
|
|
255
|
+
* ``both`` — write both shapes. Useful while migrating backends.
|
|
256
|
+
|
|
257
|
+
Content is truncated to ``max_attribute_length`` chars to stay
|
|
258
|
+
under OTLP per-attribute limits. The middleware
|
|
259
|
+
(``PiiRedactor`` / ``GuardrailFilter``) has already mutated the
|
|
260
|
+
payload by this point, so the redacted projection is what lands.
|
|
261
|
+
"""
|
|
262
|
+
if not self._capture_content or not content:
|
|
263
|
+
return
|
|
264
|
+
text = (
|
|
265
|
+
content
|
|
266
|
+
if len(content) <= self._max_attr_length
|
|
267
|
+
else (content[: self._max_attr_length - 14] + "...[truncated]")
|
|
268
|
+
)
|
|
269
|
+
if self._capture_mode in ("attributes", "both"):
|
|
270
|
+
span.set_attribute(f"{attr_prefix}.role", role)
|
|
271
|
+
span.set_attribute(f"{attr_prefix}.content", text)
|
|
272
|
+
if self._capture_mode in ("events", "both"):
|
|
273
|
+
try:
|
|
274
|
+
span.add_event(event_name, attributes={"role": role, "content": text})
|
|
275
|
+
except Exception: # noqa: BLE001 — never break a session for an event-emit
|
|
276
|
+
logger.debug("add_event(%s) failed", event_name, exc_info=True)
|
|
277
|
+
|
|
213
278
|
def _parent_ctx(self, session_id: str) -> Any:
|
|
214
279
|
"""Build an OTel Context pinned to the active invoke_agent span so
|
|
215
280
|
child spans (chat / execute_tool / policy_decision) nest correctly.
|
|
@@ -230,10 +295,36 @@ class OtelSink:
|
|
|
230
295
|
span.set_attribute(GEN_AI_SYSTEM, "claude-agent-sdk")
|
|
231
296
|
if event.agent_name:
|
|
232
297
|
span.set_attribute(GEN_AI_AGENT_NAME, event.agent_name)
|
|
298
|
+
if event.agent_description:
|
|
299
|
+
span.set_attribute(GEN_AI_AGENT_DESCRIPTION, event.agent_description)
|
|
233
300
|
span.set_attribute(GEN_AI_CONVERSATION_ID, event.session_id)
|
|
234
301
|
span.set_attribute(GEN_AI_OPERATION_NAME, "invoke_agent")
|
|
235
302
|
if event.payload.get("model"):
|
|
236
303
|
span.set_attribute(GEN_AI_REQUEST_MODEL, str(event.payload["model"]))
|
|
304
|
+
# Content capture — system prompt + initial user prompt, both on the
|
|
305
|
+
# invoke_agent root so a single span query surfaces what the agent
|
|
306
|
+
# was asked to do.
|
|
307
|
+
system_prompt = event.payload.get("system_prompt")
|
|
308
|
+
if isinstance(system_prompt, str) and system_prompt:
|
|
309
|
+
self._emit_content(
|
|
310
|
+
span,
|
|
311
|
+
"system",
|
|
312
|
+
system_prompt,
|
|
313
|
+
"gen_ai.prompt.0",
|
|
314
|
+
"gen_ai.system.message",
|
|
315
|
+
)
|
|
316
|
+
prompt = event.payload.get("prompt")
|
|
317
|
+
if isinstance(prompt, str) and prompt and prompt != "<streaming>":
|
|
318
|
+
# When the caller streams the prompt (AsyncIterable), the proxy
|
|
319
|
+
# writes the sentinel "<streaming>" instead of the body — skip
|
|
320
|
+
# capture in that case because there's nothing to record yet.
|
|
321
|
+
self._emit_content(
|
|
322
|
+
span,
|
|
323
|
+
"user",
|
|
324
|
+
prompt,
|
|
325
|
+
"gen_ai.prompt.1",
|
|
326
|
+
"gen_ai.user.message",
|
|
327
|
+
)
|
|
237
328
|
self._invoke_spans[event.session_id] = span
|
|
238
329
|
|
|
239
330
|
def _on_assistant_message(self, event: TelemetryEvent) -> None:
|
|
@@ -248,6 +339,15 @@ class OtelSink:
|
|
|
248
339
|
if event.payload.get("model"):
|
|
249
340
|
chat.set_attribute(GEN_AI_REQUEST_MODEL, str(event.payload["model"]))
|
|
250
341
|
chat.set_attribute(GEN_AI_CONVERSATION_ID, event.session_id)
|
|
342
|
+
text = event.payload.get("text")
|
|
343
|
+
if isinstance(text, str) and text:
|
|
344
|
+
self._emit_content(
|
|
345
|
+
chat,
|
|
346
|
+
"assistant",
|
|
347
|
+
text,
|
|
348
|
+
"gen_ai.completion.0",
|
|
349
|
+
"gen_ai.assistant.message",
|
|
350
|
+
)
|
|
251
351
|
chat.end()
|
|
252
352
|
|
|
253
353
|
def _on_tool_use(self, event: TelemetryEvent) -> None:
|
|
@@ -262,6 +362,23 @@ class OtelSink:
|
|
|
262
362
|
if call_id:
|
|
263
363
|
span.set_attribute(GEN_AI_TOOL_CALL_ID, call_id)
|
|
264
364
|
span.set_attribute(GEN_AI_CONVERSATION_ID, event.session_id)
|
|
365
|
+
# Tool arguments — JSON-encoded so multi-key inputs survive the
|
|
366
|
+
# attribute-string projection.
|
|
367
|
+
tool_input = event.payload.get("tool_input")
|
|
368
|
+
if isinstance(tool_input, dict) and tool_input:
|
|
369
|
+
import json
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
serialised = json.dumps(tool_input, default=str)
|
|
373
|
+
except (TypeError, ValueError):
|
|
374
|
+
serialised = str(tool_input)
|
|
375
|
+
self._emit_content(
|
|
376
|
+
span,
|
|
377
|
+
"tool",
|
|
378
|
+
serialised,
|
|
379
|
+
"gen_ai.tool.call.arguments",
|
|
380
|
+
"gen_ai.tool.call.start",
|
|
381
|
+
)
|
|
265
382
|
self._tool_spans.setdefault(event.session_id, {})[call_id] = span
|
|
266
383
|
|
|
267
384
|
def _on_tool_result(self, event: TelemetryEvent) -> None:
|
|
@@ -271,6 +388,27 @@ class OtelSink:
|
|
|
271
388
|
return
|
|
272
389
|
if event.payload.get("is_error"):
|
|
273
390
|
span.set_attribute("error.type", "tool_error")
|
|
391
|
+
# Tool output — agent-readable text projection. The block may be a
|
|
392
|
+
# str OR a list[dict] (Anthropic tool-result content blocks); we
|
|
393
|
+
# serialise dicts to JSON, leave strings alone.
|
|
394
|
+
content = event.payload.get("content")
|
|
395
|
+
if content:
|
|
396
|
+
if isinstance(content, str):
|
|
397
|
+
serialised = content
|
|
398
|
+
else:
|
|
399
|
+
import json
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
serialised = json.dumps(content, default=str)
|
|
403
|
+
except (TypeError, ValueError):
|
|
404
|
+
serialised = str(content)
|
|
405
|
+
self._emit_content(
|
|
406
|
+
span,
|
|
407
|
+
"tool",
|
|
408
|
+
serialised,
|
|
409
|
+
"gen_ai.tool.call.result",
|
|
410
|
+
"gen_ai.tool.call.end",
|
|
411
|
+
)
|
|
274
412
|
span.end()
|
|
275
413
|
|
|
276
414
|
def _on_usage_snapshot(self, event: TelemetryEvent) -> None:
|
|
@@ -329,6 +467,18 @@ class OtelSink:
|
|
|
329
467
|
v = usage.get(key)
|
|
330
468
|
if isinstance(v, int):
|
|
331
469
|
invoke.set_attribute(attr, v)
|
|
470
|
+
# Final answer — surface the ResultMessage.result (or last assistant
|
|
471
|
+
# text fallback) on the invoke_agent root so a single span query
|
|
472
|
+
# returns the agent's response.
|
|
473
|
+
result_text = event.payload.get("result")
|
|
474
|
+
if isinstance(result_text, str) and result_text:
|
|
475
|
+
self._emit_content(
|
|
476
|
+
invoke,
|
|
477
|
+
"assistant",
|
|
478
|
+
result_text,
|
|
479
|
+
"gen_ai.completion.final",
|
|
480
|
+
"gen_ai.assistant.message",
|
|
481
|
+
)
|
|
332
482
|
invoke.end()
|
|
333
483
|
duration_s = float(event.payload.get("duration_ms", 0.0) or 0.0) / 1000.0
|
|
334
484
|
if duration_s > 0:
|
|
@@ -418,6 +568,13 @@ def _signal_url(endpoint: str | None, signal: str) -> str | None:
|
|
|
418
568
|
return f"{e}/v1/{signal}"
|
|
419
569
|
|
|
420
570
|
|
|
571
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
572
|
+
raw = os.environ.get(name)
|
|
573
|
+
if raw is None:
|
|
574
|
+
return default
|
|
575
|
+
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
576
|
+
|
|
577
|
+
|
|
421
578
|
def _parse_otlp_headers_env(raw: str | None) -> dict[str, str]:
|
|
422
579
|
"""Parse ``OTEL_EXPORTER_OTLP_HEADERS`` per the OTel env-spec format.
|
|
423
580
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/middleware/pii.py
RENAMED
|
File without changes
|
|
File without changes
|
{computer_agent_py-0.1.1 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|