Skip to content

Commit b69634f

Browse files
authored
Merge branch 'main' into ag2_enhancement
2 parents 262c8bd + 83d333e commit b69634f

43 files changed

Lines changed: 923 additions & 2301 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/examples-integration-test.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,6 @@ jobs:
6767
- { path: 'examples/ag2/async_human_input.py', name: 'AG2 Async Human Input' }
6868
- { path: 'examples/ag2/tools_wikipedia_search.py', name: 'AG2 Wikipedia Search' }
6969

70-
# Context Manager examples
71-
- { path: 'examples/context_manager/basic_usage.py', name: 'Context Manager Basic' }
72-
- { path: 'examples/context_manager/error_handling.py', name: 'Context Manager Errors' }
73-
- { path: 'examples/context_manager/parallel_traces.py', name: 'Context Manager Parallel' }
74-
- { path: 'examples/context_manager/production_patterns.py', name: 'Context Manager Production' }
75-
7670
# Agno examples
7771
- { path: 'examples/agno/agno_async_operations.py', name: 'Agno Async Operations' }
7872
- { path: 'examples/agno/agno_basic_agents.py', name: 'Agno Basic Agents' }
@@ -84,7 +78,7 @@ jobs:
8478
- { path: 'examples/google_adk/human_approval.py', name: 'Google ADK Human Approval' }
8579

8680
# LlamaIndex examples
87-
- { path: 'examples/llamaindex/llamaindex_example.py', name: 'LlamaIndex' }
81+
# - { path: 'examples/llamaindex/llamaindex_example.py', name: 'LlamaIndex' }
8882

8983
# Mem0 examples
9084
- { path: 'examples/mem0/mem0_memoryclient_example.py', name: 'Mem0 Memory Client' }
@@ -157,6 +151,7 @@ jobs:
157151
LLAMA_API_KEY: ${{ secrets.LLAMA_API_KEY }}
158152
PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }}
159153
REPLICATE_API_TOKEN: ${{ secrets.REPLICATE_API_TOKEN }}
154+
PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }}
160155
PYTHONPATH: ${{ github.workspace }}
161156
run: |
162157
echo "Running ${{ matrix.example.name }}..."

agentops/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from typing import List, Optional, Union, Dict, Any
2727
from agentops.client import Client
2828
from agentops.sdk.core import TraceContext, tracer
29-
from agentops.sdk.decorators import trace, session, agent, task, workflow, operation, tool, guardrail
29+
from agentops.sdk.decorators import trace, session, agent, task, workflow, operation, tool, guardrail, track_endpoint
3030
from agentops.enums import TraceState, SUCCESS, ERROR, UNSET
3131
from opentelemetry.trace.status import StatusCode
3232

@@ -90,6 +90,7 @@ def init(
9090
env_data_opt_out: Optional[bool] = None,
9191
log_level: Optional[Union[str, int]] = None,
9292
fail_safe: Optional[bool] = None,
93+
log_session_replay_url: Optional[bool] = None,
9394
exporter_endpoint: Optional[str] = None,
9495
**kwargs,
9596
):
@@ -117,6 +118,7 @@ def init(
117118
env_data_opt_out (bool): Whether to opt out of collecting environment data.
118119
log_level (str, int): The log level to use for the client. Defaults to 'CRITICAL'.
119120
fail_safe (bool): Whether to suppress errors and continue execution when possible.
121+
log_session_replay_url (bool): Whether to log session replay URLs to the console. Defaults to True.
120122
exporter_endpoint (str, optional): Endpoint for the exporter. If none is provided, key will
121123
be read from the AGENTOPS_EXPORTER_ENDPOINT environment variable.
122124
**kwargs: Additional configuration parameters to be passed to the client.
@@ -159,6 +161,7 @@ def init(
159161
"env_data_opt_out": env_data_opt_out,
160162
"log_level": log_level,
161163
"fail_safe": fail_safe,
164+
"log_session_replay_url": log_session_replay_url,
162165
"exporter_endpoint": exporter_endpoint,
163166
**kwargs,
164167
}
@@ -472,6 +475,7 @@ def extract_key_from_attr(attr_value: str) -> str:
472475
"operation",
473476
"tool",
474477
"guardrail",
478+
"track_endpoint",
475479
# Enums
476480
"TraceState",
477481
"SUCCESS",

agentops/client/api/versions/v3.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from agentops.client.api.types import AuthTokenResponse
99
from agentops.exceptions import ApiServerException
1010
from agentops.logging import logger
11+
from termcolor import colored
1112

1213

1314
class V3Client(BaseApiClient):
@@ -47,6 +48,15 @@ def fetch_auth_token(self, api_key: str) -> AuthTokenResponse:
4748
if not token:
4849
raise ApiServerException("No token in authentication response")
4950

51+
# Check project premium status
52+
if jr.get("project_prem_status") != "pro":
53+
logger.info(
54+
colored(
55+
"\x1b[34mYou're on the agentops free plan 🤔\x1b[0m",
56+
"blue",
57+
)
58+
)
59+
5060
return jr
5161
except Exception as e:
5262
logger.error(f"Failed to process authentication response: {str(e)}")

agentops/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class ConfigDict(TypedDict):
3131
log_level: Optional[Union[str, int]]
3232
fail_safe: Optional[bool]
3333
prefetch_jwt_token: Optional[bool]
34+
log_session_replay_url: Optional[bool]
3435

3536

3637
@dataclass
@@ -115,6 +116,11 @@ class Config:
115116
metadata={"description": "Whether to prefetch JWT token during initialization"},
116117
)
117118

