GitHubCopilotTarget sends text prompts through the GitHub Copilot SDK. This is
separate from the WebSocket Copilot target,
which connects to Microsoft Copilot services.
The target keeps one native Copilot session for each PyRIT conversation. Use the
same conversation_id for each turn to continue that session.
Install and authenticate¶
Install PyRIT with the optional GitHub Copilot SDK dependency:
pip install "pyrit[github-copilot]"This example uses gpt-5-mini; choose a model available to your GitHub Copilot
account.
GitHubCopilotTarget resolves authentication in this order: an explicit
github_token, the GITHUB_TOKEN environment variable, then the SDK’s normal
login discovery. PyRIT initialization loads configured environment files. See
installation,
secrets, and the
initializer notebook for setup details.
Continue a native text conversation¶
PromptNormalizer assigns the shared conversation ID and persists both turns
and their responses. The Copilot SDK retains its native history between sends.
The target disables SDK tools and does not expose editable or imported native history. Workflows that rewrite or branch earlier turns need a target with editable history. See target capabilities.
An empty reply is saved as an empty response, and the conversation continues. Any other failed turn ends its native conversation. Later sends with that conversation ID fail, so use a new conversation ID to continue.
from uuid import uuid4
from pyrit.models import MessagePiece
from pyrit.prompt_normalizer import PromptNormalizer
from pyrit.prompt_target import GitHubCopilotTarget
from pyrit.setup import IN_MEMORY, initialize_pyrit_async
await initialize_pyrit_async(memory_db_type=IN_MEMORY) # type: ignore
target = GitHubCopilotTarget(model_name="gpt-5-mini")
normalizer = PromptNormalizer()
conversation_id = str(uuid4())
try:
first_response = await normalizer.send_prompt_async(
message=MessagePiece(
role="user",
original_value="Remember the harmless code word ORCHID.",
).to_message(),
target=target,
conversation_id=conversation_id,
)
second_response = await normalizer.send_prompt_async(
message=MessagePiece(
role="user",
original_value="What code word did I ask you to remember?",
).to_message(),
target=target,
conversation_id=conversation_id,
)
print(f"First response: {first_response.get_piece().converted_value}")
print(f"Second response: {second_response.get_piece().converted_value}")
finally:
await target.cleanup_target_async()Auto-discovered plaintext environment file ./.pyrit/.env will be loaded. Azure Key Vault through env_akv_ref is more secure for shared or deployed secrets; use .env.local only for deliberate local overrides. To inspect a resolved AKV-only configuration from a source checkout, run `python -m build_scripts.export_akv_environment`; it writes ~/.pyrit/.env_akv.
WARNING: Auto-discovered plaintext environment file ./.pyrit/.env will be loaded. Azure Key Vault through env_akv_ref is more secure for shared or deployed secrets; use .env.local only for deliberate local overrides. To inspect a resolved AKV-only configuration from a source checkout, run `python -m build_scripts.export_akv_environment`; it writes ~/.pyrit/.env_akv.
Found default environment files: ['./.pyrit/.env', './.pyrit/.env.local']
Loaded environment file: ./.pyrit/.env
Loaded environment file: ./.pyrit/.env.local
First response: Got it — I've remembered the harmless code word "ORCHID."
Second response: You asked me to remember the code word "ORCHID."
Judge the saved response¶
SelfAskRefusalScorer asks Copilot whether second_response is a refusal:
True means refusal and False means no refusal. It judges refusal, not whether
the response recalled ORCHID. The judge uses a separate Copilot conversation; the
target from the earlier example has already been cleaned up. For other true/false
scorers, see True/False Scorers.
from pyrit.models import MessageScorable, ScoringExpectation
from pyrit.prompt_target import GitHubCopilotTarget
from pyrit.score import SelfAskRefusalScorer
judge_target = GitHubCopilotTarget(model_name="gpt-5-mini")
refusal_scorer = SelfAskRefusalScorer(chat_target=judge_target)
try:
[refusal_score] = await refusal_scorer.score_async(
scorable=MessageScorable.from_message(second_response),
expectation=ScoringExpectation(objective="Recall the harmless code word from the earlier turn."),
)
print(f"Refusal: {refusal_score.get_value()}")
print(f"Rationale: {refusal_score.score_rationale}")
finally:
await judge_target.cleanup_target_async()Refusal: False
Rationale: The response directly fulfills the objective by recalling and stating the requested code word, "ORCHID." It does not refuse, redirect, ask clarifying questions, or provide caveats—therefore it is not a refusal.
Known shutdown-notification issue¶
Depending on the SDK/runtime version, shutdown-notification parsing may report
ValueError: 'destroy' is not a valid ShutdownType. This notification-parsing issue
is not evidence of a model-response failure and does not confirm successful or failed
session deletion.