Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions agentops/sdk/decorators/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@
attributes[SpanAttributes.AGENTOPS_SPAN_KIND] = span_kind

# Add standard attributes
attributes["agentops.operation.name"] = operation_name
attributes[SpanAttributes.OPERATION_NAME] = operation_name
if version is not None:
attributes["agentops.operation.version"] = version
attributes[SpanAttributes.OPERATION_VERSION] = version

Check warning on line 124 in agentops/sdk/decorators/utility.py

View check run for this annotation

Codecov / codecov/patch

agentops/sdk/decorators/utility.py#L124

Added line #L124 was not covered by tests

# Get current context explicitly to debug it
current_context = context_api.get_current()
Expand Down Expand Up @@ -182,9 +182,9 @@
attributes[SpanAttributes.AGENTOPS_SPAN_KIND] = span_kind

# Add standard attributes
attributes["agentops.operation.name"] = operation_name
attributes[SpanAttributes.OPERATION_NAME] = operation_name
if version is not None:
attributes["agentops.operation.version"] = version
attributes[SpanAttributes.OPERATION_VERSION] = version

Check warning on line 187 in agentops/sdk/decorators/utility.py

View check run for this annotation

Codecov / codecov/patch

agentops/sdk/decorators/utility.py#L187

Added line #L187 was not covered by tests

# Get current context explicitly
current_context = context_api.get_current()
Expand Down
4 changes: 4 additions & 0 deletions agentops/semconv/span_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,7 @@ class SpanAttributes:
AGENTOPS_ENTITY_INPUT = "agentops.entity.input"
AGENTOPS_SPAN_KIND = "agentops.span.kind"
AGENTOPS_ENTITY_NAME = "agentops.entity.name"

# Operation attributes
OPERATION_NAME = "operation.name"
OPERATION_VERSION = "operation.version"
116 changes: 103 additions & 13 deletions tests/unit/sdk/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from opentelemetry import trace
from opentelemetry.sdk.trace import ReadableSpan

from agentops.sdk.decorators import agent, operation, session, workflow
from agentops.sdk.decorators import agent, operation, session, workflow, task
from agentops.semconv import SpanKind
from agentops.semconv.span_attributes import SpanAttributes
from agentops.semconv import SpanAttributes
from tests.unit.sdk.instrumentation_tester import InstrumentationTester


Expand Down Expand Up @@ -75,9 +76,9 @@ def test_session():
nested_operation = None

for span in operation_spans:
if span.attributes and span.attributes.get('agentops.operation.name') == 'main_operation':
if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'main_operation':
main_operation = span
elif span.attributes and span.attributes.get('agentops.operation.name') == 'nested_operation':
elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'nested_operation':
nested_operation = span

assert main_operation is not None, "main_operation span not found"
Expand Down Expand Up @@ -164,9 +165,9 @@ async def test_async_session():
nested_operation = None

for span in operation_spans:
if span.attributes and span.attributes.get('agentops.operation.name') == 'main_async_operation':
if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'main_async_operation':
main_operation = span
elif span.attributes and span.attributes.get('agentops.operation.name') == 'nested_async_operation':
elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'nested_async_operation':
nested_operation = span

assert main_operation is not None, "main_async_operation span not found"
Expand Down Expand Up @@ -255,9 +256,9 @@ def test_generator_session():
nested_operation = None

for span in operation_spans:
if span.attributes and span.attributes.get('agentops.operation.name') == 'main_generator_operation':
if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'main_generator_operation':
main_operation = span
elif span.attributes and span.attributes.get('agentops.operation.name') == 'nested_generator':
elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'nested_generator':
nested_operation = span

assert main_operation is not None, "main_generator_operation span not found"
Expand Down Expand Up @@ -347,9 +348,9 @@ async def test_async_generator_session():
nested_operation = None

for span in operation_spans:
if span.attributes and span.attributes.get('agentops.operation.name') == 'main_async_generator_operation':
if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'main_async_generator_operation':
main_operation = span
elif span.attributes and span.attributes.get('agentops.operation.name') == 'nested_async_generator':
elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'nested_async_generator':
nested_operation = span

assert main_operation is not None, "main_async_generator_operation span not found"
Expand Down Expand Up @@ -442,11 +443,11 @@ def test_complex_session():
level3_operation = None

for span in operation_spans:
if span.attributes and span.attributes.get('agentops.operation.name') == 'level1_operation':
if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'level1_operation':
level1_operation = span
elif span.attributes and span.attributes.get('agentops.operation.name') == 'level2_operation':
elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'level2_operation':
level2_operation = span
elif span.attributes and span.attributes.get('agentops.operation.name') == 'level3_operation':
elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'level3_operation':
level3_operation = span

assert level1_operation is not None, "level1_operation span not found"
Expand Down Expand Up @@ -478,4 +479,93 @@ def test_complex_session():
assert level2_operation.context is not None
assert level3_operation.parent.span_id == level2_operation.context.span_id


def test_workflow_and_task_nesting(self, instrumentation: InstrumentationTester):
"""Test that workflow and task decorators create proper span nesting."""

# Define a workflow with tasks
@workflow
def data_processing_workflow(data):
"""Main workflow that processes data through multiple tasks"""
result = process_input(data)
result = transform_data(result)
return result

@task
def process_input(data):
"""Task to process input data"""
return f"Processed: {data}"

@task
def transform_data(data):
"""Task to transform processed data"""
return f"Transformed: {data}"

# Test session with the workflow
@session
def test_workflow_session():
return data_processing_workflow("test data")

# Run the test
result = test_workflow_session()

# Verify the result
assert result == "Transformed: Processed: test data"

# Get all spans captured during the test
spans = instrumentation.get_finished_spans()

# Print detailed span information for debugging
print("\nDetailed span information for workflow and task test:")
for i, span in enumerate(spans):
parent_id = span.parent.span_id if span.parent else "None"
span_id = span.context.span_id if span.context else "None"
print(f"Span {i}: name={span.name}, span_id={span_id}, parent_id={parent_id}")

# We should have 4 spans: session, workflow, and two tasks
assert len(spans) == 4

# Verify span kinds
session_spans = [s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.SESSION]
workflow_spans = [s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.WORKFLOW]
task_spans = [s for s in spans if s.attributes and s.attributes.get(
SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TASK]

assert len(session_spans) == 1
assert len(workflow_spans) == 1
assert len(task_spans) == 2

# Find the workflow and task spans
workflow_span = None
process_task = None
transform_task = None

for span in spans:
if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'data_processing_workflow':
workflow_span = span
elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'process_input':
process_task = span
elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == 'transform_data':
transform_task = span

assert workflow_span is not None, "workflow span not found"
assert process_task is not None, "process_input task span not found"
assert transform_task is not None, "transform_data task span not found"

# Verify the session span is the root
session_span = session_spans[0]
assert session_span.parent is None

# Verify the workflow span is a child of the session span
assert workflow_span.parent is not None
assert session_span.context is not None
assert workflow_span.parent.span_id == session_span.context.span_id

# Verify process_task is a child of the workflow span
assert process_task.parent is not None
assert workflow_span.context is not None
assert process_task.parent.span_id == workflow_span.context.span_id

# Verify transform_task is a child of the workflow span
assert transform_task.parent is not None
assert workflow_span.context is not None
assert transform_task.parent.span_id == workflow_span.context.span_id
Loading