Skip to content

Commit bdd8765

Browse files
committed
Refactor and modify the corresponding unit tests using genai 0.3b0;
Disable the eval function and modify some version information; Add a changelog.
1 parent 746be83 commit bdd8765

11 files changed

Lines changed: 260 additions & 94 deletions

File tree

CHANGELOG-loongsuite.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232

3333
- `loongsuite-instrumentation-mem0`: add support for mem0
3434
([#67](https://github.com/alibaba/loongsuite-python-agent/pull/67))
35+
36+
- `loongsuite-instrumentation-crewai`: add support for crewai
37+
([#87](https://github.com/alibaba/loongsuite-python-agent/pull/87))

instrumentation-loongsuite/loongsuite-instrumentation-crewai/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ result = crew.kickoff()
4242

4343
## Supported Versions
4444

45-
- CrewAI >= 0.70.0
45+
- CrewAI >= 0.80.0
4646

4747
## References
4848

instrumentation-loongsuite/loongsuite-instrumentation-crewai/pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ classifiers = [
2222
"Programming Language :: Python :: 3.12",
2323
]
2424
dependencies = [
25-
"opentelemetry-api ~= 1.34.0",
25+
"opentelemetry-api ~= 1.39.1",
26+
"opentelemetry-instrumentation ~= 0.60b0",
27+
"opentelemetry-semantic-conventions ~= 0.60b0",
2628
"wrapt >= 1.0.0, < 2.0.0",
27-
"opentelemetry-util-genai ~= 0.1b0",
29+
"opentelemetry-util-genai ~= 0.3b0",
2830
]
2931

3032
[project.optional-dependencies]
3133
instruments = [
32-
"crewai >= 0.1.0",
34+
"crewai >= 0.80.0",
3335
]
3436

3537
[project.entry-points.opentelemetry_instrumentor]

instrumentation-loongsuite/loongsuite-instrumentation-crewai/src/opentelemetry/instrumentation/crewai/utils.py

Lines changed: 31 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,25 @@
11
import dataclasses
2-
import json
32
import logging
4-
from base64 import b64encode
53
from typing import Any, Dict, List, Optional
64

5+
from opentelemetry.semconv._incubating.attributes import gen_ai_attributes
76
from opentelemetry.trace import Span
87
from opentelemetry.util.genai.types import (
8+
ContentCapturingMode,
99
InputMessage,
1010
MessagePart,
1111
OutputMessage,
1212
Text,
1313
ToolCall,
1414
)
15+
from opentelemetry.util.genai.utils import (
16+
gen_ai_json_dumps,
17+
get_content_capturing_mode,
18+
is_experimental_mode,
19+
)
1520

1621
logger = logging.getLogger(__name__)
1722

18-
MAX_MESSAGE_SIZE = 10000
19-
20-
21-
class _GenAiJsonEncoder(json.JSONEncoder):
22-
def default(self, o: Any) -> Any:
23-
if isinstance(o, bytes):
24-
return b64encode(o).decode()
25-
return super().default(o)
26-
27-
28-
def _safe_json_dumps(
29-
obj: Any, default: str = "", max_size: int = MAX_MESSAGE_SIZE
30-
) -> str:
31-
"""
32-
Safely serialize object to JSON string with size limit.
33-
"""
34-
if obj is None:
35-
return default
36-
37-
# Fast path for simple types
38-
if isinstance(obj, (str, int, float, bool)):
39-
result = str(obj)
40-
else:
41-
try:
42-
result = json.dumps(
43-
obj,
44-
separators=(",", ":"),
45-
cls=_GenAiJsonEncoder,
46-
ensure_ascii=False,
47-
)
48-
except Exception as e:
49-
logger.debug(f"Failed to serialize to JSON: {e}")
50-
result = str(obj)
51-
52-
if len(result) > max_size:
53-
return result[:max_size] + "...[truncated]"
54-
return result
55-
56-
57-
GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages"
58-
GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages"
59-
GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions"
60-
6123
OP_NAME_CREW = "crew.kickoff"
6224
OP_NAME_AGENT = "agent.execute"
6325
OP_NAME_TASK = "task.execute"
@@ -76,27 +38,41 @@ def on_completion(
7638
system_instructions: Optional[List[MessagePart]] = None,
7739
attributes: Optional[Dict[str, Any]] = None,
7840
):
79-
if self.capture_content and span.is_recording():
41+
if not self.capture_content or not span.is_recording():
42+
return
43+
44+
if not is_experimental_mode():
45+
return
46+
47+
capturing_mode = get_content_capturing_mode()
48+
should_capture_span = capturing_mode in (
49+
ContentCapturingMode.SPAN_ONLY,
50+
ContentCapturingMode.SPAN_AND_EVENT,
51+
)
52+
53+
if should_capture_span:
8054
if inputs:
8155
span.set_attribute(
82-
GEN_AI_INPUT_MESSAGES,
83-
_safe_json_dumps([dataclasses.asdict(i) for i in inputs]),
56+
gen_ai_attributes.GEN_AI_INPUT_MESSAGES,
57+
gen_ai_json_dumps([dataclasses.asdict(i) for i in inputs]),
8458
)
8559
if outputs:
8660
span.set_attribute(
87-
GEN_AI_OUTPUT_MESSAGES,
88-
_safe_json_dumps([dataclasses.asdict(o) for o in outputs]),
61+
gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES,
62+
gen_ai_json_dumps(
63+
[dataclasses.asdict(o) for o in outputs]
64+
),
8965
)
9066
if system_instructions:
9167
span.set_attribute(
92-
GEN_AI_SYSTEM_INSTRUCTIONS,
93-
_safe_json_dumps(
68+
gen_ai_attributes.GEN_AI_SYSTEM_INSTRUCTIONS,
69+
gen_ai_json_dumps(
9470
[dataclasses.asdict(i) for i in system_instructions]
9571
),
9672
)
9773

98-
if attributes:
99-
span.set_attributes(attributes)
74+
if attributes:
75+
span.set_attributes(attributes)
10076

10177

10278
def to_input_message(role: str, content: Any) -> List[InputMessage]:
@@ -144,7 +120,7 @@ def extract_agent_inputs(
144120

145121
def extract_tool_inputs(tool_name: str, arguments: Any) -> List[InputMessage]:
146122
args_str = (
147-
json.dumps(arguments)
123+
gen_ai_json_dumps(arguments)
148124
if isinstance(arguments, dict)
149125
else str(arguments)
150126
)

instrumentation-loongsuite/loongsuite-instrumentation-crewai/tests/test_agent_workflow.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@
2929
from opentelemetry.instrumentation.crewai import CrewAIInstrumentor
3030
from opentelemetry.test.test_base import TestBase
3131

32+
try:
33+
from opentelemetry.instrumentation._semconv import (
34+
_OpenTelemetrySemanticConventionStability,
35+
_OpenTelemetryStabilitySignalType,
36+
_StabilityMode,
37+
)
38+
except ImportError:
39+
_OpenTelemetrySemanticConventionStability = None
40+
_OpenTelemetryStabilitySignalType = None
41+
_StabilityMode = None
42+
3243
sys.modules["sqlite3"] = pysqlite3
3344

3445

@@ -53,6 +64,20 @@ def setUp(self):
5364
)
5465

5566
os.environ["CREWAI_TRACING_ENABLED"] = "false"
67+
# Enable experimental mode and content capture for testing
68+
os.environ["OTEL_SEMCONV_STABILITY_OPT_IN"] = "gen_ai"
69+
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = (
70+
"span_only"
71+
)
72+
73+
if _OpenTelemetrySemanticConventionStability:
74+
try:
75+
_OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[
76+
_OpenTelemetryStabilitySignalType.GEN_AI
77+
] = _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL
78+
except (AttributeError, KeyError):
79+
pass
80+
5681
self.instrumentor = CrewAIInstrumentor()
5782
self.instrumentor.instrument(tracer_provider=self.tracer_provider)
5883

instrumentation-loongsuite/loongsuite-instrumentation-crewai/tests/test_error_scenarios.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@
3434
from opentelemetry.test.test_base import TestBase
3535
from opentelemetry.trace import StatusCode
3636

37+
try:
38+
from opentelemetry.instrumentation._semconv import (
39+
_OpenTelemetrySemanticConventionStability,
40+
_OpenTelemetryStabilitySignalType,
41+
_StabilityMode,
42+
)
43+
except ImportError:
44+
_OpenTelemetrySemanticConventionStability = None
45+
_OpenTelemetryStabilitySignalType = None
46+
_StabilityMode = None
47+
3748
sys.modules["sqlite3"] = pysqlite3
3849

3950

@@ -60,6 +71,20 @@ def setUp(self):
6071
# Disable CrewAI's built-in tracing to avoid interference
6172
os.environ["CREWAI_TRACING_ENABLED"] = "false"
6273

74+
# Enable experimental mode and content capture for testing
75+
os.environ["OTEL_SEMCONV_STABILITY_OPT_IN"] = "gen_ai"
76+
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = (
77+
"span_only"
78+
)
79+
80+
if _OpenTelemetrySemanticConventionStability:
81+
try:
82+
_OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[
83+
_OpenTelemetryStabilitySignalType.GEN_AI
84+
] = _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL
85+
except (AttributeError, KeyError):
86+
pass
87+
6388
self.instrumentor = CrewAIInstrumentor()
6489
self.instrumentor.instrument(tracer_provider=self.tracer_provider)
6590

instrumentation-loongsuite/loongsuite-instrumentation-crewai/tests/test_flow_kickoff.py

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@
2222
- Flow name extraction from instance
2323
"""
2424

25+
import os
26+
27+
# Set environment variables for content capture
28+
os.environ["OTEL_SEMCONV_STABILITY_OPT_IN"] = "gen_ai"
29+
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = "span_only"
30+
31+
# Forcefully enable experimental mode in OpenTelemetry's internal mapping
32+
try:
33+
from opentelemetry.instrumentation._semconv import (
34+
_OpenTelemetrySemanticConventionStability,
35+
_OpenTelemetryStabilitySignalType,
36+
_StabilityMode,
37+
)
38+
39+
_OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[
40+
_OpenTelemetryStabilitySignalType.GEN_AI
41+
] = _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL
42+
except (ImportError, AttributeError):
43+
pass
44+
2545
import json
2646
import unittest
2747
from unittest.mock import AsyncMock, MagicMock
@@ -30,7 +50,7 @@
3050
GenAIHookHelper,
3151
_FlowKickoffAsyncWrapper,
3252
)
33-
from opentelemetry.instrumentation.crewai.utils import _safe_json_dumps
53+
from opentelemetry.instrumentation.crewai.utils import gen_ai_json_dumps
3454
from opentelemetry.sdk.trace import TracerProvider
3555

3656
# Use SDK tracer for testing
@@ -352,55 +372,41 @@ async def test_flow_kickoff_span_kind(self):
352372
self.assertEqual(span.kind, SpanKind.INTERNAL)
353373

354374

355-
class TestSafeJsonDumps(unittest.TestCase):
356-
"""Test _safe_json_dumps utility function."""
375+
class TestGenAiJsonDumps(unittest.TestCase):
376+
"""Test gen_ai_json_dumps utility function."""
357377

358378
def test_simple_string(self):
359379
"""Test with simple string input."""
360-
result = _safe_json_dumps("hello")
361-
self.assertEqual(result, "hello")
380+
result = gen_ai_json_dumps("hello")
381+
self.assertEqual(result, '"hello"')
362382

363383
def test_simple_int(self):
364384
"""Test with integer input."""
365-
result = _safe_json_dumps(42)
385+
result = gen_ai_json_dumps(42)
366386
self.assertEqual(result, "42")
367387

368388
def test_simple_float(self):
369389
"""Test with float input."""
370-
result = _safe_json_dumps(3.14)
390+
result = gen_ai_json_dumps(3.14)
371391
self.assertEqual(result, "3.14")
372392

373393
def test_simple_bool(self):
374394
"""Test with boolean input."""
375-
result = _safe_json_dumps(True)
376-
self.assertEqual(result, "True")
377-
378-
def test_none_returns_default(self):
379-
"""Test that None returns default value."""
380-
result = _safe_json_dumps(None, default="default_value")
381-
self.assertEqual(result, "default_value")
395+
result = gen_ai_json_dumps(True)
396+
self.assertEqual(result, "true")
382397

383398
def test_dict_serialization(self):
384399
"""Test dictionary serialization."""
385400
data = {"key": "value", "number": 123}
386-
result = _safe_json_dumps(data)
387-
self.assertIn("key", result)
388-
self.assertIn("value", result)
389-
self.assertIn("123", result)
401+
result = gen_ai_json_dumps(data)
402+
self.assertIn('"key":"value"', result)
403+
self.assertIn('"number":123', result)
390404

391405
def test_list_serialization(self):
392406
"""Test list serialization."""
393407
data = [1, 2, 3, "four"]
394-
result = _safe_json_dumps(data)
395-
self.assertIn("1", result)
396-
self.assertIn("four", result)
397-
398-
def test_truncation(self):
399-
"""Test that long strings are truncated."""
400-
long_string = "a" * 20000
401-
result = _safe_json_dumps(long_string, max_size=100)
402-
self.assertLessEqual(len(result), 120) # Allow for truncation suffix
403-
self.assertIn("[truncated]", result)
408+
result = gen_ai_json_dumps(data)
409+
self.assertEqual(result, '[1,2,3,"four"]')
404410

405411
def test_non_serializable_object(self):
406412
"""Test handling of non-serializable objects."""
@@ -409,8 +415,11 @@ class CustomObject:
409415
def __str__(self):
410416
return "CustomObject instance"
411417

412-
result = _safe_json_dumps(CustomObject())
413-
self.assertIn("CustomObject", result)
418+
# gen_ai_json_dumps uses standard json or partial json.dump
419+
# which might raise TypeError if not supported.
420+
# But our partial uses _GenAiJsonEncoder.
421+
with self.assertRaises(TypeError):
422+
gen_ai_json_dumps(CustomObject())
414423

415424

416425
if __name__ == "__main__":

0 commit comments

Comments
 (0)