Skip to content

feat: Add AG2 instrumentation integration#1849

Open
faridun-ag2 wants to merge 7 commits intopydantic:mainfrom
faridun-ag2:feat/ag2-integration
Open

feat: Add AG2 instrumentation integration#1849
faridun-ag2 wants to merge 7 commits intopydantic:mainfrom
faridun-ag2:feat/ag2-integration

Conversation

@faridun-ag2
Copy link
Copy Markdown

Summary

Adds first-class observability support for AG2 (formerly AutoGen) multi-agent conversations via `logfire.instrument_ag2()`.

What's included

Core integration

  • `logfire/_internal/integrations/ag2.py` — monkey-patches AG2 classes to emit structured OpenTelemetry spans
  • `logfire/_internal/main.py` — `instrument_ag2()` method on the `Logfire` class
  • `logfire/init.py` — public export + `all`

API stubs & shims

  • `logfire-api/logfire_api/init.py` — shim (returns `nullcontext()` when logfire not installed)
  • `logfire-api/logfire_api/init.pyi` — type stub
  • `logfire-api/logfire_api/_internal/main.pyi` — method signature

Documentation

  • `docs/integrations/llms/ag2.md` — full integration docs (install, usage, configuration)
  • `mkdocs.yml` — added to nav under AI & LLM Frameworks

Tests

  • `tests/test_ag2.py` — 4 tests: spans, tool execution, `record_content=False` privacy guard, context-manager cleanup
  • `tests/test_logfire_api.py` — coverage for the new `instrument_ag2` shim

Quickstart example

  • `examples/python/ag2-openai-quickstart/main.py` — runnable OpenAI group-chat demo
  • `examples/python/ag2-openai-quickstart/README.md` — setup instructions

Spans emitted

Span name Trigger
`AG2 conversation` Top-level `response.process()`
`AG2 group chat run` `GroupChatManager.run_chat`
`AG2 group chat round` `GroupChat.select_speaker`
`AG2 agent turn` `ConversableAgent.generate_reply`
`AG2 tool execution` `ConversableAgent.execute_function`

Validation

  • ✅ All 4 AG2 tests pass (`pytest tests/test_ag2.py`)
  • ✅ All 9 pre-commit hooks pass (`pre-commit run --all-files`)
  • ✅ End-to-end verified with a real OpenAI API call

devin-ai-integration[bot]

This comment was marked as resolved.

faridun-ag2 and others added 2 commits April 9, 2026 10:27
…ound counting

- Restructure instrument_ag2 to patch eagerly and return uninstrument_context(),
  matching the pattern used by instrument_fastapi/instrument_print
- Rewrite tests to use inline_snapshot pattern per project conventions
- Handle positional args in wrappers (generate_reply, run_chat, execute_function)
- Fix ag2.total_rounds to count speaker transitions instead of duplicating message count
- Add test for eager patching behavior (instrument_ag2 without `with` block)
- Fix DeprecationWarning from jsonschema.RefResolver in test_logfire_api.py

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread examples/python/ag2-openai-quickstart/README.md Outdated
Comment on lines +47 to +48
def should_trace(agent_obj: Any) -> bool:
return target_ids is None or id(agent_obj) in target_ids
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Inner spans (agent turns, tools, rounds) are not gated by the agent filter

The should_trace check (logfire/_internal/integrations/ag2.py:46-47) only gates conversation-level spans in wrap_run/wrap_a_run. The generate_reply, execute_function, select_speaker, and run_chat wrappers create spans unconditionally for ALL agents, even when the agent parameter restricts tracing to specific instances. The docstring at logfire/_internal/main.py:1163-1164 says inner spans are emitted "for all participants within a traced conversation," but in reality they fire globally regardless of whether a parent conversation span exists. This is a reasonable design trade-off (filtering by parent context would be much more complex), but the docstring is slightly misleading.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- Remove developer-specific absolute paths from quickstart README
- Clarify in instrument_ag2 docstring that the agent parameter only
  scopes conversation-level spans, not inner spans

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread logfire/_internal/integrations/ag2.py
Comment on lines +60 to +77
def wrap_run(original: Callable[..., Any]) -> Callable[..., Any]:
@wraps(original)
def _wrapped(self: Any, *args: Any, **kwargs: Any) -> Any:
response = original(self, *args, **kwargs)
if not should_trace(self):
return response
_wrap_response_process(
response=response,
logfire_instance=logfire_instance,
runner=self,
recipient=_resolve_recipient(args, kwargs),
message=kwargs.get('message'),
record_content=record_content,
suppress_other_instrumentation=suppress_other_instrumentation,
)
return response

