Skip to content

Commit 7a5a5c8

Browse files
committed
add phidata instrumentation
1 parent 7341490 commit 7a5a5c8

File tree

9 files changed

+385
-1
lines changed

9 files changed

+385
-1
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import asyncio
2+
from examples.phidata_example.agent import agent_run
3+
from langtrace_python_sdk import langtrace
4+
5+
langtrace.init()
6+
7+
class PhiDataRunner:
8+
def run(self):
9+
agent_run()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from langtrace_python_sdk import langtrace
2+
from phi.agent import Agent
3+
from phi.model.openai import OpenAIChat
4+
from phi.tools.duckduckgo import DuckDuckGo
5+
6+
langtrace.init()
7+
8+
def agent_run():
9+
web_agent = Agent(
10+
model=OpenAIChat(id="gpt-4o"),
11+
tools=[DuckDuckGo()],
12+
instructions=["Always include sources"],
13+
show_tool_calls=True,
14+
markdown=True,
15+
)
16+
web_agent.print_response("how do I get from lagos to nairobi, through kigali rwanda (staying a week) and how much would it cost on average?", stream=True)

src/langtrace_python_sdk/constants/instrumentation/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"CEREBRAS": "Cerebras",
4242
"MILVUS": "Milvus",
4343
"GRAPHLIT": "Graphlit",
44+
"PHIDATA": "Phidata",
4445
}
4546

4647
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
@@ -30,6 +30,7 @@
3030
from .milvus import MilvusInstrumentation
3131
from .google_genai import GoogleGenaiInstrumentation
3232
from .graphlit import GraphlitInstrumentation
33+
from .phidata import PhiDataInstrumentation
3334

