mseep-agentops 0.4.18__py3-none-any.whl → 0.4.23__py3-none-any.whl
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.
- agentops/__init__.py +0 -0
- agentops/client/api/base.py +28 -30
- agentops/client/api/versions/v3.py +29 -25
- agentops/client/api/versions/v4.py +87 -46
- agentops/client/client.py +98 -29
- agentops/client/http/README.md +87 -0
- agentops/client/http/http_client.py +126 -172
- agentops/config.py +8 -2
- agentops/instrumentation/OpenTelemetry.md +133 -0
- agentops/instrumentation/README.md +167 -0
- agentops/instrumentation/__init__.py +13 -1
- agentops/instrumentation/agentic/ag2/__init__.py +18 -0
- agentops/instrumentation/agentic/ag2/instrumentor.py +922 -0
- agentops/instrumentation/agentic/agno/__init__.py +19 -0
- agentops/instrumentation/agentic/agno/attributes/__init__.py +20 -0
- agentops/instrumentation/agentic/agno/attributes/agent.py +250 -0
- agentops/instrumentation/agentic/agno/attributes/metrics.py +214 -0
- agentops/instrumentation/agentic/agno/attributes/storage.py +158 -0
- agentops/instrumentation/agentic/agno/attributes/team.py +195 -0
- agentops/instrumentation/agentic/agno/attributes/tool.py +210 -0
- agentops/instrumentation/agentic/agno/attributes/workflow.py +254 -0
- agentops/instrumentation/agentic/agno/instrumentor.py +1313 -0
- agentops/instrumentation/agentic/crewai/LICENSE +201 -0
- agentops/instrumentation/agentic/crewai/NOTICE.md +10 -0
- agentops/instrumentation/agentic/crewai/__init__.py +6 -0
- agentops/instrumentation/agentic/crewai/crewai_span_attributes.py +335 -0
- agentops/instrumentation/agentic/crewai/instrumentation.py +535 -0
- agentops/instrumentation/agentic/crewai/version.py +1 -0
- agentops/instrumentation/agentic/google_adk/__init__.py +19 -0
- agentops/instrumentation/agentic/google_adk/instrumentor.py +68 -0
- agentops/instrumentation/agentic/google_adk/patch.py +767 -0
- agentops/instrumentation/agentic/haystack/__init__.py +1 -0
- agentops/instrumentation/agentic/haystack/instrumentor.py +186 -0
- agentops/instrumentation/agentic/langgraph/__init__.py +3 -0
- agentops/instrumentation/agentic/langgraph/attributes.py +54 -0
- agentops/instrumentation/agentic/langgraph/instrumentation.py +598 -0
- agentops/instrumentation/agentic/langgraph/version.py +1 -0
- agentops/instrumentation/agentic/openai_agents/README.md +156 -0
- agentops/instrumentation/agentic/openai_agents/SPANS.md +145 -0
- agentops/instrumentation/agentic/openai_agents/TRACING_API.md +144 -0
- agentops/instrumentation/agentic/openai_agents/__init__.py +30 -0
- agentops/instrumentation/agentic/openai_agents/attributes/common.py +549 -0
- agentops/instrumentation/agentic/openai_agents/attributes/completion.py +172 -0
- agentops/instrumentation/agentic/openai_agents/attributes/model.py +58 -0
- agentops/instrumentation/agentic/openai_agents/attributes/tokens.py +275 -0
- agentops/instrumentation/agentic/openai_agents/exporter.py +469 -0
- agentops/instrumentation/agentic/openai_agents/instrumentor.py +107 -0
- agentops/instrumentation/agentic/openai_agents/processor.py +58 -0
- agentops/instrumentation/agentic/smolagents/README.md +88 -0
- agentops/instrumentation/agentic/smolagents/__init__.py +12 -0
- agentops/instrumentation/agentic/smolagents/attributes/agent.py +354 -0
- agentops/instrumentation/agentic/smolagents/attributes/model.py +205 -0
- agentops/instrumentation/agentic/smolagents/instrumentor.py +286 -0
- agentops/instrumentation/agentic/smolagents/stream_wrapper.py +258 -0
- agentops/instrumentation/agentic/xpander/__init__.py +15 -0
- agentops/instrumentation/agentic/xpander/context.py +112 -0
- agentops/instrumentation/agentic/xpander/instrumentor.py +877 -0
- agentops/instrumentation/agentic/xpander/trace_probe.py +86 -0
- agentops/instrumentation/agentic/xpander/version.py +3 -0
- agentops/instrumentation/common/README.md +65 -0
- agentops/instrumentation/common/attributes.py +1 -2
- agentops/instrumentation/providers/anthropic/__init__.py +24 -0
- agentops/instrumentation/providers/anthropic/attributes/__init__.py +23 -0
- agentops/instrumentation/providers/anthropic/attributes/common.py +64 -0
- agentops/instrumentation/providers/anthropic/attributes/message.py +541 -0
- agentops/instrumentation/providers/anthropic/attributes/tools.py +231 -0
- agentops/instrumentation/providers/anthropic/event_handler_wrapper.py +90 -0
- agentops/instrumentation/providers/anthropic/instrumentor.py +146 -0
- agentops/instrumentation/providers/anthropic/stream_wrapper.py +436 -0
- agentops/instrumentation/providers/google_genai/README.md +33 -0
- agentops/instrumentation/providers/google_genai/__init__.py +24 -0
- agentops/instrumentation/providers/google_genai/attributes/__init__.py +25 -0
- agentops/instrumentation/providers/google_genai/attributes/chat.py +125 -0
- agentops/instrumentation/providers/google_genai/attributes/common.py +88 -0
- agentops/instrumentation/providers/google_genai/attributes/model.py +284 -0
- agentops/instrumentation/providers/google_genai/instrumentor.py +170 -0
- agentops/instrumentation/providers/google_genai/stream_wrapper.py +238 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/__init__.py +28 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/__init__.py +27 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/attributes.py +277 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/common.py +104 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/instrumentor.py +162 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/stream_wrapper.py +302 -0
- agentops/instrumentation/providers/mem0/__init__.py +45 -0
- agentops/instrumentation/providers/mem0/common.py +377 -0
- agentops/instrumentation/providers/mem0/instrumentor.py +270 -0
- agentops/instrumentation/providers/mem0/memory.py +430 -0
- agentops/instrumentation/providers/openai/__init__.py +21 -0
- agentops/instrumentation/providers/openai/attributes/__init__.py +7 -0
- agentops/instrumentation/providers/openai/attributes/common.py +55 -0
- agentops/instrumentation/providers/openai/attributes/response.py +607 -0
- agentops/instrumentation/providers/openai/config.py +36 -0
- agentops/instrumentation/providers/openai/instrumentor.py +312 -0
- agentops/instrumentation/providers/openai/stream_wrapper.py +941 -0
- agentops/instrumentation/providers/openai/utils.py +44 -0
- agentops/instrumentation/providers/openai/v0.py +176 -0
- agentops/instrumentation/providers/openai/v0_wrappers.py +483 -0
- agentops/instrumentation/providers/openai/wrappers/__init__.py +30 -0
- agentops/instrumentation/providers/openai/wrappers/assistant.py +277 -0
- agentops/instrumentation/providers/openai/wrappers/chat.py +259 -0
- agentops/instrumentation/providers/openai/wrappers/completion.py +109 -0
- agentops/instrumentation/providers/openai/wrappers/embeddings.py +94 -0
- agentops/instrumentation/providers/openai/wrappers/image_gen.py +75 -0
- agentops/instrumentation/providers/openai/wrappers/responses.py +191 -0
- agentops/instrumentation/providers/openai/wrappers/shared.py +81 -0
- agentops/instrumentation/utilities/concurrent_futures/__init__.py +10 -0
- agentops/instrumentation/utilities/concurrent_futures/instrumentation.py +206 -0
- agentops/integration/callbacks/dspy/__init__.py +11 -0
- agentops/integration/callbacks/dspy/callback.py +471 -0
- agentops/integration/callbacks/langchain/README.md +59 -0
- agentops/integration/callbacks/langchain/__init__.py +15 -0
- agentops/integration/callbacks/langchain/callback.py +791 -0
- agentops/integration/callbacks/langchain/utils.py +54 -0
- agentops/legacy/crewai.md +121 -0
- agentops/logging/instrument_logging.py +4 -0
- agentops/sdk/README.md +220 -0
- agentops/sdk/core.py +75 -32
- agentops/sdk/descriptors/classproperty.py +28 -0
- agentops/sdk/exporters.py +152 -33
- agentops/semconv/README.md +125 -0
- agentops/semconv/span_kinds.py +0 -2
- agentops/validation.py +102 -63
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.23.dist-info}/METADATA +30 -40
- mseep_agentops-0.4.23.dist-info/RECORD +178 -0
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.23.dist-info}/WHEEL +1 -2
- mseep_agentops-0.4.18.dist-info/RECORD +0 -94
- mseep_agentops-0.4.18.dist-info/top_level.txt +0 -2
- tests/conftest.py +0 -10
- tests/unit/client/__init__.py +0 -1
- tests/unit/client/test_http_adapter.py +0 -221
- tests/unit/client/test_http_client.py +0 -206
- tests/unit/conftest.py +0 -54
- tests/unit/sdk/__init__.py +0 -1
- tests/unit/sdk/instrumentation_tester.py +0 -207
- tests/unit/sdk/test_attributes.py +0 -392
- tests/unit/sdk/test_concurrent_instrumentation.py +0 -468
- tests/unit/sdk/test_decorators.py +0 -763
- tests/unit/sdk/test_exporters.py +0 -241
- tests/unit/sdk/test_factory.py +0 -1188
- tests/unit/sdk/test_internal_span_processor.py +0 -397
- tests/unit/sdk/test_resource_attributes.py +0 -35
- tests/unit/test_config.py +0 -82
- tests/unit/test_context_manager.py +0 -777
- tests/unit/test_events.py +0 -27
- tests/unit/test_host_env.py +0 -54
- tests/unit/test_init_py.py +0 -501
- tests/unit/test_serialization.py +0 -433
- tests/unit/test_session.py +0 -676
- tests/unit/test_user_agent.py +0 -34
- tests/unit/test_validation.py +0 -405
- {tests → agentops/instrumentation/agentic/openai_agents/attributes}/__init__.py +0 -0
- /tests/unit/__init__.py → /agentops/instrumentation/providers/openai/attributes/tools.py +0 -0
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
"""Agno Agent instrumentation package."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from agentops.instrumentation.common import LibraryInfo
|
5
|
+
|
6
|
+
from .instrumentor import AgnoInstrumentor
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
# Library information
|
11
|
+
_library_info = LibraryInfo(name="agno", default_version="1.5.8")
|
12
|
+
LIBRARY_NAME = _library_info.name
|
13
|
+
LIBRARY_VERSION = _library_info.version
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"AgnoInstrumentor",
|
17
|
+
"LIBRARY_NAME",
|
18
|
+
"LIBRARY_VERSION",
|
19
|
+
]
|
@@ -0,0 +1,20 @@
|
|
1
|
+
"""Agno instrumentation attribute handlers."""
|
2
|
+
|
3
|
+
from .agent import get_agent_run_attributes
|
4
|
+
from .metrics import get_metrics_attributes
|
5
|
+
from .team import get_team_run_attributes
|
6
|
+
from .tool import get_tool_execution_attributes
|
7
|
+
from .workflow import get_workflow_run_attributes, get_workflow_session_attributes, get_workflow_cache_attributes
|
8
|
+
from .storage import get_storage_read_attributes, get_storage_write_attributes
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"get_agent_run_attributes",
|
12
|
+
"get_metrics_attributes",
|
13
|
+
"get_team_run_attributes",
|
14
|
+
"get_tool_execution_attributes",
|
15
|
+
"get_workflow_run_attributes",
|
16
|
+
"get_workflow_session_attributes",
|
17
|
+
"get_workflow_cache_attributes",
|
18
|
+
"get_storage_read_attributes",
|
19
|
+
"get_storage_write_attributes",
|
20
|
+
]
|
@@ -0,0 +1,250 @@
|
|
1
|
+
"""Agno Agent run attributes handler."""
|
2
|
+
|
3
|
+
from typing import Optional, Tuple, Dict, Any
|
4
|
+
from agentops.instrumentation.common.attributes import AttributeMap
|
5
|
+
from agentops.semconv import SpanAttributes, AgentAttributes, ToolAttributes
|
6
|
+
from agentops.semconv.span_kinds import SpanKind as AgentOpsSpanKind
|
7
|
+
import json
|
8
|
+
|
9
|
+
|
10
|
+
def get_agent_run_attributes(
|
11
|
+
args: Optional[Tuple] = None,
|
12
|
+
kwargs: Optional[Dict] = None,
|
13
|
+
return_value: Optional[Any] = None,
|
14
|
+
) -> AttributeMap:
|
15
|
+
"""Extract span attributes for Agent.run/arun calls.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
args: Positional arguments passed to the run method (self, message, ...)
|
19
|
+
kwargs: Keyword arguments passed to the run method
|
20
|
+
return_value: The return value from the run method (RunResponse)
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
A dictionary of span attributes to be set on the agent span
|
24
|
+
"""
|
25
|
+
attributes: AttributeMap = {}
|
26
|
+
|
27
|
+
# Initialize variables to avoid UnboundLocalError
|
28
|
+
agent_name = None
|
29
|
+
|
30
|
+
# Base attributes
|
31
|
+
attributes[SpanAttributes.AGENTOPS_SPAN_KIND] = AgentOpsSpanKind.AGENT
|
32
|
+
attributes[SpanAttributes.LLM_SYSTEM] = "agno"
|
33
|
+
|
34
|
+
# AgentOps entity attributes
|
35
|
+
attributes[SpanAttributes.AGENTOPS_ENTITY_NAME] = "agent"
|
36
|
+
|
37
|
+
# Extract agent information from args[0] (self)
|
38
|
+
if args and len(args) >= 1:
|
39
|
+
agent = args[0]
|
40
|
+
|
41
|
+
# Core agent identification - set directly at root level
|
42
|
+
if hasattr(agent, "agent_id") and agent.agent_id:
|
43
|
+
agent_id = str(agent.agent_id)
|
44
|
+
attributes[AgentAttributes.AGENT_ID] = agent_id
|
45
|
+
|
46
|
+
if hasattr(agent, "name") and agent.name:
|
47
|
+
agent_name = str(agent.name)
|
48
|
+
attributes[AgentAttributes.AGENT_NAME] = agent_name
|
49
|
+
|
50
|
+
if hasattr(agent, "role") and agent.role:
|
51
|
+
agent_role = str(agent.role)
|
52
|
+
attributes[AgentAttributes.AGENT_ROLE] = agent_role
|
53
|
+
|
54
|
+
# Check if agent is part of a team
|
55
|
+
if hasattr(agent, "_team") and agent._team:
|
56
|
+
team = agent._team
|
57
|
+
if hasattr(team, "name") and team.name:
|
58
|
+
attributes["agent.parent_team"] = str(team.name)
|
59
|
+
attributes["agent.parent_team_display"] = f"Under {team.name}"
|
60
|
+
if hasattr(team, "team_id") and team.team_id:
|
61
|
+
attributes["agent.parent_team_id"] = str(team.team_id)
|
62
|
+
|
63
|
+
# Model information -
|
64
|
+
if hasattr(agent, "model") and agent.model:
|
65
|
+
model = agent.model
|
66
|
+
if hasattr(model, "id"):
|
67
|
+
model_id = str(model.id)
|
68
|
+
attributes[SpanAttributes.LLM_REQUEST_MODEL] = model_id
|
69
|
+
attributes[SpanAttributes.LLM_RESPONSE_MODEL] = model_id
|
70
|
+
|
71
|
+
if hasattr(model, "provider"):
|
72
|
+
model_provider = str(model.provider)
|
73
|
+
attributes["agent.model_provider"] = model_provider
|
74
|
+
|
75
|
+
# Agent configuration details - set directly at root level
|
76
|
+
if hasattr(agent, "description") and agent.description:
|
77
|
+
attributes["agent.description"] = str(agent.description)
|
78
|
+
if hasattr(agent, "goal") and agent.goal:
|
79
|
+
attributes["agent.goal"] = str(agent.goal)
|
80
|
+
|
81
|
+
if hasattr(agent, "instructions") and agent.instructions:
|
82
|
+
if isinstance(agent.instructions, list):
|
83
|
+
attributes["agent.instruction"] = " | ".join(str(i) for i in agent.instructions)
|
84
|
+
else:
|
85
|
+
attributes["agent.instruction"] = str(agent.instructions)
|
86
|
+
if hasattr(agent, "expected_output") and agent.expected_output:
|
87
|
+
attributes["agent.expected_output"] = str(agent.expected_output)
|
88
|
+
|
89
|
+
if hasattr(agent, "markdown"):
|
90
|
+
attributes["agent.markdown"] = str(agent.markdown)
|
91
|
+
|
92
|
+
if hasattr(agent, "reasoning"):
|
93
|
+
attributes[AgentAttributes.AGENT_REASONING] = str(agent.reasoning)
|
94
|
+
|
95
|
+
if hasattr(agent, "stream"):
|
96
|
+
attributes["agent.stream"] = str(agent.stream)
|
97
|
+
|
98
|
+
if hasattr(agent, "show_tool_calls"):
|
99
|
+
attributes["agent.show_tool_calls"] = str(agent.show_tool_calls)
|
100
|
+
|
101
|
+
if hasattr(agent, "tool_call_limit") and agent.tool_call_limit:
|
102
|
+
attributes["agent.tool_call_limit"] = str(agent.tool_call_limit)
|
103
|
+
|
104
|
+
# Tools information
|
105
|
+
if hasattr(agent, "tools") and agent.tools:
|
106
|
+
# Set tool count based on actual number of tools
|
107
|
+
attributes["agent.tools_count"] = str(len(agent.tools))
|
108
|
+
|
109
|
+
# Collect all tool names for the AGENT_TOOLS attribute
|
110
|
+
tool_names = []
|
111
|
+
|
112
|
+
if len(agent.tools) == 1:
|
113
|
+
# Single tool - set directly at root level
|
114
|
+
tool = agent.tools[0]
|
115
|
+
tool_name = None
|
116
|
+
if hasattr(tool, "name"):
|
117
|
+
tool_name = str(tool.name)
|
118
|
+
attributes[ToolAttributes.TOOL_NAME] = tool_name
|
119
|
+
elif hasattr(tool, "__name__"):
|
120
|
+
tool_name = str(tool.__name__)
|
121
|
+
attributes[ToolAttributes.TOOL_NAME] = tool_name
|
122
|
+
elif callable(tool):
|
123
|
+
tool_name = getattr(tool, "__name__", "unknown_tool")
|
124
|
+
attributes[ToolAttributes.TOOL_NAME] = tool_name
|
125
|
+
|
126
|
+
if tool_name:
|
127
|
+
tool_names.append(tool_name)
|
128
|
+
|
129
|
+
if hasattr(tool, "description") and tool.description:
|
130
|
+
attributes[ToolAttributes.TOOL_DESCRIPTION] = str(tool.description)
|
131
|
+
elif hasattr(tool, "__doc__") and tool.__doc__:
|
132
|
+
# Fallback to docstring if no description attribute
|
133
|
+
attributes[ToolAttributes.TOOL_DESCRIPTION] = str(tool.__doc__).strip()
|
134
|
+
else:
|
135
|
+
# Multiple tools - use indexed format
|
136
|
+
for i, tool in enumerate(agent.tools):
|
137
|
+
tool_name = None
|
138
|
+
if hasattr(tool, "name"):
|
139
|
+
tool_name = str(tool.name)
|
140
|
+
attributes[f"tool.{i}.name"] = tool_name
|
141
|
+
elif hasattr(tool, "__name__"):
|
142
|
+
tool_name = str(tool.__name__)
|
143
|
+
attributes[f"tool.{i}.name"] = tool_name
|
144
|
+
elif callable(tool):
|
145
|
+
tool_name = getattr(tool, "__name__", "unknown_tool")
|
146
|
+
attributes[f"tool.{i}.name"] = tool_name
|
147
|
+
|
148
|
+
if tool_name:
|
149
|
+
tool_names.append(tool_name)
|
150
|
+
|
151
|
+
if hasattr(tool, "description") and tool.description:
|
152
|
+
attributes[f"tool.{i}.description"] = str(tool.description)
|
153
|
+
elif hasattr(tool, "__doc__") and tool.__doc__:
|
154
|
+
# Fallback to docstring if no description attribute
|
155
|
+
attributes[f"tool.{i}.description"] = str(tool.__doc__).strip()
|
156
|
+
|
157
|
+
# Set the AGENT_TOOLS attribute with all tool names
|
158
|
+
if tool_names:
|
159
|
+
attributes[AgentAttributes.AGENT_TOOLS] = json.dumps(tool_names)
|
160
|
+
|
161
|
+
if hasattr(agent, "knowledge") and agent.knowledge:
|
162
|
+
knowledge_type = type(agent.knowledge).__name__
|
163
|
+
attributes["agent.knowledge_type"] = knowledge_type
|
164
|
+
|
165
|
+
if hasattr(agent, "storage") and agent.storage:
|
166
|
+
storage_type = type(agent.storage).__name__
|
167
|
+
attributes["agent.storage_type"] = storage_type
|
168
|
+
|
169
|
+
# Session information
|
170
|
+
if hasattr(agent, "session_id") and agent.session_id:
|
171
|
+
session_id = str(agent.session_id)
|
172
|
+
attributes["agent.session_id"] = session_id
|
173
|
+
|
174
|
+
if hasattr(agent, "user_id") and agent.user_id:
|
175
|
+
user_id = str(agent.user_id)
|
176
|
+
attributes["agent.user_id"] = user_id
|
177
|
+
|
178
|
+
# Output key if present
|
179
|
+
if hasattr(agent, "output_key") and agent.output_key:
|
180
|
+
attributes["agent.output_key"] = str(agent.output_key)
|
181
|
+
|
182
|
+
# Extract run input information
|
183
|
+
if args and len(args) >= 2:
|
184
|
+
message = args[1] # The message argument
|
185
|
+
if message:
|
186
|
+
message_str = str(message)
|
187
|
+
attributes["agent.input"] = message_str
|
188
|
+
# AgentOps entity input
|
189
|
+
attributes[SpanAttributes.AGENTOPS_ENTITY_INPUT] = message_str
|
190
|
+
|
191
|
+
# Extract kwargs information
|
192
|
+
if kwargs:
|
193
|
+
if kwargs.get("stream") is not None:
|
194
|
+
attributes[SpanAttributes.LLM_REQUEST_STREAMING] = str(kwargs["stream"])
|
195
|
+
|
196
|
+
if kwargs.get("session_id"):
|
197
|
+
attributes["agent.run_session_id"] = str(kwargs["session_id"])
|
198
|
+
|
199
|
+
if kwargs.get("user_id"):
|
200
|
+
attributes["agent.run_user_id"] = str(kwargs["user_id"])
|
201
|
+
|
202
|
+
# Extract return value information
|
203
|
+
if return_value:
|
204
|
+
if hasattr(return_value, "run_id") and return_value.run_id:
|
205
|
+
run_id = str(return_value.run_id)
|
206
|
+
attributes["agent.run_id"] = run_id
|
207
|
+
|
208
|
+
if hasattr(return_value, "content") and return_value.content:
|
209
|
+
content = str(return_value.content)
|
210
|
+
attributes["agent.output"] = content
|
211
|
+
|
212
|
+
if hasattr(return_value, "event") and return_value.event:
|
213
|
+
event = str(return_value.event)
|
214
|
+
attributes["agent.event"] = event
|
215
|
+
|
216
|
+
# Tool executions from the response
|
217
|
+
if hasattr(return_value, "tools") and return_value.tools:
|
218
|
+
# Track the number of tool executions
|
219
|
+
attributes["agent.tool_executions_count"] = str(len(return_value.tools))
|
220
|
+
|
221
|
+
for i, tool_exec in enumerate(return_value.tools): # No limit - show all tools
|
222
|
+
if hasattr(tool_exec, "tool_name") and tool_exec.tool_name:
|
223
|
+
attributes[f"tool.{i}.name"] = str(tool_exec.tool_name)
|
224
|
+
|
225
|
+
if hasattr(tool_exec, "tool_args") and tool_exec.tool_args:
|
226
|
+
try:
|
227
|
+
args_str = json.dumps(tool_exec.tool_args)
|
228
|
+
attributes[f"tool.{i}.parameters"] = args_str
|
229
|
+
except:
|
230
|
+
attributes[f"tool.{i}.parameters"] = str(tool_exec.tool_args)
|
231
|
+
|
232
|
+
if hasattr(tool_exec, "result") and tool_exec.result:
|
233
|
+
result_str = str(tool_exec.result)
|
234
|
+
attributes[f"tool.{i}.result"] = result_str
|
235
|
+
|
236
|
+
if hasattr(tool_exec, "tool_call_error") and tool_exec.tool_call_error:
|
237
|
+
attributes[f"tool.{i}.error"] = str(tool_exec.tool_call_error)
|
238
|
+
|
239
|
+
attributes[f"tool.{i}.status"] = "success" # Default to success
|
240
|
+
|
241
|
+
# Add display name for better UI visualization
|
242
|
+
if agent_name:
|
243
|
+
# Check if we have parent team info
|
244
|
+
parent_team = attributes.get("agent.parent_team")
|
245
|
+
if parent_team:
|
246
|
+
attributes["agent.display_name"] = f"{agent_name} (Agent under {parent_team})"
|
247
|
+
else:
|
248
|
+
attributes["agent.display_name"] = f"{agent_name}"
|
249
|
+
|
250
|
+
return attributes
|
@@ -0,0 +1,214 @@
|
|
1
|
+
"""Agno Agent session metrics attributes handler."""
|
2
|
+
|
3
|
+
from typing import Optional, Tuple, Dict, Any
|
4
|
+
|
5
|
+
from agentops.instrumentation.common.attributes import AttributeMap
|
6
|
+
from agentops.semconv import SpanAttributes
|
7
|
+
|
8
|
+
|
9
|
+
def get_metrics_attributes(
|
10
|
+
args: Optional[Tuple] = None,
|
11
|
+
kwargs: Optional[Dict] = None,
|
12
|
+
return_value: Optional[Any] = None,
|
13
|
+
) -> AttributeMap:
|
14
|
+
"""Extract span attributes for Agent._set_session_metrics calls.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
args: Positional arguments passed to the _set_session_metrics method (self, run_messages)
|
18
|
+
kwargs: Keyword arguments passed to the _set_session_metrics method
|
19
|
+
return_value: The return value from the _set_session_metrics method
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
A dictionary of span attributes to be set on the metrics span
|
23
|
+
"""
|
24
|
+
attributes: AttributeMap = {}
|
25
|
+
|
26
|
+
# Base attributes
|
27
|
+
attributes[SpanAttributes.AGENTOPS_SPAN_KIND] = "llm"
|
28
|
+
attributes[SpanAttributes.LLM_SYSTEM] = "agno"
|
29
|
+
attributes[SpanAttributes.AGENTOPS_ENTITY_NAME] = "LLM"
|
30
|
+
|
31
|
+
# Initialize usage tracking variables
|
32
|
+
usage_data = {}
|
33
|
+
|
34
|
+
# Initialize counters for indexed messages
|
35
|
+
prompt_count = 0
|
36
|
+
completion_count = 0
|
37
|
+
|
38
|
+
# Extract agent and run_messages from args (self, run_messages)
|
39
|
+
if args and len(args) >= 2:
|
40
|
+
agent = args[0] # self (Agent instance)
|
41
|
+
run_messages = args[1] # RunMessages object
|
42
|
+
|
43
|
+
# Add agent display name for LLM calls
|
44
|
+
if hasattr(agent, "name") and agent.name:
|
45
|
+
attributes["agno.llm.display_name"] = f"{agent.name} → LLM"
|
46
|
+
|
47
|
+
# Model information - get additional request parameters if available
|
48
|
+
if hasattr(agent, "model") and agent.model:
|
49
|
+
model = agent.model
|
50
|
+
# Set model ID first
|
51
|
+
if hasattr(model, "id"):
|
52
|
+
attributes[SpanAttributes.LLM_REQUEST_MODEL] = str(model.id)
|
53
|
+
attributes[SpanAttributes.LLM_RESPONSE_MODEL] = str(model.id)
|
54
|
+
# Additional model parameters
|
55
|
+
if hasattr(model, "temperature") and model.temperature is not None:
|
56
|
+
attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE] = str(model.temperature)
|
57
|
+
if hasattr(model, "max_tokens") and model.max_tokens is not None:
|
58
|
+
attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] = str(model.max_tokens)
|
59
|
+
if hasattr(model, "top_p") and model.top_p is not None:
|
60
|
+
attributes[SpanAttributes.LLM_REQUEST_TOP_P] = str(model.top_p)
|
61
|
+
if hasattr(model, "provider"):
|
62
|
+
attributes["agno.model.provider"] = str(model.provider)
|
63
|
+
|
64
|
+
# Add model class name for better identification (with null check)
|
65
|
+
if hasattr(model, "__class__") and hasattr(model.__class__, "__name__"):
|
66
|
+
model_class = model.__class__.__name__
|
67
|
+
attributes["agno.model.class"] = model_class
|
68
|
+
|
69
|
+
if hasattr(run_messages, "messages") and run_messages.messages:
|
70
|
+
messages = run_messages.messages
|
71
|
+
|
72
|
+
# Initialize token tracking
|
73
|
+
total_prompt_tokens = 0
|
74
|
+
total_completion_tokens = 0
|
75
|
+
total_output_tokens = 0
|
76
|
+
total_input_tokens = 0
|
77
|
+
total_tokens = 0
|
78
|
+
total_time = 0.0
|
79
|
+
|
80
|
+
# Process messages to create individual indexed gen_ai.prompt.{i} and gen_ai.completion.{i} attributes
|
81
|
+
for i, msg in enumerate(messages):
|
82
|
+
# Extract message content for prompts/completions
|
83
|
+
if hasattr(msg, "role") and hasattr(msg, "content"):
|
84
|
+
# Only process messages with actual content
|
85
|
+
if msg.content is not None and str(msg.content).strip() != "" and str(msg.content) != "None":
|
86
|
+
content = str(msg.content)
|
87
|
+
# No truncation - keep full content for observability
|
88
|
+
|
89
|
+
if msg.role == "user":
|
90
|
+
attributes[f"{SpanAttributes.LLM_PROMPTS}.{prompt_count}.role"] = "user"
|
91
|
+
attributes[f"{SpanAttributes.LLM_PROMPTS}.{prompt_count}.content"] = content
|
92
|
+
prompt_count += 1
|
93
|
+
elif msg.role == "assistant":
|
94
|
+
attributes[f"{SpanAttributes.LLM_COMPLETIONS}.{completion_count}.role"] = "assistant"
|
95
|
+
attributes[f"{SpanAttributes.LLM_COMPLETIONS}.{completion_count}.content"] = content
|
96
|
+
completion_count += 1
|
97
|
+
elif msg.role == "system":
|
98
|
+
attributes[f"{SpanAttributes.LLM_PROMPTS}.{prompt_count}.role"] = "system"
|
99
|
+
attributes[f"{SpanAttributes.LLM_PROMPTS}.{prompt_count}.content"] = content
|
100
|
+
prompt_count += 1
|
101
|
+
|
102
|
+
# Extract token metrics from message
|
103
|
+
if hasattr(msg, "metrics") and msg.metrics:
|
104
|
+
metrics = msg.metrics
|
105
|
+
|
106
|
+
# Handle different token metric patterns
|
107
|
+
if hasattr(metrics, "prompt_tokens") and metrics.prompt_tokens > 0:
|
108
|
+
total_prompt_tokens += metrics.prompt_tokens
|
109
|
+
if hasattr(metrics, "completion_tokens") and metrics.completion_tokens > 0:
|
110
|
+
total_completion_tokens += metrics.completion_tokens
|
111
|
+
if hasattr(metrics, "total_tokens") and metrics.total_tokens > 0:
|
112
|
+
total_tokens += metrics.total_tokens
|
113
|
+
# For messages that only have output_tokens
|
114
|
+
if hasattr(metrics, "output_tokens") and metrics.output_tokens > 0:
|
115
|
+
total_output_tokens += metrics.output_tokens
|
116
|
+
if hasattr(metrics, "input_tokens") and metrics.input_tokens > 0:
|
117
|
+
total_input_tokens += metrics.input_tokens
|
118
|
+
if hasattr(metrics, "time") and metrics.time:
|
119
|
+
total_time += metrics.time
|
120
|
+
|
121
|
+
# Token metrics from agent session metrics
|
122
|
+
if hasattr(agent, "session_metrics") and agent.session_metrics:
|
123
|
+
session_metrics = agent.session_metrics
|
124
|
+
|
125
|
+
# Try to get model name from session metrics if not already set
|
126
|
+
if SpanAttributes.LLM_REQUEST_MODEL not in attributes:
|
127
|
+
if hasattr(session_metrics, "model") and session_metrics.model:
|
128
|
+
model_id = str(session_metrics.model)
|
129
|
+
attributes[SpanAttributes.LLM_REQUEST_MODEL] = model_id
|
130
|
+
attributes[SpanAttributes.LLM_RESPONSE_MODEL] = model_id
|
131
|
+
|
132
|
+
# Only set token variables if the attributes actually exist
|
133
|
+
session_prompt_tokens = None
|
134
|
+
session_completion_tokens = None
|
135
|
+
session_output_tokens = None
|
136
|
+
session_input_tokens = None
|
137
|
+
session_total_tokens = None
|
138
|
+
|
139
|
+
if hasattr(session_metrics, "prompt_tokens"):
|
140
|
+
session_prompt_tokens = session_metrics.prompt_tokens
|
141
|
+
|
142
|
+
if hasattr(session_metrics, "completion_tokens"):
|
143
|
+
session_completion_tokens = session_metrics.completion_tokens
|
144
|
+
|
145
|
+
if hasattr(session_metrics, "output_tokens"):
|
146
|
+
session_output_tokens = session_metrics.output_tokens
|
147
|
+
|
148
|
+
if hasattr(session_metrics, "input_tokens"):
|
149
|
+
session_input_tokens = session_metrics.input_tokens
|
150
|
+
|
151
|
+
if hasattr(session_metrics, "total_tokens"):
|
152
|
+
session_total_tokens = session_metrics.total_tokens
|
153
|
+
|
154
|
+
# For Anthropic, output_tokens represents completion tokens
|
155
|
+
if session_output_tokens is not None and session_output_tokens > 0:
|
156
|
+
if session_completion_tokens is None or session_completion_tokens == 0:
|
157
|
+
session_completion_tokens = session_output_tokens
|
158
|
+
|
159
|
+
# For some providers, input_tokens represents prompt tokens
|
160
|
+
if session_input_tokens is not None and session_input_tokens > 0:
|
161
|
+
if session_prompt_tokens is None or session_prompt_tokens == 0:
|
162
|
+
session_prompt_tokens = session_input_tokens
|
163
|
+
|
164
|
+
# Only set token attributes if we have actual values
|
165
|
+
if session_total_tokens is not None and session_total_tokens > 0:
|
166
|
+
usage_data["total_tokens"] = session_total_tokens
|
167
|
+
|
168
|
+
# Set breakdown if available
|
169
|
+
if session_prompt_tokens is not None and session_prompt_tokens > 0:
|
170
|
+
usage_data["prompt_tokens"] = session_prompt_tokens
|
171
|
+
if session_completion_tokens is not None and session_completion_tokens > 0:
|
172
|
+
usage_data["completion_tokens"] = session_completion_tokens
|
173
|
+
|
174
|
+
# Additional token types from session metrics - only set if present
|
175
|
+
if hasattr(session_metrics, "cached_tokens") and session_metrics.cached_tokens > 0:
|
176
|
+
usage_data["cache_read_input_tokens"] = session_metrics.cached_tokens
|
177
|
+
if hasattr(session_metrics, "reasoning_tokens") and session_metrics.reasoning_tokens > 0:
|
178
|
+
usage_data["reasoning_tokens"] = session_metrics.reasoning_tokens
|
179
|
+
|
180
|
+
# If we don't have token data from session metrics, try message aggregation
|
181
|
+
if "total_tokens" not in usage_data:
|
182
|
+
# Set aggregated token usage from messages
|
183
|
+
if total_prompt_tokens > 0 or total_input_tokens > 0:
|
184
|
+
usage_data["prompt_tokens"] = total_prompt_tokens or total_input_tokens
|
185
|
+
if total_completion_tokens > 0 or total_output_tokens > 0:
|
186
|
+
usage_data["completion_tokens"] = total_completion_tokens or total_output_tokens
|
187
|
+
if total_tokens > 0:
|
188
|
+
usage_data["total_tokens"] = total_tokens
|
189
|
+
|
190
|
+
# Extract user message info if available
|
191
|
+
if hasattr(run_messages, "user_message") and run_messages.user_message:
|
192
|
+
user_msg = run_messages.user_message
|
193
|
+
if hasattr(user_msg, "content"):
|
194
|
+
content = str(user_msg.content)
|
195
|
+
attributes["agno.metrics.user_input"] = content
|
196
|
+
|
197
|
+
# Set individual LLM usage attributes only for values we actually have
|
198
|
+
if "prompt_tokens" in usage_data:
|
199
|
+
attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS] = usage_data["prompt_tokens"]
|
200
|
+
if "completion_tokens" in usage_data:
|
201
|
+
attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS] = usage_data["completion_tokens"]
|
202
|
+
if "total_tokens" in usage_data:
|
203
|
+
attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS] = usage_data["total_tokens"]
|
204
|
+
if "cache_read_input_tokens" in usage_data:
|
205
|
+
attributes[SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS] = usage_data["cache_read_input_tokens"]
|
206
|
+
if "reasoning_tokens" in usage_data:
|
207
|
+
attributes[SpanAttributes.LLM_USAGE_REASONING_TOKENS] = usage_data["reasoning_tokens"]
|
208
|
+
|
209
|
+
# But only if we have any usage data
|
210
|
+
if usage_data:
|
211
|
+
for key, value in usage_data.items():
|
212
|
+
attributes[f"gen_ai.usage.{key}"] = value
|
213
|
+
|
214
|
+
return attributes
|
@@ -0,0 +1,158 @@
|
|
1
|
+
"""Storage operation attribute handlers for Agno workflow instrumentation."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from typing import Any, Dict, Optional, Tuple
|
5
|
+
from opentelemetry.util.types import AttributeValue
|
6
|
+
|
7
|
+
from agentops.semconv.span_attributes import SpanAttributes
|
8
|
+
from agentops.semconv.span_kinds import SpanKind as AgentOpsSpanKind
|
9
|
+
from agentops.instrumentation.common.attributes import get_common_attributes
|
10
|
+
|
11
|
+
|
12
|
+
def get_storage_read_attributes(
|
13
|
+
args: Tuple[Any, ...] = (),
|
14
|
+
kwargs: Optional[Dict[str, Any]] = None,
|
15
|
+
return_value: Optional[Any] = None,
|
16
|
+
) -> Dict[str, AttributeValue]:
|
17
|
+
"""Extract attributes from storage read operations.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
args: Positional arguments passed to read_from_storage
|
21
|
+
kwargs: Keyword arguments passed to read_from_storage
|
22
|
+
return_value: Return value from read_from_storage (the cached data or None)
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
Dictionary of OpenTelemetry attributes for storage read operations
|
26
|
+
"""
|
27
|
+
attributes = get_common_attributes()
|
28
|
+
kwargs = kwargs or {}
|
29
|
+
|
30
|
+
# Mark this as a storage operation within workflow context
|
31
|
+
attributes["storage.operation"] = "read"
|
32
|
+
attributes[SpanAttributes.AGENTOPS_SPAN_KIND] = AgentOpsSpanKind.WORKFLOW
|
33
|
+
|
34
|
+
if args and len(args) > 0:
|
35
|
+
workflow = args[0]
|
36
|
+
|
37
|
+
# Get workflow information
|
38
|
+
if hasattr(workflow, "workflow_id") and workflow.workflow_id:
|
39
|
+
attributes["storage.workflow_id"] = str(workflow.workflow_id)
|
40
|
+
if hasattr(workflow, "session_id") and workflow.session_id:
|
41
|
+
attributes["storage.session_id"] = str(workflow.session_id)
|
42
|
+
|
43
|
+
# Get storage type
|
44
|
+
if hasattr(workflow, "storage") and workflow.storage:
|
45
|
+
storage_type = type(workflow.storage).__name__
|
46
|
+
attributes["storage.backend"] = storage_type
|
47
|
+
|
48
|
+
# Get session state info for context
|
49
|
+
if hasattr(workflow, "session_state") and isinstance(workflow.session_state, dict):
|
50
|
+
# Get all cache keys
|
51
|
+
cache_keys = list(workflow.session_state.keys())
|
52
|
+
attributes["storage.cache_size"] = len(cache_keys)
|
53
|
+
if cache_keys:
|
54
|
+
attributes["storage.cache_keys"] = json.dumps(cache_keys)
|
55
|
+
|
56
|
+
# Analyze the return value to determine cache hit/miss
|
57
|
+
if return_value is not None:
|
58
|
+
# Cache hit
|
59
|
+
attributes["storage.cache_hit"] = True
|
60
|
+
attributes["storage.result"] = "hit"
|
61
|
+
|
62
|
+
# Get data type and size
|
63
|
+
data_type = type(return_value).__name__
|
64
|
+
attributes["storage.data_type"] = data_type
|
65
|
+
|
66
|
+
# For dict/list, show structure
|
67
|
+
if isinstance(return_value, dict):
|
68
|
+
attributes["storage.data_keys"] = json.dumps(list(return_value.keys()))
|
69
|
+
attributes["storage.data_size"] = len(return_value)
|
70
|
+
elif isinstance(return_value, (list, tuple)):
|
71
|
+
attributes["storage.data_size"] = len(return_value)
|
72
|
+
elif isinstance(return_value, str):
|
73
|
+
attributes["storage.data_size"] = len(return_value)
|
74
|
+
# Show full string data without truncation
|
75
|
+
attributes["storage.data_preview"] = return_value
|
76
|
+
else:
|
77
|
+
# Cache miss
|
78
|
+
attributes["storage.cache_hit"] = False
|
79
|
+
attributes["storage.result"] = "miss"
|
80
|
+
|
81
|
+
return attributes
|
82
|
+
|
83
|
+
|
84
|
+
def get_storage_write_attributes(
|
85
|
+
args: Tuple[Any, ...] = (),
|
86
|
+
kwargs: Optional[Dict[str, Any]] = None,
|
87
|
+
return_value: Optional[Any] = None,
|
88
|
+
) -> Dict[str, AttributeValue]:
|
89
|
+
"""Extract attributes from storage write operations.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
args: Positional arguments passed to write_to_storage
|
93
|
+
kwargs: Keyword arguments passed to write_to_storage
|
94
|
+
return_value: Return value from write_to_storage (usually None or success indicator)
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Dictionary of OpenTelemetry attributes for storage write operations
|
98
|
+
"""
|
99
|
+
attributes = get_common_attributes()
|
100
|
+
kwargs = kwargs or {}
|
101
|
+
|
102
|
+
# Mark this as a storage operation within workflow context
|
103
|
+
attributes["storage.operation"] = "write"
|
104
|
+
attributes[SpanAttributes.AGENTOPS_SPAN_KIND] = AgentOpsSpanKind.WORKFLOW
|
105
|
+
|
106
|
+
if args and len(args) > 0:
|
107
|
+
workflow = args[0]
|
108
|
+
|
109
|
+
# Get workflow information
|
110
|
+
if hasattr(workflow, "workflow_id") and workflow.workflow_id:
|
111
|
+
attributes["storage.workflow_id"] = str(workflow.workflow_id)
|
112
|
+
if hasattr(workflow, "session_id") and workflow.session_id:
|
113
|
+
attributes["storage.session_id"] = str(workflow.session_id)
|
114
|
+
|
115
|
+
# Get storage type
|
116
|
+
if hasattr(workflow, "storage") and workflow.storage:
|
117
|
+
storage_type = type(workflow.storage).__name__
|
118
|
+
attributes["storage.backend"] = storage_type
|
119
|
+
|
120
|
+
# Get session state info to see what's being written
|
121
|
+
if hasattr(workflow, "session_state") and isinstance(workflow.session_state, dict):
|
122
|
+
# Get cache state after write
|
123
|
+
cache_keys = list(workflow.session_state.keys())
|
124
|
+
attributes["storage.cache_size"] = len(cache_keys)
|
125
|
+
if cache_keys:
|
126
|
+
attributes["storage.cache_keys"] = json.dumps(cache_keys)
|
127
|
+
|
128
|
+
# Try to identify what was written (the newest/changed data)
|
129
|
+
# This is a heuristic - in practice you might need to track state changes
|
130
|
+
if cache_keys:
|
131
|
+
# Show the last key as likely the one just written
|
132
|
+
last_key = cache_keys[-1]
|
133
|
+
attributes["storage.written_key"] = last_key
|
134
|
+
|
135
|
+
# Get value preview
|
136
|
+
value = workflow.session_state.get(last_key)
|
137
|
+
if value is not None:
|
138
|
+
value_type = type(value).__name__
|
139
|
+
attributes["storage.written_value_type"] = value_type
|
140
|
+
|
141
|
+
if isinstance(value, str):
|
142
|
+
if len(value) > 100:
|
143
|
+
attributes["storage.written_value_preview"] = value[:100] + "..."
|
144
|
+
else:
|
145
|
+
attributes["storage.written_value_preview"] = value
|
146
|
+
attributes["storage.written_value_size"] = len(value)
|
147
|
+
elif isinstance(value, (dict, list)):
|
148
|
+
attributes["storage.written_value_size"] = len(value)
|
149
|
+
attributes["storage.written_value_preview"] = f"{value_type} with {len(value)} items"
|
150
|
+
|
151
|
+
# Check write result
|
152
|
+
if return_value is not None:
|
153
|
+
attributes["storage.write_success"] = True
|
154
|
+
else:
|
155
|
+
# Most storage writes return None on success, so this is normal
|
156
|
+
attributes["storage.write_success"] = True
|
157
|
+
|
158
|
+
return attributes
|