openinference-instrumentation-beeai 0.1.5__tar.gz → 0.1.7__tar.gz

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 (28) hide show
  1. {openinference_instrumentation_beeai-0.1.5 → openinference_instrumentation_beeai-0.1.7}/PKG-INFO +12 -9
  2. {openinference_instrumentation_beeai-0.1.5 → openinference_instrumentation_beeai-0.1.7}/pyproject.toml +21 -14
  3. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/__init__.py +122 -0
  4. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/_span.py +81 -0
  5. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/_utils.py +75 -0
  6. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/__init__.py +0 -0
  7. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/agents/__init__.py +0 -0
  8. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/agents/base.py +34 -0
  9. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/agents/react.py +77 -0
  10. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/agents/requirement_agent.py +71 -0
  11. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/agents/tool_calling.py +34 -0
  12. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/base.py +60 -0
  13. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/chat.py +239 -0
  14. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/embedding.py +71 -0
  15. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/locator.py +106 -0
  16. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/requirement.py +67 -0
  17. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/tool.py +72 -0
  18. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/processors/workflow.py +108 -0
  19. openinference_instrumentation_beeai-0.1.7/src/openinference/instrumentation/beeai/version.py +1 -0
  20. openinference_instrumentation_beeai-0.1.5/src/openinference/instrumentation/beeai/__init__.py +0 -148
  21. openinference_instrumentation_beeai-0.1.5/src/openinference/instrumentation/beeai/middleware.py +0 -291
  22. openinference_instrumentation_beeai-0.1.5/src/openinference/instrumentation/beeai/utils/build_trace_tree.py +0 -170
  23. openinference_instrumentation_beeai-0.1.5/src/openinference/instrumentation/beeai/utils/create_span.py +0 -80
  24. openinference_instrumentation_beeai-0.1.5/src/openinference/instrumentation/beeai/utils/get_serialized_object_safe.py +0 -302
  25. openinference_instrumentation_beeai-0.1.5/src/openinference/instrumentation/beeai/utils/id_name_manager.py +0 -58
  26. openinference_instrumentation_beeai-0.1.5/src/openinference/instrumentation/beeai/version.py +0 -1
  27. {openinference_instrumentation_beeai-0.1.5 → openinference_instrumentation_beeai-0.1.7}/.gitignore +0 -0
  28. {openinference_instrumentation_beeai-0.1.5 → openinference_instrumentation_beeai-0.1.7}/README.md +0 -0
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openinference-instrumentation-beeai
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: OpenInference BeeAI Instrumentation
5
5
  Project-URL: Homepage, https://github.com/Arize-ai/openinference/tree/main/python/instrumentation/openinference-instrumentation-beeai
6
- Author-email: Milan Gallas <gallas.milan@gmail.com>
6
+ Author: IBM Corp.
7
+ Maintainer-email: Tomas Dvorak <toomas2d@gmail.com>
7
8
  License-Expression: Apache-2.0
8
9
  Classifier: Development Status :: 5 - Production/Stable
9
10
  Classifier: Intended Audience :: Developers
@@ -14,16 +15,18 @@ Classifier: Programming Language :: Python :: 3.11
14
15
  Classifier: Programming Language :: Python :: 3.12
15
16
  Classifier: Programming Language :: Python :: 3.13
16
17
  Requires-Python: <3.14,>=3.11
17
- Requires-Dist: openinference-instrumentation>=0.1.27
18
- Requires-Dist: openinference-semantic-conventions>=0.1.17
19
- Requires-Dist: opentelemetry-api>=1.31.1
20
- Requires-Dist: opentelemetry-instrumentation>=0.52b1
21
- Requires-Dist: opentelemetry-semantic-conventions>=0.52b1
18
+ Requires-Dist: beeai-framework<0.2.0,>=0.1.32
19
+ Requires-Dist: openinference-instrumentation>=0.1.36
20
+ Requires-Dist: openinference-semantic-conventions>=0.1.21
21
+ Requires-Dist: opentelemetry-api>=1.36.0
22
+ Requires-Dist: opentelemetry-instrumentation>=0.57b0
23
+ Requires-Dist: opentelemetry-semantic-conventions>=0.57b0
22
24
  Requires-Dist: wrapt>=1.17.2
23
25
  Provides-Extra: instruments
24
- Requires-Dist: beeai-framework[duckduckgo,wikipedia]==0.1.18; extra == 'instruments'
26
+ Requires-Dist: beeai-framework>=0.1.32; extra == 'instruments'
25
27
  Provides-Extra: test