return _wrapped
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 No handle_internal_errors guard around span/attribute operations

The AG2 wrappers (wrap_run, wrap_generate_reply, wrap_execute_function, etc.) call logfire_instance.span() and span.set_attribute() without wrapping in handle_internal_errors. If any span operation fails unexpectedly, the exception propagates into the user's AG2 code. Notably, _wrap_response_process does setattr(response, 'process', wrapped) on a response object — if this fails (e.g., frozen object), the response is lost since the exception prevents reaching return response in wrap_run at line 75. The claude_agent_sdk.py integration uses with handle_internal_errors: in several places (logfire/_internal/integrations/claude_agent_sdk.py:177,223,253,408) to prevent instrumentation errors from breaking user code.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- Add ag2[openai] as a dev dependency so pyright can resolve autogen types in CI
- Remove trailing whitespace in docs/integrations/llms/ag2.md
- Restore proper type annotations in ag2.py (ConversableAgent instead of Any)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread logfire/_internal/integrations/ag2.py Outdated
Comment on lines +261 to +267
cm = suppress_instrumentation() if suppress_other_instrumentation else nullcontext()
with cm:
attrs = _conversation_attrs(runner, recipient, message, record_content)
with logfire_instance.span(SPAN_CONVERSATION, **attrs) as span:
result = await process(*args, **kwargs)
_set_conversation_summary_attributes(span, recipient)
return result
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 suppress_instrumentation wraps around the AG2 span, suppressing all AG2 spans including the conversation span itself

When suppress_other_instrumentation=True, the suppress_instrumentation() context manager is entered before the conversation span is created, which causes CheckSuppressInstrumentationProcessorWrapper.on_start() (logfire/_internal/exporters/processor_wrapper.py:49) to drop the conversation span itself (and all inner AG2 spans). This silently produces zero telemetry output.

Compare with the correct pattern in the OpenAI integration (logfire/_internal/integrations/llm_providers/llm_provider.py:143-144), which creates the LLM span first, then enters suppression inside it so only inner HTTP spans are suppressed.

The same issue exists in both the sync wrapper (line 274-281) and the async wrapper (line 260-267).

Prompt for agents
In _wrap_response_process in logfire/_internal/integrations/ag2.py, both the sync (wrapped_process_sync, lines 274-281) and async (wrapped_process_async, lines 260-267) wrappers place suppress_instrumentation() OUTSIDE the logfire_instance.span() call. This causes the AG2 conversation span and all inner spans to be suppressed when suppress_other_instrumentation=True.

The fix is to move the suppression INSIDE the span, matching the pattern used in the OpenAI integration (logfire/_internal/integrations/llm_providers/llm_provider.py lines 143-144). The conversation span should be created first, then suppression should be entered inside it so that only non-AG2 inner instrumentation (e.g. HTTPX spans from OpenAI HTTP calls) is suppressed.

For the async wrapper, change from:
  cm = suppress_instrumentation() if suppress_other_instrumentation else nullcontext()
  with cm:
    attrs = _conversation_attrs(...)
    with logfire_instance.span(SPAN_CONVERSATION, **attrs) as span:
      result = await process(...)
To:
  attrs = _conversation_attrs(...)
  with logfire_instance.span(SPAN_CONVERSATION, **attrs) as span:
    cm = suppress_instrumentation() if suppress_other_instrumentation else nullcontext()
    with cm:
      result = await process(...)
    _set_conversation_summary_attributes(span, recipient)
    return result

Apply the same fix to the sync wrapper.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Move suppress_instrumentation() inside the conversation span so the
AG2 span itself is not suppressed — only inner non-AG2 instrumentation
(e.g. HTTPX spans from OpenAI calls) is suppressed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant