Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/python-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
# Test the oldest supported Python version to make sure compatibility is maintained.
python-version: ["3.11"]
runs-on: ${{ matrix.os }}
defaults:
run:
Expand All @@ -46,7 +48,7 @@ jobs:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-python@v6
with:
python-version: "3.12"
python-version: ${{ matrix.python-version }}
- uses: actions/setup-node@v6
with:
node-version: "22"
Expand Down
6 changes: 3 additions & 3 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,11 +401,11 @@ async def handle_user_input(request, invocation):
# request["question"] - The question to ask
# request.get("choices") - Optional list of choices for multiple choice
# request.get("allowFreeform", True) - Whether freeform input is allowed

print(f"Agent asks: {request['question']}")
if request.get("choices"):
print(f"Choices: {', '.join(request['choices'])}")

# Return the user's response
return {
"answer": "User's answer here",
Expand Down Expand Up @@ -483,5 +483,5 @@ session = await client.create_session({

## Requirements

- Python 3.9+
- Python 3.11+
- GitHub Copilot CLI installed and accessible
25 changes: 13 additions & 12 deletions python/copilot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
import subprocess
import sys
import threading
from collections.abc import Callable
from dataclasses import asdict, is_dataclass
from pathlib import Path
from typing import Any, Callable, Optional, cast
from typing import Any, cast

from .generated.rpc import ServerRpc
from .generated.session_events import session_event_from_dict
Expand Down Expand Up @@ -51,7 +52,7 @@
)


def _get_bundled_cli_path() -> Optional[str]:
def _get_bundled_cli_path() -> str | None:
"""Get the path to the bundled CLI binary, if available."""
# The binary is bundled in copilot/bin/ within the package
bin_dir = Path(__file__).parent / "bin"
Expand Down Expand Up @@ -106,7 +107,7 @@ class CopilotClient:
>>> client = CopilotClient({"cli_url": "localhost:3000"})
"""

def __init__(self, options: Optional[CopilotClientOptions] = None):
def __init__(self, options: CopilotClientOptions | None = None):
"""
Initialize a new CopilotClient.
Expand Down Expand Up @@ -151,7 +152,7 @@ def __init__(self, options: Optional[CopilotClientOptions] = None):
self._is_external_server: bool = False
if opts.get("cli_url"):
self._actual_host, actual_port = self._parse_cli_url(opts["cli_url"])
self._actual_port: Optional[int] = actual_port
self._actual_port: int | None = actual_port
self._is_external_server = True
else:
self._actual_port = None
Expand Down Expand Up @@ -197,19 +198,19 @@ def __init__(self, options: Optional[CopilotClientOptions] = None):
if github_token:
self.options["github_token"] = github_token

self._process: Optional[subprocess.Popen] = None
self._client: Optional[JsonRpcClient] = None
self._process: subprocess.Popen | None = None
self._client: JsonRpcClient | None = None
self._state: ConnectionState = "disconnected"
self._sessions: dict[str, CopilotSession] = {}
self._sessions_lock = threading.Lock()
self._models_cache: Optional[list[ModelInfo]] = None
self._models_cache: list[ModelInfo] | None = None
self._models_cache_lock = asyncio.Lock()
self._lifecycle_handlers: list[SessionLifecycleHandler] = []
self._typed_lifecycle_handlers: dict[
SessionLifecycleEventType, list[SessionLifecycleHandler]
] = {}
self._lifecycle_handlers_lock = threading.Lock()
self._rpc: Optional[ServerRpc] = None
self._rpc: ServerRpc | None = None

@property
def rpc(self) -> ServerRpc:
Expand Down Expand Up @@ -786,7 +787,7 @@ def get_state(self) -> ConnectionState:
"""
return self._state

async def ping(self, message: Optional[str] = None) -> "PingResponse":
async def ping(self, message: str | None = None) -> "PingResponse":
"""
Send a ping request to the server to verify connectivity.
Expand Down Expand Up @@ -956,7 +957,7 @@ async def delete_session(self, session_id: str) -> None:
if session_id in self._sessions:
del self._sessions[session_id]

async def get_foreground_session_id(self) -> Optional[str]:
async def get_foreground_session_id(self) -> str | None:
"""
Get the ID of the session currently displayed in the TUI.
Expand Down Expand Up @@ -1009,7 +1010,7 @@ async def set_foreground_session_id(self, session_id: str) -> None:
def on(
self,
event_type_or_handler: SessionLifecycleEventType | SessionLifecycleHandler,
handler: Optional[SessionLifecycleHandler] = None,
handler: SessionLifecycleHandler | None = None,
) -> Callable[[], None]:
"""
Subscribe to session lifecycle events.
Expand Down Expand Up @@ -1267,7 +1268,7 @@ async def read_port():

try:
await asyncio.wait_for(read_port(), timeout=10.0)
except asyncio.TimeoutError:
except TimeoutError:
raise RuntimeError("Timeout waiting for CLI server to start")

async def _connect_to_server(self) -> None:
Expand Down
55 changes: 28 additions & 27 deletions python/copilot/generated/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@


from dataclasses import dataclass
from typing import Any, Optional, List, Dict, TypeVar, Type, cast, Callable
from typing import Any, TypeVar, cast
from collections.abc import Callable
from enum import Enum


Expand Down Expand Up @@ -52,22 +53,22 @@ def from_bool(x: Any) -> bool:
return x


def to_class(c: Type[T], x: Any) -> dict:
def to_class(c: type[T], x: Any) -> dict:
assert isinstance(x, c)
return cast(Any, x).to_dict()


def from_list(f: Callable[[Any], T], x: Any) -> List[T]:
def from_list(f: Callable[[Any], T], x: Any) -> list[T]:
assert isinstance(x, list)
return [f(y) for y in x]


def from_dict(f: Callable[[Any], T], x: Any) -> Dict[str, T]:
def from_dict(f: Callable[[Any], T], x: Any) -> dict[str, T]:
assert isinstance(x, dict)
return { k: f(v) for (k, v) in x.items() }


def to_enum(c: Type[EnumT], x: Any) -> EnumT:
def to_enum(c: type[EnumT], x: Any) -> EnumT:
assert isinstance(x, c)
return x.value

Expand Down Expand Up @@ -101,7 +102,7 @@ def to_dict(self) -> dict:

@dataclass
class PingParams:
message: Optional[str] = None
message: str | None = None
"""Optional message to echo back"""

@staticmethod
Expand Down Expand Up @@ -138,8 +139,8 @@ def to_dict(self) -> dict:
@dataclass
class Limits:
max_context_window_tokens: float
max_output_tokens: Optional[float] = None
max_prompt_tokens: Optional[float] = None
max_output_tokens: float | None = None
max_prompt_tokens: float | None = None

@staticmethod
def from_dict(obj: Any) -> 'Limits':
Expand Down Expand Up @@ -233,16 +234,16 @@ class Model:
name: str
"""Display name"""

billing: Optional[Billing] = None
billing: Billing | None = None
"""Billing information"""

default_reasoning_effort: Optional[str] = None
default_reasoning_effort: str | None = None
"""Default reasoning effort level (only present if model supports reasoning effort)"""

policy: Optional[Policy] = None
policy: Policy | None = None
"""Policy state (if applicable)"""

supported_reasoning_efforts: Optional[List[str]] = None
supported_reasoning_efforts: list[str] | None = None
"""Supported reasoning effort levels (only present if model supports reasoning effort)"""

@staticmethod
Expand Down Expand Up @@ -275,7 +276,7 @@ def to_dict(self) -> dict:

@dataclass
class ModelsListResult:
models: List[Model]
models: list[Model]
"""List of available models with full metadata"""

@staticmethod
Expand All @@ -298,14 +299,14 @@ class Tool:
name: str
"""Tool identifier (e.g., "bash", "grep", "str_replace_editor")"""

instructions: Optional[str] = None
instructions: str | None = None
"""Optional instructions for how to use this tool effectively"""

namespaced_name: Optional[str] = None
namespaced_name: str | None = None
"""Optional namespaced name for declarative filtering (e.g., "playwright/navigate" for MCP
tools)
"""
parameters: Optional[Dict[str, Any]] = None
parameters: dict[str, Any] | None = None
"""JSON Schema for the tool's input parameters"""

@staticmethod
Expand Down Expand Up @@ -333,7 +334,7 @@ def to_dict(self) -> dict:

@dataclass
class ToolsListResult:
tools: List[Tool]
tools: list[Tool]
"""List of available built-in tools with metadata"""

@staticmethod
Expand All @@ -350,7 +351,7 @@ def to_dict(self) -> dict:

@dataclass
class ToolsListParams:
model: Optional[str] = None
model: str | None = None
"""Optional model ID — when provided, the returned tool list reflects model-specific
overrides
"""
Expand Down Expand Up @@ -385,7 +386,7 @@ class QuotaSnapshot:
used_requests: float
"""Number of requests used so far this period"""

reset_date: Optional[str] = None
reset_date: str | None = None
"""Date when the quota resets (ISO 8601)"""

@staticmethod
Expand Down Expand Up @@ -413,7 +414,7 @@ def to_dict(self) -> dict:

@dataclass
class AccountGetQuotaResult:
quota_snapshots: Dict[str, QuotaSnapshot]
quota_snapshots: dict[str, QuotaSnapshot]
"""Quota snapshots keyed by type (e.g., chat, completions, premium_interactions)"""

@staticmethod
Expand All @@ -430,7 +431,7 @@ def to_dict(self) -> dict:

@dataclass
class SessionModelGetCurrentResult:
model_id: Optional[str] = None
model_id: str | None = None

@staticmethod
def from_dict(obj: Any) -> 'SessionModelGetCurrentResult':
Expand All @@ -447,7 +448,7 @@ def to_dict(self) -> dict:

@dataclass
class SessionModelSwitchToResult:
model_id: Optional[str] = None
model_id: str | None = None

@staticmethod
def from_dict(obj: Any) -> 'SessionModelSwitchToResult':
Expand Down Expand Up @@ -546,7 +547,7 @@ class SessionPlanReadResult:
exists: bool
"""Whether plan.md exists in the workspace"""

content: Optional[str] = None
content: str | None = None
"""The content of plan.md, or null if it does not exist"""

@staticmethod
Expand Down Expand Up @@ -606,7 +607,7 @@ def to_dict(self) -> dict:

@dataclass
class SessionWorkspaceListFilesResult:
files: List[str]
files: list[str]
"""Relative file paths in the workspace files directory"""

@staticmethod
Expand Down Expand Up @@ -708,7 +709,7 @@ def to_dict(self) -> dict:

@dataclass
class SessionFleetStartParams:
prompt: Optional[str] = None
prompt: str | None = None
"""Optional user prompt to combine with fleet instructions"""

@staticmethod
Expand Down Expand Up @@ -753,7 +754,7 @@ def to_dict(self) -> dict:

@dataclass
class SessionAgentListResult:
agents: List[AgentElement]
agents: list[AgentElement]
"""Available custom agents"""

@staticmethod
Expand Down Expand Up @@ -797,7 +798,7 @@ def to_dict(self) -> dict:

@dataclass
class SessionAgentGetCurrentResult:
agent: Optional[SessionAgentGetCurrentResultAgent] = None
agent: SessionAgentGetCurrentResultAgent | None = None
"""Currently selected custom agent, or null if using the default agent"""

@staticmethod
Expand Down
Loading
Loading