119+
log_session_replay_url: bool = field(
120+
default_factory=lambda: get_env_bool("AGENTOPS_LOG_SESSION_REPLAY_URL", True),
121+
metadata={"description": "Whether to log session replay URLs to the console"},
122+
)
123+
118124
exporter_endpoint: Optional[str] = field(
119125
default_factory=lambda: os.getenv("AGENTOPS_EXPORTER_ENDPOINT", "https://otlp.agentops.ai/v1/traces"),
120126
metadata={
@@ -148,6 +154,7 @@ def configure(
148154
log_level: Optional[Union[str, int]] = None,
149155
fail_safe: Optional[bool] = None,
150156
prefetch_jwt_token: Optional[bool] = None,
157+
log_session_replay_url: Optional[bool] = None,
151158
exporter: Optional[SpanExporter] = None,
152159
processor: Optional[SpanProcessor] = None,
153160
exporter_endpoint: Optional[str] = None,
@@ -213,6 +220,9 @@ def configure(
213220
if prefetch_jwt_token is not None:
214221
self.prefetch_jwt_token = prefetch_jwt_token
215222

223+
if log_session_replay_url is not None:
224+
self.log_session_replay_url = log_session_replay_url
225+
216226
if exporter is not None:
217227
self.exporter = exporter
218228

@@ -243,6 +253,7 @@ def dict(self):
243253
"log_level": self.log_level,
244254
"fail_safe": self.fail_safe,
245255
"prefetch_jwt_token": self.prefetch_jwt_token,
256+
"log_session_replay_url": self.log_session_replay_url,
246257
"exporter": self.exporter,
247258
"processor": self.processor,
248259
"exporter_endpoint": self.exporter_endpoint,

agentops/helpers/dashboard.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ def log_trace_url(span: Union[Span, ReadableSpan], title: Optional[str] = None)
3939
4040
Args:
4141
span: The span to log the URL for.
42+
title: Optional title for the trace.
4243
"""
44+
from agentops import get_client
45+
46+
try:
47+
client = get_client()
48+
if not client.config.log_session_replay_url:
49+
return
50+
except Exception:
51+
return
52+
4353
session_url = get_trace_url(span)
4454
logger.info(colored(f"\x1b[34mSession Replay for {title} trace: {session_url}\x1b[0m", "blue"))

agentops/instrumentation/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def _uninstrument_providers():
206206
if instrumented_key and instrumented_key in PROVIDERS:
207207
try:
208208
instrumentor.uninstrument()
209-
logger.info(
209+
logger.debug(
210210
f"AgentOps: Uninstrumented provider: {instrumentor.__class__.__name__} (for package '{instrumented_key}') due to agentic library activation."
211211
)
212212
uninstrumented_any = True
@@ -247,19 +247,19 @@ def _should_instrument_package(package_name: str) -> bool:
247247
if _has_agentic_library:
248248
# An agentic library is already active.
249249
if is_target_agentic:
250-
logger.info(
250+
logger.debug(
251251
f"AgentOps: An agentic library is active. Skipping instrumentation for subsequent agentic library '{package_name}'."
252252
)
253253
return False
254254
if is_target_provider:
255-
logger.info(
255+
logger.debug(
256256
f"AgentOps: An agentic library is active. Skipping instrumentation for provider '{package_name}'."
257257
)
258258
return False
259259
else:
260260
# No agentic library is active yet.
261261
if is_target_agentic:
262-
logger.info(
262+
logger.debug(
263263
f"AgentOps: '{package_name}' is the first-targeted agentic library. Will uninstrument providers if any are/become active."
264264
)
265265
_uninstrument_providers()
@@ -351,7 +351,7 @@ def _perform_instrumentation(package_name: str):
351351
if concurrent_instrumentor is not None:
352352
concurrent_instrumentor._agentops_instrumented_package_key = "concurrent.futures"
353353
_active_instrumentors.append(concurrent_instrumentor)
354-
logger.info("AgentOps: Instrumented concurrent.futures as a dependency of mem0.")
354+
logger.debug("AgentOps: Instrumented concurrent.futures as a dependency of mem0.")
355355
except Exception as e:
356356
logger.debug(f"Could not instrument concurrent.futures for mem0: {e}")
357357
else:
@@ -413,7 +413,7 @@ def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(),
413413
if target_module_obj:
414414
is_sdk = _is_installed_package(target_module_obj, package_to_check)
415415
if not is_sdk:
416-
logger.info(
416+
logger.debug(
417417
f"AgentOps: Target '{package_to_check}' appears to be a local module/directory. Skipping AgentOps SDK instrumentation for it."
418418
)
419419
continue
@@ -488,7 +488,7 @@ def instrument_one(loader: InstrumentorLoader) -> Optional[BaseInstrumentor]:
488488
"""
489489
if not loader.should_activate:
490490
# This log is important for users to know why something wasn't instrumented.
491-
logger.info(
491+
logger.debug(
492492
f"AgentOps: Package '{loader.package_name or loader.module_name}' not found or version is less than minimum required ('{loader.min_version}'). Skipping instrumentation."
493493
)
494494
return None
@@ -497,7 +497,7 @@ def instrument_one(loader: InstrumentorLoader) -> Optional[BaseInstrumentor]:
497497
try:
498498
# Use the provider directly from the global tracer instance
499499
instrumentor.instrument(tracer_provider=tracer.provider)
500-
logger.info(
500+
logger.debug(
501501
f"AgentOps: Successfully instrumented '{loader.class_name}' for package '{loader.package_name or loader.module_name}'."
502502
)
503503
except Exception as e:

agentops/instrumentation/agentic/google_adk/patch.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,8 +360,10 @@ def extract_agent_attributes(instance):
360360
attributes["agent.instruction"] = instance.instruction
361361
if hasattr(instance, "tools"):
362362
for tool in instance.tools:
363-
attributes[ToolAttributes.TOOL_NAME] = tool.name
364-
attributes[ToolAttributes.TOOL_DESCRIPTION] = tool.description
363+
if hasattr(tool, "name"):
364+
attributes[ToolAttributes.TOOL_NAME] = tool.name
365+
if hasattr(tool, "description"):
366+
attributes[ToolAttributes.TOOL_DESCRIPTION] = tool.description
365367
if hasattr(instance, "output_key"):
366368
attributes["agent.output_key"] = instance.output_key
367369
# Subagents

agentops/instrumentation/providers/openai/stream_wrapper.py

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from agentops.logging import logger
1616
from agentops.instrumentation.common.wrappers import _with_tracer_wrapper
1717
from agentops.instrumentation.providers.openai.utils import is_metrics_enabled
18-
from agentops.instrumentation.providers.openai.wrappers.chat import handle_chat_attributes
18+
from agentops.instrumentation.providers.openai.wrappers.chat import handle_chat_attributes, _create_tool_span
1919
from agentops.semconv import SpanAttributes, LLMRequestTypeValues, MessageAttributes
2020

2121

@@ -192,30 +192,11 @@ def _finalize_stream(self) -> None:
192192
if self._finish_reason:
193193
self._span.set_attribute(MessageAttributes.COMPLETION_FINISH_REASON.format(i=0), self._finish_reason)
194194

195-
# Set tool calls
195+
# Create tool spans for each tool call
196196
if len(self._tool_calls) > 0:
197197
for idx, tool_call in self._tool_calls.items():
198-
# Only set attributes if values are not None
199-
if tool_call["id"] is not None:
200-
self._span.set_attribute(
201-
MessageAttributes.COMPLETION_TOOL_CALL_ID.format(i=0, j=idx), tool_call["id"]
202-
)
203-
204-
if tool_call["type"] is not None:
205-
self._span.set_attribute(
206-
MessageAttributes.COMPLETION_TOOL_CALL_TYPE.format(i=0, j=idx), tool_call["type"]
207-
)
208-
209-
if tool_call["function"]["name"] is not None:
210-
self._span.set_attribute(
211-
MessageAttributes.COMPLETION_TOOL_CALL_NAME.format(i=0, j=idx), tool_call["function"]["name"]
212-
)
213-
214-
if tool_call["function"]["arguments"] is not None:
215-
self._span.set_attribute(
216-
MessageAttributes.COMPLETION_TOOL_CALL_ARGUMENTS.format(i=0, j=idx),
217-
tool_call["function"]["arguments"],
218-
)
198+
# Create a child span for this tool call
199+
_create_tool_span(self._span, tool_call)
219200

220201
# Set usage if available from the API
221202
if self._usage is not None:
@@ -374,7 +355,7 @@ def chat_completion_stream_wrapper(tracer, wrapped, instance, args, kwargs):
374355
return OpenaiStreamWrapper(response, span, kwargs)
375356
else:
376357
# Handle non-streaming response
377-
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response)
358+
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span)
378359

379360
for key, value in response_attributes.items():
380361
if key not in request_attributes: # Avoid overwriting request attributes
@@ -439,7 +420,7 @@ async def async_chat_completion_stream_wrapper(tracer, wrapped, instance, args,
439420
return OpenAIAsyncStreamWrapper(response, span, kwargs)
440421
else:
441422
# Handle non-streaming response
442-
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response)
423+
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span)
443424

444425
for key, value in response_attributes.items():
445426
if key not in request_attributes: # Avoid overwriting request attributes

agentops/instrumentation/providers/openai/wrappers/chat.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,72 @@
88
import logging
99
from typing import Any, Dict, Optional, Tuple
1010

11+
from opentelemetry.trace import Span
12+
1113
from agentops.instrumentation.providers.openai.utils import is_openai_v1
1214
from agentops.instrumentation.providers.openai.wrappers.shared import (
1315
model_as_dict,
1416
should_send_prompts,
1517
)
1618
from agentops.instrumentation.common.attributes import AttributeMap
1719
from agentops.semconv import SpanAttributes, LLMRequestTypeValues
20+
from agentops.semconv.tool import ToolAttributes
21+
from agentops.semconv.span_kinds import AgentOpsSpanKindValues
22+
23+
from opentelemetry import context as context_api
24+
from opentelemetry.trace import SpanKind, Status, StatusCode, get_tracer
1825

1926
logger = logging.getLogger(__name__)
2027

2128
LLM_REQUEST_TYPE = LLMRequestTypeValues.CHAT
2229

2330

31+
def _create_tool_span(parent_span, tool_call_data):
32+
"""
33+
Create a distinct span for each tool call.
34+
35+
Args:
36+
parent_span: The parent LLM span
37+
tool_call_data: The tool call data dictionary
38+
"""
39+
# Get the tracer for this module
40+
tracer = get_tracer(__name__)
41+
42+
# Create a child span for the tool call
43+
with tracer.start_as_current_span(
44+
name=f"tool_call.{tool_call_data['function']['name']}",
45+
kind=SpanKind.INTERNAL,
46+
context=context_api.set_value("current_span", parent_span),
47+
) as tool_span:
48+
# Set the span kind to TOOL
49+
tool_span.set_attribute("agentops.span.kind", AgentOpsSpanKindValues.TOOL)
50+
51+
# Set tool-specific attributes
52+
tool_span.set_attribute(ToolAttributes.TOOL_NAME, tool_call_data["function"]["name"])
53+
tool_span.set_attribute(ToolAttributes.TOOL_PARAMETERS, tool_call_data["function"]["arguments"])
54+
tool_span.set_attribute("tool.call.id", tool_call_data["id"])
55+
tool_span.set_attribute("tool.call.type", tool_call_data["type"])
56+
57+
# Set status to OK for successful tool call creation
58+
tool_span.set_status(Status(StatusCode.OK))
59+
60+
2461
def handle_chat_attributes(
2562
args: Optional[Tuple] = None,
2663
kwargs: Optional[Dict] = None,
2764
return_value: Optional[Any] = None,
65+
span: Optional[Span] = None,
2866
) -> AttributeMap:
2967
"""Extract attributes from chat completion calls.
3068
3169
This function is designed to work with the common wrapper pattern,
3270
extracting attributes from the method arguments and return value.
71+
72+
Args:
73+
args: Method arguments (not used in this implementation)
74+
kwargs: Method keyword arguments
75+
return_value: Method return value
76+
span: The parent span for creating tool spans
3377
"""
3478
attributes = {
3579
SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value,
@@ -191,12 +235,20 @@ def handle_chat_attributes(
191235
# Tool calls
192236
if "tool_calls" in message:
193237
tool_calls = message["tool_calls"]
194-
if tool_calls: # Check if tool_calls is not None
238+
if tool_calls and span is not None:
195239
for i, tool_call in enumerate(tool_calls):
240+
# Convert tool_call to the format expected by _create_tool_span
196241
function = tool_call.get("function", {})
197-
attributes[f"{prefix}.tool_calls.{i}.id"] = tool_call.get("id")
198-
attributes[f"{prefix}.tool_calls.{i}.name"] = function.get("name")
199-
attributes[f"{prefix}.tool_calls.{i}.arguments"] = function.get("arguments")
242+
tool_call_data = {
243+
"id": tool_call.get("id", ""),
244+
"type": tool_call.get("type", "function"),
245+
"function": {
246+
"name": function.get("name", ""),
247+
"arguments": function.get("arguments", ""),
248+
},
249+
}
250+
# Create a child span for this tool call
251+
_create_tool_span(span, tool_call_data)
200252

201253
# Prompt filter results
202254
if "prompt_filter_results" in response_dict:

0 commit comments

Comments
 (0)