Skip to content

Commit 73033c3

Browse files
authored
Python: Updated instructions/system_message logic in GitHub Copilot agent (microsoft#3625)
* Updated instructions handling * Small improvement * Included runtime options in session creation logic
1 parent 98cd728 commit 73033c3

9 files changed

Lines changed: 206 additions & 65 deletions

File tree

python/packages/github_copilot/agent_framework_github_copilot/_agent.py

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
PermissionRequestResult,
3232
ResumeSessionConfig,
3333
SessionConfig,
34+
SystemMessageConfig,
3435
ToolInvocation,
3536
ToolResult,
3637
)
@@ -57,8 +58,9 @@
5758
class GitHubCopilotOptions(TypedDict, total=False):
5859
"""GitHub Copilot-specific options."""
5960

60-
instructions: str
61-
"""System message to append to the session."""
61+
system_message: SystemMessageConfig
62+
"""System message configuration for the session. Use mode 'append' to add to the default
63+
system prompt, or 'replace' to completely override it."""
6264

6365
cli_path: str
6466
"""Path to the Copilot CLI executable. Defaults to GITHUB_COPILOT_CLI_PATH environment variable
@@ -139,6 +141,7 @@ def get_weather(city: str) -> str:
139141

140142
def __init__(
141143
self,
144+
instructions: str | None = None,
142145
*,
143146
client: CopilotClient | None = None,
144147
id: str | None = None,
@@ -157,6 +160,9 @@ def __init__(
157160
) -> None:
158161
"""Initialize the GitHub Copilot Agent.
159162
163+
Args:
164+
instructions: System message for the agent.
165+
160166
Keyword Args:
161167
client: Optional pre-configured CopilotClient instance. If not provided,
162168
a new client will be created using the other parameters.
@@ -188,7 +194,10 @@ def __init__(
188194

189195
# Parse options
190196
opts: dict[str, Any] = dict(default_options) if default_options else {}
191-
instructions = opts.pop("instructions", None)
197+
198+
# Handle instructions - direct parameter takes precedence over default_options.system_message
199+
self._prepare_system_message(instructions, opts)
200+
192201
cli_path = opts.pop("cli_path", None)
193202
model = opts.pop("model", None)
194203
timeout = opts.pop("timeout", None)
@@ -208,7 +217,6 @@ def __init__(
208217
except ValidationError as ex:
209218
raise ServiceInitializationError("Failed to create GitHub Copilot settings.", ex) from ex
210219

211-
self._instructions = instructions
212220
self._tools = normalize_tools(tools)
213221
self._permission_handler = on_permission_request
214222
self._mcp_servers = mcp_servers
@@ -302,7 +310,7 @@ async def run(
302310
opts: dict[str, Any] = dict(options) if options else {}
303311
timeout = opts.pop("timeout", None) or self._settings.timeout or DEFAULT_TIMEOUT_SECONDS
304312

305-
session = await self._get_or_create_session(thread, streaming=False)
313+
session = await self._get_or_create_session(thread, streaming=False, runtime_options=opts)
306314
input_messages = normalize_messages(messages)
307315
prompt = "\n".join([message.text for message in input_messages])
308316

@@ -365,7 +373,9 @@ async def run_stream(
365373
if not thread:
366374
thread = self.get_new_thread()
367375

368-
session = await self._get_or_create_session(thread, streaming=True)
376+
opts: dict[str, Any] = dict(options) if options else {}
377+
378+
session = await self._get_or_create_session(thread, streaming=True, runtime_options=opts)
369379
input_messages = normalize_messages(messages)
370380
prompt = "\n".join([message.text for message in input_messages])
371381

@@ -400,6 +410,29 @@ def event_handler(event: SessionEvent) -> None:
400410
finally:
401411
unsubscribe()
402412

413+
@staticmethod
414+
def _prepare_system_message(
415+
instructions: str | None,
416+
opts: dict[str, Any],
417+
) -> None:
418+
"""Prepare system message configuration in opts.
419+
420+
If instructions is provided, it takes precedence for content.
421+
If system_message is also provided, its mode is preserved.
422+
Modifies opts in place.
423+
424+
Args:
425+
instructions: Direct instructions parameter for content.
426+
opts: Options dictionary to modify.
427+
"""
428+
opts_system_message = opts.pop("system_message", None)
429+
if instructions is not None:
430+
# Use instructions for content, but preserve mode from system_message if provided
431+
mode = opts_system_message.get("mode", "append") if opts_system_message else "append"
432+
opts["system_message"] = {"mode": mode, "content": instructions}
433+
elif opts_system_message is not None:
434+
opts["system_message"] = opts_system_message
435+
403436
def _prepare_tools(
404437
self,
405438
tools: list[ToolProtocol | MutableMapping[str, Any]],
@@ -459,12 +492,14 @@ async def _get_or_create_session(
459492
self,
460493
thread: AgentThread,
461494
streaming: bool = False,
495+
runtime_options: dict[str, Any] | None = None,
462496
) -> CopilotSession:
463497
"""Get an existing session or create a new one for the thread.
464498
465499
Args:
466500
thread: The conversation thread.
467501
streaming: Whether to enable streaming for the session.
502+
runtime_options: Runtime options from run/run_stream that take precedence.
468503
469504
Returns:
470505
A CopilotSession instance.
@@ -479,33 +514,47 @@ async def _get_or_create_session(
479514
if thread.service_thread_id:
480515
return await self._resume_session(thread.service_thread_id, streaming)
481516

482-
session = await self._create_session(streaming)
517+
session = await self._create_session(streaming, runtime_options)
483518
thread.service_thread_id = session.session_id
484519
return session
485520
except Exception as ex:
486521
raise ServiceException(f"Failed to create GitHub Copilot session: {ex}") from ex
487522

488-
async def _create_session(self, streaming: bool) -> CopilotSession:
489-
"""Create a new Copilot session."""
523+
async def _create_session(
524+
self,
525+
streaming: bool,
526+
runtime_options: dict[str, Any] | None = None,
527+
) -> CopilotSession:
528+
"""Create a new Copilot session.
529+
530+
Args:
531+
streaming: Whether to enable streaming for the session.
532+
runtime_options: Runtime options that take precedence over default_options.
533+
"""
490534
if not self._client:
491535
raise ServiceException("GitHub Copilot client not initialized. Call start() first.")
492536

537+
opts = runtime_options or {}
493538
config: SessionConfig = {"streaming": streaming}
494539

495-
if self._settings.model:
496-
config["model"] = self._settings.model # type: ignore[typeddict-item]
540+
model = opts.get("model") or self._settings.model
541+
if model:
542+
config["model"] = model # type: ignore[typeddict-item]
497543

498-
if self._instructions:
499-
config["system_message"] = {"mode": "append", "content": self._instructions}
544+
system_message = opts.get("system_message") or self._default_options.get("system_message")
545+
if system_message:
546+
config["system_message"] = system_message
500547

501548
if self._tools:
502549
config["tools"] = self._prepare_tools(self._tools)
503550

504-
if self._permission_handler:
505-
config["on_permission_request"] = self._permission_handler
551+
permission_handler = opts.get("on_permission_request") or self._permission_handler
552+
if permission_handler:
553+
config["on_permission_request"] = permission_handler
506554

507-
if self._mcp_servers:
508-
config["mcp_servers"] = self._mcp_servers
555+
mcp_servers = opts.get("mcp_servers") or self._mcp_servers
556+
if mcp_servers:
557+
config["mcp_servers"] = mcp_servers
509558

510559
return await self._client.create_session(config)
511560

python/packages/github_copilot/tests/test_github_copilot_agent.py

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,52 @@ def my_tool(arg: str) -> str:
135135
agent = GitHubCopilotAgent(tools=[my_tool])
136136
assert len(agent._tools) == 1 # type: ignore
137137

138-
def test_init_with_instructions(self) -> None:
139-
"""Test initialization with custom instructions."""
138+
def test_init_with_instructions_parameter(self) -> None:
139+
"""Test initialization with instructions parameter."""
140+
agent = GitHubCopilotAgent(instructions="You are a helpful assistant.")
141+
assert agent._default_options.get("system_message") == { # type: ignore
142+
"mode": "append",
143+
"content": "You are a helpful assistant.",
144+
}
145+
146+
def test_init_with_system_message_in_default_options(self) -> None:
147+
"""Test initialization with system_message object in default_options."""
148+
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
149+
default_options={"system_message": {"mode": "append", "content": "You are a helpful assistant."}}
150+
)
151+
assert agent._default_options.get("system_message") == { # type: ignore
152+
"mode": "append",
153+
"content": "You are a helpful assistant.",
154+
}
155+
156+
def test_init_with_system_message_replace_mode(self) -> None:
157+
"""Test initialization with system_message in replace mode."""
158+
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
159+
default_options={"system_message": {"mode": "replace", "content": "Custom system prompt."}}
160+
)
161+
assert agent._default_options.get("system_message") == { # type: ignore
162+
"mode": "replace",
163+
"content": "Custom system prompt.",
164+
}
165+
166+
def test_instructions_parameter_takes_precedence_for_content(self) -> None:
167+
"""Test that direct instructions parameter takes precedence for content but preserves mode."""
140168
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
141-
default_options={"instructions": "You are a helpful assistant."}
169+
instructions="Direct instructions",
170+
default_options={"system_message": {"mode": "replace", "content": "Options system_message"}},
142171
)
143-
assert agent._instructions == "You are a helpful assistant." # type: ignore
172+
assert agent._default_options.get("system_message") == { # type: ignore
173+
"mode": "replace",
174+
"content": "Direct instructions",
175+
}
176+
177+
def test_instructions_parameter_defaults_to_append_mode(self) -> None:
178+
"""Test that instructions parameter defaults to append mode when no system_message provided."""
179+
agent = GitHubCopilotAgent(instructions="Direct instructions")
180+
assert agent._default_options.get("system_message") == { # type: ignore
181+
"mode": "append",
182+
"content": "Direct instructions",
183+
}
144184

145185

146186
class TestGitHubCopilotAgentLifecycle:
@@ -462,10 +502,10 @@ async def test_session_config_includes_instructions(
462502
mock_client: MagicMock,
463503
mock_session: MagicMock,
464504
) -> None:
465-
"""Test that session config includes instructions."""
466-
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
505+
"""Test that session config includes instructions from direct parameter."""
506+
agent = GitHubCopilotAgent(
507+
instructions="You are a helpful assistant.",
467508
client=mock_client,
468-
default_options={"instructions": "You are a helpful assistant."},
469509
)
470510
await agent.start()
471511

@@ -476,6 +516,31 @@ async def test_session_config_includes_instructions(
476516
assert config["system_message"]["mode"] == "append"
477517
assert config["system_message"]["content"] == "You are a helpful assistant."
478518

519+
async def test_runtime_options_take_precedence_over_default(
520+
self,
521+
mock_client: MagicMock,
522+
mock_session: MagicMock,
523+
) -> None:
524+
"""Test that runtime options from run() take precedence over default_options."""
525+
agent = GitHubCopilotAgent(
526+
instructions="Default instructions",
527+
client=mock_client,
528+
)
529+
await agent.start()
530+
531+
runtime_options: GitHubCopilotOptions = {
532+
"system_message": {"mode": "replace", "content": "Runtime instructions"}
533+
}
534+
await agent._get_or_create_session( # type: ignore
535+
AgentThread(),
536+
runtime_options=runtime_options,
537+
)
538+
539+
call_args = mock_client.create_session.call_args
540+
config = call_args[0][0]
541+
assert config["system_message"]["mode"] == "replace"
542+
assert config["system_message"]["content"] == "Runtime instructions"
543+
479544
async def test_session_config_includes_streaming_flag(
480545
self,
481546
mock_client: MagicMock,

python/samples/getting_started/agents/github_copilot/github_copilot_basic.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from typing import Annotated
1919

2020
from agent_framework import tool
21-
from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions
21+
from agent_framework.github import GitHubCopilotAgent
2222
from pydantic import Field
2323

2424

@@ -36,8 +36,8 @@ async def non_streaming_example() -> None:
3636
"""Example of non-streaming response (get the complete result at once)."""
3737
print("=== Non-streaming Response Example ===")
3838

39-
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
40-
default_options={"instructions": "You are a helpful weather agent."},
39+
agent = GitHubCopilotAgent(
40+
instructions="You are a helpful weather agent.",
4141
tools=[get_weather],
4242
)
4343

@@ -52,8 +52,8 @@ async def streaming_example() -> None:
5252
"""Example of streaming response (get results as they are generated)."""
5353
print("=== Streaming Response Example ===")
5454

55-
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
56-
default_options={"instructions": "You are a helpful weather agent."},
55+
agent = GitHubCopilotAgent(
56+
instructions="You are a helpful weather agent.",
5757
tools=[get_weather],
5858
)
5959

@@ -67,11 +67,46 @@ async def streaming_example() -> None:
6767
print("\n")
6868

6969

70+
async def runtime_options_example() -> None:
71+
"""Example of overriding system message at runtime."""
72+
print("=== Runtime Options Example ===")
73+
74+
agent = GitHubCopilotAgent(
75+
instructions="Always respond in exactly 3 words.",
76+
tools=[get_weather],
77+
)
78+
79+
async with agent:
80+
query = "What's the weather like in Paris?"
81+
82+
# First call uses default instructions (3 words response)
83+
print("Using default instructions (3 words):")
84+
print(f"User: {query}")
85+
result1 = await agent.run(query)
86+
print(f"Agent: {result1}\n")
87+
88+
# Second call overrides with runtime system_message in replace mode
89+
print("Using runtime system_message with replace mode (detailed response):")
90+
print(f"User: {query}")
91+
result2 = await agent.run(
92+
query,
93+
options={
94+
"system_message": {
95+
"mode": "replace",
96+
"content": "You are a weather expert. Provide detailed weather information "
97+
"with temperature, and recommendations.",
98+
}
99+
},
100+
)
101+
print(f"Agent: {result2}\n")
102+
103+
70104
async def main() -> None:
71105
print("=== Basic GitHub Copilot Agent Example ===")
72106

73107
await non_streaming_example()
74108
await streaming_example()
109+
await runtime_options_example()
75110

76111

77112
if __name__ == "__main__":

python/samples/getting_started/agents/github_copilot/github_copilot_with_file_operations.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import asyncio
1616

17-
from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions
17+
from agent_framework.github import GitHubCopilotAgent
1818
from copilot.types import PermissionRequest, PermissionRequestResult
1919

2020

@@ -35,11 +35,9 @@ def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> Pe
3535
async def main() -> None:
3636
print("=== GitHub Copilot Agent with File Operation Permissions ===\n")
3737

38-
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
39-
default_options={
40-
"instructions": "You are a helpful assistant that can read and write files.",
41-
"on_permission_request": prompt_permission,
42-
},
38+
agent = GitHubCopilotAgent(
39+
instructions="You are a helpful assistant that can read and write files.",
40+
default_options={"on_permission_request": prompt_permission},
4341
)
4442

4543
async with agent:

0 commit comments

Comments
 (0)