From 269f850df376c9363965fcb309aca8a120a650e0 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 18 Sep 2026 11:09:50 +0200 Subject: [PATCH] fix: type errors in tracing and a2a adapter, check extras in CI mypy reported 18 errors in tracing.py and the a2a adapter when run with all extras installed. CI never saw them because the lint and test jobs install without extras. The same gap let #289 pass CI while breaking the a2a adapter at runtime. tracing.py: type the exporter argument, pass endpoint directly, import the two OTLP exporters under distinct names, drop the dead provider reassignment. a2a: import SecurityScheme unconditionally, remove the executor fallback that enqueued a raw dict. CI: lint and test jobs install all extras. test_tracing.py: skip on opentelemetry.sdk instead of the bare namespace package, which resolves to an empty leftover directory after the extras are uninstalled from a venv. --- .github/workflows/ci.yaml | 4 ++-- sdk/CHANGELOG.md | 10 +++++++++ sdk/eggai/adapters/a2a/config.py | 7 +----- sdk/eggai/adapters/a2a/executor.py | 34 ++++++++---------------------- sdk/eggai/tracing.py | 33 +++++++++++++++++------------ sdk/pyproject.toml | 1 - sdk/tests/test_tracing.py | 2 +- 7 files changed, 42 insertions(+), 49 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8178d6cc..e9fcbded 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,7 +42,7 @@ jobs: - name: Install dependencies working-directory: sdk - run: poetry install + run: poetry install --all-extras - name: Run ruff linter working-directory: sdk @@ -180,7 +180,7 @@ jobs: - name: Install dependencies working-directory: sdk - run: poetry install + run: poetry install --all-extras - name: Run tests with coverage working-directory: sdk diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index c23e0d63..1685114e 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `setup_tracing` type errors: exporter selection is typed, `endpoint` is + passed explicitly, and the unused provider reassignment is gone. +- A2A adapter type errors: `A2AConfig` imports `SecurityScheme` + unconditionally, and the executor no longer falls back to enqueuing a raw + dict the event queue cannot model. +- CI lint and test jobs install all extras, so the tracing, a2a and mcp + modules are type-checked and tested. + ## [0.5.0] - 2026-09-10 **Migration note for DLQ consumers.** The *shape of DLQ entries* changes in diff --git a/sdk/eggai/adapters/a2a/config.py b/sdk/eggai/adapters/a2a/config.py index f2fa6fef..62e0f7b4 100644 --- a/sdk/eggai/adapters/a2a/config.py +++ b/sdk/eggai/adapters/a2a/config.py @@ -1,13 +1,8 @@ """A2A configuration for EggAI agents.""" +from a2a.types import SecurityScheme from pydantic import BaseModel, ConfigDict, Field -try: - from a2a.types import SecurityScheme -except ImportError: - # Fallback if a2a-sdk not installed - SecurityScheme = dict - class A2AConfig(BaseModel): """Simple A2A configuration for EggAI agents.""" diff --git a/sdk/eggai/adapters/a2a/executor.py b/sdk/eggai/adapters/a2a/executor.py index 1193baa8..d090c814 100644 --- a/sdk/eggai/adapters/a2a/executor.py +++ b/sdk/eggai/adapters/a2a/executor.py @@ -189,30 +189,14 @@ def _extract_message_data(self, request_context: RequestContext) -> dict[str, An async def _send_agent_response(self, event_queue: EventQueue, data: dict): """Send agent response through event queue.""" - try: - from uuid import uuid4 - - from a2a.types import DataPart, Message, Part, Role + from uuid import uuid4 - # Create A2A message with response data - response_message = Message( - message_id=str(uuid4()), - role=Role.agent, - parts=[Part(root=DataPart(data=data))], - ) + from a2a.types import DataPart, Message, Part, Role - # Enqueue the message as an event - await event_queue.enqueue_event(response_message) - logger.debug(f"Sent agent response: {data}") - - except ImportError as e: - logger.error(f"A2A types not available: {e}") - raise - except (ValueError, TypeError) as e: - logger.exception(f"Failed to create A2A message from data: {e}") - # Fallback: try to send as simple event - try: - await event_queue.enqueue_event({"text": json.dumps(data)}) - except (TypeError, ValueError) as json_err: - logger.error(f"Failed to serialize response data: {json_err}") - raise + response_message = Message( + message_id=str(uuid4()), + role=Role.agent, + parts=[Part(root=DataPart(data=data))], + ) + await event_queue.enqueue_event(response_message) + logger.debug(f"Sent agent response: {data}") diff --git a/sdk/eggai/tracing.py b/sdk/eggai/tracing.py index a7a49c7d..a18ee566 100644 --- a/sdk/eggai/tracing.py +++ b/sdk/eggai/tracing.py @@ -3,7 +3,10 @@ import os from collections.abc import Callable from contextlib import contextmanager -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from opentelemetry.sdk.trace.export import SpanExporter class _NoOpSpan: @@ -101,7 +104,7 @@ def get_backend(): def setup_tracing( - exporter="otlp", + exporter: str | SpanExporter = "otlp", *, service_name: str | None = None, endpoint: str | None = None, @@ -110,29 +113,34 @@ def setup_tracing( from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter + from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, + ConsoleSpanExporter, + SpanExporter, + ) resource = Resource.create( - {"service.name": service_name or os.getenv("OTEL_SERVICE_NAME", "eggai")} + {"service.name": service_name or os.getenv("OTEL_SERVICE_NAME") or "eggai"} ) provider = TracerProvider(resource=resource) - if exporter == "console": + span_exporter: SpanExporter + if isinstance(exporter, SpanExporter): + span_exporter = exporter + elif exporter == "console": span_exporter = ConsoleSpanExporter() elif exporter == "otlp-http": from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( - OTLPSpanExporter, + OTLPSpanExporter as HttpSpanExporter, ) - span_exporter = OTLPSpanExporter(**({"endpoint": endpoint} if endpoint else {})) - elif hasattr(exporter, "export"): - span_exporter = exporter # accept pre-built exporter object (for testing) + span_exporter = HttpSpanExporter(endpoint=endpoint) else: # "otlp" / "otlp-grpc" (default) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( - OTLPSpanExporter, + OTLPSpanExporter as GrpcSpanExporter, ) - span_exporter = OTLPSpanExporter(**({"endpoint": endpoint} if endpoint else {})) + span_exporter = GrpcSpanExporter(endpoint=endpoint) provider.add_span_processor(BatchSpanProcessor(span_exporter)) @@ -140,9 +148,6 @@ def setup_tracing( if isinstance(trace.get_tracer_provider(), ProxyTracerProvider): trace.set_tracer_provider(provider) - else: - # Adopt the already-configured provider (API base type, not the SDK one). - provider = trace.get_tracer_provider() from importlib.metadata import version diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 14cce52c..0ac340e8 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -119,7 +119,6 @@ line-ending = "auto" [tool.mypy] python_version = "3.10" -# The lint job installs no extras, so the a2a adapter and tracing are not type-checked yet. files = ["eggai"] ignore_missing_imports = true warn_unused_ignores = true diff --git a/sdk/tests/test_tracing.py b/sdk/tests/test_tracing.py index 00fc23b5..397f0161 100644 --- a/sdk/tests/test_tracing.py +++ b/sdk/tests/test_tracing.py @@ -55,7 +55,7 @@ async def handler(msg): # OTEL does not allow overriding the global TracerProvider, so a single shared # provider + exporter is configured once at module level and cleared between tests. -otel = pytest.importorskip("opentelemetry") +otel = pytest.importorskip("opentelemetry.sdk") from opentelemetry import trace as _otel_trace # noqa: E402 from opentelemetry.sdk.trace import TracerProvider as _TracerProvider # noqa: E402