3435
__all__ = [
3536
"AnthropicInstrumentation",
@@ -61,4 +62,5 @@
6162
"GoogleGenaiInstrumentation",
6263
"CrewaiToolsInstrumentation",
6364
"GraphlitInstrumentation",
65+
"PhiDataInstrumentation",
6466
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .instrumentation import PhiDataInstrumentation
2+
3+
__all__ = [
4+
"PhiDataInstrumentation",
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 PhiDataInstrumentation(BaseInstrumentor):
25+
def instrumentation_dependencies(self) -> Collection[str]:
26+
return ["phidata >= 2.7.10"] # Adjust version as needed
27+
28+
def _instrument(self, **kwargs):
29+
tracer_provider = kwargs.get("tracer_provider")
30+
tracer = get_tracer(__name__, "", tracer_provider)
31+
version = v("phidata")
32+
33+
try:
34+
_W(
35+
"phi.agent.agent",
36+
"Agent.run",
37+
patch_agent("Agent.run", version, tracer),
38+
)
39+
_W(
40+
"phi.agent.agent",
41+
"Agent.arun",
42+
patch_agent("Agent.arun", version, tracer),
43+
)
44+
_W(
45+
"phi.agent.agent",
46+
"Agent._run",
47+
patch_agent("Agent._run", version, tracer),
48+
)
49+
_W(
50+
"phi.agent.agent",
51+
"Agent._arun",
52+
patch_agent("Agent._arun", version, tracer),
53+
)
54+
55+
_W(
56+
"phi.memory.agent",
57+
"AgentMemory.update_memory",
58+
patch_memory("AgentMemory.update_memory", version, tracer),
59+
)
60+
_W(
61+
"phi.memory.agent",
62+
"AgentMemory.aupdate_memory",
63+
patch_memory("AgentMemory.aupdate_memory", version, tracer),
64+
)
65+
_W(
66+
"phi.memory.agent",
67+
"AgentMemory.update_summary",
68+
patch_memory("AgentMemory.update_summary", version, tracer),
69+
)
70+
_W(
71+
"phi.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: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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["PHIDATA"]
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+
"phidata.memory.type": type(instance).__name__,
53+
"phidata.memory.create_session_summary": str(instance.create_session_summary),
54+
"phidata.memory.create_user_memories": str(instance.create_user_memories),
55+
"phidata.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["phidata.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, "phidata.memory.output", str(result))
76+
77+
if instance.summary is not None:
78+
set_span_attribute(span, "phidata.memory.summary", str(instance.summary))
79+
if instance.memories is not None:
80+
set_span_attribute(span, "phidata.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["PHIDATA"]
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+
PhiDataSpanAttributes(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, 'model_dump'):
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"phidata.agent.{key}", str(value))
137+
138+
if response.content:
139+
accumulated_content += response.content
140+
set_span_attribute(span, "phidata.agent.accumulated_content", 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"phidata.agent.tool_call.{tool_id}", json.dumps(tool_info))
157+
158+
if msg.metrics:
159+
metrics = _extract_metrics(msg.metrics)
160+
role_prefix = f"phidata.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"phidata.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("phidata.agent.total_tool_calls", len(seen_tool_calls))
188+
189+
except Exception as err:
190+
span.record_exception(err)
191+
span.set_status(Status(StatusCode.ERROR, str(err)))
192+
raise
193+
194+
return traced_method
195+
196+
class PhiDataSpanAttributes:
197+
span: Span
198+
agent_data: dict
199+
200+
def __init__(self, span: Span, instance) -> None:
201+
self.span = span
202+
self.instance = instance
203+
self.agent_data = {
204+
"memory": {},
205+
"model": {},
206+
"tools": [],
207+
}
208+
209+
self.run()
210+
211+
def run(self):
212+
instance_attrs = {
213+
"agent_id": self.instance.agent_id,
214+
"session_id": self.instance.session_id,
215+
"name": self.instance.name,
216+
"markdown": self.instance.markdown,
217+
"reasoning": self.instance.reasoning,
218+
"add_references": self.instance.add_references,
219+
"show_tool_calls": self.instance.show_tool_calls,
220+
"stream": self.instance.stream,
221+
"stream_intermediate_steps": self.instance.stream_intermediate_steps,
222+
}
223+
224+
for key, value in instance_attrs.items():
225+
if value is not None:
226+
set_span_attribute(self.span, f"phidata.agent.{key}", str(value))
227+
228+
if self.instance.model:
229+
model_attrs = {
230+
"id": self.instance.model.id,
231+
"name": self.instance.model.name,
232+
"provider": self.instance.model.provider,
233+
"structured_outputs": self.instance.model.structured_outputs,
234+
"supports_structured_outputs": self.instance.model.supports_structured_outputs,
235+
}
236+
for key, value in model_attrs.items():
237+
if value is not None:
238+
set_span_attribute(self.span, f"phidata.agent.model.{key}", str(value))
239+
240+
if hasattr(self.instance.model, 'metrics') and self.instance.model.metrics:
241+
metrics = _extract_metrics(self.instance.model.metrics)
242+
set_span_attribute(self.span, "phidata.agent.model.metrics", json.dumps(metrics))
243+
244+
if self.instance.tools:
245+
tool_list = []
246+
for tool in self.instance.tools:
247+
if hasattr(tool, "name"):
248+
tool_list.append(tool.name)
249+
elif hasattr(tool, "__name__"):
250+
tool_list.append(tool.__name__)
251+
set_span_attribute(self.span, "phidata.agent.tools", str(tool_list))
252+
253+
if self.instance.memory:
254+
memory_attrs = {
255+
"create_session_summary": self.instance.memory.create_session_summary,
256+
"create_user_memories": self.instance.memory.create_user_memories,
257+
"update_session_summary_after_run": self.instance.memory.update_session_summary_after_run,
258+
"update_user_memories_after_run": self.instance.memory.update_user_memories_after_run,
259+
}
260+
for key, value in memory_attrs.items():
261+
if value is not None:
262+
set_span_attribute(self.span, f"phidata.agent.memory.{key}", str(value))

0 commit comments

Comments
 (0)