progress-observability 1.4.6__tar.gz → 1.5.1__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.
- {progress_observability-1.4.6 → progress_observability-1.5.1}/PKG-INFO +2 -2
- {progress_observability-1.4.6 → progress_observability-1.5.1}/pyproject.toml +2 -2
- progress_observability-1.5.1/src/progress/observability/genkit_span_processor.py +187 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/sdk.py +8 -1
- {progress_observability-1.4.6 → progress_observability-1.5.1}/uv.lock +1 -1
- {progress_observability-1.4.6 → progress_observability-1.5.1}/.gitignore +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/.python-version +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/LICENSE +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/README.md +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/notices.txt +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/__init__.py +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/constants.py +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/decorators.py +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/enums.py +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/exceptions.py +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/genai_message_processor.py +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/helpers.py +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/model_fix_processor.py +0 -0
- {progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/tags.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: progress-observability
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: Progress Observability instrumentation for Python agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/telerik/agentclarity
|
|
6
6
|
Project-URL: Repository, https://github.com/telerik/agentclarity
|
|
7
|
-
Project-URL: Documentation, https://
|
|
7
|
+
Project-URL: Documentation, https://www.telerik.com/ai-observability-platform/documentation/sdk/python
|
|
8
8
|
Project-URL: Bug Reports, https://github.com/telerik/agentclarity/issues
|
|
9
9
|
License: MIT
|
|
10
10
|
License-File: LICENSE
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "progress-observability"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.1"
|
|
8
8
|
description = "Progress Observability instrumentation for Python agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
[project.urls]
|
|
18
18
|
Homepage = "https://github.com/telerik/agentclarity"
|
|
19
19
|
Repository = "https://github.com/telerik/agentclarity"
|
|
20
|
-
Documentation = "https://
|
|
20
|
+
Documentation = "https://www.telerik.com/ai-observability-platform/documentation/sdk/python"
|
|
21
21
|
"Bug Reports" = "https://github.com/telerik/agentclarity/issues"
|
|
22
22
|
|
|
23
23
|
[tool.uv]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Normalize native Genkit spans into AgentClarity attributes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .helpers import get_writable_attributes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_PROVIDERS = {
|
|
10
|
+
"anthropic": "Anthropic",
|
|
11
|
+
"bedrock": "AWS",
|
|
12
|
+
"cohere": "Cohere",
|
|
13
|
+
"googleai": "Google",
|
|
14
|
+
"ollama": "Ollama",
|
|
15
|
+
"openai": "OpenAI",
|
|
16
|
+
"vertexai": "Google",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_number(value: Any) -> bool:
|
|
21
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_json(value: Any) -> dict[str, Any] | None:
|
|
25
|
+
if not isinstance(value, str):
|
|
26
|
+
return None
|
|
27
|
+
try:
|
|
28
|
+
parsed = json.loads(value)
|
|
29
|
+
except (TypeError, ValueError):
|
|
30
|
+
return None
|
|
31
|
+
return parsed if isinstance(parsed, dict) else None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normalize_role(role: Any) -> str:
|
|
35
|
+
if role == "model":
|
|
36
|
+
return "assistant"
|
|
37
|
+
return role if isinstance(role, str) else "unknown"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _merge_consecutive_text_parts(parts: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
41
|
+
# Streaming responses (e.g. Genkit's generate_stream) record the final message content
|
|
42
|
+
# as a sequence of raw text deltas rather than one coalesced string. Merge adjacent
|
|
43
|
+
# text parts so consumers see the reassembled text instead of fragmented chunks.
|
|
44
|
+
merged: list[dict[str, Any]] = []
|
|
45
|
+
for part in parts:
|
|
46
|
+
previous = merged[-1] if merged else None
|
|
47
|
+
if part.get("type") == "text" and previous is not None and previous.get("type") == "text":
|
|
48
|
+
previous["content"] = f"{previous.get('content', '')}{part.get('content', '')}"
|
|
49
|
+
continue
|
|
50
|
+
merged.append(dict(part))
|
|
51
|
+
return merged
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _normalize_parts(content: Any) -> list[dict[str, Any]]:
|
|
55
|
+
if not isinstance(content, list):
|
|
56
|
+
return [{"type": "text", "content": content}] if isinstance(content, str) else []
|
|
57
|
+
|
|
58
|
+
parts: list[dict[str, Any]] = []
|
|
59
|
+
for item in content:
|
|
60
|
+
if not isinstance(item, dict):
|
|
61
|
+
continue
|
|
62
|
+
if isinstance(item.get("text"), str):
|
|
63
|
+
parts.append({"type": "text", "content": item["text"]})
|
|
64
|
+
elif isinstance(item.get("toolRequest"), dict):
|
|
65
|
+
request = item["toolRequest"]
|
|
66
|
+
part: dict[str, Any] = {"type": "tool_call"}
|
|
67
|
+
if isinstance(request.get("name"), str):
|
|
68
|
+
part["name"] = request["name"]
|
|
69
|
+
if "input" in request:
|
|
70
|
+
part["arguments"] = request["input"]
|
|
71
|
+
parts.append(part)
|
|
72
|
+
elif isinstance(item.get("toolResponse"), dict):
|
|
73
|
+
response = item["toolResponse"]
|
|
74
|
+
part = {"type": "tool_call_response"}
|
|
75
|
+
if isinstance(response.get("name"), str):
|
|
76
|
+
part["name"] = response["name"]
|
|
77
|
+
if "output" in response:
|
|
78
|
+
part["response"] = response["output"]
|
|
79
|
+
parts.append(part)
|
|
80
|
+
else:
|
|
81
|
+
parts.append({"type": item.get("type", "unknown"), **item})
|
|
82
|
+
return _merge_consecutive_text_parts(parts)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _normalize_messages(value: Any) -> list[dict[str, Any]]:
|
|
86
|
+
if not isinstance(value, list):
|
|
87
|
+
return []
|
|
88
|
+
return [
|
|
89
|
+
{"role": _normalize_role(message.get("role")), "parts": _normalize_parts(message.get("content"))}
|
|
90
|
+
for message in value
|
|
91
|
+
if isinstance(message, dict)
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class GenkitSpanProcessor:
|
|
96
|
+
"""Post-process native Genkit action spans before export."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, trace_content: bool = True) -> None:
|
|
99
|
+
self._trace_content = trace_content
|
|
100
|
+
|
|
101
|
+
def on_end(self, span: Any) -> None:
|
|
102
|
+
attributes = get_writable_attributes(span)
|
|
103
|
+
if not attributes or "genkit:type" not in attributes:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
subtype = attributes.get("genkit:metadata:subtype")
|
|
107
|
+
if subtype == "flow":
|
|
108
|
+
attributes.setdefault("traceloop.span.kind", "workflow")
|
|
109
|
+
return
|
|
110
|
+
if subtype == "tool":
|
|
111
|
+
attributes.setdefault("traceloop.span.kind", "tool")
|
|
112
|
+
attributes.setdefault("gen_ai.operation.name", "execute_tool")
|
|
113
|
+
tool_name = attributes.get("genkit:name")
|
|
114
|
+
if isinstance(tool_name, str) and tool_name:
|
|
115
|
+
attributes.setdefault("gen_ai.tool.name", tool_name)
|
|
116
|
+
if self._trace_content:
|
|
117
|
+
tool_input = attributes.get("genkit:input")
|
|
118
|
+
if isinstance(tool_input, str):
|
|
119
|
+
attributes.setdefault("gen_ai.tool.call.arguments", tool_input)
|
|
120
|
+
tool_output = attributes.get("genkit:output")
|
|
121
|
+
if isinstance(tool_output, str):
|
|
122
|
+
attributes.setdefault("gen_ai.tool.call.result", tool_output)
|
|
123
|
+
return
|
|
124
|
+
if subtype != "model":
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
name = attributes.get("genkit:name")
|
|
128
|
+
if not isinstance(name, str) or not name:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
separator = name.find("/")
|
|
132
|
+
if separator >= 0:
|
|
133
|
+
plugin = name[:separator].lower()
|
|
134
|
+
model = name[separator + 1 :]
|
|
135
|
+
if "gen_ai.provider.name" not in attributes and plugin:
|
|
136
|
+
attributes["gen_ai.provider.name"] = _PROVIDERS.get(plugin, plugin[:1].upper() + plugin[1:])
|
|
137
|
+
else:
|
|
138
|
+
model = name
|
|
139
|
+
attributes.setdefault("gen_ai.request.model", model)
|
|
140
|
+
attributes.setdefault("gen_ai.response.model", model)
|
|
141
|
+
|
|
142
|
+
if self._trace_content:
|
|
143
|
+
input_data = _parse_json(attributes.get("genkit:input"))
|
|
144
|
+
if input_data and "gen_ai.input.messages" not in attributes:
|
|
145
|
+
messages = _normalize_messages(input_data.get("messages"))
|
|
146
|
+
if messages:
|
|
147
|
+
attributes["gen_ai.input.messages"] = json.dumps(messages)
|
|
148
|
+
|
|
149
|
+
output_data = _parse_json(attributes.get("genkit:output"))
|
|
150
|
+
if not output_data:
|
|
151
|
+
return
|
|
152
|
+
if self._trace_content:
|
|
153
|
+
output_message = output_data.get("message")
|
|
154
|
+
if output_message is None and isinstance(output_data.get("candidates"), list):
|
|
155
|
+
candidates = output_data["candidates"]
|
|
156
|
+
if candidates and isinstance(candidates[0], dict):
|
|
157
|
+
output_message = candidates[0].get("message")
|
|
158
|
+
if isinstance(output_message, dict) and "gen_ai.output.messages" not in attributes:
|
|
159
|
+
messages = _normalize_messages([output_message])
|
|
160
|
+
if messages:
|
|
161
|
+
attributes["gen_ai.output.messages"] = json.dumps(messages)
|
|
162
|
+
|
|
163
|
+
usage = output_data.get("usage")
|
|
164
|
+
if isinstance(usage, dict):
|
|
165
|
+
input_tokens = usage.get("inputTokens")
|
|
166
|
+
output_tokens = usage.get("outputTokens")
|
|
167
|
+
thoughts_tokens = usage.get("thoughtsTokens")
|
|
168
|
+
total_tokens = usage.get("totalTokens")
|
|
169
|
+
if _is_number(input_tokens) and "gen_ai.usage.input_tokens" not in attributes:
|
|
170
|
+
attributes["gen_ai.usage.input_tokens"] = int(input_tokens)
|
|
171
|
+
if (
|
|
172
|
+
_is_number(thoughts_tokens)
|
|
173
|
+
and "gen_ai.usage.reasoning.output_tokens" not in attributes
|
|
174
|
+
):
|
|
175
|
+
attributes["gen_ai.usage.reasoning.output_tokens"] = int(thoughts_tokens)
|
|
176
|
+
if _is_number(output_tokens) and "gen_ai.usage.output_tokens" not in attributes:
|
|
177
|
+
# Genkit providers differ on whether outputTokens includes thoughtsTokens.
|
|
178
|
+
# totalTokens disambiguates the two representations when available.
|
|
179
|
+
thoughts_included = (
|
|
180
|
+
_is_number(total_tokens)
|
|
181
|
+
and _is_number(input_tokens)
|
|
182
|
+
and int(total_tokens) == int(input_tokens) + int(output_tokens)
|
|
183
|
+
)
|
|
184
|
+
effective_output_tokens = int(output_tokens) + (
|
|
185
|
+
0 if thoughts_included or not _is_number(thoughts_tokens) else int(thoughts_tokens)
|
|
186
|
+
)
|
|
187
|
+
attributes["gen_ai.usage.output_tokens"] = effective_output_tokens
|
{progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/sdk.py
RENAMED
|
@@ -32,6 +32,7 @@ from .constants import DEFAULTS
|
|
|
32
32
|
from .helpers import init_environment, setup_api_key_headers, patch_traceloop_modules, clear_sdk_env_vars
|
|
33
33
|
from .model_fix_processor import ModelFixProcessor
|
|
34
34
|
from .genai_message_processor import GenAIMessageProcessor
|
|
35
|
+
from .genkit_span_processor import GenkitSpanProcessor
|
|
35
36
|
from .exceptions import (
|
|
36
37
|
EndpointValidationError,
|
|
37
38
|
InvalidPortError,
|
|
@@ -279,16 +280,22 @@ class Observability:
|
|
|
279
280
|
handler.addFilter(lambda record: record.levelno == logging.DEBUG)
|
|
280
281
|
pkg_logger.addHandler(handler)
|
|
281
282
|
|
|
283
|
+
# Resolve the effective trace_content setting (init_environment() already
|
|
284
|
+
# applied env var precedence and may have set TRACELOOP_TRACE_CONTENT).
|
|
285
|
+
trace_content_enabled = os.getenv("TRACELOOP_TRACE_CONTENT", "true").lower() == "true"
|
|
286
|
+
|
|
282
287
|
# Create processors with debug mode
|
|
283
288
|
genai_message_processor = GenAIMessageProcessor(debug=debug_enabled)
|
|
289
|
+
genkit_span_processor = GenkitSpanProcessor(trace_content=trace_content_enabled)
|
|
284
290
|
model_fix_processor = ModelFixProcessor(debug=debug_enabled)
|
|
285
291
|
cls._model_fix_processor = model_fix_processor
|
|
286
292
|
|
|
287
|
-
# Chain processors: message normalization
|
|
293
|
+
# Chain processors: Genkit normalization, message normalization, then model fixes.
|
|
288
294
|
# Preserve any existing user-provided callback by chaining with it
|
|
289
295
|
existing_callback = kwargs.get('span_postprocess_callback')
|
|
290
296
|
|
|
291
297
|
def chained_callback(span):
|
|
298
|
+
genkit_span_processor.on_end(span)
|
|
292
299
|
genai_message_processor.on_end(span)
|
|
293
300
|
model_fix_processor.on_end(span)
|
|
294
301
|
if existing_callback:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/enums.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{progress_observability-1.4.6 → progress_observability-1.5.1}/src/progress/observability/tags.py
RENAMED
|
File without changes
|