From e8faf39d2550f1692b6375045777bd5d55ee4cb1 Mon Sep 17 00:00:00 2001 From: droideronline Date: Tue, 7 Apr 2026 14:59:05 +0530 Subject: [PATCH 1/5] Python: Add OpenTelemetry integration for GitHubCopilotAgent - Split GitHubCopilotAgent into RawGitHubCopilotAgent (core, no OTel) and GitHubCopilotAgent(AgentTelemetryLayer, RawGitHubCopilotAgent) with tracing - Add default_options property to expose model for span attributes - Export RawGitHubCopilotAgent from all public namespaces - Add github_copilot_with_observability.py sample and update README --- .../core/agent_framework/github/__init__.py | 2 + .../core/agent_framework/github/__init__.pyi | 2 + .../__init__.py | 3 +- .../agent_framework_github_copilot/_agent.py | 142 +++++++++++++++--- .../providers/github_copilot/README.md | 11 ++ .../github_copilot_with_observability.py | 112 ++++++++++++++ 6 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 python/samples/02-agents/providers/github_copilot/github_copilot_with_observability.py diff --git a/python/packages/core/agent_framework/github/__init__.py b/python/packages/core/agent_framework/github/__init__.py index 831a838c0d..0ff654fa17 100644 --- a/python/packages/core/agent_framework/github/__init__.py +++ b/python/packages/core/agent_framework/github/__init__.py @@ -9,6 +9,7 @@ - GitHubCopilotAgent - GitHubCopilotOptions - GitHubCopilotSettings +- RawGitHubCopilotAgent """ import importlib @@ -18,6 +19,7 @@ "GitHubCopilotAgent": ("agent_framework_github_copilot", "agent-framework-github-copilot"), "GitHubCopilotOptions": ("agent_framework_github_copilot", "agent-framework-github-copilot"), "GitHubCopilotSettings": ("agent_framework_github_copilot", "agent-framework-github-copilot"), + "RawGitHubCopilotAgent": ("agent_framework_github_copilot", "agent-framework-github-copilot"), } diff --git a/python/packages/core/agent_framework/github/__init__.pyi b/python/packages/core/agent_framework/github/__init__.pyi index 567ab9490d..f7b68966cf 100644 --- a/python/packages/core/agent_framework/github/__init__.pyi +++ b/python/packages/core/agent_framework/github/__init__.pyi @@ -4,10 +4,12 @@ from agent_framework_github_copilot import ( GitHubCopilotAgent, GitHubCopilotOptions, GitHubCopilotSettings, + RawGitHubCopilotAgent, ) __all__ = [ "GitHubCopilotAgent", "GitHubCopilotOptions", "GitHubCopilotSettings", + "RawGitHubCopilotAgent", ] diff --git a/python/packages/github_copilot/agent_framework_github_copilot/__init__.py b/python/packages/github_copilot/agent_framework_github_copilot/__init__.py index 432427fd9d..56b46f8dee 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/__init__.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/__init__.py @@ -2,7 +2,7 @@ import importlib.metadata -from ._agent import GitHubCopilotAgent, GitHubCopilotOptions, GitHubCopilotSettings +from ._agent import GitHubCopilotAgent, GitHubCopilotOptions, GitHubCopilotSettings, RawGitHubCopilotAgent try: __version__ = importlib.metadata.version(__name__) @@ -13,5 +13,6 @@ "GitHubCopilotAgent", "GitHubCopilotOptions", "GitHubCopilotSettings", + "RawGitHubCopilotAgent", "__version__", ] diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 4744013b56..a9e26fc7cc 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -27,6 +27,7 @@ from agent_framework._tools import FunctionTool, ToolTypes from agent_framework._types import AgentRunInputs, normalize_tools from agent_framework.exceptions import AgentException +from agent_framework.observability import AgentTelemetryLayer try: from copilot import CopilotClient, CopilotSession, SubprocessConfig @@ -129,8 +130,11 @@ class GitHubCopilotOptions(TypedDict, total=False): ) -class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): - """A GitHub Copilot Agent. +class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]): + """A GitHub Copilot Agent without telemetry layers. + + This is the core GitHub Copilot agent implementation without OpenTelemetry instrumentation. + For most use cases, prefer :class:`GitHubCopilotAgent` which includes telemetry support. This agent wraps the GitHub Copilot SDK to provide Copilot agentic capabilities within the Agent Framework. It supports both streaming and non-streaming responses, @@ -143,7 +147,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): .. code-block:: python - async with GitHubCopilotAgent() as agent: + async with RawGitHubCopilotAgent() as agent: response = await agent.run("Hello, world!") print(response) @@ -151,22 +155,11 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): .. code-block:: python - from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions + from agent_framework_github_copilot import RawGitHubCopilotAgent, GitHubCopilotOptions - agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + agent: RawGitHubCopilotAgent[GitHubCopilotOptions] = RawGitHubCopilotAgent( default_options={"model": "claude-sonnet-4", "timeout": 120} ) - - With tools: - - .. code-block:: python - - def get_weather(city: str) -> str: - return f"Weather in {city} is sunny" - - - async with GitHubCopilotAgent(tools=[get_weather]) as agent: - response = await agent.run("What's the weather in Seattle?") """ AGENT_PROVIDER_NAME: ClassVar[str] = "github.copilot" @@ -194,9 +187,9 @@ def __init__( Keyword Args: client: Optional pre-configured CopilotClient instance. If not provided, a new client will be created using the other parameters. - id: ID of the GitHubCopilotAgent. - name: Name of the GitHubCopilotAgent. - description: Description of the GitHubCopilotAgent. + id: ID of the RawGitHubCopilotAgent. + name: Name of the RawGitHubCopilotAgent. + description: Description of the RawGitHubCopilotAgent. context_providers: Context Providers, to be used by the agent. middleware: Agent middleware used by the agent. tools: Tools to use for the agent. Can be functions @@ -250,7 +243,7 @@ def __init__( self._default_options = opts self._started = False - async def __aenter__(self) -> GitHubCopilotAgent[OptionsT]: + async def __aenter__(self) -> RawGitHubCopilotAgent[OptionsT]: """Start the agent when entering async context.""" await self.start() return self @@ -300,6 +293,20 @@ async def stop(self) -> None: self._started = False + @property + def default_options(self) -> dict[str, Any]: + """Expose default options including model from settings. + + Returns a merged dict of ``_default_options`` with the resolved ``model`` + from settings injected under the ``model`` key. This is read by + :class:`AgentTelemetryLayer` to include the model name in span attributes. + """ + opts = dict(self._default_options) + model = self._settings.get("model") + if model: + opts["model"] = model + return opts + @overload def run( self, @@ -308,6 +315,7 @@ def run( stream: Literal[False] = False, session: AgentSession | None = None, options: OptionsT | None = None, + **kwargs: Any, ) -> Awaitable[AgentResponse]: ... @overload @@ -318,6 +326,7 @@ def run( stream: Literal[True], session: AgentSession | None = None, options: OptionsT | None = None, + **kwargs: Any, ) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ... def run( @@ -327,6 +336,7 @@ def run( stream: bool = False, session: AgentSession | None = None, options: OptionsT | None = None, + **kwargs: Any, # type: ignore[override] ) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]: """Get a response from the agent. @@ -341,6 +351,8 @@ def run( stream: Whether to stream the response. Defaults to False. session: The conversation session associated with the message(s). options: Runtime options (model, timeout, etc.). + kwargs: Additional keyword arguments for compatibility with the shared agent + interface (e.g. compaction_strategy, tokenizer). Not used by this agent. Returns: When stream=False: An Awaitable[AgentResponse]. @@ -756,3 +768,93 @@ async def _resume_session(self, session_id: str, streaming: bool) -> CopilotSess tools=tools or None, mcp_servers=self._mcp_servers or None, ) + + +class GitHubCopilotAgent(AgentTelemetryLayer, RawGitHubCopilotAgent[OptionsT], Generic[OptionsT]): + """A GitHub Copilot Agent with OpenTelemetry instrumentation. + + This is the recommended agent class for most use cases. It includes + OpenTelemetry-based telemetry for observability. For a minimal + implementation without telemetry, use :class:`RawGitHubCopilotAgent`. + + This agent wraps the GitHub Copilot SDK to provide Copilot agentic capabilities + within the Agent Framework. It supports both streaming and non-streaming responses, + custom tools, and session management. + + The agent can be used as an async context manager to ensure proper cleanup: + + Examples: + Basic usage: + + .. code-block:: python + + async with GitHubCopilotAgent() as agent: + response = await agent.run("Hello, world!") + print(response) + + With explicitly typed options: + + .. code-block:: python + + from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions + + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + default_options={"model": "claude-sonnet-4", "timeout": 120} + ) + + With observability: + + .. code-block:: python + + from agent_framework.observability import configure_otel_providers + + configure_otel_providers() + async with GitHubCopilotAgent() as agent: + response = await agent.run("Hello, world!") + """ + + @overload # type: ignore[override] + def run( + self, + messages: AgentRunInputs | None = None, + *, + stream: Literal[False] = ..., + session: AgentSession | None = None, + options: OptionsT | None = None, + **kwargs: Any, + ) -> Awaitable[AgentResponse]: ... + + @overload # type: ignore[override] + def run( + self, + messages: AgentRunInputs | None = None, + *, + stream: Literal[True], + session: AgentSession | None = None, + options: OptionsT | None = None, + **kwargs: Any, + ) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ... + + def run( # pyright: ignore[reportIncompatibleMethodOverride] # type: ignore[override] + self, + messages: AgentRunInputs | None = None, + *, + stream: bool = False, + session: AgentSession | None = None, + options: OptionsT | None = None, + **kwargs: Any, + ) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]: + """Run the GitHub Copilot agent with telemetry enabled.""" + from typing import cast + + super_run = cast( + "Callable[..., Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]]", + super().run, + ) + return super_run( + messages=messages, + stream=stream, + session=session, + options=options, + **kwargs, + ) diff --git a/python/samples/02-agents/providers/github_copilot/README.md b/python/samples/02-agents/providers/github_copilot/README.md index 572ec9c444..af22e6ac0f 100644 --- a/python/samples/02-agents/providers/github_copilot/README.md +++ b/python/samples/02-agents/providers/github_copilot/README.md @@ -24,12 +24,23 @@ The following environment variables can be configured: | `GITHUB_COPILOT_TIMEOUT` | Request timeout in seconds | `60` | | `GITHUB_COPILOT_LOG_LEVEL` | CLI log level | `info` | +### OpenTelemetry Environment Variables + +When using [`github_copilot_with_observability.py`](github_copilot_with_observability.py), the following OTel variables can be configured: + +| Variable | Description | Default | +|----------|-------------|---------| +| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint (e.g., `http://localhost:4317`) | Console only | +| `OTEL_SERVICE_NAME` | Service name shown in traces | `agent-framework` | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol: `grpc` or `http/protobuf` | `grpc` | + ## Examples | File | Description | |------|-------------| | [`github_copilot_basic.py`](github_copilot_basic.py) | The simplest way to create an agent using `GitHubCopilotAgent`. Demonstrates both streaming and non-streaming responses with function tools. | | [`github_copilot_with_session.py`](github_copilot_with_session.py) | Shows session management with automatic creation, persistence via session objects, and resuming sessions by ID. | +| [`github_copilot_with_observability.py`](github_copilot_with_observability.py) | Shows how to enable OpenTelemetry tracing with `configure_otel_providers()`. Traces agent runs with spans and metrics sent to a configured OTLP backend or console. | | [`github_copilot_with_shell.py`](github_copilot_with_shell.py) | Shows how to enable shell command execution permissions. Demonstrates running system commands like listing files and getting system information. | | [`github_copilot_with_file_operations.py`](github_copilot_with_file_operations.py) | Shows how to enable file read and write permissions. Demonstrates reading file contents and creating new files. | | [`github_copilot_with_url.py`](github_copilot_with_url.py) | Shows how to enable URL fetching permissions. Demonstrates fetching and processing web content. | diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_observability.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_observability.py new file mode 100644 index 0000000000..de0d5956e2 --- /dev/null +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_observability.py @@ -0,0 +1,112 @@ +# Copyright (c) Microsoft. All rights reserved. + +""" +GitHub Copilot Agent with OpenTelemetry Observability + +This sample demonstrates how to enable OpenTelemetry tracing for GitHubCopilotAgent. +Traces are exported via OTLP (configure via environment variables) and/or to the +console for local development. + +Environment variables (OTel): +- OTEL_EXPORTER_OTLP_ENDPOINT - OTLP endpoint (e.g., "http://localhost:4317") +- OTEL_SERVICE_NAME - Service name shown in traces +- OTEL_EXPORTER_OTLP_PROTOCOL - "grpc" or "http/protobuf" + +Environment variables (agent): +- GITHUB_COPILOT_CLI_PATH - Path to the Copilot CLI executable +- GITHUB_COPILOT_MODEL - Model to use (e.g., "gpt-5", "claude-sonnet-4") +- GITHUB_COPILOT_TIMEOUT - Request timeout in seconds +- GITHUB_COPILOT_LOG_LEVEL - CLI log level +""" + +import asyncio +from random import randint +from typing import Annotated + +from agent_framework import tool +from agent_framework.github import GitHubCopilotAgent +from agent_framework.observability import configure_otel_providers +from copilot.generated.session_events import PermissionRequest +from copilot.session import PermissionRequestResult +from dotenv import load_dotenv +from pydantic import Field + +# Load environment variables from .env file +load_dotenv() + + +def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: + """Permission handler that prompts the user for approval.""" + print(f"\n[Permission Request: {request.kind}]") + + if request.full_command_text is not None: + print(f" Command: {request.full_command_text}") + + response = input("Approve? (y/n): ").strip().lower() + if response in ("y", "yes"): + return PermissionRequestResult(kind="approved") + return PermissionRequestResult(kind="denied-interactively-by-user") + + +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/02-agents/tools/function_tool_with_approval.py +# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. +@tool(approval_mode="never_require") +def get_weather( + location: Annotated[str, Field(description="The location to get the weather for.")], +) -> str: + """Get the weather for a given location.""" + conditions = ["sunny", "cloudy", "rainy", "stormy"] + return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}C." + + +async def non_streaming_with_telemetry() -> None: + """Non-streaming example with OTel tracing.""" + print("=== Non-streaming with Telemetry ===") + + agent = GitHubCopilotAgent( + instructions="You are a helpful weather agent.", + tools=[get_weather], + default_options={"on_permission_request": prompt_permission}, + ) + + async with agent: + query = "What's the weather like in Seattle and Tokyo?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + + +async def streaming_with_telemetry() -> None: + """Streaming example with OTel tracing.""" + print("=== Streaming with Telemetry ===") + + agent = GitHubCopilotAgent( + instructions="You are a helpful weather agent.", + tools=[get_weather], + default_options={"on_permission_request": prompt_permission}, + ) + + async with agent: + query = "What's the weather like in Paris?" + print(f"User: {query}") + print("Agent: ", end="", flush=True) + async for chunk in agent.run(query, stream=True): + if chunk.text: + print(chunk.text, end="", flush=True) + print("\n") + + +async def main() -> None: + # Configure OTel providers before creating any agents. + # - enable_console_exporters=True writes spans to stdout for local development. + # - Set OTEL_EXPORTER_OTLP_ENDPOINT to send traces to a collector instead. + configure_otel_providers(enable_console_exporters=True) + + print("=== GitHub Copilot Agent with OpenTelemetry ===\n") + await non_streaming_with_telemetry() + await streaming_with_telemetry() + + +if __name__ == "__main__": + asyncio.run(main()) From e877d62d6e0d09ce3b93d2d250bd49f86b9b69a2 Mon Sep 17 00:00:00 2001 From: droideronline Date: Tue, 7 Apr 2026 15:15:23 +0530 Subject: [PATCH 2/5] Python: Fix OTEL_SERVICE_NAME default in GitHub Copilot README Co-Authored-By: Claude Sonnet 4.6 --- python/samples/02-agents/providers/github_copilot/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/samples/02-agents/providers/github_copilot/README.md b/python/samples/02-agents/providers/github_copilot/README.md index af22e6ac0f..a9403ae9b9 100644 --- a/python/samples/02-agents/providers/github_copilot/README.md +++ b/python/samples/02-agents/providers/github_copilot/README.md @@ -31,7 +31,7 @@ When using [`github_copilot_with_observability.py`](github_copilot_with_observab | Variable | Description | Default | |----------|-------------|---------| | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint (e.g., `http://localhost:4317`) | Console only | -| `OTEL_SERVICE_NAME` | Service name shown in traces | `agent-framework` | +| `OTEL_SERVICE_NAME` | Service name shown in traces | `agent_framework` | | `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol: `grpc` or `http/protobuf` | `grpc` | ## Examples From 42680ad3ddfe307b59ee3fe4026f1c1f0acf68aa Mon Sep 17 00:00:00 2001 From: droideronline Date: Fri, 10 Apr 2026 12:49:17 +0530 Subject: [PATCH 3/5] Python: Add unit tests for RawGitHubCopilotAgent.default_options property --- .../tests/test_github_copilot_agent.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index 17e9432e40..45125cdc22 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -189,6 +189,30 @@ def test_instructions_parameter_defaults_to_append_mode(self) -> None: "content": "Direct instructions", } + def test_default_options_includes_model_for_telemetry(self) -> None: + """Test that default_options merges model from settings for AgentTelemetryLayer span attributes.""" + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + default_options={"model": "claude-sonnet-4-5", "timeout": 120} + ) + opts = agent.default_options + assert opts["model"] == "claude-sonnet-4-5" + + def test_default_options_without_model_configured(self) -> None: + """Test that default_options works correctly when no model is configured.""" + agent = GitHubCopilotAgent(instructions="Helper") + opts = agent.default_options + assert "model" not in opts + assert opts.get("system_message") == {"mode": "append", "content": "Helper"} + + def test_default_options_returns_independent_copy(self) -> None: + """Test that mutating the returned dict does not affect internal state.""" + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + default_options={"model": "gpt-5.1-mini"} + ) + opts = agent.default_options + opts["model"] = "mutated" + assert agent._settings.get("model") == "gpt-5.1-mini" + class TestGitHubCopilotAgentLifecycle: """Test cases for agent lifecycle management.""" From 248b2cb1d47c387f381ecacd842b2abf23e6748a Mon Sep 17 00:00:00 2001 From: droideronline Date: Fri, 10 Apr 2026 15:04:51 +0530 Subject: [PATCH 4/5] Python: Address review feedback on GitHubCopilotAgent OTel integration - Add middleware param to GitHubCopilotAgent.run() overloads so per-call middleware is explicitly forwarded through AgentTelemetryLayer - Remove github_copilot_with_observability.py sample per feedback; replace with inline snippet + link to observability samples in README --- .../agent_framework_github_copilot/_agent.py | 4 + .../providers/github_copilot/README.md | 21 ++-- .../github_copilot_with_observability.py | 112 ------------------ 3 files changed, 17 insertions(+), 120 deletions(-) delete mode 100644 python/samples/02-agents/providers/github_copilot/github_copilot_with_observability.py diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index a9e26fc7cc..a87768a1c7 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -820,6 +820,7 @@ def run( *, stream: Literal[False] = ..., session: AgentSession | None = None, + middleware: Sequence[AgentMiddlewareTypes] | None = None, options: OptionsT | None = None, **kwargs: Any, ) -> Awaitable[AgentResponse]: ... @@ -831,6 +832,7 @@ def run( *, stream: Literal[True], session: AgentSession | None = None, + middleware: Sequence[AgentMiddlewareTypes] | None = None, options: OptionsT | None = None, **kwargs: Any, ) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ... @@ -841,6 +843,7 @@ def run( # pyright: ignore[reportIncompatibleMethodOverride] # type: ignore[ov *, stream: bool = False, session: AgentSession | None = None, + middleware: Sequence[AgentMiddlewareTypes] | None = None, options: OptionsT | None = None, **kwargs: Any, ) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]: @@ -855,6 +858,7 @@ def run( # pyright: ignore[reportIncompatibleMethodOverride] # type: ignore[ov messages=messages, stream=stream, session=session, + middleware=middleware, options=options, **kwargs, ) diff --git a/python/samples/02-agents/providers/github_copilot/README.md b/python/samples/02-agents/providers/github_copilot/README.md index a9403ae9b9..7fb25025bc 100644 --- a/python/samples/02-agents/providers/github_copilot/README.md +++ b/python/samples/02-agents/providers/github_copilot/README.md @@ -24,15 +24,21 @@ The following environment variables can be configured: | `GITHUB_COPILOT_TIMEOUT` | Request timeout in seconds | `60` | | `GITHUB_COPILOT_LOG_LEVEL` | CLI log level | `info` | -### OpenTelemetry Environment Variables +## Observability -When using [`github_copilot_with_observability.py`](github_copilot_with_observability.py), the following OTel variables can be configured: +`GitHubCopilotAgent` has OpenTelemetry tracing built-in. To enable it, call `configure_otel_providers()` before running the agent: -| Variable | Description | Default | -|----------|-------------|---------| -| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint (e.g., `http://localhost:4317`) | Console only | -| `OTEL_SERVICE_NAME` | Service name shown in traces | `agent_framework` | -| `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol: `grpc` or `http/protobuf` | `grpc` | +```python +from agent_framework.observability import configure_otel_providers +from agent_framework_github_copilot import GitHubCopilotAgent + +configure_otel_providers(enable_console_exporters=True) + +async with GitHubCopilotAgent() as agent: + response = await agent.run("Hello!") +``` + +See the [observability samples](../../../02-agents/observability/) for full examples with OTLP exporters. ## Examples @@ -40,7 +46,6 @@ When using [`github_copilot_with_observability.py`](github_copilot_with_observab |------|-------------| | [`github_copilot_basic.py`](github_copilot_basic.py) | The simplest way to create an agent using `GitHubCopilotAgent`. Demonstrates both streaming and non-streaming responses with function tools. | | [`github_copilot_with_session.py`](github_copilot_with_session.py) | Shows session management with automatic creation, persistence via session objects, and resuming sessions by ID. | -| [`github_copilot_with_observability.py`](github_copilot_with_observability.py) | Shows how to enable OpenTelemetry tracing with `configure_otel_providers()`. Traces agent runs with spans and metrics sent to a configured OTLP backend or console. | | [`github_copilot_with_shell.py`](github_copilot_with_shell.py) | Shows how to enable shell command execution permissions. Demonstrates running system commands like listing files and getting system information. | | [`github_copilot_with_file_operations.py`](github_copilot_with_file_operations.py) | Shows how to enable file read and write permissions. Demonstrates reading file contents and creating new files. | | [`github_copilot_with_url.py`](github_copilot_with_url.py) | Shows how to enable URL fetching permissions. Demonstrates fetching and processing web content. | diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_observability.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_observability.py deleted file mode 100644 index de0d5956e2..0000000000 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_observability.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -""" -GitHub Copilot Agent with OpenTelemetry Observability - -This sample demonstrates how to enable OpenTelemetry tracing for GitHubCopilotAgent. -Traces are exported via OTLP (configure via environment variables) and/or to the -console for local development. - -Environment variables (OTel): -- OTEL_EXPORTER_OTLP_ENDPOINT - OTLP endpoint (e.g., "http://localhost:4317") -- OTEL_SERVICE_NAME - Service name shown in traces -- OTEL_EXPORTER_OTLP_PROTOCOL - "grpc" or "http/protobuf" - -Environment variables (agent): -- GITHUB_COPILOT_CLI_PATH - Path to the Copilot CLI executable -- GITHUB_COPILOT_MODEL - Model to use (e.g., "gpt-5", "claude-sonnet-4") -- GITHUB_COPILOT_TIMEOUT - Request timeout in seconds -- GITHUB_COPILOT_LOG_LEVEL - CLI log level -""" - -import asyncio -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.github import GitHubCopilotAgent -from agent_framework.observability import configure_otel_providers -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - - -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.full_command_text is not None: - print(f" Command: {request.full_command_text}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}C." - - -async def non_streaming_with_telemetry() -> None: - """Non-streaming example with OTel tracing.""" - print("=== Non-streaming with Telemetry ===") - - agent = GitHubCopilotAgent( - instructions="You are a helpful weather agent.", - tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, - ) - - async with agent: - query = "What's the weather like in Seattle and Tokyo?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - -async def streaming_with_telemetry() -> None: - """Streaming example with OTel tracing.""" - print("=== Streaming with Telemetry ===") - - agent = GitHubCopilotAgent( - instructions="You are a helpful weather agent.", - tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, - ) - - async with agent: - query = "What's the weather like in Paris?" - print(f"User: {query}") - print("Agent: ", end="", flush=True) - async for chunk in agent.run(query, stream=True): - if chunk.text: - print(chunk.text, end="", flush=True) - print("\n") - - -async def main() -> None: - # Configure OTel providers before creating any agents. - # - enable_console_exporters=True writes spans to stdout for local development. - # - Set OTEL_EXPORTER_OTLP_ENDPOINT to send traces to a collector instead. - configure_otel_providers(enable_console_exporters=True) - - print("=== GitHub Copilot Agent with OpenTelemetry ===\n") - await non_streaming_with_telemetry() - await streaming_with_telemetry() - - -if __name__ == "__main__": - asyncio.run(main()) From 45449ea93ffe5db74a489b39d2ab5c04b9e191eb Mon Sep 17 00:00:00 2001 From: droideronline Date: Fri, 10 Apr 2026 21:20:59 +0530 Subject: [PATCH 5/5] Python: Address review feedback on log_level and session kwargs typing - Add middleware param to RawGitHubCopilotAgent.run() overloads for interface compatibility with AgentTelemetryLayer - Fix import in README observability snippet to use agent_framework.github --- .../agent_framework_github_copilot/_agent.py | 7 +++++++ .../samples/02-agents/providers/github_copilot/README.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index a87768a1c7..142801d9a8 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -314,6 +314,7 @@ def run( *, stream: Literal[False] = False, session: AgentSession | None = None, + middleware: Sequence[AgentMiddlewareTypes] | None = None, options: OptionsT | None = None, **kwargs: Any, ) -> Awaitable[AgentResponse]: ... @@ -325,6 +326,7 @@ def run( *, stream: Literal[True], session: AgentSession | None = None, + middleware: Sequence[AgentMiddlewareTypes] | None = None, options: OptionsT | None = None, **kwargs: Any, ) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ... @@ -335,6 +337,7 @@ def run( *, stream: bool = False, session: AgentSession | None = None, + middleware: Sequence[AgentMiddlewareTypes] | None = None, options: OptionsT | None = None, **kwargs: Any, # type: ignore[override] ) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]: @@ -350,6 +353,9 @@ def run( Keyword Args: stream: Whether to stream the response. Defaults to False. session: The conversation session associated with the message(s). + middleware: Not used by this agent directly. Accepted for interface + compatibility; pass middleware via :class:`GitHubCopilotAgent` which + forwards it through :class:`AgentTelemetryLayer`. options: Runtime options (model, timeout, etc.). kwargs: Additional keyword arguments for compatibility with the shared agent interface (e.g. compaction_strategy, tokenizer). Not used by this agent. @@ -361,6 +367,7 @@ def run( Raises: AgentException: If the request fails. """ + del middleware # not used; accepted for interface compatibility with AgentTelemetryLayer if stream: ctx_holder: dict[str, Any] = {} diff --git a/python/samples/02-agents/providers/github_copilot/README.md b/python/samples/02-agents/providers/github_copilot/README.md index 7fb25025bc..5f8ee344d6 100644 --- a/python/samples/02-agents/providers/github_copilot/README.md +++ b/python/samples/02-agents/providers/github_copilot/README.md @@ -30,7 +30,7 @@ The following environment variables can be configured: ```python from agent_framework.observability import configure_otel_providers -from agent_framework_github_copilot import GitHubCopilotAgent +from agent_framework.github import GitHubCopilotAgent configure_otel_providers(enable_console_exporters=True)