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.
- agentops/__init__.py +488 -0
- agentops/client/__init__.py +5 -0
- agentops/client/api/__init__.py +71 -0
- agentops/client/api/base.py +162 -0
- agentops/client/api/types.py +21 -0
- agentops/client/api/versions/__init__.py +10 -0
- agentops/client/api/versions/v3.py +65 -0
- agentops/client/api/versions/v4.py +104 -0
- agentops/client/client.py +211 -0
- agentops/client/http/__init__.py +0 -0
- agentops/client/http/http_adapter.py +116 -0
- agentops/client/http/http_client.py +215 -0
- agentops/config.py +268 -0
- agentops/enums.py +36 -0
- agentops/exceptions.py +38 -0
- agentops/helpers/__init__.py +44 -0
- agentops/helpers/dashboard.py +54 -0
- agentops/helpers/deprecation.py +50 -0
- agentops/helpers/env.py +52 -0
- agentops/helpers/serialization.py +137 -0
- agentops/helpers/system.py +178 -0
- agentops/helpers/time.py +11 -0
- agentops/helpers/version.py +36 -0
- agentops/instrumentation/__init__.py +598 -0
- agentops/instrumentation/common/__init__.py +82 -0
- agentops/instrumentation/common/attributes.py +278 -0
- agentops/instrumentation/common/instrumentor.py +147 -0
- agentops/instrumentation/common/metrics.py +100 -0
- agentops/instrumentation/common/objects.py +26 -0
- agentops/instrumentation/common/span_management.py +176 -0
- agentops/instrumentation/common/streaming.py +218 -0
- agentops/instrumentation/common/token_counting.py +177 -0
- agentops/instrumentation/common/version.py +71 -0
- agentops/instrumentation/common/wrappers.py +235 -0
- agentops/legacy/__init__.py +277 -0
- agentops/legacy/event.py +156 -0
- agentops/logging/__init__.py +4 -0
- agentops/logging/config.py +86 -0
- agentops/logging/formatters.py +34 -0
- agentops/logging/instrument_logging.py +91 -0
- agentops/sdk/__init__.py +27 -0
- agentops/sdk/attributes.py +151 -0
- agentops/sdk/core.py +607 -0
- agentops/sdk/decorators/__init__.py +51 -0
- agentops/sdk/decorators/factory.py +486 -0
- agentops/sdk/decorators/utility.py +216 -0
- agentops/sdk/exporters.py +87 -0
- agentops/sdk/processors.py +71 -0
- agentops/sdk/types.py +21 -0
- agentops/semconv/__init__.py +36 -0
- agentops/semconv/agent.py +29 -0
- agentops/semconv/core.py +19 -0
- agentops/semconv/enum.py +11 -0
- agentops/semconv/instrumentation.py +13 -0
- agentops/semconv/langchain.py +63 -0
- agentops/semconv/message.py +61 -0
- agentops/semconv/meters.py +24 -0
- agentops/semconv/resource.py +52 -0
- agentops/semconv/span_attributes.py +118 -0
- agentops/semconv/span_kinds.py +50 -0
- agentops/semconv/status.py +11 -0
- agentops/semconv/tool.py +15 -0
- agentops/semconv/workflow.py +69 -0
- agentops/validation.py +357 -0
- mseep_agentops-0.4.18.dist-info/METADATA +49 -0
- mseep_agentops-0.4.18.dist-info/RECORD +94 -0
- mseep_agentops-0.4.18.dist-info/WHEEL +5 -0
- mseep_agentops-0.4.18.dist-info/licenses/LICENSE +21 -0
- mseep_agentops-0.4.18.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +10 -0
- tests/unit/__init__.py +0 -0
- tests/unit/client/__init__.py +1 -0
- tests/unit/client/test_http_adapter.py +221 -0
- tests/unit/client/test_http_client.py +206 -0
- tests/unit/conftest.py +54 -0
- tests/unit/sdk/__init__.py +1 -0
- tests/unit/sdk/instrumentation_tester.py +207 -0
- tests/unit/sdk/test_attributes.py +392 -0
- tests/unit/sdk/test_concurrent_instrumentation.py +468 -0
- tests/unit/sdk/test_decorators.py +763 -0
- tests/unit/sdk/test_exporters.py +241 -0
- tests/unit/sdk/test_factory.py +1188 -0
- tests/unit/sdk/test_internal_span_processor.py +397 -0
- tests/unit/sdk/test_resource_attributes.py +35 -0
- tests/unit/test_config.py +82 -0
- tests/unit/test_context_manager.py +777 -0
- tests/unit/test_events.py +27 -0
- tests/unit/test_host_env.py +54 -0
- tests/unit/test_init_py.py +501 -0
- tests/unit/test_serialization.py +433 -0
- tests/unit/test_session.py +676 -0
- tests/unit/test_user_agent.py +34 -0
- 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
|
agentops/semconv/tool.py
ADDED
@@ -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!")
|