Skip to content
Open
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
309 changes: 309 additions & 0 deletions examples/tracing/copilot_sdk/copilot_sdk_tracing.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "7fb27b941602401d91542211134fc71a",
"metadata": {},
"source": [
"# GitHub Copilot SDK tracing with Openlayer\n",
"\n",
"[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)]",
"(https://colab.research.google.com/github/openlayer-ai/openlayer-python/blob/main/examples/tracing/copilot_sdk/copilot_sdk_tracing.ipynb)\n",
"\n",
"This notebook shows how to stream traces from agents built on the ",
"[GitHub Copilot SDK](https://github.com/github/copilot-sdk) to Openlayer.\n",
"\n",
"Each `send()` becomes one Openlayer trace:\n",
"\n",
"```\n",
"AGENT \"GitHub Copilot\" <- the user prompt in, the final answer out\n",
"\u251c\u2500 CHAT_COMPLETION \"turn 0\" <- model, tokens, cost, latency\n",
"\u251c\u2500 TOOL \"bash\" <- arguments in, result out\n",
"\u251c\u2500 AGENT \"subagent: Explore\" <- a `task` dispatch\n",
"\u2502 \u251c\u2500 CHAT_COMPLETION \"turn 0\"\n",
"\u2502 \u2514\u2500 TOOL \"view\"\n",
"\u2514\u2500 CHAT_COMPLETION \"turn 1\"\n",
"```\n",
"\n",
"> **Requires Python 3.11+** \u2014 that is `github-copilot-sdk`'s own floor."
]
},
{
"cell_type": "markdown",
"id": "acae54e37e7d407bbb7b55eff062a284",
"metadata": {},
"source": [
"## 1. Install"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9a63283cbaf04dbcab1f6479b197f3a8",
"metadata": {},
"outputs": [],
"source": [
"%pip install openlayer 'github-copilot-sdk>=1.0.11'"
]
},
{
"cell_type": "markdown",
"id": "8dd0d8092fe74a7c96281538738b07e2",
"metadata": {},
"source": [
"## 2. Credentials\n",
"\n",
"The Copilot SDK authenticates with your logged-in GitHub user by default; set ",
"`GITHUB_TOKEN` if you'd rather be explicit. You need a GitHub account with Copilot access."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "72eea5119410473aa328ad9291626812",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\"OPENLAYER_API_KEY\"] = \"YOUR_OPENLAYER_API_KEY_HERE\"\n",
"os.environ[\"OPENLAYER_INFERENCE_PIPELINE_ID\"] = \"YOUR_OPENLAYER_INFERENCE_PIPELINE_ID_HERE\"\n",
"# os.environ[\"GITHUB_TOKEN\"] = \"YOUR_GITHUB_TOKEN_HERE\""
]
},
{
"cell_type": "markdown",
"id": "8edb47106e1a46a883d545849b8ab81b",
"metadata": {},
"source": [
"## 3. Enable tracing\n",
"\n",
"`init()` is the canonical entry point. With `auto_instrument` on (the default) it\n",
"detects every supported SDK you have installed \u2014 including the Copilot SDK \u2014 and\n",
"patches it, so every session you create is traced with no change to the code that\n",
"builds them.\n",
"\n",
"If you'd rather be explicit about which sessions are traced, pass\n",
"`on_event=openlayer_event_handler()` to `create_session()` instead. Mixing the two\n",
"is safe \u2014 the patch defers to a handler you supply rather than adding a second one."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "10185d26023b46108eb7d9f57d49d2b3",
"metadata": {},
"outputs": [],
"source": [
"from openlayer.lib import init\n",
"\n",
"init()\n"
]
},
{
"cell_type": "markdown",
"id": "8763a12b2bbd4a93a75aff182afb95dc",
"metadata": {},
"source": [
"## 4. A workspace to work in\n",
"\n",
"Copilot is a coding agent, so give it some real files to look at."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7623eae2785240b9bd12b16a66d81610",
"metadata": {},
"outputs": [],
"source": [
"import pathlib\n",
"import tempfile\n",
"\n",
"workspace = pathlib.Path(tempfile.mkdtemp(prefix=\"openlayer-copilot-\"))\n",
"(workspace / \"app.py\").write_text(\"def greet(name):\\n return f'hi {name}'\\n\")\n",
"(workspace / \"README.md\").write_text(\"# Demo\\n\\nA tiny example project.\\n\")\n",
"print(workspace) # noqa: T201\n"
]
},
{
"cell_type": "markdown",
"id": "7cdc8c89c7104fffa095e18ddfef8986",
"metadata": {},
"source": [
"## Scenario 1 \u2014 a basic session\n",
"\n",
"`PermissionHandler.approve_all` auto-approves tool use. In production you'd supply your own ",
"policy; the decision is captured on the tool step either way."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b118ea5561624da68c537baed56e602f",
"metadata": {},
"outputs": [],
"source": [
"\n",
"from copilot import CopilotClient, PermissionHandler\n",
"\n",
"\n",
"async def basic_session():\n",
" client = CopilotClient(working_directory=str(workspace), log_level=\"error\")\n",
" await client.start()\n",
" try:\n",
" session = await client.create_session(\n",
" working_directory=str(workspace),\n",
" on_permission_request=PermissionHandler.approve_all,\n",
" )\n",
" reply = await session.send_and_wait(\n",
" \"List the files here with bash, then summarize the project in one sentence.\",\n",
" timeout=300,\n",
" )\n",
" print(reply.data.content) # noqa: T201\n",
" await session.disconnect()\n",
" finally:\n",
" await client.stop()\n",
"\n",
"\n",
"await basic_session()"
]
},
{
"cell_type": "markdown",
"id": "938c804e27f84196a10c8828c723f798",
"metadata": {},
"source": [
"## Scenario 2 \u2014 a client-side tool and a subagent\n",
"\n",
"Tools you define with `define_tool` run in *your* process and still appear as `TOOL` steps. ",
"A `task` dispatch becomes a nested `AGENT` step, with the subagent's own turns and tool calls inside it.\n",
"\n",
"This also shows `openlayer_event_handler()` \u2014 the explicit alternative to `trace_copilot()`, ",
"useful when you build sessions yourself. It composes with your own `on_event`: pass both and ",
"each still receives every event."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "504fb2a444614c0babb325280ed9130a",
"metadata": {},
"outputs": [],
"source": [
"from copilot import CopilotClient, PermissionHandler, define_tool\n",
"from pydantic import BaseModel\n",
"\n",
"\n",
"class WeatherParams(BaseModel):\n",
" city: str\n",
"\n",
"\n",
"@define_tool(description=\"Get the current weather for a city.\")\n",
"def get_weather(params: WeatherParams) -> str:\n",
" return f\"It is 22C and sunny in {params.city}.\"\n",
"\n",
"\n",
"async def tools_and_subagent():\n",
" client = CopilotClient(working_directory=str(workspace), log_level=\"error\")\n",
" await client.start()\n",
" try:\n",
" session = await client.create_session(\n",
" working_directory=str(workspace),\n",
" on_permission_request=PermissionHandler.approve_all,\n",
" tools=[get_weather],\n",
" )\n",
" reply = await session.send_and_wait(\n",
" \"Do two things: call get_weather for Lisbon, and delegate to a subagent \"\n",
" \"to read app.py and summarize it.\",\n",
" timeout=300,\n",
" )\n",
" print(reply.data.content) # noqa: T201\n",
" await session.disconnect()\n",
" finally:\n",
" await client.stop()\n",
"\n",
"\n",
"await tools_and_subagent()"
]
},
{
"cell_type": "markdown",
"id": "59bbdb311c014d738909a11f9e486628",
"metadata": {},
"source": [
"## Scenario 3 \u2014 multi-turn session grouping\n",
"\n",
"Each `send()` is its own trace, but they all carry the same Copilot session id, so Openlayer ",
"groups them into one session."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b43b363d81ae4b689946ece5c682cd59",
"metadata": {},
"outputs": [],
"source": [
"from copilot import CopilotClient, PermissionHandler\n",
"\n",
"\n",
"async def multi_turn():\n",
" client = CopilotClient(working_directory=str(workspace), log_level=\"error\")\n",
" await client.start()\n",
" try:\n",
" session = await client.create_session(\n",
" working_directory=str(workspace),\n",
" on_permission_request=PermissionHandler.approve_all,\n",
" )\n",
" for prompt in (\n",
" \"What files are in this directory?\",\n",
" \"What does app.py define?\",\n",
" ):\n",
" reply = await session.send_and_wait(prompt, timeout=300)\n",
" print(f\"Q: {prompt}\\nA: {reply.data.content}\\n\") # noqa: T201\n",
" await session.disconnect()\n",
" finally:\n",
" await client.stop()\n",
"\n",
"\n",
"await multi_turn()"
]
},
{
"cell_type": "markdown",
"id": "8a65eabff63a45729fe45fb5ade58bdc",
"metadata": {},
"source": [
"## What lands in Openlayer\n",
"\n",
"| | |\n",
"|---|---|\n",
"| **Row prompt / output** | the user's message and the final assistant answer |\n",
"| **Cost** | priced by Openlayer from the real provider and model |\n",
"| **Tokens** | input / output / cached / cache-creation, as a non-overlapping partition |\n",
"| **Session** | the Copilot session id, so multi-turn conversations group |\n",
"| **Tools** | arguments, results, success/failure, permission decisions, MCP server |\n",
"| **Subagents** | nested agent steps with their own turns, tools and token totals |\n",
"\n",
"One thing worth knowing: Copilot's own `cost` field is **premium-request units, not dollars** ",
"(a flat per-model multiplier, identical on every call regardless of size). Openlayer therefore ",
"prices the call itself from the provider and model, and keeps Copilot's figure in step metadata ",
"as `copilot_premium_requests`."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
59 changes: 59 additions & 0 deletions src/openlayer/lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"unpatch_google_adk",
"trace_claude_agent_sdk",
"traced_claude_agent_sdk_query",
"trace_copilot",
"untrace_copilot",
"trace_gemini",
"trace_google_genai",
"update_current_trace",
Expand Down Expand Up @@ -393,6 +395,63 @@ def traced_claude_agent_sdk_query(*, prompt, options=None, inference_pipeline_id
)


# ---------------------------- GitHub Copilot SDK ---------------------------- #
def trace_copilot(
*,
inference_pipeline_id=None,
truncate_tool_output_chars: int = 8192,
capture_reasoning: bool = True,
):
"""Enable Openlayer tracing for the GitHub Copilot SDK.

Monkey-patches ``copilot.CopilotClient.create_session`` so every session
becomes an Openlayer trace with nested steps for assistant turns, tool
calls and subagents, including tokens and cost. Idempotent, and composes
with any ``on_event`` handler you pass yourself.

Requirements:
``github-copilot-sdk>=1.0.11`` must be installed:
``pip install 'github-copilot-sdk>=1.0.11'``

Args:
inference_pipeline_id: Optional Openlayer inference pipeline ID. Falls
back to the ``OPENLAYER_INFERENCE_PIPELINE_ID`` env var.
truncate_tool_output_chars: Maximum characters of tool output to
capture per TOOL step. Defaults to 8192.
capture_reasoning: Whether to capture reasoning text into
chat-completion step metadata. Defaults to True.

Example:
>>> import os
>>> os.environ["OPENLAYER_API_KEY"] = "..."
>>> os.environ["OPENLAYER_INFERENCE_PIPELINE_ID"] = "..."
>>> from openlayer.lib import trace_copilot
>>> trace_copilot()
>>>
>>> from copilot import CopilotClient
>>> client = CopilotClient()
>>> await client.start()
>>> session = await client.create_session()
>>> await session.send_and_wait("Summarize this repository")
"""
# pylint: disable=import-outside-toplevel
from .integrations import copilot_sdk as _integration

return _integration.trace_copilot(
inference_pipeline_id=inference_pipeline_id,
truncate_tool_output_chars=truncate_tool_output_chars,
capture_reasoning=capture_reasoning,
)


def untrace_copilot():
"""Undo :func:`trace_copilot`, restoring the original ``create_session``."""
# pylint: disable=import-outside-toplevel
from .integrations import copilot_sdk as _integration

return _integration.untrace_copilot()


# -------------------------------- Google Gemini --------------------------------- #
def _legacy_gemini_model_class():
"""``google.generativeai.GenerativeModel``, or None if not installed."""
Expand Down
9 changes: 9 additions & 0 deletions src/openlayer/lib/integrations/_auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ def _do_patch() -> None:
_patch_via("google_adk_tracer", "trace_google_adk"),
_patch_via("google_adk_tracer", "unpatch_google_adk"),
),
# `github-copilot-sdk` imports as `copilot`. Unlike the entries above this
# one patches a *session factory* rather than a client class, but the
# patch/unpatch contract is the same: idempotent and reversible.
IntegrationSpec(
"copilot",
"copilot",
_patch_via("copilot_sdk", "trace_copilot"),
_patch_via("copilot_sdk", "untrace_copilot"),
),
)


Expand Down
Loading