-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Python: Add BaseAgent implementation for GitHub Copilot SDK #3404
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a new Python package agent-framework-github-copilot that integrates the GitHub Copilot SDK into the Microsoft Agent Framework, enabling developers to create agents powered by GitHub Copilot with support for streaming responses, custom tools, session management, and configurable permissions for system operations.
Changes:
- New
agent-framework-github-copilotpackage with GithubCopilotAgent implementation - Support for streaming and non-streaming responses with custom function tools
- Permission handling system for shell, file operations, MCP, and URL access
- Comprehensive test suite and sample applications demonstrating various usage patterns
- Integration with core package through lazy imports and stub files
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
python/uv.lock |
Added github-copilot-sdk dependency (v0.1.15) |
python/pyproject.toml |
Added agent-framework-github-copilot to workspace dependencies |
python/packages/core/pyproject.toml |
Added to 'all' extras group |
python/packages/core/agent_framework/github_copilot/__init__.py |
Lazy import mechanism for GitHub Copilot package |
python/packages/core/agent_framework/github_copilot/__init__.pyi |
Type stub file for IDE support |
python/packages/github_copilot/pyproject.toml |
Package configuration and metadata |
python/packages/github_copilot/LICENSE |
MIT license file |
python/packages/github_copilot/README.md |
Package documentation with examples |
python/packages/github_copilot/agent_framework_github_copilot/__init__.py |
Package exports and version management |
python/packages/github_copilot/agent_framework_github_copilot/_settings.py |
Settings class for configuration management |
python/packages/github_copilot/agent_framework_github_copilot/_agent.py |
Main GithubCopilotAgent implementation with streaming, tools, and sessions |
python/packages/github_copilot/tests/__init__.py |
Test package marker |
python/packages/github_copilot/tests/conftest.py |
Pytest fixtures for testing |
python/packages/github_copilot/tests/test_github_copilot_agent.py |
Comprehensive unit tests for agent functionality |
python/samples/getting_started/agents/github_copilot/README.md |
Sample documentation |
python/samples/getting_started/agents/github_copilot/github_copilot_basic.py |
Basic usage example with tools |
python/samples/getting_started/agents/github_copilot/github_copilot_with_shell.py |
Shell permissions example |
python/samples/getting_started/agents/github_copilot/github_copilot_with_file_operations.py |
File operations example |
python/samples/getting_started/agents/github_copilot/github_copilot_with_multiple_permissions.py |
Multiple permissions example |
Comments suppressed due to low confidence (12)
python/packages/github_copilot/README.md:129
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
For more comprehensive examples, see the [Github Copilot examples](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/agents/github_copilot/) which demonstrate:
python/samples/getting_started/agents/github_copilot/github_copilot_basic.py:4
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding throughout the documentation.
Github Copilot Agent Basic Example
python/packages/github_copilot/agent_framework_github_copilot/_agent.py:316
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
raise ServiceException(f"Failed to start Github Copilot client: {ex}") from ex
async def stop(self) -> None:
"""Stop the Copilot client and clean up resources.
This method destroys all active sessions created by this agent and stops
the Copilot client. It is called automatically when using the agent as
an async context manager.
Note:
Only sessions created by this agent instance (stored in self._sessions)
are destroyed. Sessions created elsewhere are not affected.
"""
for session in self._sessions.values():
with contextlib.suppress(Exception):
await session.destroy()
self._sessions.clear()
if self._client and self._owns_client:
with contextlib.suppress(Exception):
await self._client.stop()
self._started = False
async def run(
self,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
tools: ToolProtocol
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
options: TOptions | None = None,
**kwargs: Any,
) -> AgentResponse:
"""Get a response from the agent.
This method returns the final result of the agent's execution
as a single AgentResponse object. The caller is blocked until
the final result is available.
Args:
messages: The message(s) to send to the agent.
Keyword Args:
thread: The conversation thread associated with the message(s).
tools: Additional tools to use for this specific run.
options: Runtime options (model, timeout, etc.).
kwargs: Additional keyword arguments.
Returns:
An agent response item.
Raises:
ServiceException: If the request fails.
"""
if not self._started:
await self.start()
if not thread:
thread = self.get_new_thread()
opts: dict[str, Any] = dict(options) if options else {}
timeout = opts.pop("timeout", None) or self._settings.timeout or DEFAULT_TIMEOUT_SECONDS
merged_tools = self._merge_tools(tools)
session = await self._get_or_create_session(thread, merged_tools, streaming=False)
input_messages = normalize_messages(messages)
prompt = "\n".join([message.text for message in input_messages])
try:
response_event = await session.send_and_wait({"prompt": prompt}, timeout=timeout)
except Exception as ex:
raise ServiceException(f"Github Copilot request failed: {ex}") from ex
python/samples/getting_started/agents/github_copilot/README.md:1
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
# Github Copilot Agent Examples
python/samples/getting_started/agents/github_copilot/README.md:17
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
2. **Github Copilot Subscription**: An active Github Copilot subscription
python/packages/github_copilot/agent_framework_github_copilot/_agent.py:84
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated throughout the codebase to match GitHub's official branding.
"""A Github Copilot Agent.
python/packages/github_copilot/agent_framework_github_copilot/_agent.py:550
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
ServiceException: If the request fails.
"""
if not self._started:
await self.start()
if not thread:
thread = self.get_new_thread()
merged_tools = self._merge_tools(tools)
session = await self._get_or_create_session(thread, merged_tools, streaming=True)
input_messages = normalize_messages(messages)
prompt = "\n".join([message.text for message in input_messages])
import asyncio
completion_event = asyncio.Event()
updates: list[AgentResponseUpdate] = []
errors: list[Exception] = []
def event_handler(event: SessionEvent) -> None:
if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
delta_content = event.data.delta_content or ""
if delta_content:
updates.append(
AgentResponseUpdate(
role=Role.ASSISTANT,
contents=[Content.from_text(delta_content)],
response_id=event.data.message_id,
message_id=event.data.message_id,
raw_representation=event,
)
)
elif event.type == SessionEventType.SESSION_IDLE:
completion_event.set()
elif event.type == SessionEventType.SESSION_ERROR:
error_msg = event.data.message or "Unknown error"
errors.append(ServiceException(f"Github Copilot session error: {error_msg}"))
completion_event.set()
unsubscribe = session.on(event_handler)
try:
await session.send({"prompt": prompt})
await completion_event.wait()
if errors:
raise errors[0]
for update in updates:
yield update
finally:
unsubscribe()
def _create_permission_handler(
self,
allowed_permissions: Sequence[str] | None,
) -> PermissionHandlerType | None:
"""Create a permission handler from a list of allowed permission kinds."""
if not allowed_permissions:
return None
allowed_set = set(allowed_permissions)
def handler(
request: PermissionRequest,
context: dict[str, str],
) -> PermissionRequestResult:
kind = request.get("kind")
if kind in allowed_set:
return PermissionRequestResult(kind="approved")
return PermissionRequestResult(kind="denied-interactively-by-user")
return handler
def _merge_tools(
self,
runtime_tools: ToolProtocol
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| None,
) -> list[ToolProtocol | MutableMapping[str, Any]]:
"""Merge runtime tools with default tools."""
result = list(self._tools)
result.extend(normalize_tools(runtime_tools))
return result
def _convert_tools_to_copilot_tools(
self,
tools: list[ToolProtocol | MutableMapping[str, Any]],
) -> list[CopilotTool]:
"""Convert Agent Framework tools to Copilot SDK tools.
Args:
tools: List of Agent Framework tools.
Returns:
List of Copilot SDK tools.
"""
copilot_tools: list[CopilotTool] = []
for tool in tools:
if isinstance(tool, AIFunction):
copilot_tools.append(self._ai_function_to_copilot_tool(tool)) # type: ignore
return copilot_tools
def _ai_function_to_copilot_tool(self, ai_func: AIFunction[Any, Any]) -> CopilotTool:
"""Convert an AIFunction to a Copilot SDK tool."""
async def handler(invocation: ToolInvocation) -> ToolResult:
args = invocation.get("arguments", {})
try:
if ai_func.input_model:
args_instance = ai_func.input_model(**args)
result = await ai_func.invoke(arguments=args_instance)
else:
result = await ai_func.invoke(arguments=args)
return ToolResult(
textResultForLlm=str(result),
resultType="success",
toolTelemetry={},
)
except Exception as e:
return ToolResult(
textResultForLlm=f"Error: {e}",
resultType="failure",
error=str(e),
toolTelemetry={},
)
return CopilotTool(
name=ai_func.name,
description=ai_func.description,
handler=handler,
parameters=ai_func.parameters(),
)
async def _get_or_create_session(
self,
thread: AgentThread,
tools: list[ToolProtocol | MutableMapping[str, Any]],
streaming: bool = False,
) -> CopilotSession:
"""Get an existing session or create a new one for the thread.
Args:
thread: The conversation thread.
tools: Tools to register with the session.
streaming: Whether to enable streaming for the session.
Returns:
A CopilotSession instance.
Raises:
ServiceException: If the session cannot be created.
"""
if not self._client:
raise ServiceException("Github Copilot client not initialized. Call start() first.")
if thread.service_thread_id and thread.service_thread_id in self._sessions:
return self._sessions[thread.service_thread_id]
config: SessionConfig = {"streaming": streaming}
if self._settings.model:
config["model"] = self._settings.model # type: ignore[typeddict-item]
if self._instructions:
config["system_message"] = {"mode": "append", "content": self._instructions}
if tools:
config["tools"] = self._convert_tools_to_copilot_tools(tools)
if self._permission_handler:
config["on_permission_request"] = self._permission_handler
try:
session = await self._client.create_session(config)
thread.service_thread_id = session.session_id
self._sessions[session.session_id] = session
return session
except Exception as ex:
raise ServiceException(f"Failed to create Github Copilot session: {ex}") from ex
python/packages/github_copilot/README.md:11
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
## Github Copilot Agent
The Github Copilot agent enables integration with Github Copilot, allowing you to interact with Copilot's agentic capabilities through the Agent Framework.
python/packages/github_copilot/README.md:18
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
Before using the Github Copilot agent, you need:
1. **Github Copilot CLI**: The Copilot CLI must be installed and authenticated
2. **Github Copilot Subscription**: An active Github Copilot subscription
python/samples/getting_started/agents/github_copilot/github_copilot_with_shell.py:4
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
Github Copilot Agent with Shell Permissions
python/samples/getting_started/agents/github_copilot/github_copilot_with_file_operations.py:53
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
print("=== Github Copilot Agent with File Operation Permissions ===\n")
python/samples/getting_started/agents/github_copilot/github_copilot_with_multiple_permissions.py:27
- The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
print("=== Github Copilot Agent with Multiple Permissions ===\n")
python/packages/github_copilot/agent_framework_github_copilot/_agent.py
Outdated
Show resolved
Hide resolved
python/samples/getting_started/agents/github_copilot/github_copilot_basic.py
Outdated
Show resolved
Hide resolved
python/packages/github_copilot/agent_framework_github_copilot/_agent.py
Outdated
Show resolved
Hide resolved
...on/samples/getting_started/agents/github_copilot/github_copilot_with_multiple_permissions.py
Outdated
Show resolved
Hide resolved
python/packages/github_copilot/agent_framework_github_copilot/_agent.py
Outdated
Show resolved
Hide resolved
python/packages/github_copilot/agent_framework_github_copilot/_settings.py
Show resolved
Hide resolved
python/packages/github_copilot/agent_framework_github_copilot/__init__.py
Show resolved
Hide resolved
python/samples/getting_started/agents/github_copilot/github_copilot_with_file_operations.py
Outdated
Show resolved
Hide resolved
eavanvalkenburg
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll review the implementation later, but loving this!
Motivation and Context
This PR adds
agent-framework-github-copilotpackage that wraps the GitHub Copilot SDK to provide Copilot agentic capabilities within the Agent Framework.Contribution Checklist