mseep-agentops 0.4.18__py3-none-any.whl → 0.4.22__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.22.dist-info}/METADATA +30 -40
- mseep_agentops-0.4.22.dist-info/RECORD +178 -0
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.22.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.22.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,877 @@
|
|
1
|
+
"""Xpander SDK instrumentation for AgentOps.
|
2
|
+
|
3
|
+
This module provides instrumentation for the Xpander SDK, which uses JSII to convert
|
4
|
+
TypeScript code to Python at runtime. The instrumentation tracks agent sessions,
|
5
|
+
tool executions, and LLM interactions.
|
6
|
+
|
7
|
+
MODIFIED VERSION: Using existing AgentOps utilities where possible while keeping
|
8
|
+
runtime-specific instrumentation logic that cannot be replaced.
|
9
|
+
|
10
|
+
REPLACEMENTS MADE:
|
11
|
+
✅ Span creation: Using tracer.make_span() instead of manual span creation
|
12
|
+
✅ Error handling: Using _finish_span_success/_finish_span_error utilities
|
13
|
+
✅ Attribute management: Using existing SpanAttributeManager
|
14
|
+
✅ Serialization: Using safe_serialize and model_to_dict utilities
|
15
|
+
✅ Attribute setting: Using _update_span utility
|
16
|
+
|
17
|
+
RUNTIME-SPECIFIC LOGIC KEPT (Cannot be replaced):
|
18
|
+
❌ Method wrapping: Runtime method creation requires custom hooks
|
19
|
+
❌ Context persistence: XpanderContext must handle runtime object lifecycle
|
20
|
+
❌ Agent detection: Custom logic for dynamically created agents
|
21
|
+
"""
|
22
|
+
|
23
|
+
import logging
|
24
|
+
import time
|
25
|
+
import json
|
26
|
+
from typing import Any, Optional
|
27
|
+
from opentelemetry.metrics import Meter
|
28
|
+
from opentelemetry.trace import SpanKind as OTelSpanKind
|
29
|
+
from opentelemetry import trace
|
30
|
+
|
31
|
+
# Use existing AgentOps utilities
|
32
|
+
from agentops.instrumentation.common import (
|
33
|
+
CommonInstrumentor,
|
34
|
+
InstrumentorConfig,
|
35
|
+
StandardMetrics,
|
36
|
+
)
|
37
|
+
from agentops.instrumentation.common.span_management import SpanAttributeManager
|
38
|
+
from agentops.instrumentation.common.wrappers import _finish_span_success, _finish_span_error, _update_span
|
39
|
+
from agentops.helpers.serialization import safe_serialize, model_to_dict
|
40
|
+
from agentops.sdk.core import tracer
|
41
|
+
from agentops.instrumentation.agentic.xpander.context import XpanderContext
|
42
|
+
from agentops.semconv import SpanAttributes, SpanKind, ToolAttributes
|
43
|
+
from agentops.semconv.message import MessageAttributes
|
44
|
+
|
45
|
+
# Use existing OpenAI attribute extraction patterns (lazy import to avoid circular imports)
|
46
|
+
# from agentops.instrumentation.providers.openai.attributes.common import (
|
47
|
+
# get_response_attributes,
|
48
|
+
# )
|
49
|
+
|
50
|
+
logger = logging.getLogger(__name__)
|
51
|
+
|
52
|
+
_instruments = ("xpander-sdk >= 1.0.0",)
|
53
|
+
|
54
|
+
|
55
|
+
# Use existing AgentOps utility instead of custom implementation
|
56
|
+
def safe_set_attribute(span, key: str, value: Any) -> None:
|
57
|
+
"""Set attribute on span using existing AgentOps utility."""
|
58
|
+
try:
|
59
|
+
_update_span(span, {key: value})
|
60
|
+
except Exception as e:
|
61
|
+
logger.warning(f"Failed to set attribute {key}: {e}")
|
62
|
+
|
63
|
+
|
64
|
+
class XpanderInstrumentor(CommonInstrumentor):
|
65
|
+
"""Instrumentor for Xpander SDK interactions."""
|
66
|
+
|
67
|
+
def __init__(self, config: Optional[InstrumentorConfig] = None):
|
68
|
+
if config is None:
|
69
|
+
config = InstrumentorConfig(
|
70
|
+
library_name="xpander-sdk", library_version="1.0.0", dependencies=_instruments, metrics_enabled=True
|
71
|
+
)
|
72
|
+
super().__init__(config)
|
73
|
+
self._context = XpanderContext()
|
74
|
+
self._tracer = None
|
75
|
+
# Use existing AgentOps attribute manager
|
76
|
+
self._attribute_manager = SpanAttributeManager("xpander-service", "production")
|
77
|
+
|
78
|
+
def _get_session_id_from_agent(self, agent) -> str:
|
79
|
+
"""Generate consistent session ID from agent."""
|
80
|
+
# First try to get memory_thread_id from agent context if available
|
81
|
+
if hasattr(agent, "memory_thread_id"):
|
82
|
+
return f"session_{agent.memory_thread_id}"
|
83
|
+
|
84
|
+
# Check for execution context
|
85
|
+
if hasattr(agent, "execution") and hasattr(agent.execution, "memory_thread_id"):
|
86
|
+
return f"session_{agent.execution.memory_thread_id}"
|
87
|
+
|
88
|
+
# Fallback to agent-based ID
|
89
|
+
agent_name = getattr(agent, "name", "unknown")
|
90
|
+
agent_id = getattr(agent, "id", "unknown")
|
91
|
+
return f"agent_{agent_name}_{agent_id}"
|
92
|
+
|
93
|
+
def _extract_session_id(self, execution, agent=None) -> str:
|
94
|
+
"""Extract session ID from execution data."""
|
95
|
+
if isinstance(execution, dict):
|
96
|
+
if "memory_thread_id" in execution:
|
97
|
+
return f"session_{execution['memory_thread_id']}"
|
98
|
+
elif "thread_id" in execution:
|
99
|
+
return f"session_{execution['thread_id']}"
|
100
|
+
elif "session_id" in execution:
|
101
|
+
return f"session_{execution['session_id']}"
|
102
|
+
|
103
|
+
# Fallback to agent-based ID if available
|
104
|
+
if agent:
|
105
|
+
return self._get_session_id_from_agent(agent)
|
106
|
+
|
107
|
+
# Last resort fallback
|
108
|
+
return f"session_{int(time.time())}"
|
109
|
+
|
110
|
+
def _extract_tool_name(self, tool_call) -> str:
|
111
|
+
"""Extract tool name from tool call."""
|
112
|
+
# Handle different tool call formats
|
113
|
+
if hasattr(tool_call, "function_name"):
|
114
|
+
return tool_call.function_name
|
115
|
+
elif hasattr(tool_call, "function") and hasattr(tool_call.function, "name"):
|
116
|
+
return tool_call.function.name
|
117
|
+
elif hasattr(tool_call, "name"):
|
118
|
+
return tool_call.name
|
119
|
+
elif isinstance(tool_call, dict):
|
120
|
+
if "function" in tool_call:
|
121
|
+
return tool_call["function"].get("name", "unknown")
|
122
|
+
elif "function_name" in tool_call:
|
123
|
+
return tool_call["function_name"]
|
124
|
+
elif "name" in tool_call:
|
125
|
+
return tool_call["name"]
|
126
|
+
|
127
|
+
# Try to extract from string representation
|
128
|
+
import re
|
129
|
+
|
130
|
+
patterns = [
|
131
|
+
r'function[\'"]\s*:\s*[\'"]([^\'"]+)[\'"]',
|
132
|
+
r'name[\'"]\s*:\s*[\'"]([^\'"]+)[\'"]',
|
133
|
+
r"([a-zA-Z_][a-zA-Z0-9_]*)\.tool",
|
134
|
+
r'function_name[\'"]\s*:\s*[\'"]([^\'"]+)[\'"]',
|
135
|
+
]
|
136
|
+
|
137
|
+
tool_str = str(tool_call)
|
138
|
+
for pattern in patterns:
|
139
|
+
match = re.search(pattern, tool_str, re.IGNORECASE)
|
140
|
+
if match:
|
141
|
+
return match.group(1)
|
142
|
+
|
143
|
+
return "unknown"
|
144
|
+
|
145
|
+
def _extract_tool_params(self, tool_call) -> dict:
|
146
|
+
"""Extract tool parameters from tool call."""
|
147
|
+
# Handle different parameter formats
|
148
|
+
if hasattr(tool_call, "function") and hasattr(tool_call.function, "arguments"):
|
149
|
+
try:
|
150
|
+
args = tool_call.function.arguments
|
151
|
+
if isinstance(args, str):
|
152
|
+
return json.loads(args)
|
153
|
+
elif isinstance(args, dict):
|
154
|
+
return args
|
155
|
+
except (json.JSONDecodeError, AttributeError):
|
156
|
+
pass
|
157
|
+
elif hasattr(tool_call, "arguments"):
|
158
|
+
try:
|
159
|
+
args = tool_call.arguments
|
160
|
+
if isinstance(args, str):
|
161
|
+
return json.loads(args)
|
162
|
+
elif isinstance(args, dict):
|
163
|
+
return args
|
164
|
+
except (json.JSONDecodeError, AttributeError):
|
165
|
+
pass
|
166
|
+
elif isinstance(tool_call, dict):
|
167
|
+
if "function" in tool_call:
|
168
|
+
args = tool_call["function"].get("arguments", "{}")
|
169
|
+
try:
|
170
|
+
return json.loads(args) if isinstance(args, str) else args
|
171
|
+
except json.JSONDecodeError:
|
172
|
+
pass
|
173
|
+
elif "arguments" in tool_call:
|
174
|
+
args = tool_call["arguments"]
|
175
|
+
try:
|
176
|
+
return json.loads(args) if isinstance(args, str) else args
|
177
|
+
except json.JSONDecodeError:
|
178
|
+
pass
|
179
|
+
|
180
|
+
return {}
|
181
|
+
|
182
|
+
def _extract_llm_data_from_messages(self, messages) -> dict:
|
183
|
+
"""Extract LLM metadata from messages."""
|
184
|
+
data = {}
|
185
|
+
|
186
|
+
if isinstance(messages, dict):
|
187
|
+
# Direct model and usage fields
|
188
|
+
if "model" in messages:
|
189
|
+
data["model"] = messages["model"]
|
190
|
+
if "usage" in messages:
|
191
|
+
data["usage"] = messages["usage"]
|
192
|
+
|
193
|
+
# Check in choices array (OpenAI format)
|
194
|
+
if "choices" in messages and messages["choices"]:
|
195
|
+
choice = messages["choices"][0]
|
196
|
+
if "message" in choice:
|
197
|
+
message = choice["message"]
|
198
|
+
if "model" in message:
|
199
|
+
data["model"] = message["model"]
|
200
|
+
|
201
|
+
elif isinstance(messages, list):
|
202
|
+
# Look for assistant messages with metadata
|
203
|
+
for msg in messages:
|
204
|
+
if isinstance(msg, dict) and msg.get("role") == "assistant":
|
205
|
+
if "model" in msg:
|
206
|
+
data["model"] = msg["model"]
|
207
|
+
if "usage" in msg:
|
208
|
+
data["usage"] = msg["usage"]
|
209
|
+
break
|
210
|
+
|
211
|
+
# Try to extract from any nested structures
|
212
|
+
if not data and hasattr(messages, "__dict__"):
|
213
|
+
msg_dict = messages.__dict__
|
214
|
+
if "model" in msg_dict:
|
215
|
+
data["model"] = msg_dict["model"]
|
216
|
+
if "usage" in msg_dict:
|
217
|
+
data["usage"] = msg_dict["usage"]
|
218
|
+
|
219
|
+
return data
|
220
|
+
|
221
|
+
def _extract_and_set_openai_message_attributes(self, span, messages, result, agent=None):
|
222
|
+
"""Extract and set OpenAI message attributes from messages and response."""
|
223
|
+
try:
|
224
|
+
# Manual extraction since we don't need the OpenAI module for this
|
225
|
+
# Try to get the agent's current message history for prompts
|
226
|
+
agent_messages = []
|
227
|
+
if agent and hasattr(agent, "messages"):
|
228
|
+
agent_messages = getattr(agent, "messages", [])
|
229
|
+
elif agent and hasattr(agent, "conversation_history"):
|
230
|
+
agent_messages = getattr(agent, "conversation_history", [])
|
231
|
+
elif agent and hasattr(agent, "history"):
|
232
|
+
agent_messages = getattr(agent, "history", [])
|
233
|
+
|
234
|
+
# Also try to extract messages from the messages parameter itself
|
235
|
+
if isinstance(messages, list):
|
236
|
+
# If messages is a list of messages, use it directly
|
237
|
+
agent_messages.extend(messages)
|
238
|
+
elif isinstance(messages, dict) and "messages" in messages:
|
239
|
+
# If messages contains a messages key
|
240
|
+
agent_messages.extend(messages.get("messages", []))
|
241
|
+
|
242
|
+
# Set prompt messages (input to LLM)
|
243
|
+
prompt_index = 0
|
244
|
+
for msg in agent_messages[-10:]: # Get last 10 messages to avoid huge context
|
245
|
+
if isinstance(msg, dict):
|
246
|
+
role = msg.get("role", "user")
|
247
|
+
content = msg.get("content", "")
|
248
|
+
|
249
|
+
# Handle different content formats
|
250
|
+
if content and isinstance(content, str) and content.strip():
|
251
|
+
safe_set_attribute(span, MessageAttributes.PROMPT_ROLE.format(i=prompt_index), role)
|
252
|
+
safe_set_attribute(
|
253
|
+
span, MessageAttributes.PROMPT_CONTENT.format(i=prompt_index), content[:2000]
|
254
|
+
)
|
255
|
+
prompt_index += 1
|
256
|
+
elif content and isinstance(content, list):
|
257
|
+
# Handle multi-modal content
|
258
|
+
content_str = str(content)[:2000]
|
259
|
+
safe_set_attribute(span, MessageAttributes.PROMPT_ROLE.format(i=prompt_index), role)
|
260
|
+
safe_set_attribute(span, MessageAttributes.PROMPT_CONTENT.format(i=prompt_index), content_str)
|
261
|
+
prompt_index += 1
|
262
|
+
elif hasattr(msg, "content"):
|
263
|
+
# Handle object with content attribute
|
264
|
+
content = getattr(msg, "content", "")
|
265
|
+
role = getattr(msg, "role", "user")
|
266
|
+
if content and isinstance(content, str) and content.strip():
|
267
|
+
safe_set_attribute(span, MessageAttributes.PROMPT_ROLE.format(i=prompt_index), role)
|
268
|
+
safe_set_attribute(
|
269
|
+
span, MessageAttributes.PROMPT_CONTENT.format(i=prompt_index), str(content)[:2000]
|
270
|
+
)
|
271
|
+
prompt_index += 1
|
272
|
+
|
273
|
+
# Set completion messages (response from LLM)
|
274
|
+
completion_index = 0
|
275
|
+
response_data = result if result else messages
|
276
|
+
|
277
|
+
# Handle different response formats
|
278
|
+
if isinstance(response_data, dict):
|
279
|
+
choices = response_data.get("choices", [])
|
280
|
+
for choice in choices:
|
281
|
+
message = choice.get("message", {})
|
282
|
+
role = message.get("role", "assistant")
|
283
|
+
content = message.get("content", "")
|
284
|
+
|
285
|
+
if content:
|
286
|
+
safe_set_attribute(span, MessageAttributes.COMPLETION_ROLE.format(i=completion_index), role)
|
287
|
+
safe_set_attribute(
|
288
|
+
span, MessageAttributes.COMPLETION_CONTENT.format(i=completion_index), content[:2000]
|
289
|
+
)
|
290
|
+
|
291
|
+
# Handle tool calls in the response
|
292
|
+
tool_calls = message.get("tool_calls", [])
|
293
|
+
for j, tool_call in enumerate(tool_calls):
|
294
|
+
tool_id = tool_call.get("id", "")
|
295
|
+
tool_name = tool_call.get("function", {}).get("name", "")
|
296
|
+
tool_args = tool_call.get("function", {}).get("arguments", "")
|
297
|
+
|
298
|
+
if tool_id:
|
299
|
+
safe_set_attribute(
|
300
|
+
span, MessageAttributes.COMPLETION_TOOL_CALL_ID.format(i=completion_index, j=j), tool_id
|
301
|
+
)
|
302
|
+
if tool_name:
|
303
|
+
safe_set_attribute(
|
304
|
+
span,
|
305
|
+
MessageAttributes.COMPLETION_TOOL_CALL_NAME.format(i=completion_index, j=j),
|
306
|
+
tool_name,
|
307
|
+
)
|
308
|
+
if tool_args:
|
309
|
+
safe_set_attribute(
|
310
|
+
span,
|
311
|
+
MessageAttributes.COMPLETION_TOOL_CALL_ARGUMENTS.format(i=completion_index, j=j),
|
312
|
+
tool_args[:500],
|
313
|
+
)
|
314
|
+
safe_set_attribute(
|
315
|
+
span,
|
316
|
+
MessageAttributes.COMPLETION_TOOL_CALL_TYPE.format(i=completion_index, j=j),
|
317
|
+
"function",
|
318
|
+
)
|
319
|
+
|
320
|
+
completion_index += 1
|
321
|
+
elif hasattr(response_data, "choices"):
|
322
|
+
# Handle response object with choices attribute
|
323
|
+
choices = getattr(response_data, "choices", [])
|
324
|
+
for choice in choices:
|
325
|
+
message = getattr(choice, "message", None)
|
326
|
+
if message:
|
327
|
+
role = getattr(message, "role", "assistant")
|
328
|
+
content = getattr(message, "content", "")
|
329
|
+
|
330
|
+
if content:
|
331
|
+
safe_set_attribute(span, MessageAttributes.COMPLETION_ROLE.format(i=completion_index), role)
|
332
|
+
safe_set_attribute(
|
333
|
+
span,
|
334
|
+
MessageAttributes.COMPLETION_CONTENT.format(i=completion_index),
|
335
|
+
str(content)[:2000],
|
336
|
+
)
|
337
|
+
|
338
|
+
# Handle tool calls
|
339
|
+
tool_calls = getattr(message, "tool_calls", [])
|
340
|
+
for j, tool_call in enumerate(tool_calls):
|
341
|
+
tool_id = getattr(tool_call, "id", "")
|
342
|
+
function = getattr(tool_call, "function", None)
|
343
|
+
if function:
|
344
|
+
tool_name = getattr(function, "name", "")
|
345
|
+
tool_args = getattr(function, "arguments", "")
|
346
|
+
|
347
|
+
if tool_id:
|
348
|
+
safe_set_attribute(
|
349
|
+
span,
|
350
|
+
MessageAttributes.COMPLETION_TOOL_CALL_ID.format(i=completion_index, j=j),
|
351
|
+
tool_id,
|
352
|
+
)
|
353
|
+
if tool_name:
|
354
|
+
safe_set_attribute(
|
355
|
+
span,
|
356
|
+
MessageAttributes.COMPLETION_TOOL_CALL_NAME.format(i=completion_index, j=j),
|
357
|
+
tool_name,
|
358
|
+
)
|
359
|
+
if tool_args:
|
360
|
+
safe_set_attribute(
|
361
|
+
span,
|
362
|
+
MessageAttributes.COMPLETION_TOOL_CALL_ARGUMENTS.format(
|
363
|
+
i=completion_index, j=j
|
364
|
+
),
|
365
|
+
str(tool_args)[:500],
|
366
|
+
)
|
367
|
+
safe_set_attribute(
|
368
|
+
span,
|
369
|
+
MessageAttributes.COMPLETION_TOOL_CALL_TYPE.format(i=completion_index, j=j),
|
370
|
+
"function",
|
371
|
+
)
|
372
|
+
|
373
|
+
completion_index += 1
|
374
|
+
|
375
|
+
except Exception as e:
|
376
|
+
logger.error(f"Error extracting OpenAI message attributes: {e}")
|
377
|
+
|
378
|
+
def _wrap_init_task(self, original_method):
|
379
|
+
"""Wrap init_task and add_task to create agent span hierarchy."""
|
380
|
+
instrumentor = self
|
381
|
+
|
382
|
+
def wrapper(self, execution=None, input=None, **kwargs):
|
383
|
+
# Normalize parameters - handle both add_task(input=...) and init_task(execution=...)
|
384
|
+
if execution is None and input is not None:
|
385
|
+
# add_task call with input parameter - normalize to execution format
|
386
|
+
if isinstance(input, str):
|
387
|
+
execution = {"input": {"text": input}}
|
388
|
+
else:
|
389
|
+
execution = {"input": input}
|
390
|
+
elif execution is None:
|
391
|
+
# Neither execution nor input provided - create empty execution
|
392
|
+
execution = {}
|
393
|
+
|
394
|
+
# Extract session ID and agent info
|
395
|
+
session_id = instrumentor._extract_session_id(execution)
|
396
|
+
agent_name = getattr(self, "name", "unknown")
|
397
|
+
agent_id = getattr(self, "id", "unknown")
|
398
|
+
|
399
|
+
# Check if session already exists
|
400
|
+
existing_session = instrumentor._context.get_session(session_id)
|
401
|
+
if existing_session:
|
402
|
+
# Session already exists, just continue
|
403
|
+
# Call with original parameters
|
404
|
+
if input is not None:
|
405
|
+
result = original_method(self, input=input, **kwargs)
|
406
|
+
else:
|
407
|
+
result = original_method(self, execution)
|
408
|
+
return result
|
409
|
+
|
410
|
+
# Extract task input
|
411
|
+
task_input = None
|
412
|
+
if isinstance(execution, dict):
|
413
|
+
if "input" in execution:
|
414
|
+
input_data = execution["input"]
|
415
|
+
if isinstance(input_data, dict) and "text" in input_data:
|
416
|
+
task_input = input_data["text"]
|
417
|
+
elif isinstance(input_data, str):
|
418
|
+
task_input = input_data
|
419
|
+
|
420
|
+
# Create top-level conversation/session span - this is the ROOT span
|
421
|
+
conversation_span_attributes = {
|
422
|
+
SpanAttributes.AGENTOPS_ENTITY_NAME: f"Session - {agent_name}",
|
423
|
+
"xpander.span.type": "session",
|
424
|
+
"xpander.session.name": f"Session - {agent_name}",
|
425
|
+
"xpander.agent.name": agent_name,
|
426
|
+
"xpander.agent.id": agent_id,
|
427
|
+
"xpander.session.id": session_id,
|
428
|
+
}
|
429
|
+
session_span, session_ctx, session_token = tracer.make_span(
|
430
|
+
operation_name=f"session.{agent_name}",
|
431
|
+
span_kind=SpanKind.AGENT, # Use AGENT kind for the root session span
|
432
|
+
attributes=conversation_span_attributes,
|
433
|
+
)
|
434
|
+
|
435
|
+
# Set task input on session span
|
436
|
+
if task_input:
|
437
|
+
safe_set_attribute(session_span, SpanAttributes.AGENTOPS_ENTITY_INPUT, task_input[:1000])
|
438
|
+
safe_set_attribute(session_span, "xpander.session.initial_input", task_input[:500])
|
439
|
+
|
440
|
+
# Create workflow span as child of session span (this will be the main execution span)
|
441
|
+
trace.set_span_in_context(session_span)
|
442
|
+
workflow_span_attributes = {
|
443
|
+
"xpander.span.type": "workflow",
|
444
|
+
"xpander.workflow.phase": "planning",
|
445
|
+
"xpander.agent.name": agent_name,
|
446
|
+
"xpander.agent.id": agent_id,
|
447
|
+
"xpander.session.id": session_id,
|
448
|
+
"agent.name": agent_name,
|
449
|
+
"agent.id": agent_id,
|
450
|
+
}
|
451
|
+
workflow_span, workflow_ctx, workflow_token = tracer.make_span(
|
452
|
+
operation_name=f"workflow.{agent_name}",
|
453
|
+
span_kind=SpanKind.WORKFLOW,
|
454
|
+
attributes=workflow_span_attributes,
|
455
|
+
)
|
456
|
+
|
457
|
+
# No separate agent span - workflow span contains all agent info
|
458
|
+
|
459
|
+
# Initialize workflow state with persistent spans
|
460
|
+
agent_info = {
|
461
|
+
"agent_name": agent_name,
|
462
|
+
"agent_id": agent_id,
|
463
|
+
"task_input": task_input,
|
464
|
+
"thread_id": execution.get("memory_thread_id") if isinstance(execution, dict) else None,
|
465
|
+
}
|
466
|
+
instrumentor._context.start_session(session_id, agent_info, workflow_span, None) # No agent span
|
467
|
+
# Store the session span as well
|
468
|
+
instrumentor._context.start_conversation(session_id, session_span)
|
469
|
+
|
470
|
+
try:
|
471
|
+
# Execute original method - don't end agent span here, it will be ended in retrieve_execution_result
|
472
|
+
# Call with original parameters
|
473
|
+
if input is not None:
|
474
|
+
result = original_method(self, input=input, **kwargs)
|
475
|
+
else:
|
476
|
+
result = original_method(self, execution)
|
477
|
+
return result
|
478
|
+
except Exception as e:
|
479
|
+
# Use existing AgentOps error handling utilities
|
480
|
+
_finish_span_error(workflow_span, e)
|
481
|
+
raise
|
482
|
+
|
483
|
+
return wrapper
|
484
|
+
|
485
|
+
def _wrap_run_tools(self, original_method):
|
486
|
+
"""Wrap run_tools to create execution phase tool spans."""
|
487
|
+
instrumentor = self
|
488
|
+
|
489
|
+
def wrapper(self, tool_calls, payload_extension=None):
|
490
|
+
session_id = instrumentor._get_session_id_from_agent(self)
|
491
|
+
current_session = instrumentor._context.get_session(session_id)
|
492
|
+
|
493
|
+
# Update workflow state
|
494
|
+
step_num = (current_session.get("step_count", 0) + 1) if current_session else 1
|
495
|
+
instrumentor._context.update_session(
|
496
|
+
session_id,
|
497
|
+
{
|
498
|
+
"step_count": step_num,
|
499
|
+
"phase": "executing",
|
500
|
+
"tools_executed": (current_session.get("tools_executed", []) if current_session else [])
|
501
|
+
+ [instrumentor._extract_tool_name(tc) for tc in tool_calls],
|
502
|
+
},
|
503
|
+
)
|
504
|
+
|
505
|
+
# Get current span context (should be the LLM span)
|
506
|
+
current_span = trace.get_current_span()
|
507
|
+
|
508
|
+
# Create execution phase span as child of current LLM span
|
509
|
+
execution_span_context = trace.set_span_in_context(current_span) if current_span else None
|
510
|
+
|
511
|
+
with instrumentor._tracer.start_as_current_span(
|
512
|
+
"xpander.execution",
|
513
|
+
kind=OTelSpanKind.INTERNAL,
|
514
|
+
context=execution_span_context,
|
515
|
+
attributes={
|
516
|
+
SpanAttributes.AGENTOPS_SPAN_KIND: SpanKind.TASK,
|
517
|
+
"xpander.span.type": "execution",
|
518
|
+
"xpander.workflow.phase": "executing",
|
519
|
+
"xpander.step.number": step_num,
|
520
|
+
"xpander.step.tool_count": len(tool_calls),
|
521
|
+
"xpander.session.id": session_id,
|
522
|
+
},
|
523
|
+
) as execution_span:
|
524
|
+
# Execute tools and create individual tool spans
|
525
|
+
results = []
|
526
|
+
conversation_finished = False
|
527
|
+
|
528
|
+
for i, tool_call in enumerate(tool_calls):
|
529
|
+
tool_name = instrumentor._extract_tool_name(tool_call)
|
530
|
+
tool_params = instrumentor._extract_tool_params(tool_call)
|
531
|
+
|
532
|
+
# Check if this is the conversation finish tool
|
533
|
+
if tool_name == "xpfinish-agent-execution-finished":
|
534
|
+
conversation_finished = True
|
535
|
+
|
536
|
+
start_time = time.time()
|
537
|
+
|
538
|
+
# Create tool span as child of execution span
|
539
|
+
tool_span_context = trace.set_span_in_context(execution_span)
|
540
|
+
|
541
|
+
with instrumentor._tracer.start_as_current_span(
|
542
|
+
f"tool.{tool_name}",
|
543
|
+
kind=OTelSpanKind.CLIENT,
|
544
|
+
context=tool_span_context,
|
545
|
+
attributes={
|
546
|
+
SpanAttributes.AGENTOPS_SPAN_KIND: SpanKind.TOOL,
|
547
|
+
ToolAttributes.TOOL_NAME: tool_name,
|
548
|
+
ToolAttributes.TOOL_PARAMETERS: str(tool_params)[:500],
|
549
|
+
"xpander.span.type": "tool",
|
550
|
+
"xpander.workflow.phase": "executing",
|
551
|
+
"xpander.tool.step": step_num,
|
552
|
+
"xpander.tool.index": i,
|
553
|
+
},
|
554
|
+
) as tool_span:
|
555
|
+
# Execute single tool
|
556
|
+
single_result = original_method(self, [tool_call], payload_extension)
|
557
|
+
results.extend(single_result)
|
558
|
+
|
559
|
+
# Record tool execution details
|
560
|
+
execution_time = time.time() - start_time
|
561
|
+
safe_set_attribute(tool_span, "xpander.tool.execution_time", execution_time)
|
562
|
+
|
563
|
+
# Add tool result if available
|
564
|
+
if single_result:
|
565
|
+
result_summary = f"Executed successfully with {len(single_result)} results"
|
566
|
+
safe_set_attribute(tool_span, "xpander.tool.result_summary", result_summary)
|
567
|
+
|
568
|
+
# Store actual result data using existing AgentOps utilities
|
569
|
+
try:
|
570
|
+
result_content = ""
|
571
|
+
|
572
|
+
for i, result_item in enumerate(single_result):
|
573
|
+
# Handle xpander_sdk.ToolCallResult objects specifically
|
574
|
+
if hasattr(result_item, "__class__") and "ToolCallResult" in str(type(result_item)):
|
575
|
+
# Extract the actual result content from ToolCallResult
|
576
|
+
try:
|
577
|
+
if hasattr(result_item, "result") and result_item.result is not None:
|
578
|
+
actual_result = result_item.result
|
579
|
+
if isinstance(actual_result, str):
|
580
|
+
result_content += actual_result[:1000] + "\n"
|
581
|
+
else:
|
582
|
+
result_content += safe_serialize(actual_result)[:1000] + "\n"
|
583
|
+
elif hasattr(result_item, "data") and result_item.data is not None:
|
584
|
+
result_content += safe_serialize(result_item.data)[:1000] + "\n"
|
585
|
+
else:
|
586
|
+
# Fallback: try to find any content attribute
|
587
|
+
for attr_name in ["content", "output", "value", "response"]:
|
588
|
+
if hasattr(result_item, attr_name):
|
589
|
+
attr_value = getattr(result_item, attr_name)
|
590
|
+
if attr_value is not None:
|
591
|
+
result_content += safe_serialize(attr_value)[:1000] + "\n"
|
592
|
+
break
|
593
|
+
else:
|
594
|
+
# If no content attributes found, indicate this
|
595
|
+
result_content += "ToolCallResult object (no extractable content)\n"
|
596
|
+
except Exception as attr_e:
|
597
|
+
logger.debug(f"Error extracting from ToolCallResult: {attr_e}")
|
598
|
+
result_content += "ToolCallResult object (extraction failed)\n"
|
599
|
+
|
600
|
+
# Handle regular objects and primitives
|
601
|
+
elif isinstance(result_item, (str, int, float, bool)):
|
602
|
+
result_content += str(result_item)[:1000] + "\n"
|
603
|
+
elif hasattr(result_item, "__dict__"):
|
604
|
+
# Convert objects to dict using existing utility
|
605
|
+
result_dict = model_to_dict(result_item)
|
606
|
+
result_content += safe_serialize(result_dict)[:1000] + "\n"
|
607
|
+
else:
|
608
|
+
# Use safe_serialize for consistent conversion
|
609
|
+
result_content += safe_serialize(result_item)[:1000] + "\n"
|
610
|
+
|
611
|
+
if result_content.strip():
|
612
|
+
final_content = result_content.strip()[:2000]
|
613
|
+
safe_set_attribute(tool_span, ToolAttributes.TOOL_RESULT, final_content)
|
614
|
+
else:
|
615
|
+
safe_set_attribute(
|
616
|
+
tool_span, ToolAttributes.TOOL_RESULT, "No extractable content found"
|
617
|
+
)
|
618
|
+
|
619
|
+
except Exception as e:
|
620
|
+
logger.error(f"Error setting tool result: {e}")
|
621
|
+
safe_set_attribute(
|
622
|
+
tool_span, ToolAttributes.TOOL_RESULT, f"Error capturing result: {e}"
|
623
|
+
)
|
624
|
+
else:
|
625
|
+
safe_set_attribute(tool_span, "xpander.tool.result_summary", "No results returned")
|
626
|
+
|
627
|
+
# If conversation is finished, mark for session closure
|
628
|
+
if conversation_finished:
|
629
|
+
# Since session span is now the conversation span, we need to close all spans
|
630
|
+
# when the conversation finishes
|
631
|
+
pass # Session closure will be handled in retrieve_execution_result
|
632
|
+
|
633
|
+
return results
|
634
|
+
|
635
|
+
return wrapper
|
636
|
+
|
637
|
+
def _wrap_add_messages(self, original_method):
|
638
|
+
"""Wrap add_messages to create LLM spans with proper parent-child relationship."""
|
639
|
+
instrumentor = self
|
640
|
+
|
641
|
+
def wrapper(self, messages):
|
642
|
+
session_id = instrumentor._get_session_id_from_agent(self)
|
643
|
+
current_session = instrumentor._context.get_session(session_id)
|
644
|
+
current_phase = instrumentor._context.get_workflow_phase(session_id)
|
645
|
+
workflow_span = instrumentor._context.get_workflow_span(session_id)
|
646
|
+
|
647
|
+
# Create LLM span as child of workflow span (not conversation span)
|
648
|
+
# The hierarchy should be: session -> agent/workflow -> LLM -> execution -> tools
|
649
|
+
llm_span_context = trace.set_span_in_context(workflow_span) if workflow_span else None
|
650
|
+
|
651
|
+
# Call original method first to get the actual OpenAI response
|
652
|
+
result = original_method(self, messages)
|
653
|
+
|
654
|
+
# Now create a span that captures the LLM interaction with the actual response data
|
655
|
+
with instrumentor._tracer.start_as_current_span(
|
656
|
+
f"llm.{current_phase}",
|
657
|
+
kind=OTelSpanKind.CLIENT,
|
658
|
+
context=llm_span_context,
|
659
|
+
attributes={
|
660
|
+
SpanAttributes.AGENTOPS_SPAN_KIND: SpanKind.LLM,
|
661
|
+
"xpander.span.type": "llm",
|
662
|
+
"xpander.workflow.phase": current_phase,
|
663
|
+
"xpander.session.id": session_id,
|
664
|
+
},
|
665
|
+
) as llm_span:
|
666
|
+
# Extract and set OpenAI message data from the messages and response
|
667
|
+
instrumentor._extract_and_set_openai_message_attributes(llm_span, messages, result, self)
|
668
|
+
|
669
|
+
# Extract and set LLM metadata from the result if possible
|
670
|
+
llm_data = instrumentor._extract_llm_data_from_messages(result if result else messages)
|
671
|
+
if llm_data:
|
672
|
+
if "model" in llm_data:
|
673
|
+
safe_set_attribute(llm_span, SpanAttributes.LLM_REQUEST_MODEL, llm_data["model"])
|
674
|
+
safe_set_attribute(llm_span, SpanAttributes.LLM_RESPONSE_MODEL, llm_data["model"])
|
675
|
+
|
676
|
+
if "usage" in llm_data:
|
677
|
+
usage = llm_data["usage"]
|
678
|
+
if "prompt_tokens" in usage:
|
679
|
+
safe_set_attribute(llm_span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, usage["prompt_tokens"])
|
680
|
+
if "completion_tokens" in usage:
|
681
|
+
safe_set_attribute(
|
682
|
+
llm_span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, usage["completion_tokens"]
|
683
|
+
)
|
684
|
+
if "total_tokens" in usage:
|
685
|
+
safe_set_attribute(llm_span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, usage["total_tokens"])
|
686
|
+
# Update workflow state
|
687
|
+
instrumentor._context.update_session(
|
688
|
+
session_id,
|
689
|
+
{
|
690
|
+
"total_tokens": (current_session.get("total_tokens", 0) if current_session else 0)
|
691
|
+
+ usage["total_tokens"]
|
692
|
+
},
|
693
|
+
)
|
694
|
+
|
695
|
+
return result
|
696
|
+
|
697
|
+
return wrapper
|
698
|
+
|
699
|
+
def _wrap_is_finished(self, original_method):
|
700
|
+
"""Wrap is_finished to track workflow completion."""
|
701
|
+
instrumentor = self
|
702
|
+
|
703
|
+
def wrapper(self):
|
704
|
+
result = original_method(self)
|
705
|
+
|
706
|
+
if result:
|
707
|
+
session_id = instrumentor._get_session_id_from_agent(self)
|
708
|
+
|
709
|
+
# Update session to finished state
|
710
|
+
instrumentor._context.update_session(session_id, {"phase": "finished", "end_time": time.time()})
|
711
|
+
|
712
|
+
return result
|
713
|
+
|
714
|
+
return wrapper
|
715
|
+
|
716
|
+
def _wrap_extract_tool_calls(self, original_method):
|
717
|
+
"""Wrap extract_tool_calls to track tool planning."""
|
718
|
+
|
719
|
+
def wrapper(self, messages):
|
720
|
+
result = original_method(self, messages)
|
721
|
+
return result
|
722
|
+
|
723
|
+
return wrapper
|
724
|
+
|
725
|
+
def _wrap_report_execution_metrics(self, original_method):
|
726
|
+
"""Wrap report_execution_metrics to track metrics."""
|
727
|
+
|
728
|
+
def wrapper(self, llm_tokens=None, ai_model=None):
|
729
|
+
result = original_method(self, llm_tokens, ai_model)
|
730
|
+
return result
|
731
|
+
|
732
|
+
return wrapper
|
733
|
+
|
734
|
+
def _wrap_retrieve_execution_result(self, original_method):
|
735
|
+
"""Wrap retrieve_execution_result to finalize agent and workflow spans."""
|
736
|
+
instrumentor = self
|
737
|
+
|
738
|
+
def wrapper(self):
|
739
|
+
session_id = instrumentor._get_session_id_from_agent(self)
|
740
|
+
current_session = instrumentor._context.get_session(session_id)
|
741
|
+
workflow_span = instrumentor._context.get_workflow_span(session_id)
|
742
|
+
session_span = instrumentor._context.get_conversation_span(session_id) # This is now the root session span
|
743
|
+
|
744
|
+
try:
|
745
|
+
# Execute and capture result
|
746
|
+
result = original_method(self)
|
747
|
+
|
748
|
+
# Add workflow summary to the persistent workflow span
|
749
|
+
if workflow_span and current_session:
|
750
|
+
safe_set_attribute(
|
751
|
+
workflow_span, "xpander.workflow.total_steps", current_session.get("step_count", 0)
|
752
|
+
)
|
753
|
+
safe_set_attribute(
|
754
|
+
workflow_span, "xpander.workflow.total_tokens", current_session.get("total_tokens", 0)
|
755
|
+
)
|
756
|
+
safe_set_attribute(
|
757
|
+
workflow_span, "xpander.workflow.tools_used", len(current_session.get("tools_executed", []))
|
758
|
+
)
|
759
|
+
|
760
|
+
# Calculate total execution time
|
761
|
+
start_time = current_session.get("start_time", time.time())
|
762
|
+
execution_time = time.time() - start_time
|
763
|
+
safe_set_attribute(workflow_span, "xpander.workflow.execution_time", execution_time)
|
764
|
+
safe_set_attribute(workflow_span, "xpander.workflow.phase", "completed")
|
765
|
+
|
766
|
+
# Set result details on session and workflow spans
|
767
|
+
if result:
|
768
|
+
result_content = ""
|
769
|
+
if hasattr(result, "result"):
|
770
|
+
result_content = str(result.result)[:1000]
|
771
|
+
|
772
|
+
# Set on session span (root span)
|
773
|
+
if session_span and result_content:
|
774
|
+
safe_set_attribute(session_span, SpanAttributes.AGENTOPS_ENTITY_OUTPUT, result_content)
|
775
|
+
safe_set_attribute(session_span, "xpander.session.final_result", result_content)
|
776
|
+
if hasattr(result, "memory_thread_id"):
|
777
|
+
safe_set_attribute(session_span, "xpander.session.thread_id", result.memory_thread_id)
|
778
|
+
|
779
|
+
if workflow_span:
|
780
|
+
if result_content:
|
781
|
+
safe_set_attribute(workflow_span, "xpander.result.content", result_content)
|
782
|
+
if hasattr(result, "memory_thread_id"):
|
783
|
+
safe_set_attribute(workflow_span, "xpander.result.thread_id", result.memory_thread_id)
|
784
|
+
|
785
|
+
# Add session summary to session span
|
786
|
+
if session_span and current_session:
|
787
|
+
safe_set_attribute(
|
788
|
+
session_span, "xpander.session.total_steps", current_session.get("step_count", 0)
|
789
|
+
)
|
790
|
+
safe_set_attribute(
|
791
|
+
session_span, "xpander.session.total_tokens", current_session.get("total_tokens", 0)
|
792
|
+
)
|
793
|
+
safe_set_attribute(
|
794
|
+
session_span, "xpander.session.tools_used", len(current_session.get("tools_executed", []))
|
795
|
+
)
|
796
|
+
|
797
|
+
start_time = current_session.get("start_time", time.time())
|
798
|
+
execution_time = time.time() - start_time
|
799
|
+
safe_set_attribute(session_span, "xpander.session.execution_time", execution_time)
|
800
|
+
|
801
|
+
# Close all spans - session span should be closed last
|
802
|
+
if workflow_span:
|
803
|
+
_finish_span_success(workflow_span)
|
804
|
+
workflow_span.end()
|
805
|
+
|
806
|
+
if session_span:
|
807
|
+
_finish_span_success(session_span)
|
808
|
+
session_span.end()
|
809
|
+
|
810
|
+
return result
|
811
|
+
|
812
|
+
except Exception as e:
|
813
|
+
# Mark spans as failed and close them in proper order
|
814
|
+
if workflow_span:
|
815
|
+
_finish_span_error(workflow_span, e)
|
816
|
+
workflow_span.end()
|
817
|
+
|
818
|
+
if session_span:
|
819
|
+
_finish_span_error(session_span, e)
|
820
|
+
session_span.end()
|
821
|
+
raise
|
822
|
+
finally:
|
823
|
+
# Clean up session
|
824
|
+
instrumentor._context.end_session(session_id)
|
825
|
+
|
826
|
+
return wrapper
|
827
|
+
|
828
|
+
def _instrument(self, **kwargs):
|
829
|
+
"""Instrument the Xpander SDK."""
|
830
|
+
try:
|
831
|
+
# Import xpander modules
|
832
|
+
from xpander_sdk import Agent
|
833
|
+
|
834
|
+
# Set up tracing using existing AgentOps tracer
|
835
|
+
self._tracer = tracer.get_tracer()
|
836
|
+
# Attribute manager already initialized in __init__
|
837
|
+
|
838
|
+
# Wrap Agent methods
|
839
|
+
Agent.add_task = self._wrap_init_task(Agent.add_task)
|
840
|
+
Agent.init_task = self._wrap_init_task(Agent.init_task) # Also wrap init_task for completeness
|
841
|
+
Agent.run_tools = self._wrap_run_tools(Agent.run_tools)
|
842
|
+
Agent.add_messages = self._wrap_add_messages(Agent.add_messages)
|
843
|
+
Agent.is_finished = self._wrap_is_finished(Agent.is_finished)
|
844
|
+
Agent.extract_tool_calls = self._wrap_extract_tool_calls(Agent.extract_tool_calls)
|
845
|
+
Agent.report_execution_metrics = self._wrap_report_execution_metrics(Agent.report_execution_metrics)
|
846
|
+
Agent.retrieve_execution_result = self._wrap_retrieve_execution_result(Agent.retrieve_execution_result)
|
847
|
+
|
848
|
+
except ImportError:
|
849
|
+
logger.debug("Xpander SDK not available")
|
850
|
+
except Exception as e:
|
851
|
+
logger.error(f"Failed to instrument Xpander SDK: {e}")
|
852
|
+
|
853
|
+
def _uninstrument(self, **kwargs):
|
854
|
+
"""Uninstrument the Xpander SDK."""
|
855
|
+
pass
|
856
|
+
|
857
|
+
def _create_metrics(self, meter: Meter) -> StandardMetrics:
|
858
|
+
"""Create metrics for Xpander instrumentation."""
|
859
|
+
return StandardMetrics(
|
860
|
+
requests_active=meter.create_up_down_counter(
|
861
|
+
name="xpander_requests_active",
|
862
|
+
description="Number of active Xpander requests",
|
863
|
+
),
|
864
|
+
requests_duration=meter.create_histogram(
|
865
|
+
name="xpander_requests_duration",
|
866
|
+
description="Duration of Xpander requests",
|
867
|
+
unit="s",
|
868
|
+
),
|
869
|
+
requests_total=meter.create_counter(
|
870
|
+
name="xpander_requests_total",
|
871
|
+
description="Total number of Xpander requests",
|
872
|
+
),
|
873
|
+
requests_error=meter.create_counter(
|
874
|
+
name="xpander_requests_error",
|
875
|
+
description="Number of Xpander request errors",
|
876
|
+
),
|
877
|
+
)
|