26
- Requires-Dist: beeai-framework[duckduckgo,wikipedia]==0.1.18; extra == 'test'
28
+ Requires-Dist: beeai-framework>=0.1.32; extra == 'test'
29
+ Requires-Dist: opentelemetry-exporter-otlp; extra == 'test'
27
30
  Requires-Dist: opentelemetry-sdk; extra == 'test'
28
31
  Description-Content-Type: text/markdown
29
32
 
@@ -7,9 +7,8 @@ name = "openinference-instrumentation-beeai"
7
7
  dynamic = ["version"]
8
8
  description = "OpenInference BeeAI Instrumentation"
9
9
  license = "Apache-2.0"
10
- authors = [
11
- {name = "Milan Gallas",email = "gallas.milan@gmail.com"}
12
- ]
10
+ authors = [{ name = "IBM Corp." }]
11
+ maintainers = [{name = "Tomas Dvorak", email = "toomas2d@gmail.com"}]
13
12
  readme = "README.md"
14
13
  requires-python = ">=3.11,<3.14"
15
14
  classifiers = [
@@ -23,21 +22,23 @@ classifiers = [
23
22
  "Programming Language :: Python :: 3.13",
24
23
  ]
25
24
  dependencies = [
26
- "openinference-semantic-conventions>=0.1.17",
27
- "opentelemetry-api>=1.31.1",
28
- "opentelemetry-instrumentation>=0.52b1",
29
- "opentelemetry-semantic-conventions>=0.52b1",
30
- "openinference-instrumentation>=0.1.27",
31
- "wrapt>=1.17.2",
25
+ "beeai-framework (>=0.1.32,<0.2.0)",
26
+ "openinference-instrumentation>=0.1.36",
27
+ "openinference-semantic-conventions>=0.1.21",
28
+ "opentelemetry-api>=1.36.0",
29
+ "opentelemetry-instrumentation>=0.57b0",
30
+ "opentelemetry-semantic-conventions>=0.57b0",
31
+ "wrapt>=1.17.2"
32
32
  ]
33
33
 
34
34
  [project.optional-dependencies]
35
35
  instruments = [
36
- "beeai-framework[duckduckgo,wikipedia]==0.1.18",
36
+ "beeai-framework >= 0.1.32",
37
37
  ]
38
38
  test = [
39
- "beeai-framework[duckduckgo,wikipedia]==0.1.18",
40
- "opentelemetry-sdk"
39
+ "beeai-framework >= 0.1.32",
40
+ "opentelemetry-sdk",
41
+ "opentelemetry-exporter-otlp"
41
42
  ]
42
43
 
43
44
  [project.entry-points.opentelemetry_instrumentor]
@@ -69,7 +70,6 @@ testpaths = [
69
70
  [tool.mypy]
70
71
  strict = true
71
72
  explicit_package_bases = true
72
- mypy_path = "src"
73
73
  exclude = [
74
74
  "examples",
75
75
  "dist",
@@ -84,7 +84,7 @@ module = [
84
84
 
85
85
  [tool.ruff]
86
86
  line-length = 100
87
- target-version = "py38"
87
+ target-version = "py311"
88
88
 
89
89
  [tool.ruff.lint.per-file-ignores]
90
90
  "*.ipynb" = ["E402", "E501"]
@@ -94,3 +94,10 @@ select = ["E", "F", "W", "I"]
94
94
 
95
95
  [tool.ruff.lint.isort]
96
96
  force-single-line = false
97
+
98
+ [tool.poetry]
99
+ version = "0.1"
100
+
101
+ [tool.poetry.group.dev.dependencies]
102
+ ruff = "^0.12.2"
103
+
@@ -0,0 +1,122 @@
1
+ import logging
2
+ from importlib.metadata import PackageNotFoundError, version
3
+ from typing import TYPE_CHECKING, Any, Callable, Collection
4
+
5
+ if TYPE_CHECKING:
6
+ from beeai_framework.emitter import EventMeta
7
+
8
+ from opentelemetry import trace as trace_api
9
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore
10
+
11
+ from openinference.instrumentation import (
12
+ OITracer,
13
+ TraceConfig,
14
+ )
15
+ from openinference.instrumentation.beeai._span import SpanWrapper
16
+ from openinference.instrumentation.beeai._utils import _datetime_to_span_time, exception_handler
17
+ from openinference.instrumentation.beeai.processors.base import Processor
18
+ from openinference.instrumentation.beeai.processors.locator import ProcessorLocator
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ _instruments = ("beeai-framework >= 0.1.32",)
23
+ try:
24
+ __version__ = version("beeai-framework")
25
+ except PackageNotFoundError:
26
+ __version__ = "unknown"
27
+
28
+
29
+ class BeeAIInstrumentor(BaseInstrumentor): # type: ignore
30
+ __slots__ = ("_tracer", "_cleanup", "_processes")
31
+
32
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
33
+ super().__init__(*args, **kwargs)
34
+ self._cleanup: Callable[[], None] = lambda: None
35
+ self._processes: dict[str, Processor] = {}
36
+
37
+ def instrumentation_dependencies(self) -> Collection[str]:
38
+ return _instruments
39
+
40
+ def _instrument(self, **kwargs: Any) -> None:
41
+ try:
42
+ if not (tracer_provider := kwargs.get("tracer_provider")):
43
+ tracer_provider = trace_api.get_tracer_provider()
44
+ if not (config := kwargs.get("config")):
45
+ config = TraceConfig()
46
+ else:
47
+ assert isinstance(config, TraceConfig)
48
+
49
+ self._tracer = OITracer(
50
+ trace_api.get_tracer(__name__, __version__, tracer_provider),
51
+ config=config,
52
+ )
53
+
54
+ from beeai_framework.emitter import Emitter, EmitterOptions
55
+
56
+ self._cleanup = Emitter.root().match(
57
+ "*.*",
58
+ self._handler,
59
+ EmitterOptions(match_nested=True, is_blocking=True),
60
+ )
61
+ except Exception as e:
62
+ logger.error("Instrumentation error", exc_info=e)
63
+
64
+ def _uninstrument(self, **kwargs: Any) -> None:
65
+ self._cleanup()
66
+ self._processes.clear()
67
+
68
+ def _build_tree(self, node: SpanWrapper) -> None:
69
+ with self._tracer.start_as_current_span(
70
+ name=node.name,
71
+ openinference_span_kind=node.kind,
72
+ attributes=node.attributes,
73
+ start_time=_datetime_to_span_time(node.started_at) if node.started_at else None,
74
+ end_on_exit=False, # we do it manually
75
+ ) as current_span:
76
+ for event in node.events:
77
+ current_span.add_event(
78
+ name=event.name, attributes=event.attributes, timestamp=event.timestamp
79
+ )
80
+
81
+ for children in node.children:
82
+ self._build_tree(children)
83
+
84
+ current_span.set_status(node.status)
85
+ if node.error is not None:
86
+ current_span.record_exception(node.error)
87
+
88
+ current_span.end(_datetime_to_span_time(node.ended_at) if node.ended_at else None)
89
+
90
+ @exception_handler
91
+ async def _handler(self, data: Any, event: "EventMeta") -> None:
92
+ assert event.trace is not None, "Event must have a trace"
93
+
94
+ if event.trace.run_id not in self._processes:
95
+ parent = (
96
+ self._processes.get(event.trace.parent_run_id)
97
+ if event.trace.parent_run_id
98
+ else None
99
+ )
100
+ if event.trace.parent_run_id and not parent:
101
+ raise ValueError(f"Parent run with ID {event.trace.parent_run_id} was not found!")
102
+
103
+ node = self._processes[event.trace.run_id] = ProcessorLocator.locate(data, event)
104
+ if parent is not None:
105
+ parent.span.children.append(node.span)
106
+ else:
107
+ node = self._processes[event.trace.run_id]
108
+
109
+ from beeai_framework.context import RunContextFinishEvent
110
+
111
+ if isinstance(data, RunContextFinishEvent):
112
+ await node.end(data, event)
113
+ self._build_tree(node.span)
114
+ self._processes.pop(event.trace.run_id)
115
+ else:
116
+ if event.context.get("internal"):
117
+ return
118
+
119
+ await node.update(
120
+ data,
121
+ event,
122
+ )
@@ -0,0 +1,81 @@
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING, Any
3
+
4
+ from opentelemetry.sdk.trace import Event
5
+ from opentelemetry.trace import StatusCode
6
+
7
+ from openinference.instrumentation.beeai._utils import _datetime_to_span_time, _unpack_object
8
+
9
+ if TYPE_CHECKING:
10
+ from beeai_framework.emitter import EventMeta
11
+
12
+ from openinference.semconv.trace import (
13
+ OpenInferenceMimeTypeValues,
14
+ OpenInferenceSpanKindValues,
15
+ SpanAttributes,
16
+ )
17
+
18
+
19
+ class SpanWrapper:
20
+ def __init__(self, *, name: str, kind: OpenInferenceSpanKindValues):
21
+ self.name = name
22
+ self.attributes: dict[str, Any] = {SpanAttributes.OPENINFERENCE_SPAN_KIND: kind}
23
+ self.events: list[Event] = []
24
+ self.status: StatusCode = StatusCode.OK
25
+ self.error: Exception | None = None
26
+ self.started_at: datetime | None = None
27
+ self.ended_at: datetime | None = None
28
+ self.children: list["SpanWrapper"] = []
29
+ self.kind = kind
30
+
31
+ def child(
32
+ self, name: str | None = None, event: tuple[Any, "EventMeta"] | None = None
33
+ ) -> "SpanWrapper":
34
+ child = SpanWrapper(name=name or f"{self.name}_child", kind=self.kind)
35
+ if event is not None:
36
+ from beeai_framework.utils.dicts import include_keys
37
+ from beeai_framework.utils.strings import to_json
38
+
39
+ value, meta = event
40
+
41
+ child.started_at = meta.created_at
42
+ child.ended_at = meta.created_at
43
+ child.attributes.update(
44
+ {
45
+ SpanAttributes.INPUT_VALUE: to_json(value) if value is not None else value,
46
+ SpanAttributes.INPUT_MIME_TYPE: OpenInferenceMimeTypeValues.JSON.value,
47
+ **_unpack_object(
48
+ include_keys(meta.model_dump(), {"id", "context", "path", "trace"}),
49
+ prefix=SpanAttributes.METADATA,
50
+ ),
51
+ }
52
+ )
53
+
54
+ self.children.append(child)
55
+ return child
56
+
57
+ def add_event(
58
+ self,
59
+ name: str,
60
+ attributes: dict[str, Any] | None = None,
61
+ timestamp: datetime | None = None,
62
+ ) -> None:
63
+ self.events.append(
64
+ Event(
65
+ name=name,
66
+ attributes=attributes or {},
67
+ timestamp=_datetime_to_span_time(timestamp) if timestamp else None,
68
+ )
69
+ )
70
+
71
+ def set_attribute(self, name: str, value: Any) -> None:
72
+ self.attributes[name] = value
73
+
74
+ def set_attributes(self, attributes: dict[str, Any]) -> None:
75
+ self.attributes.update(attributes)
76
+
77
+ def set_status(self, status: StatusCode) -> None:
78
+ self.status = status
79
+
80
+ def record_exception(self, error: Exception) -> None:
81
+ self.error = error
@@ -0,0 +1,75 @@
1
+ import datetime
2
+ import functools
3
+ import json
4
+ import logging
5
+ from collections.abc import Awaitable
6
+ from typing import Any, Callable, ParamSpec, TypeVar
7
+
8
+ from pydantic import BaseModel
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def _datetime_to_span_time(dt: datetime.datetime) -> int:
14
+ if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None:
15
+ dt = dt.replace(tzinfo=datetime.timezone.utc)
16
+
17
+ return int(dt.timestamp() * 1_000_000_000)
18
+
19
+
20
+ def _unpack_object(obj: dict[str, Any] | list[Any] | BaseModel, prefix: str = "") -> dict[str, Any]:
21
+ if not isinstance(obj, dict) and not isinstance(obj, list):
22
+ obj = json.loads(stringify(obj))
23
+ if not isinstance(obj, dict) and not isinstance(obj, list):
24
+ raise ValueError(f"Cannot unpack object of type {type(obj)}")
25
+
26
+ if prefix and prefix.startswith("."):
27
+ prefix = prefix[1:]
28
+ if prefix and not prefix.endswith("."):
29
+ prefix += "."
30
+
31
+ output = {}
32
+ for key, value in obj.items() if isinstance(obj, dict) else enumerate(obj):
33
+ if value is None:
34
+ continue
35
+ if is_primitive(value):
36
+ output[f"{prefix}{key}"] = str(value)
37
+ else:
38
+ output.update(_unpack_object(value, prefix=f"{prefix}{key}"))
39
+ return output
40
+
41
+
42
+ def is_primitive(value: Any) -> bool:
43
+ return isinstance(value, str | bool | int | float | type(None))
44
+
45
+
46
+ def stringify(value: Any, pretty: bool = False) -> str:
47
+ if is_primitive(value):
48
+ return str(value)
49
+
50
+ from beeai_framework.utils.strings import to_json
51
+
52
+ return to_json(value, sort_keys=False, indent=4 if pretty else None)
53
+
54
+
55
+ T = TypeVar("T")
56
+ P = ParamSpec("P")
57
+
58
+
59
+ def exception_handler(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T | None]]:
60
+ @functools.wraps(func)
61
+ async def wrapped(*args: P.args, **kwargs: P.kwargs) -> T | None:
62
+ try:
63
+ return await func(*args, **kwargs)
64
+ except Exception as e:
65
+ logger.error("Error has occurred in the telemetry package.", exc_info=e)
66
+ return None
67
+
68
+ return wrapped
69
+
70
+
71
+ def safe_dump_model_schema(model: type[BaseModel]) -> dict[str, Any]:
72
+ try:
73
+ return model.model_json_schema(mode="serialization")
74
+ except: # noqa: E722
75
+ return {}
@@ -0,0 +1,34 @@
1
+ from typing import ClassVar
2
+
3
+ from beeai_framework.agents import BaseAgent
4
+ from beeai_framework.context import RunContext, RunContextStartEvent
5
+ from beeai_framework.emitter import EventMeta
6
+
7
+ from openinference.instrumentation.beeai._utils import _unpack_object
8
+ from openinference.instrumentation.beeai.processors.base import Processor
9
+ from openinference.semconv.trace import (
10
+ OpenInferenceSpanKindValues,
11
+ SpanAttributes,
12
+ )
13
+
14
+
15
+ class AgentProcessor(Processor):
16
+ kind: ClassVar[OpenInferenceSpanKindValues] = OpenInferenceSpanKindValues.AGENT
17
+
18
+ def __init__(self, event: RunContextStartEvent, meta: "EventMeta") -> None:
19
+ super().__init__(event, meta)
20
+
21
+ assert isinstance(meta.creator, RunContext)
22
+ assert isinstance(meta.creator.instance, BaseAgent)
23
+
24
+ agent = meta.creator.instance
25
+ self.span.name = agent.meta.name or self.span.name
26
+ self.span.set_attributes(
27
+ {
28
+ SpanAttributes.AGENT_NAME: agent.meta.name,
29
+ f"{SpanAttributes.METADATA}.agent_description": agent.meta.description,
30
+ }
31
+ )
32
+ self.span.set_attributes(
33
+ _unpack_object(agent.memory.to_json_safe(), prefix=f"{SpanAttributes.METADATA}.memory")
34
+ )
@@ -0,0 +1,77 @@
1
+ from typing import Any
2
+
3
+ from beeai_framework.context import RunContextStartEvent
4
+ from beeai_framework.emitter import EventMeta
5
+ from typing_extensions import override
6
+
7
+ from openinference.instrumentation.beeai._utils import _unpack_object, stringify
8
+ from openinference.instrumentation.beeai.processors.agents.base import AgentProcessor
9
+ from openinference.semconv.trace import OpenInferenceMimeTypeValues, SpanAttributes
10
+
11
+
12
+ class ReActAgentProcessor(AgentProcessor):
13
+ def __init__(self, event: "RunContextStartEvent", meta: "EventMeta"):
14
+ super().__init__(event, meta)
15
+ self._last_chunk_index = 0
16
+
17
+ @override
18
+ async def update(
19
+ self,
20
+ event: Any,
21
+ meta: "EventMeta",
22
+ ) -> None:
23
+ from beeai_framework.agents.react import (
24
+ ReActAgentErrorEvent,
25
+ ReActAgentRetryEvent,
26
+ ReActAgentStartEvent,
27
+ ReActAgentSuccessEvent,
28
+ ReActAgentUpdateEvent,
29
+ )
30
+ from beeai_framework.backend import UserMessage
31
+
32
+ await super().update(event, meta)
33
+
34
+ self.span.add_event(f"{meta.name} ({meta.path})", timestamp=meta.created_at)
35
+
36
+ match event:
37
+ case ReActAgentStartEvent():
38
+ last_user_message: UserMessage | None = next(
39
+ (
40
+ msg
41
+ for msg in reversed(event.memory.messages)
42
+ if isinstance(msg, UserMessage)
43
+ ),
44
+ None,
45
+ )
46
+ self.span.set_attributes(
47
+ {
48
+ SpanAttributes.INPUT_VALUE: last_user_message.text
49
+ if last_user_message
50
+ else "",
51
+ SpanAttributes.INPUT_MIME_TYPE: OpenInferenceMimeTypeValues.TEXT.value,
52
+ }
53
+ )
54
+ case ReActAgentUpdateEvent():
55
+ value = event.update.parsed_value or event.update.value
56
+ self.span.set_attribute(
57
+ f"{SpanAttributes.METADATA}.iteration", event.meta.iteration
58
+ )
59
+ self.span.set_attributes(
60
+ _unpack_object(
61
+ {event.update.key: stringify(value)},
62
+ prefix=f"{SpanAttributes.METADATA}.iterations.{event.meta.iteration}",
63
+ )
64
+ )
65
+
66
+ case ReActAgentErrorEvent():
67
+ span = self.span.child(meta.name, event=(event, meta))
68
+ span.record_exception(event.error)
69
+ case ReActAgentRetryEvent():
70
+ self.span.child(meta.name, event=(event, meta))
71
+ case ReActAgentSuccessEvent():
72
+ self.span.set_attribute(SpanAttributes.OUTPUT_VALUE, event.data.text)
73
+ self.span.set_attribute(
74
+ SpanAttributes.OUTPUT_MIME_TYPE, OpenInferenceMimeTypeValues.TEXT.value
75
+ )
76
+ case _:
77
+ self.span.child(meta.name, event=(event, meta))
@@ -0,0 +1,71 @@
1
+ from typing import Any
2
+
3
+ from beeai_framework.agents.experimental.events import (
4
+ RequirementAgentStartEvent,
5
+ RequirementAgentSuccessEvent,
6
+ )
7
+ from beeai_framework.agents.experimental.types import RequirementAgentRunStateStep
8
+ from beeai_framework.context import RunContextStartEvent
9
+ from beeai_framework.emitter import EventMeta
10
+ from typing_extensions import override
11
+
12
+ from openinference.instrumentation.beeai._utils import _unpack_object
13
+ from openinference.instrumentation.beeai.processors.agents.base import AgentProcessor
14
+ from openinference.semconv.trace import OpenInferenceMimeTypeValues, SpanAttributes
15
+
16
+
17
+ class RequirementAgentProcessor(AgentProcessor):
18
+ def __init__(self, event: "RunContextStartEvent", meta: "EventMeta"):
19
+ super().__init__(event, meta)
20
+ self._steps: list[RequirementAgentRunStateStep] = []
21
+
22
+ @override
23
+ async def update(
24
+ self,
25
+ event: Any,
26
+ meta: "EventMeta",
27
+ ) -> None:
28
+ await super().update(event, meta)
29
+ self.span.add_event(f"{meta.name} ({meta.path})", timestamp=meta.created_at)
30
+
31
+ match event:
32
+ case RequirementAgentStartEvent():
33
+ self._sync_steps(event.state.steps)
34
+ case RequirementAgentSuccessEvent():
35
+ self._sync_steps(event.state.steps)
36
+
37
+ if event.state.answer is not None:
38
+ self.span.set_attributes(
39
+ {
40
+ SpanAttributes.OUTPUT_VALUE: event.state.answer.text,
41
+ SpanAttributes.OUTPUT_MIME_TYPE: OpenInferenceMimeTypeValues.TEXT.value,
42
+ }
43
+ )
44
+ if event.state.result is not None:
45
+ self.span.set_attributes(
46
+ _unpack_object(
47
+ event.state.result, prefix=f"{SpanAttributes.METADATA}.result"
48
+ )
49
+ )
50
+ case _:
51
+ self.span.child(meta.name, event=(event, meta))
52
+
53
+ def _sync_steps(self, steps: list["RequirementAgentRunStateStep"]) -> None:
54
+ new_items = steps[len(self._steps) :]
55
+ self._steps.extend(new_items)
56
+
57
+ for idx, (new_step, old_step) in enumerate(zip(steps, self._steps)):
58
+ if new_step.id != old_step.id:
59
+ # TODO: cleanup old keys
60
+ self._steps.clear()
61
+ self._sync_steps(steps)
62
+ return
63
+
64
+ if new_step in new_items or new_step != old_step:
65
+ self._steps[idx] = new_step
66
+ self.span.set_attributes(
67
+ _unpack_object(
68
+ new_step,
69
+ prefix=f"{SpanAttributes.METADATA}.steps.{idx}",
70
+ )
71
+ )
@@ -0,0 +1,34 @@
1
+ from typing import Any
2
+
3
+ from beeai_framework.agents.tool_calling import (
4
+ ToolCallingAgentStartEvent,
5
+ ToolCallingAgentSuccessEvent,
6
+ )
7
+ from beeai_framework.emitter import EventMeta
8
+ from typing_extensions import override
9
+
10
+ from openinference.instrumentation.beeai.processors.agents.base import AgentProcessor
11
+ from openinference.semconv.trace import OpenInferenceMimeTypeValues, SpanAttributes
12
+
13
+
14
+ class ToolCallingAgentProcessor(AgentProcessor):
15
+ @override
16
+ async def update(
17
+ self,
18
+ event: Any,
19
+ meta: "EventMeta",
20
+ ) -> None:
21
+ await super().update(event, meta)
22
+ self.span.add_event(f"{meta.name} ({meta.path})", timestamp=meta.created_at)
23
+
24
+ match event:
25
+ case ToolCallingAgentStartEvent():
26
+ pass
27
+ case ToolCallingAgentSuccessEvent():
28
+ if event.state.result is not None:
29
+ self.span.set_attribute(SpanAttributes.OUTPUT_VALUE, event.state.result.text)
30
+ self.span.set_attribute(
31
+ SpanAttributes.OUTPUT_MIME_TYPE, OpenInferenceMimeTypeValues.TEXT.value
32
+ )
33
+ case _:
34
+ self.span.child(meta.name, event=(event, meta))
@@ -0,0 +1,60 @@
1
+ from typing import TYPE_CHECKING, Any, ClassVar
2
+
3
+ if TYPE_CHECKING:
4
+ from beeai_framework.context import RunContextFinishEvent, RunContextStartEvent
5
+ from beeai_framework.emitter import EventMeta
6
+
7
+ from opentelemetry.trace import StatusCode
8
+
9
+ from openinference.instrumentation.beeai._span import SpanWrapper
10
+ from openinference.instrumentation.beeai._utils import stringify
11
+ from openinference.semconv.trace import (
12
+ OpenInferenceMimeTypeValues,
13
+ OpenInferenceSpanKindValues,
14
+ SpanAttributes,
15
+ )
16
+
17
+
18
+ class Processor:
19
+ kind: ClassVar[OpenInferenceSpanKindValues] = OpenInferenceSpanKindValues.UNKNOWN
20
+
21
+ def __init__(self, event: "RunContextStartEvent", meta: "EventMeta"):
22
+ from beeai_framework.context import RunContext
23
+
24
+ assert isinstance(meta.creator, RunContext)
25
+ target_cls = type(meta.creator.instance)
26
+
27
+ self.span = SpanWrapper(name=target_cls.__name__, kind=type(self).kind)
28
+ self.span.started_at = meta.created_at
29
+ self.span.attributes.update(
30
+ {
31
+ SpanAttributes.OPENINFERENCE_SPAN_KIND: type(self).kind,
32
+ SpanAttributes.INPUT_VALUE: stringify(event.input),
33
+ SpanAttributes.INPUT_MIME_TYPE: OpenInferenceMimeTypeValues.JSON.value,
34
+ f"{SpanAttributes.METADATA}.class_name": target_cls.__name__,
35
+ }
36
+ )
37
+
38
+ async def update(
39
+ self,
40
+ event: Any,
41
+ meta: "EventMeta",
42
+ ) -> None:
43
+ pass
44
+
45
+ async def end(self, event: "RunContextFinishEvent", meta: "EventMeta") -> None:
46
+ if event.output is not None:
47
+ if SpanAttributes.OUTPUT_VALUE not in self.span.attributes:
48
+ self.span.attributes.update(
49
+ {
50
+ SpanAttributes.OUTPUT_VALUE: stringify(event.output),
51
+ SpanAttributes.OUTPUT_MIME_TYPE: OpenInferenceMimeTypeValues.JSON.value,
52
+ }
53
+ )
54
+ self.span.set_status(StatusCode.OK)
55
+
56
+ if event.error is not None:
57
+ self.span.set_status(StatusCode.ERROR)
58
+ self.span.record_exception(event.error)
59
+
60
+ self.span.ended_at = meta.created_at