diff --git a/python/frameworks/swarm/CHANGELOG.md b/python/frameworks/swarm/CHANGELOG.md new file mode 100644 index 0000000..503789f --- /dev/null +++ b/python/frameworks/swarm/CHANGELOG.md @@ -0,0 +1,33 @@ +## [0.1.9] - 2026-07-13 +### Fixed +- Fix `crew.kickoff()` crashing with `TypeError: '_NotSpecified' object is not + iterable` when a `Task` is created without an explicit `context`. CrewAI + defaults `Task.context` to a `NOT_SPECIFIED` sentinel that is truthy but not + iterable, so the wrapper now guards on type before iterating. Affected `crewai` + 0.203.2 and 1.15.1; the `context=[]` workaround is no longer needed. The + emitted `crew_tasks` payload is unchanged for every input that already worked. + + Known limitation: an unspecified context is recorded as `null`, the same as an + explicit `None`/`[]`. CrewAI treats these differently — unspecified means "use + every upstream task's output", whereas `None`/`[]` mean "no context" — so + `null` does not distinguish the two. This matches the value the span carried + before CrewAI introduced the sentinel. +- Fix stale `__version__` in `traceai_crewai/version.py` (was `0.1.0` while the + published wheel was `0.1.8`), which mislabelled the instrumentation scope + version on every span. + +## [0.1.6] - 2025-05-29 +### Feature +- Updated dependencies to the latest versions. + +## [0.1.5] - 2025-05-23 +### Feature +- Added support for FutureAGI's protect + +## [0.1.4] - 2025-05-08 +### Feature +- Updated dependencies to the latest versions. + +## [0.1.3] - 2025-04-14 +### Changed +- Updated dependencies to the latest versions. diff --git a/python/frameworks/swarm/README.md b/python/frameworks/swarm/README.md new file mode 100644 index 0000000..f9dfaf9 --- /dev/null +++ b/python/frameworks/swarm/README.md @@ -0,0 +1,42 @@ +# TraceAI Instrumentation for OpenAI Swarm + +This package provides OpenTelemetry instrumentation for the [OpenAI Swarm](https://github.com/openai/swarm) framework. + +## Installation + +```bash +pip install traceai-swarm +``` + +## Usage + +```python +from swarm import Swarm, Agent +from traceai_swarm import SwarmInstrumentor + +# Instrument Swarm +SwarmInstrumentor().instrument() + +client = Swarm() + +def transfer_to_agent_b(): + return agent_b + +agent_a = Agent( + name="Agent A", + instructions="You are a helpful agent.", + functions=[transfer_to_agent_b], +) + +agent_b = Agent( + name="Agent B", + instructions="Only speak in Haikus.", +) + +response = client.run( + agent=agent_a, + messages=[{"role": "user", "content": "I want to talk to agent B."}], +) + +print(response.messages[-1]["content"]) +``` diff --git a/python/frameworks/swarm/pyproject.toml b/python/frameworks/swarm/pyproject.toml new file mode 100644 index 0000000..e491934 --- /dev/null +++ b/python/frameworks/swarm/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "traceai-swarm" +version = "0.1.0" +description = "TraceAI Instrumentation for OpenAI Swarm" +authors = ["Future AGI "] +readme = "README.md" +packages = [{include = "traceai_swarm"}] + +[tool.poetry.dependencies] +python = "^3.9" +traceai-instrumentation = "^0.1.0" +wrapt = "^1.16.0" + +[tool.poetry.group.dev.dependencies] +swarm = "^0.1.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/python/frameworks/swarm/traceai_swarm/__init__.py b/python/frameworks/swarm/traceai_swarm/__init__.py new file mode 100644 index 0000000..21df22c --- /dev/null +++ b/python/frameworks/swarm/traceai_swarm/__init__.py @@ -0,0 +1,52 @@ +import logging +from importlib import import_module +from typing import Any, Collection + +from fi_instrumentation import FITracer, TraceConfig +from opentelemetry import trace as trace_api +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore +from traceai_swarm._wrappers import ( + _SwarmRunWrapper, +) +from traceai_swarm.version import __version__ +from wrapt import wrap_function_wrapper + +_instruments = ("swarm >= 0.1.0",) + +logger = logging.getLogger(__name__) + + +class SwarmInstrumentor(BaseInstrumentor): # type: ignore + __slots__ = ( + "_original_run", + "_tracer", + ) + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs: Any) -> None: + if not (tracer_provider := kwargs.get("tracer_provider")): + tracer_provider = trace_api.get_tracer_provider() + if not (config := kwargs.get("config")): + config = TraceConfig() + else: + assert isinstance(config, TraceConfig) + self._tracer = FITracer( + trace_api.get_tracer(__name__, __version__, tracer_provider), + config=config, + ) + + run_wrapper = _SwarmRunWrapper(tracer=self._tracer) + self._original_run = getattr(import_module("swarm.core"), "Swarm.run", None) + wrap_function_wrapper( + module="swarm.core", + name="Swarm.run", + wrapper=run_wrapper, + ) + + def _uninstrument(self, **kwargs: Any) -> None: + if self._original_run is not None: + swarm_module = import_module("swarm.core") + swarm_module.Swarm.run = self._original_run + self._original_run = None diff --git a/python/frameworks/swarm/traceai_swarm/_wrappers.py b/python/frameworks/swarm/traceai_swarm/_wrappers.py new file mode 100644 index 0000000..99e75e6 --- /dev/null +++ b/python/frameworks/swarm/traceai_swarm/_wrappers.py @@ -0,0 +1,38 @@ +import logging +from typing import Any, Callable + +from fi_instrumentation import FITracer +from fi_instrumentation.events import Events +from opentelemetry.trace import SpanKind + +logger = logging.getLogger(__name__) + + +class _SwarmRunWrapper: + def __init__(self, tracer: FITracer): + self._tracer = tracer + + def __call__( + self, + wrapped: Callable[..., Any], + instance: Any, + args: Any, + kwargs: Any, + ) -> Any: + span_name = "Swarm.run" + with self._tracer.start_as_current_span( + name=span_name, + kind=SpanKind.INTERNAL, + ) as span: + try: + result = wrapped(*args, **kwargs) + span.set_attribute(Events.AGENT_NAME, "Swarm") + if result and hasattr(result, "messages"): + # Record the final messages + self._tracer.record_event( + span, Events.AGENT_RUN_SUCCESS, {"messages": result.messages} + ) + return result + except Exception as e: + self._tracer.record_exception(span, e) + raise diff --git a/python/frameworks/swarm/traceai_swarm/version.py b/python/frameworks/swarm/traceai_swarm/version.py new file mode 100644 index 0000000..3dc1f76 --- /dev/null +++ b/python/frameworks/swarm/traceai_swarm/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0"