Script: src/python/scripts/tutorials/02_issue_triage.py
- How to define custom tools with the
@define_tooldecorator - How to use Pydantic models for tool input and output schemas
- How to build a tool-calling agent that classifies and labels GitHub issues
- The
copilotCLI installed and authenticated (see Getting Started) github-copilot-sdkandpydanticinstalled
Custom tools let you give the Copilot agent access to your own functions. The agent decides when to call them based on their descriptions. You define:
- The tool's name and description (used by the LLM to decide when to call it)
- The input schema (a Pydantic
BaseModel) - The output schema (another Pydantic
BaseModel) - The implementation (a regular Python function)
from pydantic import BaseModel
class ListIssuesInput(BaseModel):
pass # No parameters needed
class IssueItem(BaseModel):
id: int
title: str
body: str
labels: list[str]
class ListIssuesOutput(BaseModel):
issues: list[IssueItem]
class LabelIssueInput(BaseModel):
issue_id: int
labels: list[str]
class LabelIssueOutput(BaseModel):
success: bool
issue_id: int
applied_labels: list[str]Clear, typed schemas help the LLM understand what data to pass and what to expect back.
from copilot.tools import define_tool
@define_tool(
name="list_issues",
description="Return the list of open GitHub issues to triage.",
)
def list_issues(_input: ListIssuesInput) -> ListIssuesOutput:
return ListIssuesOutput(
issues=[IssueItem(**issue) for issue in SAMPLE_ISSUES]
)
@define_tool(
name="label_issue",
description="Apply one or more labels to a GitHub issue.",
)
def label_issue(input: LabelIssueInput) -> LabelIssueOutput:
# In a real scenario, call the GitHub API here
return LabelIssueOutput(
success=True,
issue_id=input.issue_id,
applied_labels=input.labels,
)Tip: Write descriptive
descriptionstrings. The LLM uses them to decide when to invoke each tool.
session = await client.create_session(
SessionConfig(
on_permission_request=approve_all,
tools=[list_issues, label_issue], # ← register here
streaming=False,
system_message=SystemMessageReplaceConfig(
mode="replace",
content=(
"You are an expert GitHub issue triage assistant. "
"Use list_issues to fetch open issues, classify each one "
"as 'bug', 'enhancement', or 'documentation', then call "
"label_issue to apply the appropriate label."
),
),
)
)Note SystemMessageReplaceConfig — this replaces the default system message entirely, giving the agent a focused persona.
reply = await session.send_and_wait(
MessageOptions(prompt="Please triage all open issues and apply the appropriate labels."),
timeout=300,
)
print(reply.data.content)The agent will:
- Call
list_issues()to fetch the issues - Analyse each issue
- Call
label_issue()for each one with the appropriate label - Return a summary
cd src/python
uv run python scripts/tutorials/02_issue_triage.py
uv run python scripts/tutorials/02_issue_triage.py --cli-url localhost:3000 # optional: use a running CLI serverExpected output:
[Tool] Calling: list_issues
[Tool] Calling: label_issue
[Tool] Calling: label_issue
[Tool] Calling: label_issue
=== Triage Summary ===
I've triaged all 3 open issues...
=== Applied Labels ===
[
{"id": 1, "labels": ["bug"]},
{"id": 2, "labels": ["enhancement"]},
{"id": 3, "labels": ["documentation"]}
]
@define_tool(name, description)registers a function as a callable tool- Pydantic
BaseModeldefines strongly-typed input/output contracts - Tools are registered per-session in
SessionConfig(tools=[...]) - The LLM decides when to call tools based on the task and the description strings
SystemMessageReplaceConfiggives the agent a dedicated persona for the task