mseep-agentops 0.4.18__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.
Files changed (94) hide show
  1. agentops/__init__.py +488 -0
  2. agentops/client/__init__.py +5 -0
  3. agentops/client/api/__init__.py +71 -0
  4. agentops/client/api/base.py +162 -0
  5. agentops/client/api/types.py +21 -0
  6. agentops/client/api/versions/__init__.py +10 -0
  7. agentops/client/api/versions/v3.py +65 -0
  8. agentops/client/api/versions/v4.py +104 -0
  9. agentops/client/client.py +211 -0
  10. agentops/client/http/__init__.py +0 -0
  11. agentops/client/http/http_adapter.py +116 -0
  12. agentops/client/http/http_client.py +215 -0
  13. agentops/config.py +268 -0
  14. agentops/enums.py +36 -0
  15. agentops/exceptions.py +38 -0
  16. agentops/helpers/__init__.py +44 -0
  17. agentops/helpers/dashboard.py +54 -0
  18. agentops/helpers/deprecation.py +50 -0
  19. agentops/helpers/env.py +52 -0
  20. agentops/helpers/serialization.py +137 -0
  21. agentops/helpers/system.py +178 -0
  22. agentops/helpers/time.py +11 -0
  23. agentops/helpers/version.py +36 -0
  24. agentops/instrumentation/__init__.py +598 -0
  25. agentops/instrumentation/common/__init__.py +82 -0
  26. agentops/instrumentation/common/attributes.py +278 -0
  27. agentops/instrumentation/common/instrumentor.py +147 -0
  28. agentops/instrumentation/common/metrics.py +100 -0
  29. agentops/instrumentation/common/objects.py +26 -0
  30. agentops/instrumentation/common/span_management.py +176 -0
  31. agentops/instrumentation/common/streaming.py +218 -0
  32. agentops/instrumentation/common/token_counting.py +177 -0
  33. agentops/instrumentation/common/version.py +71 -0
  34. agentops/instrumentation/common/wrappers.py +235 -0
  35. agentops/legacy/__init__.py +277 -0
  36. agentops/legacy/event.py +156 -0
  37. agentops/logging/__init__.py +4 -0
  38. agentops/logging/config.py +86 -0
  39. agentops/logging/formatters.py +34 -0
  40. agentops/logging/instrument_logging.py +91 -0
  41. agentops/sdk/__init__.py +27 -0
  42. agentops/sdk/attributes.py +151 -0
  43. agentops/sdk/core.py +607 -0
  44. agentops/sdk/decorators/__init__.py +51 -0
  45. agentops/sdk/decorators/factory.py +486 -0
  46. agentops/sdk/decorators/utility.py +216 -0
  47. agentops/sdk/exporters.py +87 -0
  48. agentops/sdk/processors.py +71 -0
  49. agentops/sdk/types.py +21 -0
  50. agentops/semconv/__init__.py +36 -0
  51. agentops/semconv/agent.py +29 -0
  52. agentops/semconv/core.py +19 -0
  53. agentops/semconv/enum.py +11 -0
  54. agentops/semconv/instrumentation.py +13 -0
  55. agentops/semconv/langchain.py +63 -0
  56. agentops/semconv/message.py +61 -0
  57. agentops/semconv/meters.py +24 -0
  58. agentops/semconv/resource.py +52 -0
  59. agentops/semconv/span_attributes.py +118 -0
  60. agentops/semconv/span_kinds.py +50 -0
  61. agentops/semconv/status.py +11 -0
  62. agentops/semconv/tool.py +15 -0
  63. agentops/semconv/workflow.py +69 -0
  64. agentops/validation.py +357 -0
  65. mseep_agentops-0.4.18.dist-info/METADATA +49 -0
  66. mseep_agentops-0.4.18.dist-info/RECORD +94 -0
  67. mseep_agentops-0.4.18.dist-info/WHEEL +5 -0
  68. mseep_agentops-0.4.18.dist-info/licenses/LICENSE +21 -0
  69. mseep_agentops-0.4.18.dist-info/top_level.txt +2 -0
  70. tests/__init__.py +0 -0
  71. tests/conftest.py +10 -0
  72. tests/unit/__init__.py +0 -0
  73. tests/unit/client/__init__.py +1 -0
  74. tests/unit/client/test_http_adapter.py +221 -0
  75. tests/unit/client/test_http_client.py +206 -0
  76. tests/unit/conftest.py +54 -0
  77. tests/unit/sdk/__init__.py +1 -0
  78. tests/unit/sdk/instrumentation_tester.py +207 -0
  79. tests/unit/sdk/test_attributes.py +392 -0
  80. tests/unit/sdk/test_concurrent_instrumentation.py +468 -0
  81. tests/unit/sdk/test_decorators.py +763 -0
  82. tests/unit/sdk/test_exporters.py +241 -0
  83. tests/unit/sdk/test_factory.py +1188 -0
  84. tests/unit/sdk/test_internal_span_processor.py +397 -0
  85. tests/unit/sdk/test_resource_attributes.py +35 -0
  86. tests/unit/test_config.py +82 -0
  87. tests/unit/test_context_manager.py +777 -0
  88. tests/unit/test_events.py +27 -0
  89. tests/unit/test_host_env.py +54 -0
  90. tests/unit/test_init_py.py +501 -0
  91. tests/unit/test_serialization.py +433 -0
  92. tests/unit/test_session.py +676 -0
  93. tests/unit/test_user_agent.py +34 -0
  94. tests/unit/test_validation.py +405 -0
