feat: refactor system_prompt to support preset and append options (#183)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (3.10) (push) Waiting to run
Test / test (3.11) (push) Waiting to run
Test / test (3.12) (push) Waiting to run
Test / test (3.13) (push) Waiting to run
Test / test-e2e (3.10) (push) Blocked by required conditions
Test / test-e2e (3.11) (push) Blocked by required conditions
Test / test-e2e (3.12) (push) Blocked by required conditions
Test / test-e2e (3.13) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions

Replace separate system_prompt and append_system_prompt fields with a
single system_prompt field that accepts:
- string: custom system prompt
- {"preset": "claude_code"}: use default Claude Code prompt
- {"preset": "claude_code", "append": "..."}: default prompt with
additions
- None/undefined: vanilla Claude with no system prompt

This matches the TypeScript SDK API design and provides more flexible
system prompt configuration.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Ashwin Bhat 2025-09-26 12:59:27 -07:00 committed by GitHub
parent 507e22cb4a
commit dbb153b1f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 178 additions and 13 deletions

87
examples/system_prompt.py Normal file
View file

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Example demonstrating different system_prompt configurations."""
import anyio
from claude_code_sdk import (
AssistantMessage,
ClaudeCodeOptions,
TextBlock,
query,
)
async def no_system_prompt():
"""Example with no system_prompt (vanilla Claude)."""
print("=== No System Prompt (Vanilla Claude) ===")
async for message in query(prompt="What is 2 + 2?"):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
print()
async def string_system_prompt():
"""Example with system_prompt as a string."""
print("=== String System Prompt ===")
options = ClaudeCodeOptions(
system_prompt="You are a pirate assistant. Respond in pirate speak.",
)
async for message in query(prompt="What is 2 + 2?", options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
print()
async def preset_system_prompt():
"""Example with system_prompt preset (uses default Claude Code prompt)."""
print("=== Preset System Prompt (Default) ===")
options = ClaudeCodeOptions(
system_prompt={"type": "preset", "preset": "claude_code"},
)
async for message in query(prompt="What is 2 + 2?", options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
print()
async def preset_with_append():
"""Example with system_prompt preset and append."""
print("=== Preset System Prompt with Append ===")
options = ClaudeCodeOptions(
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": "Always end your response with a fun fact.",
},
)
async for message in query(prompt="What is 2 + 2?", options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
print()
async def main():
"""Run all examples."""
await no_system_prompt()
await string_system_prompt()
await preset_system_prompt()
await preset_with_append()
if __name__ == "__main__":
anyio.run(main)

View file

@ -85,11 +85,18 @@ class SubprocessCLITransport(Transport):
"""Build CLI command with arguments."""
cmd = [self._cli_path, "--output-format", "stream-json", "--verbose"]
if self._options.system_prompt:
if self._options.system_prompt is None:
pass
elif isinstance(self._options.system_prompt, str):
cmd.extend(["--system-prompt", self._options.system_prompt])
if self._options.append_system_prompt:
cmd.extend(["--append-system-prompt", self._options.append_system_prompt])
else:
if (
self._options.system_prompt.get("type") == "preset"
and "append" in self._options.system_prompt
):
cmd.extend(
["--append-system-prompt", self._options.system_prompt["append"]]
)
if self._options.allowed_tools:
cmd.extend(["--allowedTools", ",".join(self._options.allowed_tools)])

View file

@ -18,6 +18,14 @@ PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"]
SettingSource = Literal["user", "project", "local"]
class SystemPromptPreset(TypedDict):
"""System prompt preset configuration."""
type: Literal["preset"]
preset: Literal["claude_code"]
append: NotRequired[str]
@dataclass
class AgentDefinition:
"""Agent definition configuration."""
@ -296,8 +304,7 @@ class ClaudeCodeOptions:
"""Query options for Claude SDK."""
allowed_tools: list[str] = field(default_factory=list)
system_prompt: str | None = None
append_system_prompt: str | None = None
system_prompt: str | SystemPromptPreset | None = None
mcp_servers: dict[str, McpServerConfig] | str | Path = field(default_factory=dict)
permission_mode: PermissionMode | None = None
continue_conversation: bool = False

View file

@ -52,12 +52,58 @@ class TestSubprocessCLITransport:
assert transport._cli_path == "/usr/bin/claude"
def test_build_command_with_system_prompt_string(self):
"""Test building CLI command with system prompt as string."""
transport = SubprocessCLITransport(
prompt="test",
options=ClaudeCodeOptions(
system_prompt="Be helpful",
),
cli_path="/usr/bin/claude",
)
cmd = transport._build_command()
assert "--system-prompt" in cmd
assert "Be helpful" in cmd
def test_build_command_with_system_prompt_preset(self):
"""Test building CLI command with system prompt preset."""
transport = SubprocessCLITransport(
prompt="test",
options=ClaudeCodeOptions(
system_prompt={"type": "preset", "preset": "claude_code"},
),
cli_path="/usr/bin/claude",
)
cmd = transport._build_command()
assert "--system-prompt" not in cmd
assert "--append-system-prompt" not in cmd
def test_build_command_with_system_prompt_preset_and_append(self):
"""Test building CLI command with system prompt preset and append."""
transport = SubprocessCLITransport(
prompt="test",
options=ClaudeCodeOptions(
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": "Be concise.",
},
),
cli_path="/usr/bin/claude",
)
cmd = transport._build_command()
assert "--system-prompt" not in cmd
assert "--append-system-prompt" in cmd
assert "Be concise." in cmd
def test_build_command_with_options(self):
"""Test building CLI command with options."""
transport = SubprocessCLITransport(
prompt="test",
options=ClaudeCodeOptions(
system_prompt="Be helpful",
allowed_tools=["Read", "Write"],
disallowed_tools=["Bash"],
model="claude-3-5-sonnet",
@ -68,8 +114,6 @@ class TestSubprocessCLITransport:
)
cmd = transport._build_command()
assert "--system-prompt" in cmd
assert "Be helpful" in cmd
assert "--allowedTools" in cmd
assert "Read,Write" in cmd
assert "--disallowedTools" in cmd

View file

@ -107,14 +107,34 @@ class TestOptions:
options_accept = ClaudeCodeOptions(permission_mode="acceptEdits")
assert options_accept.permission_mode == "acceptEdits"
def test_claude_code_options_with_system_prompt(self):
"""Test Options with system prompt."""
def test_claude_code_options_with_system_prompt_string(self):
"""Test Options with system prompt as string."""
options = ClaudeCodeOptions(
system_prompt="You are a helpful assistant.",
append_system_prompt="Be concise.",
)
assert options.system_prompt == "You are a helpful assistant."
assert options.append_system_prompt == "Be concise."
def test_claude_code_options_with_system_prompt_preset(self):
"""Test Options with system prompt preset."""
options = ClaudeCodeOptions(
system_prompt={"type": "preset", "preset": "claude_code"},
)
assert options.system_prompt == {"type": "preset", "preset": "claude_code"}
def test_claude_code_options_with_system_prompt_preset_and_append(self):
"""Test Options with system prompt preset and append."""
options = ClaudeCodeOptions(
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": "Be concise.",
},
)
assert options.system_prompt == {
"type": "preset",
"preset": "claude_code",
"append": "Be concise.",
}
def test_claude_code_options_with_session_continuation(self):
"""Test Options with session continuation."""