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,763 @@
1
+ from typing import AsyncGenerator
2
+ import asyncio
3
+ import pytest
4
+
5
+ from agentops.sdk.decorators import agent, operation, session, workflow, task, tool
6
+ from agentops.semconv import SpanKind
7
+ from agentops.semconv.span_attributes import SpanAttributes
8
+ from tests.unit.sdk.instrumentation_tester import InstrumentationTester
9
+ from agentops.sdk.decorators.factory import create_entity_decorator
10
+
11
+
12
+ class TestSpanNesting:
13
+ """Tests for proper nesting of spans in the tracing hierarchy."""
14
+
15
+ def test_operation_nests_under_agent(self, instrumentation: InstrumentationTester):
16
+ """Test that operation spans are properly nested under their agent spans."""
17
+
18
+ # Define the test agent with nested operations
19
+ @agent
20
+ class NestedAgent:
21
+ def __init__(self):
22
+ pass # No logic needed
23
+
24
+ @operation
25
+ def nested_operation(self, message):
26
+ """Nested operation that should appear as a child of the agent"""
27
+ return f"Processed: {message}"
28
+
29
+ @operation
30
+ def main_operation(self):
31
+ """Main operation that calls the nested operation"""
32
+ # Call the nested operation
33
+ result = self.nested_operation("test message")
34
+ return result
35
+
36
+ # Test session with the agent
37
+ @session
38
+ def test_session():
39
+ agent = NestedAgent()
40
+ return agent.main_operation()
41
+
42
+ # Run the test with our instrumentor
43
+ result = test_session()
44
+
45
+ # Verify the result
46
+ assert result == "Processed: test message"
47
+
48
+ # Get all spans captured during the test
49
+ spans = instrumentation.get_finished_spans()
50
+
51
+ # Print detailed span information for debugging
52
+ print("\nDetailed span information:")
53
+ for i, span in enumerate(spans):
54
+ parent_id = span.parent.span_id if span.parent else "None"
55
+ span_id = span.context.span_id if span.context else "None"
56
+ print(f"Span {i}: name={span.name}, span_id={span_id}, parent_id={parent_id}")
57
+
58
+ # We should have 4 spans: session, agent, and two operations
59
+ assert len(spans) == 4
60
+
61
+ # Verify span kinds
62
+ session_spans = [
63
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.SESSION
64
+ ]
65
+ agent_spans = [
66
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.AGENT
67
+ ]
68
+ operation_spans = [
69
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TASK
70
+ ]
71
+
72
+ assert len(session_spans) == 1
73
+ assert len(agent_spans) == 1
74
+ assert len(operation_spans) == 2
75
+
76
+ # Find the main_operation and nested_operation spans
77
+ main_operation = None
78
+ nested_operation = None
79
+
80
+ for span in operation_spans:
81
+ if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "main_operation":
82
+ main_operation = span
83
+ elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "nested_operation":
84
+ nested_operation = span
85
+
86
+ assert main_operation is not None, "main_operation span not found"
87
+ assert nested_operation is not None, "nested_operation span not found"
88
+
89
+ # Verify the session span is the root
90
+ session_span = session_spans[0]
91
+ assert session_span.parent is None
92
+
93
+ # Verify the agent span is a child of the session span
94
+ agent_span = agent_spans[0]
95
+ assert agent_span.parent is not None
96
+ assert session_span.context is not None
97
+ assert agent_span.parent.span_id == session_span.context.span_id
98
+
99
+ # Verify main_operation is a child of the agent span
100
+ assert main_operation.parent is not None
101
+ assert agent_span.context is not None
102
+ assert main_operation.parent.span_id == agent_span.context.span_id
103
+
104
+ # Verify nested_operation is a child of main_operation
105
+ assert nested_operation.parent is not None
106
+ assert main_operation.context is not None
107
+ assert nested_operation.parent.span_id == main_operation.context.span_id
108
+
109
+ def test_async_operations(self, instrumentation: InstrumentationTester):
110
+ """Test that async operations are properly nested."""
111
+
112
+ # Define the test agent with async operations
113
+ @agent
114
+ class AsyncAgent:
115
+ def __init__(self):
116
+ pass
117
+
118
+ @operation
119
+ async def nested_async_operation(self, message):
120
+ """Async operation that should appear as a child of the main operation"""
121
+ await asyncio.sleep(0.01) # Small delay to simulate async work
122
+ return f"Processed async: {message}"
123
+
124
+ @operation
125
+ async def main_async_operation(self):
126
+ """Main async operation that calls the nested async operation"""
127
+ result = await self.nested_async_operation("test async")
128
+ return result
129
+
130
+ # Test session with the async agent
131
+ @session
132
+ async def test_async_session():
133
+ agent = AsyncAgent()
134
+ return await agent.main_async_operation()
135
+
136
+ # Run the async test
137
+ result = asyncio.run(test_async_session())
138
+
139
+ # Verify the result
140
+ assert result == "Processed async: test async"
141
+
142
+ # Get all spans captured during the test
143
+ spans = instrumentation.get_finished_spans()
144
+
145
+ # Print detailed span information for debugging
146
+ print("\nDetailed span information for async test:")
147
+ for i, span in enumerate(spans):
148
+ parent_id = span.parent.span_id if span.parent else "None"
149
+ span_id = span.context.span_id if span.context else "None"
150
+ print(f"Span {i}: name={span.name}, span_id={span_id}, parent_id={parent_id}")
151
+
152
+ # We should have 4 spans: session, agent, and two operations
153
+ assert len(spans) == 4
154
+
155
+ # Verify span kinds
156
+ session_spans = [
157
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.SESSION
158
+ ]
159
+ agent_spans = [
160
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.AGENT
161
+ ]
162
+ operation_spans = [
163
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TASK
164
+ ]
165
+
166
+ assert len(session_spans) == 1
167
+ assert len(agent_spans) == 1
168
+ assert len(operation_spans) == 2
169
+
170
+ # Find the main_operation and nested_operation spans
171
+ main_operation = None
172
+ nested_operation = None
173
+
174
+ for span in operation_spans:
175
+ if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "main_async_operation":
176
+ main_operation = span
177
+ elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "nested_async_operation":
178
+ nested_operation = span
179
+
180
+ assert main_operation is not None, "main_async_operation span not found"
181
+ assert nested_operation is not None, "nested_async_operation span not found"
182
+
183
+ # Verify the session span is the root
184
+ session_span = session_spans[0]
185
+ assert session_span.parent is None
186
+
187
+ # Verify the agent span is a child of the session span
188
+ agent_span = agent_spans[0]
189
+ assert agent_span.parent is not None
190
+ assert session_span.context is not None
191
+ assert agent_span.parent.span_id == session_span.context.span_id
192
+
193
+ # Verify main_operation is a child of the agent span
194
+ assert main_operation.parent is not None
195
+ assert agent_span.context is not None
196
+ assert main_operation.parent.span_id == agent_span.context.span_id
197
+
198
+ # Verify nested_operation is a child of main_operation
199
+ assert nested_operation.parent is not None
200
+ assert main_operation.context is not None
201
+ assert nested_operation.parent.span_id == main_operation.context.span_id
202
+
203
+ def test_generator_operations(self, instrumentation: InstrumentationTester):
204
+ """Test that generator operations are properly nested."""
205
+
206
+ # Define the test agent with generator operations
207
+ @agent
208
+ class GeneratorAgent:
209
+ def __init__(self):
210
+ pass
211
+
212
+ @operation
213
+ def nested_generator(self, count):
214
+ """Generator operation that should appear as a child of the main operation"""
215
+ for i in range(count):
216
+ yield f"Item {i}"
217
+
218
+ @operation
219
+ def main_generator_operation(self, count):
220
+ """Main operation that calls the nested generator"""
221
+ results = []
222
+ for item in self.nested_generator(count):
223
+ results.append(item)
224
+ return results
225
+
226
+ # Test session with the generator agent
227
+ @session
228
+ def test_generator_session():
229
+ agent = GeneratorAgent()
230
+ return agent.main_generator_operation(3)
231
+
232
+ # Run the test
233
+ result = test_generator_session()
234
+
235
+ # Verify the result
236
+ assert result == ["Item 0", "Item 1", "Item 2"]
237
+
238
+ # Get all spans captured during the test
239
+ spans = instrumentation.get_finished_spans()
240
+
241
+ # Print detailed span information for debugging
242
+ print("\nDetailed span information for generator test:")
243
+ for i, span in enumerate(spans):
244
+ parent_id = span.parent.span_id if span.parent else "None"
245
+ span_id = span.context.span_id if span.context else "None"
246
+ print(f"Span {i}: name={span.name}, span_id={span_id}, parent_id={parent_id}")
247
+
248
+ # We should have 4 spans: session, agent, and two operations
249
+ assert len(spans) == 4
250
+
251
+ # Verify span kinds
252
+ session_spans = [
253
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.SESSION
254
+ ]
255
+ agent_spans = [
256
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.AGENT
257
+ ]
258
+ operation_spans = [
259
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TASK
260
+ ]
261
+
262
+ assert len(session_spans) == 1
263
+ assert len(agent_spans) == 1
264
+ assert len(operation_spans) == 2
265
+
266
+ # Find the main_operation and nested_operation spans
267
+ main_operation = None
268
+ nested_operation = None
269
+
270
+ for span in operation_spans:
271
+ if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "main_generator_operation":
272
+ main_operation = span
273
+ elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "nested_generator":
274
+ nested_operation = span
275
+
276
+ assert main_operation is not None, "main_generator_operation span not found"
277
+ assert nested_operation is not None, "nested_generator span not found"
278
+
279
+ # Verify the session span is the root
280
+ session_span = session_spans[0]
281
+ assert session_span.parent is None
282
+
283
+ # Verify the agent span is a child of the session span
284
+ agent_span = agent_spans[0]
285
+ assert agent_span.parent is not None
286
+ assert session_span.context is not None
287
+ assert agent_span.parent.span_id == session_span.context.span_id
288
+
289
+ # Verify main_operation is a child of the agent span
290
+ assert main_operation.parent is not None
291
+ assert agent_span.context is not None
292
+ assert main_operation.parent.span_id == agent_span.context.span_id
293
+
294
+ # Verify nested_operation is a child of main_operation
295
+ assert nested_operation.parent is not None
296
+ assert main_operation.context is not None
297
+ assert nested_operation.parent.span_id == main_operation.context.span_id
298
+
299
+ def test_async_generator_operations(self, instrumentation: InstrumentationTester):
300
+ """Test that async generator operations are properly nested."""
301
+
302
+ # Define the test agent with async generator operations
303
+ @agent
304
+ class AsyncGeneratorAgent:
305
+ def __init__(self):
306
+ pass
307
+
308
+ @operation
309
+ async def nested_async_generator(self, count) -> AsyncGenerator[str, None]:
310
+ """Async generator operation that should appear as a child of the main operation"""
311
+ for i in range(count):
312
+ await asyncio.sleep(0.01) # Small delay to simulate async work
313
+ yield f"Async Item {i}"
314
+
315
+ @operation
316
+ async def main_async_generator_operation(self, count):
317
+ """Main async operation that calls the nested async generator"""
318
+ results = []
319
+ async for item in self.nested_async_generator(count):
320
+ results.append(item)
321
+ return results
322
+
323
+ # Test session with the async generator agent
324
+ @session
325
+ async def test_async_generator_session():
326
+ agent = AsyncGeneratorAgent()
327
+ return await agent.main_async_generator_operation(3)
328
+
329
+ # Run the async test
330
+ result = asyncio.run(test_async_generator_session())
331
+
332
+ # Verify the result
333
+ assert result == ["Async Item 0", "Async Item 1", "Async Item 2"]
334
+
335
+ # Get all spans captured during the test
336
+ spans = instrumentation.get_finished_spans()
337
+
338
+ # Print detailed span information for debugging
339
+ print("\nDetailed span information for async generator test:")
340
+ for i, span in enumerate(spans):
341
+ parent_id = span.parent.span_id if span.parent else "None"
342
+ span_id = span.context.span_id if span.context else "None"
343
+ print(f"Span {i}: name={span.name}, span_id={span_id}, parent_id={parent_id}")
344
+
345
+ # We should have 4 spans: session, agent, and two operations
346
+ assert len(spans) == 4
347
+
348
+ # Verify span kinds
349
+ session_spans = [
350
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.SESSION
351
+ ]
352
+ agent_spans = [
353
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.AGENT
354
+ ]
355
+ operation_spans = [
356
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TASK
357
+ ]
358
+
359
+ assert len(session_spans) == 1
360
+ assert len(agent_spans) == 1
361
+ assert len(operation_spans) == 2
362
+
363
+ # Find the main_operation and nested_operation spans
364
+ main_operation = None
365
+ nested_operation = None
366
+
367
+ for span in operation_spans:
368
+ if (
369
+ span.attributes
370
+ and span.attributes.get(SpanAttributes.OPERATION_NAME) == "main_async_generator_operation"
371
+ ):
372
+ main_operation = span
373
+ elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "nested_async_generator":
374
+ nested_operation = span
375
+
376
+ assert main_operation is not None, "main_async_generator_operation span not found"
377
+ assert nested_operation is not None, "nested_async_generator span not found"
378
+
379
+ # Verify the session span is the root
380
+ session_span = session_spans[0]
381
+ assert session_span.parent is None
382
+
383
+ # Verify the agent span is a child of the session span
384
+ agent_span = agent_spans[0]
385
+ assert agent_span.parent is not None
386
+ assert session_span.context is not None
387
+ assert agent_span.parent.span_id == session_span.context.span_id
388
+
389
+ # Verify main_operation is a child of the agent span
390
+ assert main_operation.parent is not None
391
+ assert agent_span.context is not None
392
+ assert main_operation.parent.span_id == agent_span.context.span_id
393
+
394
+ # Verify nested_operation is a child of main_operation
395
+ assert nested_operation.parent is not None
396
+ assert main_operation.context is not None
397
+ assert nested_operation.parent.span_id == main_operation.context.span_id
398
+
399
+ def test_complex_nesting(self, instrumentation: InstrumentationTester):
400
+ """Test complex nesting with multiple levels of operations."""
401
+
402
+ # Define the test agent with complex nesting
403
+ @agent
404
+ class ComplexAgent:
405
+ def __init__(self):
406
+ pass
407
+
408
+ @operation
409
+ def level3_operation(self, message):
410
+ """Level 3 operation (deepest)"""
411
+ return f"Level 3: {message}"
412
+
413
+ @operation
414
+ def level2_operation(self, message):
415
+ """Level 2 operation that calls level 3"""
416
+ result = self.level3_operation(message)
417
+ return f"Level 2: {result}"
418
+
419
+ @operation
420
+ def level1_operation(self, message):
421
+ """Level 1 operation that calls level 2"""
422
+ result = self.level2_operation(message)
423
+ return f"Level 1: {result}"
424
+
425
+ # Test session with the complex agent
426
+ @session
427
+ def test_complex_session():
428
+ agent = ComplexAgent()
429
+ return agent.level1_operation("test message")
430
+
431
+ # Run the test
432
+ result = test_complex_session()
433
+
434
+ # Verify the result
435
+ assert result == "Level 1: Level 2: Level 3: test message"
436
+
437
+ # Get all spans captured during the test
438
+ spans = instrumentation.get_finished_spans()
439
+
440
+ # Print detailed span information for debugging
441
+ print("\nDetailed span information for complex nesting test:")
442
+ for i, span in enumerate(spans):
443
+ parent_id = span.parent.span_id if span.parent else "None"
444
+ span_id = span.context.span_id if span.context else "None"
445
+ print(f"Span {i}: name={span.name}, span_id={span_id}, parent_id={parent_id}")
446
+
447
+ # We should have 5 spans: session, agent, and three operations
448
+ assert len(spans) == 5
449
+
450
+ # Verify span kinds
451
+ session_spans = [
452
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.SESSION
453
+ ]
454
+ agent_spans = [
455
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.AGENT
456
+ ]
457
+ operation_spans = [
458
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TASK
459
+ ]
460
+
461
+ assert len(session_spans) == 1
462
+ assert len(agent_spans) == 1
463
+ assert len(operation_spans) == 3
464
+
465
+ # Find the operation spans
466
+ level1_operation = None
467
+ level2_operation = None
468
+ level3_operation = None
469
+
470
+ for span in operation_spans:
471
+ if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "level1_operation":
472
+ level1_operation = span
473
+ elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "level2_operation":
474
+ level2_operation = span
475
+ elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "level3_operation":
476
+ level3_operation = span
477
+
478
+ assert level1_operation is not None, "level1_operation span not found"
479
+ assert level2_operation is not None, "level2_operation span not found"
480
+ assert level3_operation is not None, "level3_operation span not found"
481
+
482
+ # Verify the session span is the root
483
+ session_span = session_spans[0]
484
+ assert session_span.parent is None
485
+
486
+ # Verify the agent span is a child of the session span
487
+ agent_span = agent_spans[0]
488
+ assert agent_span.parent is not None
489
+ assert session_span.context is not None
490
+ assert agent_span.parent.span_id == session_span.context.span_id
491
+
492
+ # Verify level1_operation is a child of the agent span
493
+ assert level1_operation.parent is not None
494
+ assert agent_span.context is not None
495
+ assert level1_operation.parent.span_id == agent_span.context.span_id
496
+
497
+ # Verify level2_operation is a child of level1_operation
498
+ assert level2_operation.parent is not None
499
+ assert level1_operation.context is not None
500
+ assert level2_operation.parent.span_id == level1_operation.context.span_id
501
+
502
+ # Verify level3_operation is a child of level2_operation
503
+ assert level3_operation.parent is not None
504
+ assert level2_operation.context is not None
505
+ assert level3_operation.parent.span_id == level2_operation.context.span_id
506
+
507
+ def test_workflow_and_task_nesting(self, instrumentation: InstrumentationTester):
508
+ """Test that workflow and task decorators create proper span nesting."""
509
+
510
+ # Define a workflow with tasks
511
+ @workflow
512
+ def data_processing_workflow(data):
513
+ """Main workflow that processes data through multiple tasks"""
514
+ result = process_input(data)
515
+ result = transform_data(result)
516
+ return result
517
+
518
+ @task
519
+ def process_input(data):
520
+ """Task to process input data"""
521
+ return f"Processed: {data}"
522
+
523
+ @task
524
+ def transform_data(data):
525
+ """Task to transform processed data"""
526
+ return f"Transformed: {data}"
527
+
528
+ # Test session with the workflow
529
+ @session
530
+ def test_workflow_session():
531
+ return data_processing_workflow("test data")
532
+
533
+ # Run the test
534
+ result = test_workflow_session()
535
+
536
+ # Verify the result
537
+ assert result == "Transformed: Processed: test data"
538
+
539
+ # Get all spans captured during the test
540
+ spans = instrumentation.get_finished_spans()
541
+
542
+ # Print detailed span information for debugging
543
+ print("\nDetailed span information for workflow and task test:")
544
+ for i, span in enumerate(spans):
545
+ parent_id = span.parent.span_id if span.parent else "None"
546
+ span_id = span.context.span_id if span.context else "None"
547
+ print(f"Span {i}: name={span.name}, span_id={span_id}, parent_id={parent_id}")
548
+
549
+ # We should have 4 spans: session, workflow, and two tasks
550
+ assert len(spans) == 4
551
+
552
+ # Verify span kinds
553
+ session_spans = [
554
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.SESSION
555
+ ]
556
+ workflow_spans = [
557
+ s
558
+ for s in spans
559
+ if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.WORKFLOW
560
+ ]
561
+ task_spans = [
562
+ s for s in spans if s.attributes and s.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TASK
563
+ ]
564
+
565
+ assert len(session_spans) == 1
566
+ assert len(workflow_spans) == 1
567
+ assert len(task_spans) == 2
568
+
569
+ # Find the workflow and task spans
570
+ workflow_span = None
571
+ process_task = None
572
+ transform_task = None
573
+
574
+ for span in spans:
575
+ if span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "data_processing_workflow":
576
+ workflow_span = span
577
+ elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "process_input":
578
+ process_task = span
579
+ elif span.attributes and span.attributes.get(SpanAttributes.OPERATION_NAME) == "transform_data":
580
+ transform_task = span
581
+
582
+ assert workflow_span is not None, "workflow span not found"
583
+ assert process_task is not None, "process_input task span not found"
584
+ assert transform_task is not None, "transform_data task span not found"
585
+
586
+ # Verify the session span is the root
587
+ session_span = session_spans[0]
588
+ assert session_span.parent is None
589
+
590
+ # Verify the workflow span is a child of the session span
591
+ assert workflow_span.parent is not None
592
+ assert session_span.context is not None
593
+ assert workflow_span.parent.span_id == session_span.context.span_id
594
+
595
+ # Verify process_task is a child of the workflow span
596
+ assert process_task.parent is not None
597
+ assert workflow_span.context is not None
598
+ assert process_task.parent.span_id == workflow_span.context.span_id
599
+
600
+ # Verify transform_task is a child of the workflow span
601
+ assert transform_task.parent is not None
602
+ assert workflow_span.context is not None
603
+ assert transform_task.parent.span_id == workflow_span.context.span_id
604
+
605
+
606
+ @pytest.mark.asyncio
607
+ async def test_async_context_manager():
608
+ """
609
+ Tests async context manager functionality (__aenter__, __aexit__).
610
+ """
611
+
612
+ # Create a simple decorated class
613
+ @create_entity_decorator("test")
614
+ class TestClass:
615
+ def __init__(self):
616
+ self.value = 42
617
+
618
+ # Cover __aenter__ and __aexit__ (normal exit)
619
+ async with TestClass() as instance:
620
+ assert hasattr(instance, "_agentops_active_span")
621
+ assert instance._agentops_active_span is not None
622
+
623
+ # Cover __aenter__ and __aexit__ (exceptional exit)
624
+ with pytest.raises(ValueError):
625
+ async with TestClass() as instance:
626
+ raise ValueError("Trigger exception for __aexit__ coverage")
627
+
628
+
629
+ class TestToolDecorator:
630
+ """Tests for the tool decorator functionality."""
631
+
632
+ @pytest.fixture
633
+ def agent_class(self):
634
+ @agent
635
+ class TestAgent:
636
+ @tool(cost=0.01)
637
+ def process_item(self, item):
638
+ return f"Processed {item}"
639
+
640
+ @tool(cost=0.02)
641
+ async def async_process_item(self, item):
642
+ await asyncio.sleep(0.1)
643
+ return f"Async processed {item}"
644
+
645
+ @tool(cost=0.03)
646
+ def generator_process_items(self, items):
647
+ for item in items:
648
+ yield self.process_item(item)
649
+
650
+ @tool(cost=0.04)
651
+ async def async_generator_process_items(self, items):
652
+ for item in items:
653
+ await asyncio.sleep(0.1)
654
+ yield await self.async_process_item(item)
655
+
656
+ return TestAgent()
657
+
658
+ def test_sync_tool_cost(self, agent_class, instrumentation: InstrumentationTester):
659
+ """Test synchronous tool with cost attribute."""
660
+ result = agent_class.process_item("test")
661
+
662
+ assert result == "Processed test"
663
+
664
+ spans = instrumentation.get_finished_spans()
665
+ tool_span = next(
666
+ span for span in spans if span.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TOOL
667
+ )
668
+ assert tool_span.attributes.get(SpanAttributes.LLM_USAGE_TOOL_COST) == 0.01
669
+
670
+ @pytest.mark.asyncio
671
+ async def test_async_tool_cost(self, agent_class, instrumentation: InstrumentationTester):
672
+ """Test asynchronous tool with cost attribute."""
673
+ result = await agent_class.async_process_item("test")
674
+
675
+ assert result == "Async processed test"
676
+
677
+ spans = instrumentation.get_finished_spans()
678
+ tool_span = next(
679
+ span for span in spans if span.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TOOL
680
+ )
681
+ assert tool_span.attributes.get(SpanAttributes.LLM_USAGE_TOOL_COST) == 0.02
682
+
683
+ def test_generator_tool_cost(self, agent_class, instrumentation: InstrumentationTester):
684
+ """Test generator tool with cost attribute."""
685
+ items = ["item1", "item2", "item3"]
686
+ results = list(agent_class.generator_process_items(items))
687
+
688
+ assert len(results) == 3
689
+ assert results[0] == "Processed item1"
690
+ assert results[1] == "Processed item2"
691
+ assert results[2] == "Processed item3"
692
+
693
+ spans = instrumentation.get_finished_spans()
694
+ tool_spans = [span for span in spans if span.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TOOL]
695
+ assert len(tool_spans) == 4 # Only one span for the generator
696
+ assert tool_spans[0].attributes.get(SpanAttributes.LLM_USAGE_TOOL_COST) == 0.01
697
+ assert tool_spans[3].attributes.get(SpanAttributes.LLM_USAGE_TOOL_COST) == 0.03
698
+
699
+ @pytest.mark.asyncio
700
+ async def test_async_generator_tool_cost(self, agent_class, instrumentation: InstrumentationTester):
701
+ """Test async generator tool with cost attribute."""
702
+ items = ["item1", "item2", "item3"]
703
+ results = [result async for result in agent_class.async_generator_process_items(items)]
704
+
705
+ assert len(results) == 3
706
+ assert results[0] == "Async processed item1"
707
+ assert results[1] == "Async processed item2"
708
+ assert results[2] == "Async processed item3"
709
+
710
+ spans = instrumentation.get_finished_spans()
711
+ tool_span = [span for span in spans if span.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TOOL]
712
+ assert len(tool_span) == 4 # Only one span for the generator
713
+ assert tool_span[0].attributes.get(SpanAttributes.LLM_USAGE_TOOL_COST) == 0.02
714
+ assert tool_span[3].attributes.get(SpanAttributes.LLM_USAGE_TOOL_COST) == 0.04
715
+
716
+ def test_multiple_tool_calls(self, agent_class, instrumentation: InstrumentationTester):
717
+ """Test multiple calls to the same tool."""
718
+ for i in range(3):
719
+ result = agent_class.process_item(f"item{i}")
720
+ assert result == f"Processed item{i}"
721
+
722
+ spans = instrumentation.get_finished_spans()
723
+ tool_spans = [span for span in spans if span.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TOOL]
724
+ assert len(tool_spans) == 3
725
+ for span in tool_spans:
726
+ assert span.attributes.get(SpanAttributes.LLM_USAGE_TOOL_COST) == 0.01
727
+
728
+ @pytest.mark.asyncio
729
+ async def test_parallel_tool_calls(self, agent_class, instrumentation: InstrumentationTester):
730
+ """Test parallel execution of async tools."""
731
+ results = await asyncio.gather(
732
+ agent_class.async_process_item("item1"),
733
+ agent_class.async_process_item("item2"),
734
+ agent_class.async_process_item("item3"),
735
+ )
736
+
737
+ assert len(results) == 3
738
+ assert results[0] == "Async processed item1"
739
+ assert results[1] == "Async processed item2"
740
+ assert results[2] == "Async processed item3"
741
+
742
+ spans = instrumentation.get_finished_spans()
743
+ tool_spans = [span for span in spans if span.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TOOL]
744
+ assert len(tool_spans) == 3
745
+ for span in tool_spans:
746
+ assert span.attributes.get(SpanAttributes.LLM_USAGE_TOOL_COST) == 0.02
747
+
748
+ def test_tool_without_cost(self, agent_class, instrumentation: InstrumentationTester):
749
+ """Test tool without cost parameter."""
750
+
751
+ @tool
752
+ def no_cost_tool(self):
753
+ return "No cost tool result"
754
+
755
+ result = no_cost_tool(agent_class)
756
+
757
+ assert result == "No cost tool result"
758
+
759
+ spans = instrumentation.get_finished_spans()
760
+ tool_span = next(
761
+ span for span in spans if span.attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND) == SpanKind.TOOL
762
+ )
763
+ assert SpanAttributes.LLM_USAGE_TOOL_COST not in tool_span.attributes