Skip to content

Commit 1debc78

Browse files
committed
Merge branch 'development' into release-3.8.0
2 parents 25ddc11 + b6e9387 commit 1debc78

File tree

7 files changed

+352
-1
lines changed

7 files changed

+352
-1
lines changed

src/langtrace_python_sdk/constants/instrumentation/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"MILVUS": "Milvus",
4343
"GRAPHLIT": "Graphlit",
4444
"PHIDATA": "Phidata",
45+
"AGNO": "Agno",
4546
}
4647

4748
LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY = "langtrace_additional_attributes"

src/langtrace_python_sdk/instrumentation/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from .google_genai import GoogleGenaiInstrumentation
3232
from .graphlit import GraphlitInstrumentation
3333
from .phidata import PhiDataInstrumentation
34+
from .agno import AgnoInstrumentation
3435

3536
__all__ = [
3637
"AnthropicInstrumentation",
@@ -63,4 +64,5 @@
6364
"CrewaiToolsInstrumentation",
6465
"GraphlitInstrumentation",
6566
"PhiDataInstrumentation",
67+
"AgnoInstrumentation",
6668
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .instrumentation import AgnoInstrumentation
2+
3+
__all__ = [
4+
"AgnoInstrumentation",
5+
]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
Copyright (c) 2024 Scale3 Labs
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
18+
from opentelemetry.trace import get_tracer
19+
from wrapt import wrap_function_wrapper as _W
20+
from typing import Collection
21+
from importlib_metadata import version as v
22+
from .patch import patch_agent, patch_memory
23+
24+
class AgnoInstrumentation(BaseInstrumentor):
25+
def instrumentation_dependencies(self) -> Collection[str]:
26+
return ["agno >= 1.1.4"]
27+
28+
def _instrument(self, **kwargs):
29+
tracer_provider = kwargs.get("tracer_provider")
30+
tracer = get_tracer(__name__, "", tracer_provider)
31+
version = v("agno")
32+
33+
try:
34+
_W(
35+
"agno.agent.agent",
36+
"Agent.run",
37+
patch_agent("Agent.run", version, tracer),
38+
)
39+
_W(
40+
"agno.agent.agent",
41+
"Agent.arun",
42+
patch_agent("Agent.arun", version, tracer),
43+
)
44+
_W(
45+
"agno.agent.agent",
46+
"Agent._run",
47+
patch_agent("Agent._run", version, tracer),
48+
)
49+
_W(
50+
"agno.agent.agent",
51+
"Agent._arun",
52+
patch_agent("Agent._arun", version, tracer),
53+
)
54+
55+
_W(
56+
"agno.memory.agent",
57+
"AgentMemory.update_memory",
58+
patch_memory("AgentMemory.update_memory", version, tracer),
59+
)
60+
_W(
61+
"agno.memory.agent",
62+
"AgentMemory.aupdate_memory",
63+
patch_memory("AgentMemory.aupdate_memory", version, tracer),
64+
)
65+
_W(
66+
"agno.memory.agent",
67+
"AgentMemory.update_summary",
68+
patch_memory("AgentMemory.update_summary", version, tracer),
69+
)
70+
_W(
71+
"agno.memory.agent",
72+
"AgentMemory.aupdate_summary",
73+
patch_memory("AgentMemory.aupdate_summary", version, tracer),
74+
)
75+
76+
except Exception:
77+
pass
78+
79+
def _uninstrument(self, **kwargs):
80+
pass
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import json
2+
from importlib_metadata import version as v
3+
from langtrace.trace_attributes import FrameworkSpanAttributes
4+
from opentelemetry import baggage
5+
from opentelemetry.trace import Span, SpanKind, Tracer
6+
from opentelemetry.trace.status import Status, StatusCode
7+
from typing import Dict, Any, Optional
8+
9+
from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME
10+
from langtrace_python_sdk.constants.instrumentation.common import (
11+
LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY,
12+
SERVICE_PROVIDERS,
13+
)
14+
from langtrace_python_sdk.utils import set_span_attribute
15+
from langtrace_python_sdk.utils.llm import get_span_name, set_span_attributes
16+
from langtrace_python_sdk.utils.misc import serialize_args, serialize_kwargs
17+
18+
def _extract_metrics(metrics: Dict[str, Any]) -> Dict[str, Any]:
19+
"""Helper function to extract and format metrics"""
20+
formatted_metrics = {}
21+
22+
# Extract basic metrics
23+
for key in ['time', 'time_to_first_token', 'input_tokens', 'output_tokens',
24+
'prompt_tokens', 'completion_tokens', 'total_tokens']:
25+
if key in metrics:
26+
formatted_metrics[key] = metrics[key]
27+
28+
# Extract nested metric details if present
29+
if 'prompt_tokens_details' in metrics:
30+
formatted_metrics['prompt_tokens_details'] = metrics['prompt_tokens_details']
31+
if 'completion_tokens_details' in metrics:
32+
formatted_metrics['completion_tokens_details'] = metrics['completion_tokens_details']
33+
if 'tool_call_times' in metrics:
34+
formatted_metrics['tool_call_times'] = metrics['tool_call_times']
35+
36+
return formatted_metrics
37+
38+
def patch_memory(operation_name, version, tracer: Tracer):
39+
def traced_method(wrapped, instance, args, kwargs):
40+
service_provider = SERVICE_PROVIDERS["AGNO"]
41+
extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
42+
span_attributes = {
43+
"langtrace.sdk.name": "langtrace-python-sdk",
44+
"langtrace.service.name": service_provider,
45+
"langtrace.service.type": "framework",
46+
"langtrace.service.version": version,
47+
"langtrace.version": v(LANGTRACE_SDK_NAME),
48+
**(extra_attributes if extra_attributes is not None else {}),
49+
}
50+
51+
span_attributes.update({
52+
"agno.memory.type": type(instance).__name__,
53+
"agno.memory.create_session_summary": str(instance.create_session_summary),
54+
"agno.memory.create_user_memories": str(instance.create_user_memories),
55+
"agno.memory.retrieval": str(instance.retrieval)
56+
})
57+
58+
inputs = {}
59+
if len(args) > 0:
60+
inputs["args"] = serialize_args(*args)
61+
if len(kwargs) > 0:
62+
inputs["kwargs"] = serialize_kwargs(**kwargs)
63+
span_attributes["agno.memory.inputs"] = json.dumps(inputs)
64+
65+
attributes = FrameworkSpanAttributes(**span_attributes)
66+
67+
with tracer.start_as_current_span(
68+
get_span_name(operation_name), kind=SpanKind.CLIENT
69+
) as span:
70+
try:
71+
set_span_attributes(span, attributes)
72+
result = wrapped(*args, **kwargs)
73+
74+
if result is not None:
75+
set_span_attribute(span, "agno.memory.output", str(result))
76+
77+
if instance.summary is not None:
78+
set_span_attribute(span, "agno.memory.summary", str(instance.summary))
79+
if instance.memories is not None:
80+
set_span_attribute(span, "agno.memory.memories_count", str(len(instance.memories)))
81+
82+
span.set_status(Status(StatusCode.OK))
83+
return result
84+
85+
except Exception as err:
86+
span.record_exception(err)
87+
span.set_status(Status(StatusCode.ERROR, str(err)))
88+
raise
89+
90+
return traced_method
91+
92+
def patch_agent(operation_name, version, tracer: Tracer):
93+
def traced_method(wrapped, instance, args, kwargs):
94+
service_provider = SERVICE_PROVIDERS["AGNO"]
95+
extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
96+
span_attributes = {
97+
"langtrace.sdk.name": "langtrace-python-sdk",
98+
"langtrace.service.name": service_provider,
99+
"langtrace.service.type": "framework",
100+
"langtrace.service.version": version,
101+
"langtrace.version": v(LANGTRACE_SDK_NAME),
102+
**(extra_attributes if extra_attributes is not None else {}),
103+
}
104+
105+
attributes = FrameworkSpanAttributes(**span_attributes)
106+
107+
with tracer.start_as_current_span(
108+
get_span_name(operation_name), kind=SpanKind.CLIENT
109+
) as span:
110+
try:
111+
set_span_attributes(span, attributes)
112+
AgnoSpanAttributes(span=span, instance=instance)
113+
result_generator = wrapped(*args, **kwargs)
114+
115+
accumulated_content = ""
116+
current_tool_call = None
117+
response_metadata = None
118+
seen_tool_calls = set()
119+
120+
try:
121+
for response in result_generator:
122+
if not hasattr(response, 'to_dict'):
123+
yield response
124+
continue
125+
126+
if not response_metadata:
127+
response_metadata = {
128+
"run_id": response.run_id,
129+
"agent_id": response.agent_id,
130+
"session_id": response.session_id,
131+
"model": response.model,
132+
"content_type": response.content_type,
133+
}
134+
for key, value in response_metadata.items():
135+
if value is not None:
136+
set_span_attribute(span, f"agno.agent.{key}", str(value))
137+
138+
if response.content:
139+
accumulated_content += response.content
140+
set_span_attribute(span, "agno.agent.response", accumulated_content)
141+
142+
if response.messages:
143+
for msg in response.messages:
144+
if msg.tool_calls:
145+
for tool_call in msg.tool_calls:
146+
tool_id = tool_call.get('id')
147+
if tool_id and tool_id not in seen_tool_calls:
148+
seen_tool_calls.add(tool_id)
149+
tool_info = {
150+
'id': tool_id,
151+
'name': tool_call.get('function', {}).get('name'),
152+
'arguments': tool_call.get('function', {}).get('arguments'),
153+
'start_time': msg.created_at,
154+
}
155+
current_tool_call = tool_info
156+
set_span_attribute(span, f"agno.agent.tool_call.{tool_id}", json.dumps(tool_info))
157+
158+
if msg.metrics:
159+
metrics = _extract_metrics(msg.metrics)
160+
role_prefix = f"agno.agent.metrics.{msg.role}"
161+
for key, value in metrics.items():
162+
set_span_attribute(span, f"{role_prefix}.{key}", str(value))
163+
164+
if response.tools:
165+
for tool in response.tools:
166+
tool_id = tool.get('tool_call_id')
167+
if tool_id and current_tool_call and current_tool_call['id'] == tool_id:
168+
tool_result = {
169+
**current_tool_call,
170+
'result': tool.get('content'),
171+
'error': tool.get('tool_call_error'),
172+
'end_time': tool.get('created_at'),
173+
'metrics': tool.get('metrics'),
174+
}
175+
set_span_attribute(span, f"agno.agent.tool_call.{tool_id}", json.dumps(tool_result))
176+
current_tool_call = None
177+
178+
yield response
179+
180+
except Exception as err:
181+
span.record_exception(err)
182+
span.set_status(Status(StatusCode.ERROR, str(err)))
183+
raise
184+
finally:
185+
span.set_status(Status(StatusCode.OK))
186+
if len(seen_tool_calls) > 0:
187+
span.set_attribute("agno.agent.total_tool_calls", len(seen_tool_calls))
188+
except Exception as err:
189+
span.record_exception(err)
190+
span.set_status(Status(StatusCode.ERROR, str(err)))
191+
raise
192+
193+
return traced_method
194+
195+
class AgnoSpanAttributes:
196+
span: Span
197+
agent_data: dict
198+
199+
def __init__(self, span: Span, instance) -> None:
200+
self.span = span
201+
self.instance = instance
202+
self.agent_data = {
203+
"memory": {},
204+
"model": {},
205+
"tools": [],
206+
}
207+
208+
self.run()
209+
210+
def run(self):
211+
instance_attrs = {
212+
"agent_id": self.instance.agent_id,
213+
"session_id": self.instance.session_id,
214+
"name": self.instance.name,
215+
"markdown": self.instance.markdown,
216+
"reasoning": self.instance.reasoning,
217+
"add_references": self.instance.add_references,
218+
"show_tool_calls": self.instance.show_tool_calls,
219+
"stream": self.instance.stream,
220+
"stream_intermediate_steps": self.instance.stream_intermediate_steps,
221+
}
222+
223+
for key, value in instance_attrs.items():
224+
if value is not None:
225+
set_span_attribute(self.span, f"agno.agent.{key}", str(value))
226+
227+
if self.instance.model:
228+
model_attrs = {
229+
"id": self.instance.model.id,
230+
"name": self.instance.model.name,
231+
"provider": self.instance.model.provider,
232+
"structured_outputs": self.instance.model.structured_outputs,
233+
"supports_structured_outputs": self.instance.model.supports_structured_outputs,
234+
}
235+
for key, value in model_attrs.items():
236+
if value is not None:
237+
set_span_attribute(self.span, f"agno.agent.model.{key}", str(value))
238+
239+
if hasattr(self.instance.model, 'metrics') and self.instance.model.metrics:
240+
metrics = _extract_metrics(self.instance.model.metrics)
241+
set_span_attribute(self.span, "agno.agent.model.metrics", json.dumps(metrics))
242+
243+
if self.instance.tools:
244+
tool_list = []
245+
for tool in self.instance.tools:
246+
if hasattr(tool, "name"):
247+
tool_list.append(tool.name)
248+
elif hasattr(tool, "__name__"):
249+
tool_list.append(tool.__name__)
250+
set_span_attribute(self.span, "agno.agent.tools", str(tool_list))
251+
252+
if self.instance.memory:
253+
memory_attrs = {
254+
"create_session_summary": self.instance.memory.create_session_summary,
255+
"create_user_memories": self.instance.memory.create_user_memories,
256+
"update_session_summary_after_run": self.instance.memory.update_session_summary_after_run,
257+
"update_user_memories_after_run": self.instance.memory.update_user_memories_after_run,
258+
}
259+
for key, value in memory_attrs.items():
260+
if value is not None:
261+
set_span_attribute(self.span, f"agno.agent.memory.{key}", str(value))

src/langtrace_python_sdk/langtrace.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
GoogleGenaiInstrumentation,
7676
GraphlitInstrumentation,
7777
PhiDataInstrumentation,
78+
AgnoInstrumentation,
7879
)
7980
from opentelemetry.util.re import parse_env_headers
8081
from sentry_sdk.types import Event, Hint
@@ -325,6 +326,7 @@ def init(
325326
"google-genai": GoogleGenaiInstrumentation(),
326327
"graphlit-client": GraphlitInstrumentation(),
327328
"phidata": PhiDataInstrumentation(),
329+
"agno": AgnoInstrumentation(),
328330
"mistralai": MistralInstrumentation(),
329331
"boto3": AWSBedrockInstrumentation(),
330332
"autogen": AutogenInstrumentation(),
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "3.7.0"
1+
__version__ = "3.8.0"

0 commit comments

Comments
 (0)