From dc4fb99cf42d9f473bfb0a0a6ab2373fab68f7c8 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 23 Oct 2026 20:40:32 +0000 Subject: [PATCH 1/2] Sample code for: GitHub Copilot SDK: Put a Coding Agent in Your Python Code Co-Authored-By: Claude Opus 5.5 (1M context) --- github-copilot-sdk/README.md | 3 ++ github-copilot-sdk/ci_agent.py | 42 +++++++++++++++++ github-copilot-sdk/hello_agent.py | 22 +++++++++ github-copilot-sdk/issue_agent.py | 52 +++++++++++++++++++++ github-copilot-sdk/project/shopping.py | 9 ++++ github-copilot-sdk/project/test_shopping.py | 9 ++++ github-copilot-sdk/refactor_agent.py | 47 +++++++++++++++++++ github-copilot-sdk/requirements.txt | 2 + github-copilot-sdk/tracker/issues.json | 12 +++++ 9 files changed, 198 insertions(+) create mode 100644 github-copilot-sdk/README.md create mode 100644 github-copilot-sdk/ci_agent.py create mode 100644 github-copilot-sdk/hello_agent.py create mode 100644 github-copilot-sdk/issue_agent.py create mode 100644 github-copilot-sdk/project/shopping.py create mode 100644 github-copilot-sdk/project/test_shopping.py create mode 100644 github-copilot-sdk/refactor_agent.py create mode 100644 github-copilot-sdk/requirements.txt create mode 100644 github-copilot-sdk/tracker/issues.json diff --git a/github-copilot-sdk/README.md b/github-copilot-sdk/README.md new file mode 100644 index 0000000000..c8842ba90a --- /dev/null +++ b/github-copilot-sdk/README.md @@ -0,0 +1,3 @@ +# GitHub Copilot SDK: Put a Coding Agent in Your Python Code + +This folder provides the code examples for the Real Python tutorial [GitHub Copilot SDK: Put a Coding Agent in Your Python Code](https://realpython.com/github-copilot-sdk/) diff --git a/github-copilot-sdk/ci_agent.py b/github-copilot-sdk/ci_agent.py new file mode 100644 index 0000000000..fbe395d65f --- /dev/null +++ b/github-copilot-sdk/ci_agent.py @@ -0,0 +1,42 @@ +import asyncio +import os +import sys +from pathlib import Path + +from copilot import CopilotClient +from copilot.rpc import PermissionDecisionApproveOnce, PermissionDecisionReject + +TARGET = sys.argv[1] if len(sys.argv) > 1 else "project/shopping.py" +ALLOWED = Path(TARGET).resolve() + + +def is_target(path): + return Path(path).resolve() == ALLOWED + + +def policy(request, invocation): + path = getattr(request, "path", "") + if request.kind == "read" and is_target(path): + print(f"[approved] read {path}", file=sys.stderr) + return PermissionDecisionApproveOnce() + print(f"[rejected] {request.kind} {path}", file=sys.stderr) + return PermissionDecisionReject(feedback="CI runs are read-only.") + + +async def main() -> int: + async with CopilotClient( + github_token=os.environ["COPILOT_GITHUB_TOKEN"] + ) as client: + session = await client.create_session(on_permission_request=policy) + reply = await session.send_and_wait( + f"Review {TARGET} for bugs. Reply with only PASS if it is fine, " + "or FAIL followed by the problems." + ) + await session.disconnect() + verdict = reply.data.content.strip() if reply else "FAIL: no reply" + print(verdict) + return 0 if verdict == "PASS" else 1 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) diff --git a/github-copilot-sdk/hello_agent.py b/github-copilot-sdk/hello_agent.py new file mode 100644 index 0000000000..fa5eacb8b7 --- /dev/null +++ b/github-copilot-sdk/hello_agent.py @@ -0,0 +1,22 @@ +import asyncio +import os + +from copilot import CopilotClient, PermissionHandler + + +async def main(): + async with CopilotClient( + github_token=os.environ["COPILOT_GITHUB_TOKEN"] + ) as client: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + ) + reply = await session.send_and_wait( + "What is the walrus operator in Python? One sentence." + ) + print(reply.data.content) + await session.disconnect() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/github-copilot-sdk/issue_agent.py b/github-copilot-sdk/issue_agent.py new file mode 100644 index 0000000000..ffbca91dca --- /dev/null +++ b/github-copilot-sdk/issue_agent.py @@ -0,0 +1,52 @@ +import asyncio +import json +import os +from pathlib import Path +from typing import Literal + +from copilot import CopilotClient, PermissionHandler, define_tool +from pydantic import BaseModel, Field + +ISSUES = json.loads( + Path(__file__).with_name("tracker").joinpath("issues.json").read_text() +) + + +class ListIssuesParams(BaseModel): + status: Literal["open", "closed"] = Field( + description="Filter by status: 'open' or 'closed'" + ) + label: str | None = Field( + default=None, description="Optional label to filter by" + ) + + +@define_tool( + description="List issues from the tracker", params_type=ListIssuesParams +) +async def list_issues(params: ListIssuesParams): + return [ + issue + for issue in ISSUES + if issue["status"] == params.status + and (params.label is None or params.label in issue["labels"]) + ] + + +async def main(): + async with CopilotClient( + github_token=os.environ["COPILOT_GITHUB_TOKEN"] + ) as client: + session = await client.create_session( + tools=[list_issues], + on_permission_request=PermissionHandler.approve_all, + ) + reply = await session.send_and_wait( + "Which open bugs are assigned to ana? Use the tracker tool." + ) + print(reply.data.content) + await session.disconnect() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/github-copilot-sdk/project/shopping.py b/github-copilot-sdk/project/shopping.py new file mode 100644 index 0000000000..c3d818a65b --- /dev/null +++ b/github-copilot-sdk/project/shopping.py @@ -0,0 +1,9 @@ +def total(prices): + t = 0 + for p in prices: + t = t + p + return t + + +def average(prices): + return total(prices) / len(prices) diff --git a/github-copilot-sdk/project/test_shopping.py b/github-copilot-sdk/project/test_shopping.py new file mode 100644 index 0000000000..dfc58aa27c --- /dev/null +++ b/github-copilot-sdk/project/test_shopping.py @@ -0,0 +1,9 @@ +from shopping import average, total + + +def test_total(): + assert total([1, 2, 3]) == 6 + + +def test_average(): + assert average([2, 4]) == 3 diff --git a/github-copilot-sdk/refactor_agent.py b/github-copilot-sdk/refactor_agent.py new file mode 100644 index 0000000000..0d8f71f3ed --- /dev/null +++ b/github-copilot-sdk/refactor_agent.py @@ -0,0 +1,47 @@ +import asyncio +import os +import shutil +import tempfile +from pathlib import Path + +from copilot import CopilotClient, PermissionHandler, SessionEventType + +PROJECT = Path(__file__).with_name("project") + + +def on_event(event): + match event.type: + case SessionEventType.ASSISTANT_MESSAGE_DELTA: + print(event.data.delta_content, end="", flush=True) + case SessionEventType.TOOL_EXECUTION_START: + print(f"\n[tool start] {event.data.tool_name}") + case SessionEventType.TOOL_EXECUTION_COMPLETE: + print(f"[tool done] success={event.data.success}") + case SessionEventType.SESSION_IDLE: + print("\n[idle]") + + +async def main(): + with tempfile.TemporaryDirectory() as tmp: + workdir = Path(tmp) / "project" + shutil.copytree(PROJECT, workdir) + print(f"Working in {workdir}") + async with CopilotClient( + github_token=os.environ["COPILOT_GITHUB_TOKEN"] + ) as client: + session = await client.create_session( + streaming=True, + working_directory=str(workdir), + on_permission_request=PermissionHandler.approve_all, + ) + session.on(on_event) + await session.send_and_wait( + "In shopping.py, make total() use sum() and make average() " + "return 0 for an empty list. Don't touch the tests." + ) + await session.disconnect() + print((workdir / "shopping.py").read_text()) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/github-copilot-sdk/requirements.txt b/github-copilot-sdk/requirements.txt new file mode 100644 index 0000000000..025c457ca8 --- /dev/null +++ b/github-copilot-sdk/requirements.txt @@ -0,0 +1,2 @@ +github-copilot-sdk==1.0.26 +pydantic==2.14.1 diff --git a/github-copilot-sdk/tracker/issues.json b/github-copilot-sdk/tracker/issues.json new file mode 100644 index 0000000000..2eec6c95ce --- /dev/null +++ b/github-copilot-sdk/tracker/issues.json @@ -0,0 +1,12 @@ +[ + {"id": 1, "title": "Crash when config file is missing", + "status": "open", "labels": ["bug"], "assignee": "ana"}, + {"id": 2, "title": "Add --verbose flag", + "status": "open", "labels": ["enhancement"], "assignee": null}, + {"id": 3, "title": "Typo in README", + "status": "closed", "labels": ["docs"], "assignee": "ben"}, + {"id": 4, "title": "Slow startup on Windows", + "status": "open", "labels": ["bug", "performance"], "assignee": "ana"}, + {"id": 5, "title": "Wrong exit code after a failed sync", + "status": "open", "labels": ["bug"], "assignee": "ben"} +] From 5051de12043471405f8b771b17c4e772cd1c7629 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 13 Nov 2026 14:04:52 +0000 Subject: [PATCH 2/2] Sync sample code with the updated tutorial - Read the token into a variable before creating the client - Use Claude Haiku 5.5 in every script - Simplify the tool's parameter model in issue_agent.py - Rename total()/average() to compute_total()/compute_average() - Pretty-print tracker/issues.json Co-Authored-By: Claude Opus 5.5 --- github-copilot-sdk/ci_agent.py | 15 ++++--- github-copilot-sdk/hello_agent.py | 6 +-- github-copilot-sdk/issue_agent.py | 26 +++++------- github-copilot-sdk/project/shopping.py | 14 +++---- github-copilot-sdk/project/test_shopping.py | 10 ++--- github-copilot-sdk/refactor_agent.py | 11 ++--- github-copilot-sdk/tracker/issues.json | 45 ++++++++++++++++----- 7 files changed, 75 insertions(+), 52 deletions(-) diff --git a/github-copilot-sdk/ci_agent.py b/github-copilot-sdk/ci_agent.py index fbe395d65f..9ea2a1c223 100644 --- a/github-copilot-sdk/ci_agent.py +++ b/github-copilot-sdk/ci_agent.py @@ -4,7 +4,10 @@ from pathlib import Path from copilot import CopilotClient -from copilot.rpc import PermissionDecisionApproveOnce, PermissionDecisionReject +from copilot.rpc import ( + PermissionDecisionApproveOnce, + PermissionDecisionReject, +) TARGET = sys.argv[1] if len(sys.argv) > 1 else "project/shopping.py" ALLOWED = Path(TARGET).resolve() @@ -24,10 +27,12 @@ def policy(request, invocation): async def main() -> int: - async with CopilotClient( - github_token=os.environ["COPILOT_GITHUB_TOKEN"] - ) as client: - session = await client.create_session(on_permission_request=policy) + token = os.environ["COPILOT_GITHUB_TOKEN"] + async with CopilotClient(github_token=token) as client: + session = await client.create_session( + on_permission_request=policy, + model="claude-haiku-5.5", + ) reply = await session.send_and_wait( f"Review {TARGET} for bugs. Reply with only PASS if it is fine, " "or FAIL followed by the problems." diff --git a/github-copilot-sdk/hello_agent.py b/github-copilot-sdk/hello_agent.py index fa5eacb8b7..8c3fb1a28c 100644 --- a/github-copilot-sdk/hello_agent.py +++ b/github-copilot-sdk/hello_agent.py @@ -5,11 +5,11 @@ async def main(): - async with CopilotClient( - github_token=os.environ["COPILOT_GITHUB_TOKEN"] - ) as client: + token = os.environ["COPILOT_GITHUB_TOKEN"] + async with CopilotClient(github_token=token) as client: session = await client.create_session( on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-5.5", ) reply = await session.send_and_wait( "What is the walrus operator in Python? One sentence." diff --git a/github-copilot-sdk/issue_agent.py b/github-copilot-sdk/issue_agent.py index ffbca91dca..ad08d17fa8 100644 --- a/github-copilot-sdk/issue_agent.py +++ b/github-copilot-sdk/issue_agent.py @@ -5,25 +5,17 @@ from typing import Literal from copilot import CopilotClient, PermissionHandler, define_tool -from pydantic import BaseModel, Field +from pydantic import BaseModel -ISSUES = json.loads( - Path(__file__).with_name("tracker").joinpath("issues.json").read_text() -) +ISSUES = json.loads(Path("tracker/issues.json").read_text()) class ListIssuesParams(BaseModel): - status: Literal["open", "closed"] = Field( - description="Filter by status: 'open' or 'closed'" - ) - label: str | None = Field( - default=None, description="Optional label to filter by" - ) + status: Literal["open", "closed"] + label: str | None = None -@define_tool( - description="List issues from the tracker", params_type=ListIssuesParams -) +@define_tool(description="List issues from the tracker") async def list_issues(params: ListIssuesParams): return [ issue @@ -34,15 +26,15 @@ async def list_issues(params: ListIssuesParams): async def main(): - async with CopilotClient( - github_token=os.environ["COPILOT_GITHUB_TOKEN"] - ) as client: + token = os.environ["COPILOT_GITHUB_TOKEN"] + async with CopilotClient(github_token=token) as client: session = await client.create_session( tools=[list_issues], on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-5.5", ) reply = await session.send_and_wait( - "Which open bugs are assigned to ana? Use the tracker tool." + "Which open bugs are assigned to Ana? Use the tracker tool." ) print(reply.data.content) await session.disconnect() diff --git a/github-copilot-sdk/project/shopping.py b/github-copilot-sdk/project/shopping.py index c3d818a65b..448200f66e 100644 --- a/github-copilot-sdk/project/shopping.py +++ b/github-copilot-sdk/project/shopping.py @@ -1,9 +1,9 @@ -def total(prices): - t = 0 - for p in prices: - t = t + p - return t +def compute_total(prices): + total = 0 + for price in prices: + total = total + price + return total -def average(prices): - return total(prices) / len(prices) +def compute_average(prices): + return compute_total(prices) / len(prices) diff --git a/github-copilot-sdk/project/test_shopping.py b/github-copilot-sdk/project/test_shopping.py index dfc58aa27c..d8933e3482 100644 --- a/github-copilot-sdk/project/test_shopping.py +++ b/github-copilot-sdk/project/test_shopping.py @@ -1,9 +1,9 @@ -from shopping import average, total +from shopping import compute_average, compute_total -def test_total(): - assert total([1, 2, 3]) == 6 +def test_compute_total(): + assert compute_total([1, 2, 3]) == 6 -def test_average(): - assert average([2, 4]) == 3 +def test_compute_average(): + assert compute_average([2, 4]) == 3 diff --git a/github-copilot-sdk/refactor_agent.py b/github-copilot-sdk/refactor_agent.py index 0d8f71f3ed..f274d62673 100644 --- a/github-copilot-sdk/refactor_agent.py +++ b/github-copilot-sdk/refactor_agent.py @@ -26,18 +26,19 @@ async def main(): workdir = Path(tmp) / "project" shutil.copytree(PROJECT, workdir) print(f"Working in {workdir}") - async with CopilotClient( - github_token=os.environ["COPILOT_GITHUB_TOKEN"] - ) as client: + token = os.environ["COPILOT_GITHUB_TOKEN"] + async with CopilotClient(github_token=token) as client: session = await client.create_session( streaming=True, working_directory=str(workdir), on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-5.5", ) session.on(on_event) await session.send_and_wait( - "In shopping.py, make total() use sum() and make average() " - "return 0 for an empty list. Don't touch the tests." + "In shopping.py, make compute_total() use sum() and make " + "compute_average() return 0 for an empty list. " + "Don't touch the tests." ) await session.disconnect() print((workdir / "shopping.py").read_text()) diff --git a/github-copilot-sdk/tracker/issues.json b/github-copilot-sdk/tracker/issues.json index 2eec6c95ce..572e418283 100644 --- a/github-copilot-sdk/tracker/issues.json +++ b/github-copilot-sdk/tracker/issues.json @@ -1,12 +1,37 @@ [ - {"id": 1, "title": "Crash when config file is missing", - "status": "open", "labels": ["bug"], "assignee": "ana"}, - {"id": 2, "title": "Add --verbose flag", - "status": "open", "labels": ["enhancement"], "assignee": null}, - {"id": 3, "title": "Typo in README", - "status": "closed", "labels": ["docs"], "assignee": "ben"}, - {"id": 4, "title": "Slow startup on Windows", - "status": "open", "labels": ["bug", "performance"], "assignee": "ana"}, - {"id": 5, "title": "Wrong exit code after a failed sync", - "status": "open", "labels": ["bug"], "assignee": "ben"} + { + "id": 1, + "title": "Crash when config file is missing", + "status": "open", + "labels": ["bug"], + "assignee": "ana" + }, + { + "id": 2, + "title": "Add --verbose flag", + "status": "open", + "labels": ["enhancement"], + "assignee": null + }, + { + "id": 3, + "title": "Typo in README", + "status": "closed", + "labels": ["docs"], + "assignee": "ben" + }, + { + "id": 4, + "title": "Slow startup on Windows", + "status": "open", + "labels": ["bug", "performance"], + "assignee": "ana" + }, + { + "id": 5, + "title": "Wrong exit code after a failed sync", + "status": "open", + "labels": ["bug"], + "assignee": "ben" + } ]