diff --git a/pydantic_ai_slim/pydantic_ai/models/anthropic.py b/pydantic_ai_slim/pydantic_ai/models/anthropic.py index ecdb9fe61f..1df861e84e 100644 --- a/pydantic_ai_slim/pydantic_ai/models/anthropic.py +++ b/pydantic_ai_slim/pydantic_ai/models/anthropic.py @@ -853,7 +853,7 @@ async def _map_message( # noqa: C901 else: assert_never(m) if instructions := self._get_instructions(messages, model_request_parameters): - system_prompt_parts.insert(0, instructions) + system_prompt_parts.append(instructions) system_prompt = '\n\n'.join(system_prompt_parts) # Add cache_control to the last message content if anthropic_cache_messages is enabled diff --git a/pydantic_ai_slim/pydantic_ai/models/bedrock.py b/pydantic_ai_slim/pydantic_ai/models/bedrock.py index ff03460904..abc60e1fd1 100644 --- a/pydantic_ai_slim/pydantic_ai/models/bedrock.py +++ b/pydantic_ai_slim/pydantic_ai/models/bedrock.py @@ -615,7 +615,7 @@ async def _map_messages( # noqa: C901 last_message = cast(dict[str, Any], current_message) if instructions := self._get_instructions(messages, model_request_parameters): - system_prompt.insert(0, {'text': instructions}) + system_prompt.append({'text': instructions}) return system_prompt, processed_messages diff --git a/pydantic_ai_slim/pydantic_ai/models/cohere.py b/pydantic_ai_slim/pydantic_ai/models/cohere.py index 60ae329065..bef81e3063 100644 --- a/pydantic_ai_slim/pydantic_ai/models/cohere.py +++ b/pydantic_ai_slim/pydantic_ai/models/cohere.py @@ -271,7 +271,8 @@ def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): - cohere_messages.insert(0, SystemChatMessageV2(role='system', content=instructions)) + system_prompt_count = sum(1 for m in cohere_messages if isinstance(m, SystemChatMessageV2)) + cohere_messages.insert(system_prompt_count, SystemChatMessageV2(role='system', content=instructions)) return cohere_messages def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[ToolV2]: diff --git a/pydantic_ai_slim/pydantic_ai/models/gemini.py b/pydantic_ai_slim/pydantic_ai/models/gemini.py index 4da92018fd..7cc1eca0ff 100644 --- a/pydantic_ai_slim/pydantic_ai/models/gemini.py +++ b/pydantic_ai_slim/pydantic_ai/models/gemini.py @@ -363,7 +363,7 @@ async def _message_to_gemini_content( else: assert_never(m) if instructions := self._get_instructions(messages, model_request_parameters): - sys_prompt_parts.insert(0, _GeminiTextPart(text=instructions)) + sys_prompt_parts.append(_GeminiTextPart(text=instructions)) return sys_prompt_parts, contents async def _map_user_prompt(self, part: UserPromptPart) -> list[_GeminiPartUnion]: diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index 89290ea3ce..4ead464808 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -588,7 +588,7 @@ async def _map_messages( contents = [{'role': 'user', 'parts': [{'text': ''}]}] if instructions := self._get_instructions(messages, model_request_parameters): - system_parts.insert(0, {'text': instructions}) + system_parts.append({'text': instructions}) system_instruction = ContentDict(role='user', parts=system_parts) if system_parts else None return system_instruction, contents diff --git a/pydantic_ai_slim/pydantic_ai/models/groq.py b/pydantic_ai_slim/pydantic_ai/models/groq.py index 64f7ddcf85..acb84c90d2 100644 --- a/pydantic_ai_slim/pydantic_ai/models/groq.py +++ b/pydantic_ai_slim/pydantic_ai/models/groq.py @@ -428,7 +428,10 @@ def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): - groq_messages.insert(0, chat.ChatCompletionSystemMessageParam(role='system', content=instructions)) + system_prompt_count = sum(1 for m in groq_messages if m.get('role') == 'system') + groq_messages.insert( + system_prompt_count, chat.ChatCompletionSystemMessageParam(role='system', content=instructions) + ) return groq_messages @staticmethod diff --git a/pydantic_ai_slim/pydantic_ai/models/huggingface.py b/pydantic_ai_slim/pydantic_ai/models/huggingface.py index 790b30bec3..1b9524c822 100644 --- a/pydantic_ai_slim/pydantic_ai/models/huggingface.py +++ b/pydantic_ai_slim/pydantic_ai/models/huggingface.py @@ -361,7 +361,8 @@ async def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): - hf_messages.insert(0, ChatCompletionInputMessage(content=instructions, role='system')) # type: ignore + system_prompt_count = sum(1 for m in hf_messages if getattr(m, 'role', None) == 'system') + hf_messages.insert(system_prompt_count, ChatCompletionInputMessage(content=instructions, role='system')) # type: ignore return hf_messages @staticmethod diff --git a/pydantic_ai_slim/pydantic_ai/models/mistral.py b/pydantic_ai_slim/pydantic_ai/models/mistral.py index cefa28e9dc..7493d63bea 100644 --- a/pydantic_ai_slim/pydantic_ai/models/mistral.py +++ b/pydantic_ai_slim/pydantic_ai/models/mistral.py @@ -557,7 +557,8 @@ def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): - mistral_messages.insert(0, MistralSystemMessage(content=instructions)) + system_prompt_count = sum(1 for m in mistral_messages if isinstance(m, MistralSystemMessage)) + mistral_messages.insert(system_prompt_count, MistralSystemMessage(content=instructions)) # Post-process messages to insert fake assistant message after tool message if followed by user message # to work around `Unexpected role 'user' after role 'tool'` error. diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index 10af284ee8..1c831364d5 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -840,7 +840,10 @@ async def _map_messages( else: assert_never(message) if instructions := self._get_instructions(messages, model_request_parameters): - openai_messages.insert(0, chat.ChatCompletionSystemMessageParam(content=instructions, role='system')) + system_prompt_count = sum(1 for m in openai_messages if m.get('role') == 'system') + openai_messages.insert( + system_prompt_count, chat.ChatCompletionSystemMessageParam(content=instructions, role='system') + ) return openai_messages @staticmethod @@ -1313,7 +1316,10 @@ async def _responses_create( # noqa: C901 # > Response input messages must contain the word 'json' in some form to use 'text.format' of type 'json_object'. # Apparently they're only checking input messages for "JSON", not instructions. assert isinstance(instructions, str) - openai_messages.insert(0, responses.EasyInputMessageParam(role='system', content=instructions)) + system_prompt_count = sum(1 for m in openai_messages if m.get('role') == 'system') + openai_messages.insert( + system_prompt_count, responses.EasyInputMessageParam(role='system', content=instructions) + ) instructions = OMIT if verbosity := model_settings.get('openai_text_verbosity'): diff --git a/tests/test_agent.py b/tests/test_agent.py index c912334434..ed038ccc3d 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -6313,3 +6313,43 @@ def llm(messages: list[ModelMessage], _info: AgentInfo) -> ModelResponse: ] ) assert run.all_messages_json().startswith(b'[{"parts":[{"content":"Hello",') + + +def test_instructions_inserted_after_system_prompt(): + """Tests that instructions are inserted after system prompts.""" + + agent = Agent('test') + + @agent.system_prompt + def system_prompt_1() -> str: + return 'System prompt 1' + + @agent.system_prompt + def system_prompt_2() -> str: + return 'System prompt 2' + + @agent.instructions + def instructions() -> str: + return 'Instructions' + + result = agent.run_sync('Hello') + assert result.all_messages()[0] == snapshot( + ModelRequest( + parts=[ + SystemPromptPart( + content='System prompt 1', + timestamp=IsNow(tz=timezone.utc), + ), + SystemPromptPart( + content='System prompt 2', + timestamp=IsNow(tz=timezone.utc), + ), + UserPromptPart( + content='Hello', + timestamp=IsNow(tz=timezone.utc), + ), + ], + instructions='Instructions', + run_id=IsStr(), + ) + )