@@ -0,0 +1,52 @@
1
+ """
2
+ Resource attribute semantic conventions for AgentOps.
3
+
4
+ This module defines standard resource attributes used to identify resources in
5
+ AgentOps telemetry data.
6
+ """
7
+
8
+
9
+ class ResourceAttributes:
10
+ """
11
+ Resource attributes for AgentOps.
12
+
13
+ These attributes provide standard identifiers for resources being monitored
14
+ or interacted with by AgentOps.
15
+ """
16
+
17
+ # Project identifier - uniquely identifies an AgentOps project
18
+ PROJECT_ID = "agentops.project.id"
19
+
20
+ # Service attributes
21
+ SERVICE_NAME = "service.name"
22
+ SERVICE_VERSION = "service.version"
23
+
24
+ # Environment attributes
25
+ ENVIRONMENT = "agentops.environment"
26
+ DEPLOYMENT_ENVIRONMENT = "deployment.environment"
27
+
28
+ # SDK attributes
29
+ SDK_NAME = "agentops.sdk.name"
30
+ SDK_VERSION = "agentops.sdk.version"
31
+
32
+ # Host machine attributes
33
+ HOST_MACHINE = "host.machine"
34
+ HOST_NAME = "host.name"
35
+ HOST_NODE = "host.node"
36
+ HOST_OS_RELEASE = "host.os_release"
37
+ HOST_PROCESSOR = "host.processor"
38
+ HOST_SYSTEM = "host.system"
39
+ HOST_VERSION = "host.version"
40
+
41
+ # CPU attributes
42
+ CPU_COUNT = "cpu.count"
43
+ CPU_PERCENT = "cpu.percent"
44
+
45
+ # Memory attributes
46
+ MEMORY_TOTAL = "memory.total"
47
+ MEMORY_AVAILABLE = "memory.available"
48
+ MEMORY_USED = "memory.used"
49
+ MEMORY_PERCENT = "memory.percent"
50
+
51
+ # Libraries
52
+ IMPORTED_LIBRARIES = "imported_libraries"
@@ -0,0 +1,118 @@
1
+ """Span attributes for OpenTelemetry semantic conventions."""
2
+
3
+
4
+ class SpanAttributes:
5
+ # Semantic Conventions for LLM requests based on OpenTelemetry Gen AI conventions
6
+ # Refer to https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md
7
+ #
8
+ # TODO: There is an important deviation from the OpenTelemetry spec in our current implementation.
9
+ # In our OpenAI instrumentation, we're mapping from source→target keys incorrectly in the _token_type function
10
+ # in shared/__init__.py. According to our established pattern, mapping dictionaries should consistently use
11
+ # target→source format (where keys are target attributes and values are source fields).
12
+ #
13
+ # Current implementation (incorrect):
14
+ # def _token_type(token_type: str):
15
+ # if token_type == "prompt_tokens": # source
16
+ # return "input" # target
17
+ #
18
+ # Correct implementation should be:
19
+ # token_type_mapping = {
20
+ # "input": "prompt_tokens", # target → source
21
+ # "output": "completion_tokens"
22
+ # }
23
+ #
24
+ # Then we have to adapt code using the function to handle the inverted mapping.
25
+
26
+ # System
27
+ LLM_SYSTEM = "gen_ai.system"
28
+
29
+ # Request attributes
30
+ LLM_REQUEST_MODEL = "gen_ai.request.model"
31
+ LLM_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens"
32
+ LLM_REQUEST_TEMPERATURE = "gen_ai.request.temperature"
33
+ LLM_REQUEST_TOP_P = "gen_ai.request.top_p"
34
+ LLM_REQUEST_TOP_K = "gen_ai.request.top_k"
35
+ LLM_REQUEST_SEED = "gen_ai.request.seed"
36
+ LLM_REQUEST_SYSTEM_INSTRUCTION = "gen_ai.request.system_instruction"
37
+ LLM_REQUEST_CANDIDATE_COUNT = "gen_ai.request.candidate_count"
38
+ LLM_REQUEST_STOP_SEQUENCES = "gen_ai.request.stop_sequences"
39
+ LLM_REQUEST_TYPE = "gen_ai.request.type"
40
+ LLM_REQUEST_STREAMING = "gen_ai.request.streaming"
41
+ LLM_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty"
42
+ LLM_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty"
43
+ LLM_REQUEST_FUNCTIONS = "gen_ai.request.functions"
44
+ LLM_REQUEST_HEADERS = "gen_ai.request.headers"
45
+ LLM_REQUEST_INSTRUCTIONS = "gen_ai.request.instructions"
46
+ LLM_REQUEST_VOICE = "gen_ai.request.voice"
47
+ LLM_REQUEST_SPEED = "gen_ai.request.speed"
48
+
49
+ # Content
50
+ LLM_PROMPTS = "gen_ai.prompt"
51
+ LLM_COMPLETIONS = "gen_ai.completion" # DO NOT SET THIS DIRECTLY
52
+ LLM_CONTENT_COMPLETION_CHUNK = "gen_ai.completion.chunk"
53
+
54
+ # Response attributes
55
+ LLM_RESPONSE_MODEL = "gen_ai.response.model"
56
+ LLM_RESPONSE_FINISH_REASON = "gen_ai.response.finish_reason"
57
+ LLM_RESPONSE_STOP_REASON = "gen_ai.response.stop_reason"
58
+ LLM_RESPONSE_ID = "gen_ai.response.id"
59
+
60
+ # Usage metrics
61
+ LLM_USAGE_COMPLETION_TOKENS = "gen_ai.usage.completion_tokens"
62
+ LLM_USAGE_PROMPT_TOKENS = "gen_ai.usage.prompt_tokens"
63
+ LLM_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens"
64
+ LLM_USAGE_CACHE_CREATION_INPUT_TOKENS = "gen_ai.usage.cache_creation_input_tokens"
65
+ LLM_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read_input_tokens"
66
+ LLM_USAGE_REASONING_TOKENS = "gen_ai.usage.reasoning_tokens"
67
+ LLM_USAGE_STREAMING_TOKENS = "gen_ai.usage.streaming_tokens"
68
+ LLM_USAGE_TOOL_COST = "gen_ai.usage.total_cost"
69
+
70
+ # Message attributes
71
+ # see ./message.py for message-related attributes
72
+
73
+ # Token type
74
+ LLM_TOKEN_TYPE = "gen_ai.token.type"
75
+
76
+ # User
77
+ LLM_USER = "gen_ai.user"
78
+
79
+ # OpenAI specific
80
+ LLM_OPENAI_RESPONSE_SYSTEM_FINGERPRINT = "gen_ai.openai.system_fingerprint"
81
+ LLM_OPENAI_RESPONSE_INSTRUCTIONS = "gen_ai.openai.instructions"
82
+ LLM_OPENAI_API_BASE = "gen_ai.openai.api_base"
83
+ LLM_OPENAI_API_VERSION = "gen_ai.openai.api_version"
84
+ LLM_OPENAI_API_TYPE = "gen_ai.openai.api_type"
85
+
86
+ # AgentOps specific attributes
87
+ AGENTOPS_ENTITY_OUTPUT = "agentops.entity.output"
88
+ AGENTOPS_ENTITY_INPUT = "agentops.entity.input"
89
+ AGENTOPS_SPAN_KIND = "agentops.span.kind"
90
+ AGENTOPS_ENTITY_NAME = "agentops.entity.name"
91
+ AGENTOPS_DECORATOR_SPEC = "agentops.{entity_kind}.spec"
92
+ AGENTOPS_DECORATOR_INPUT = "agentops.{entity_kind}.input"
93
+ AGENTOPS_DECORATOR_OUTPUT = "agentops.{entity_kind}.output"
94
+
95
+ # Operation attributes
96
+ OPERATION_NAME = "operation.name"
97
+ OPERATION_VERSION = "operation.version"
98
+
99
+ # Session/Trace attributes
100
+ AGENTOPS_SESSION_END_STATE = "agentops.session.end_state"
101
+
102
+ # Streaming-specific attributes
103
+ LLM_STREAMING_TIME_TO_FIRST_TOKEN = "gen_ai.streaming.time_to_first_token"
104
+ LLM_STREAMING_TIME_TO_GENERATE = "gen_ai.streaming.time_to_generate"
105
+ LLM_STREAMING_DURATION = "gen_ai.streaming_duration"
106
+ LLM_STREAMING_CHUNK_COUNT = "gen_ai.streaming.chunk_count"
107
+
108
+ # HTTP-specific attributes
109
+ HTTP_METHOD = "http.method"
110
+ HTTP_URL = "http.url"
111
+ HTTP_ROUTE = "http.route"
112
+ HTTP_STATUS_CODE = "http.status_code"
113
+ HTTP_REQUEST_HEADERS = "http.request.headers"
114
+ HTTP_RESPONSE_HEADERS = "http.response.headers"
115
+ HTTP_REQUEST_BODY = "http.request.body"
116
+ HTTP_RESPONSE_BODY = "http.response.body"
117
+ HTTP_USER_AGENT = "http.user_agent"
118
+ HTTP_REQUEST_ID = "http.request_id"
@@ -0,0 +1,50 @@
1
+ """Span kinds for AgentOps."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class AgentOpsSpanKindValues(Enum):
7
+ """Standard span kind values for AgentOps."""
8
+
9
+ WORKFLOW = "workflow"
10
+ SESSION = "session"
11
+ TASK = "task"
12
+ OPERATION = "operation"
13
+ AGENT = "agent"
14
+ TOOL = "tool"
15
+ LLM = "llm"
16
+ TEAM = "team"
17
+ CHAIN = "chain"
18
+ TEXT = "text"
19
+ GUARDRAIL = "guardrail"
20
+ HTTP = "http"
21
+ UNKNOWN = "unknown"
22
+
23
+
24
+ # Legacy SpanKind class for backward compatibility
25
+ class SpanKind:
26
+ """Legacy span kind definitions - use AgentOpsSpanKindValues instead."""
27
+
28
+ # Agent action kinds
29
+ AGENT_ACTION = "agent.action" # Agent performing an action
30
+ AGENT_THINKING = "agent.thinking" # Agent reasoning/planning
31
+ AGENT_DECISION = "agent.decision" # Agent making a decision
32
+
33
+ # LLM interaction kinds
34
+ LLM_CALL = "llm.call" # LLM API call
35
+
36
+ # Workflow kinds
37
+ WORKFLOW_STEP = "workflow.step" # Step in a workflow
38
+ WORKFLOW = AgentOpsSpanKindValues.WORKFLOW.value
39
+ SESSION = AgentOpsSpanKindValues.SESSION.value
40
+ TASK = AgentOpsSpanKindValues.TASK.value
41
+ OPERATION = AgentOpsSpanKindValues.OPERATION.value
42
+ AGENT = AgentOpsSpanKindValues.AGENT.value
43
+ TOOL = AgentOpsSpanKindValues.TOOL.value
44
+ LLM = AgentOpsSpanKindValues.LLM.value
45
+ TEAM = AgentOpsSpanKindValues.TEAM.value
46
+ UNKNOWN = AgentOpsSpanKindValues.UNKNOWN.value
47
+ CHAIN = AgentOpsSpanKindValues.CHAIN.value
48
+ TEXT = AgentOpsSpanKindValues.TEXT.value
49
+ GUARDRAIL = AgentOpsSpanKindValues.GUARDRAIL.value
50
+ HTTP = AgentOpsSpanKindValues.HTTP.value
@@ -0,0 +1,11 @@
1
+ """Status enumerations for spans."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class ToolStatus(Enum):
7
+ """Tool status values."""
8
+
9
+ EXECUTING = "executing"
10
+ SUCCEEDED = "succeeded"
11
+ FAILED = "failed"
@@ -0,0 +1,15 @@
1
+ """Attributes specific to tool spans."""
2
+
3
+
4
+ class ToolAttributes:
5
+ """Attributes specific to tool spans."""
6
+
7
+ # Identity
8
+ TOOL_ID = "tool.id" # Unique identifier for the tool
9
+ TOOL_NAME = "tool.name" # Name of the tool
10
+ TOOL_DESCRIPTION = "tool.description" # Description of the tool
11
+
12
+ # Execution
13
+ TOOL_PARAMETERS = "tool.parameters" # Parameters passed to the tool
14
+ TOOL_RESULT = "tool.result" # Result returned by the tool
15
+ TOOL_STATUS = "tool.status" # Status of tool execution
@@ -0,0 +1,69 @@
1
+ """Attributes specific to workflow spans."""
2
+
3
+
4
+ class WorkflowAttributes:
5
+ """Workflow specific attributes."""
6
+
7
+ # Core workflow attributes
8
+ WORKFLOW_NAME = "workflow.name" # Name of the workflow
9
+ WORKFLOW_TYPE = "workflow.type" # Type of workflow
10
+ WORKFLOW_ID = "workflow.workflow_id" # Unique identifier for the workflow instance
11
+ WORKFLOW_RUN_ID = "workflow.run_id" # Unique identifier for this workflow run
12
+ WORKFLOW_DESCRIPTION = "workflow.description" # Description of the workflow
13
+
14
+ # Input/Output
15
+ WORKFLOW_INPUT = "workflow.input" # Input to the workflow
16
+ WORKFLOW_INPUT_TYPE = "workflow.input.type" # Type of input to the workflow
17
+ WORKFLOW_OUTPUT = "workflow.output" # Output from the workflow
18
+ WORKFLOW_OUTPUT_TYPE = "workflow.output.type" # Type of output from the workflow
19
+ WORKFLOW_FINAL_OUTPUT = "workflow.final_output" # Final output of the workflow
20
+
21
+ # Workflow step attributes (only keep used ones)
22
+ WORKFLOW_STEP_TYPE = "workflow.step.type" # Type of workflow step
23
+ WORKFLOW_STEP_STATUS = "workflow.step.status" # Status of the workflow step
24
+
25
+ # Configuration
26
+ WORKFLOW_MAX_TURNS = "workflow.max_turns" # Maximum number of turns in a workflow
27
+ WORKFLOW_DEBUG_MODE = "workflow.debug_mode" # Whether debug mode is enabled
28
+ WORKFLOW_MONITORING = "workflow.monitoring" # Whether monitoring is enabled
29
+ WORKFLOW_TELEMETRY = "workflow.telemetry" # Whether telemetry is enabled
30
+
31
+ # Memory and Storage
32
+ WORKFLOW_MEMORY_TYPE = "workflow.memory_type" # Type of memory used by the workflow
33
+ WORKFLOW_STORAGE_TYPE = "workflow.storage_type" # Type of storage used by the workflow
34
+
35
+ # Session context (simplified)
36
+ WORKFLOW_SESSION_ID = "workflow.session_id" # Session ID for the workflow execution
37
+ WORKFLOW_SESSION_NAME = "workflow.session_name" # Session name for the workflow
38
+ WORKFLOW_USER_ID = "workflow.user_id" # User ID associated with the workflow
39
+ WORKFLOW_APP_ID = "workflow.app_id" # Application ID associated with the workflow
40
+
41
+ # Input metadata
42
+ WORKFLOW_INPUT_PARAMETER_COUNT = "workflow.input.parameter_count" # Number of input parameters
43
+ WORKFLOW_INPUT_PARAMETER_KEYS = "workflow.input.parameter_keys" # Keys of input parameters
44
+ WORKFLOW_METHOD_PARAMETER_COUNT = "workflow.method.parameter_count" # Number of method parameters
45
+ WORKFLOW_METHOD_RETURN_TYPE = "workflow.method.return_type" # Return type of the workflow method
46
+
47
+ # Output metadata (commonly used)
48
+ WORKFLOW_OUTPUT_CONTENT_TYPE = "workflow.output.content_type" # Content type of the output
49
+ WORKFLOW_OUTPUT_EVENT = "workflow.output.event" # Event type of the output
50
+ WORKFLOW_OUTPUT_MODEL = "workflow.output.model" # Model used for the output
51
+ WORKFLOW_OUTPUT_MODEL_PROVIDER = "workflow.output.model_provider" # Model provider for the output
52
+ WORKFLOW_OUTPUT_MESSAGE_COUNT = "workflow.output.message_count" # Number of messages in output
53
+ WORKFLOW_OUTPUT_TOOL_COUNT = "workflow.output.tool_count" # Number of tools in output
54
+ WORKFLOW_OUTPUT_IS_STREAMING = "workflow.output.is_streaming" # Whether output is streaming
55
+
56
+ # Media counts (used by agno)
57
+ WORKFLOW_OUTPUT_IMAGE_COUNT = "workflow.output.image_count" # Number of images in output
58
+ WORKFLOW_OUTPUT_VIDEO_COUNT = "workflow.output.video_count" # Number of videos in output
59
+ WORKFLOW_OUTPUT_AUDIO_COUNT = "workflow.output.audio_count" # Number of audio items in output
60
+
61
+ # Session-specific attributes (used by agno)
62
+ WORKFLOW_SESSION_WORKFLOW_ID = "workflow.session.workflow_id" # Workflow ID in session context
63
+ WORKFLOW_SESSION_USER_ID = "workflow.session.user_id" # User ID in session context
64
+ WORKFLOW_SESSION_STATE_KEYS = "workflow.session.state_keys" # Keys in session state
65
+ WORKFLOW_SESSION_STATE_SIZE = "workflow.session.state_size" # Size of session state
66
+ WORKFLOW_SESSION_STORAGE_TYPE = "workflow.session.storage_type" # Storage type for session
67
+ WORKFLOW_SESSION_RETURNED_SESSION_ID = "workflow.session.returned_session_id" # Returned session ID
68
+ WORKFLOW_SESSION_CREATED_AT = "workflow.session.created_at" # Session creation timestamp
69
+ WORKFLOW_SESSION_UPDATED_AT = "workflow.session.updated_at" # Session update timestamp
agentops/validation.py ADDED
@@ -0,0 +1,357 @@
1
+ """
2
+ AgentOps Validation Module
3
+
4
+ This module provides functions to validate that spans have been sent to AgentOps
5
+ using the public API. This is useful for testing and verification purposes.
6
+ """
7
+
8
+ import time
9
+ import requests
10
+ from typing import Optional, Dict, List, Any, Tuple
11
+
12
+ from agentops.logging import logger
13
+ from agentops.exceptions import ApiServerException
14
+
15
+
16
+ class ValidationError(Exception):
17
+ """Raised when span validation fails."""
18
+
19
+ pass
20
+
21
+
22
+ def get_jwt_token(api_key: Optional[str] = None) -> str:
23
+ """
24
+ Exchange API key for JWT token.
25
+
26
+ Args:
27
+ api_key: Optional API key. If not provided, uses AGENTOPS_API_KEY env var.
28
+
29
+ Returns:
30
+ JWT bearer token
31
+
32
+ Raises:
33
+ ApiServerException: If token exchange fails
34
+ """
35
+ if api_key is None:
36
+ from agentops import get_client
37
+
38
+ client = get_client()
39
+ if client and client.config.api_key:
40
+ api_key = client.config.api_key
41
+ else:
42
+ import os
43
+
44
+ api_key = os.getenv("AGENTOPS_API_KEY")
45
+ if not api_key:
46
+ raise ValueError("No API key provided and AGENTOPS_API_KEY environment variable not set")
47
+
48
+ try:
49
+ response = requests.post(
50
+ "https://api.agentops.ai/public/v1/auth/access_token", json={"api_key": api_key}, timeout=10
51
+ )
52
+ response.raise_for_status()
53
+ return response.json()["bearer"]
54
+ except requests.exceptions.RequestException as e:
55
+ raise ApiServerException(f"Failed to get JWT token: {e}")
56
+
57
+
58
+ def get_trace_details(trace_id: str, jwt_token: str) -> Dict[str, Any]:
59
+ """
60
+ Get trace details from AgentOps API.
61
+
62
+ Args:
63
+ trace_id: The trace ID to query
64
+ jwt_token: JWT authentication token
65
+
66
+ Returns:
67
+ Trace details including spans
68
+
69
+ Raises:
70
+ ApiServerException: If API request fails
71
+ """
72
+ try:
73
+ response = requests.get(
74
+ f"https://api.agentops.ai/public/v1/traces/{trace_id}",
75
+ headers={"Authorization": f"Bearer {jwt_token}"},
76
+ timeout=10,
77
+ )
78
+ response.raise_for_status()
79
+ return response.json()
80
+ except requests.exceptions.RequestException as e:
81
+ raise ApiServerException(f"Failed to get trace details: {e}")
82
+
83
+
84
+ def get_trace_metrics(trace_id: str, jwt_token: str) -> Dict[str, Any]:
85
+ """
86
+ Get trace metrics from AgentOps API.
87
+
88
+ Args:
89
+ trace_id: The trace ID to query
90
+ jwt_token: JWT authentication token
91
+
92
+ Returns:
93
+ Trace metrics including token counts and costs
94
+
95
+ Raises:
96
+ ApiServerException: If API request fails
97
+ """
98
+ try:
99
+ response = requests.get(
100
+ f"https://api.agentops.ai/public/v1/traces/{trace_id}/metrics",
101
+ headers={"Authorization": f"Bearer {jwt_token}"},
102
+ timeout=10,
103
+ )
104
+ response.raise_for_status()
105
+ return response.json()
106
+ except requests.exceptions.RequestException as e:
107
+ raise ApiServerException(f"Failed to get trace metrics: {e}")
108
+
109
+
110
+ def check_llm_spans(spans: List[Dict[str, Any]]) -> Tuple[bool, List[str]]:
111
+ """
112
+ Check if any LLM spans are present in the trace.
113
+
114
+ Args:
115
+ spans: List of span dictionaries
116
+
117
+ Returns:
118
+ Tuple of (has_llm_spans, llm_span_names)
119
+ """
120
+ llm_spans = []
121
+
122
+ for span in spans:
123
+ span_name = span.get("span_name", "unnamed_span")
124
+
125
+ # Check span attributes for LLM span kind
126
+ span_attributes = span.get("span_attributes", {})
127
+ is_llm_span = False
128
+
129
+ # If we have span_attributes, check them
130
+ if span_attributes:
131
+ # Try different possible structures for span kind
132
+ span_kind = None
133
+
134
+ # Structure 1: span_attributes.agentops.span.kind
135
+ if isinstance(span_attributes, dict):
136
+ agentops_attrs = span_attributes.get("agentops", {})
137
+ if isinstance(agentops_attrs, dict):
138
+ span_info = agentops_attrs.get("span", {})
139
+ if isinstance(span_info, dict):
140
+ span_kind = span_info.get("kind", "")
141
+
142
+ # Structure 2: Direct in span_attributes
143
+ if not span_kind and isinstance(span_attributes, dict):
144
+ # Try looking for agentops.span.kind as a flattened key
145
+ span_kind = span_attributes.get("agentops.span.kind", "")
146
+
147
+ # Structure 3: Look for SpanAttributes.AGENTOPS_SPAN_KIND
148
+ if not span_kind and isinstance(span_attributes, dict):
149
+ from agentops.semconv import SpanAttributes
150
+
151
+ span_kind = span_attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND, "")
152
+
153
+ # Check if this is an LLM span by span kind
154
+ is_llm_span = span_kind == "llm"
155
+
156
+ # Alternative check: Look for gen_ai.prompt or gen_ai.completion attributes
157
+ # These are standard semantic conventions for LLM spans
158
+ if not is_llm_span and isinstance(span_attributes, dict):
159
+ gen_ai_attrs = span_attributes.get("gen_ai", {})
160
+ if isinstance(gen_ai_attrs, dict):
161
+ # If we have prompt or completion data, it's an LLM span
162
+ if "prompt" in gen_ai_attrs or "completion" in gen_ai_attrs:
163
+ is_llm_span = True
164
+
165
+ # Check for LLM_REQUEST_TYPE attribute (used by provider instrumentations)
166
+ if not is_llm_span and isinstance(span_attributes, dict):
167
+ from agentops.semconv import SpanAttributes, LLMRequestTypeValues
168
+
169
+ # Check for LLM request type - try both gen_ai.* and llm.* prefixes
170
+ # The instrumentation sets gen_ai.* but the API might return llm.*
171
+ llm_request_type = span_attributes.get(SpanAttributes.LLM_REQUEST_TYPE, "")
172
+ if not llm_request_type:
173
+ # Try the llm.* prefix version
174
+ llm_request_type = span_attributes.get("llm.request.type", "")
175
+
176
+ # Check if it's a chat or completion request (the main LLM types)
177
+ if llm_request_type in [LLMRequestTypeValues.CHAT.value, LLMRequestTypeValues.COMPLETION.value]:
178
+ is_llm_span = True
179
+
180
+ if is_llm_span:
181
+ llm_spans.append(span_name)
182
+
183
+ return len(llm_spans) > 0, llm_spans
184
+
185
+
186
+ def validate_trace_spans(
187
+ trace_id: Optional[str] = None,
188
+ trace_context: Optional[Any] = None,
189
+ max_retries: int = 10,
190
+ retry_delay: float = 1.0,
191
+ check_llm: bool = True,
192
+ min_spans: int = 1,
193
+ api_key: Optional[str] = None,
194
+ ) -> Dict[str, Any]:
195
+ """
196
+ Validate that spans have been sent to AgentOps.
197
+
198
+ Args:
199
+ trace_id: Direct trace ID to validate
200
+ trace_context: TraceContext object from start_trace (alternative to trace_id)
201
+ max_retries: Maximum number of retries to wait for spans to appear
202
+ retry_delay: Delay between retries in seconds
203
+ check_llm: Whether to specifically check for LLM spans
204
+ min_spans: Minimum number of spans expected
205
+ api_key: Optional API key (uses environment variable if not provided)
206
+
207
+ Returns:
208
+ Dictionary containing validation results and metrics
209
+
210
+ Raises:
211
+ ValidationError: If validation fails
212
+ ValueError: If neither trace_id nor trace_context is provided
213
+ """
214
+ # Extract trace ID
215
+ if trace_id is None and trace_context is None:
216
+ # Try to get from current span
217
+ try:
218
+ from opentelemetry.trace import get_current_span
219
+
220
+ current_span = get_current_span()
221
+ if current_span and hasattr(current_span, "get_span_context"):
222
+ span_context = current_span.get_span_context()
223
+ if hasattr(span_context, "trace_id") and span_context.trace_id:
224
+ if isinstance(span_context.trace_id, int):
225
+ trace_id = format(span_context.trace_id, "032x")
226
+ else:
227
+ trace_id = str(span_context.trace_id)
228
+ except ImportError:
229
+ pass
230
+
231
+ elif trace_context is not None and trace_id is None:
232
+ # Extract from TraceContext
233
+ if hasattr(trace_context, "span") and trace_context.span:
234
+ span_context = trace_context.span.get_span_context()
235
+ if hasattr(span_context, "trace_id"):
236
+ if isinstance(span_context.trace_id, int):
237
+ trace_id = format(span_context.trace_id, "032x")
238
+ else:
239
+ trace_id = str(span_context.trace_id)
240
+
241
+ if trace_id is None:
242
+ raise ValueError("No trace ID found. Provide either trace_id or trace_context parameter.")
243
+
244
+ # Get JWT token
245
+ jwt_token = get_jwt_token(api_key)
246
+
247
+ logger.info(f"Validating spans for trace ID: {trace_id}")
248
+
249
+ for attempt in range(max_retries):
250
+ try:
251
+ # Get trace details
252
+ trace_details = get_trace_details(trace_id, jwt_token)
253
+ spans = trace_details.get("spans", [])
254
+
255
+ if len(spans) >= min_spans:
256
+ logger.info(f"Found {len(spans)} span(s) in trace")
257
+
258
+ # Prepare result
259
+ result = {
260
+ "trace_id": trace_id,
261
+ "span_count": len(spans),
262
+ "spans": spans,
263
+ "has_llm_spans": False,
264
+ "llm_span_names": [],
265
+ "metrics": None,
266
+ }
267
+
268
+ # Get metrics first - if we have token usage, we definitely have LLM spans
269
+ try:
270
+ metrics = get_trace_metrics(trace_id, jwt_token)
271
+ result["metrics"] = metrics
272
+
273
+ if metrics:
274
+ logger.info(
275
+ f"Trace metrics - Total tokens: {metrics.get('total_tokens', 0)}, "
276
+ f"Cost: ${metrics.get('total_cost', '0.0000')}"
277
+ )
278
+
279
+ # If we have token usage > 0, we definitely have LLM activity
280
+ if metrics.get("total_tokens", 0) > 0:
281
+ result["has_llm_spans"] = True
282
+ logger.info("LLM activity confirmed via token usage metrics")
283
+ except Exception as e:
284
+ logger.warning(f"Could not retrieve metrics: {e}")
285
+
286
+ # Check for LLM spans if requested and not already confirmed via metrics
287
+ if check_llm and not result["has_llm_spans"]:
288
+ has_llm_spans, llm_span_names = check_llm_spans(spans)
289
+ result["has_llm_spans"] = has_llm_spans
290
+ result["llm_span_names"] = llm_span_names
291
+
292
+ if has_llm_spans:
293
+ logger.info(f"Found LLM spans: {', '.join(llm_span_names)}")
294
+ else:
295
+ logger.warning("No LLM spans found via attribute inspection")
296
+
297
+ # Final validation
298
+ if check_llm and not result["has_llm_spans"]:
299
+ raise ValidationError(
300
+ f"No LLM activity detected in trace {trace_id}. "
301
+ f"Found spans: {[s.get('span_name', 'unnamed') for s in spans]}, "
302
+ f"Token usage: {result.get('metrics', {}).get('total_tokens', 0)}"
303
+ )
304
+
305
+ return result
306
+
307
+ else:
308
+ logger.debug(
309
+ f"Only {len(spans)} spans found, expected at least {min_spans}. "
310
+ f"Retrying... ({attempt + 1}/{max_retries})"
311
+ )
312
+
313
+ except ApiServerException as e:
314
+ logger.debug(f"API error during validation: {e}. Retrying... ({attempt + 1}/{max_retries})")
315
+
316
+ if attempt < max_retries - 1:
317
+ time.sleep(retry_delay)
318
+
319
+ raise ValidationError(
320
+ f"Validation failed for trace {trace_id} after {max_retries} attempts. "
321
+ f"Expected at least {min_spans} spans"
322
+ + (", including LLM activity" if check_llm else "")
323
+ + ". Please check that tracking is properly configured."
324
+ )
325
+
326
+
327
+ def print_validation_summary(result: Dict[str, Any]) -> None:
328
+ """
329
+ Print a user-friendly summary of validation results.
330
+
331
+ Args:
332
+ result: Validation result dictionary from validate_trace_spans
333
+ """
334
+ print("\n" + "=" * 50)
335
+ print("šŸ” AgentOps Span Validation Results")
336
+ print("=" * 50)
337
+
338
+ print(f"āœ… Found {result['span_count']} span(s) in trace")
339
+
340
+ if result.get("has_llm_spans"):
341
+ if result.get("llm_span_names"):
342
+ print(f"āœ… Found LLM spans: {', '.join(result['llm_span_names'])}")
343
+ else:
344
+ # LLM activity confirmed via metrics
345
+ print("āœ… LLM activity confirmed via token usage metrics")
346
+ elif "has_llm_spans" in result:
347
+ print("āš ļø No LLM activity detected")
348
+
349
+ if result.get("metrics"):
350
+ metrics = result["metrics"]
351
+ print("\nšŸ“Š Trace Metrics:")
352
+ print(f" - Total tokens: {metrics.get('total_tokens', 0)}")
353
+ print(f" - Prompt tokens: {metrics.get('prompt_tokens', 0)}")
354
+ print(f" - Completion tokens: {metrics.get('completion_tokens', 0)}")
355
+ print(f" - Total cost: ${metrics.get('total_cost', '0.0000')}")
356
+
357
+ print("\nāœ… Validation successful!")