Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pydantic_ai_slim/pydantic_ai/models/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pydantic_ai_slim/pydantic_ai/models/bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion pydantic_ai_slim/pydantic_ai/models/cohere.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ def _map_messages(
) -> list[ChatMessageV2]:
"""Just maps a `pydantic_ai.Message` to a `cohere.ChatMessageV2`."""
cohere_messages: list[ChatMessageV2] = []
system_prompt_count = sum(
1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will result in a wrong index when the system prompt parts are not all of the beginning, and it seems brittle to use an index derived from Pydantic AI messages in a list named cohere_messages, which may or may not map 1:1. So I think we should count system parts at the start of the actual cohere_messages list instead

)
for message in messages:
if isinstance(message, ModelRequest):
cohere_messages.extend(self._map_user_message(message))
Expand Down Expand Up @@ -271,7 +274,7 @@ 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))
cohere_messages.insert(system_prompt_count, SystemChatMessageV2(role='system', content=instructions))
return cohere_messages

def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[ToolV2]:
Expand Down
2 changes: 1 addition & 1 deletion pydantic_ai_slim/pydantic_ai/models/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
2 changes: 1 addition & 1 deletion pydantic_ai_slim/pydantic_ai/models/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion pydantic_ai_slim/pydantic_ai/models/groq.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,9 @@ def _map_messages(
) -> list[chat.ChatCompletionMessageParam]:
"""Just maps a `pydantic_ai.Message` to a `groq.types.ChatCompletionMessageParam`."""
groq_messages: list[chat.ChatCompletionMessageParam] = []
system_prompt_count = sum(
1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as up

)
for message in messages:
if isinstance(message, ModelRequest):
groq_messages.extend(self._map_user_message(message))
Expand Down Expand Up @@ -428,7 +431,9 @@ 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))
groq_messages.insert(
system_prompt_count, chat.ChatCompletionSystemMessageParam(role='system', content=instructions)
)
return groq_messages

@staticmethod
Expand Down
5 changes: 4 additions & 1 deletion pydantic_ai_slim/pydantic_ai/models/huggingface.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ async def _map_messages(
) -> list[ChatCompletionInputMessage | ChatCompletionOutputMessage]:
"""Just maps a `pydantic_ai.Message` to a `huggingface_hub.ChatCompletionInputMessage`."""
hf_messages: list[ChatCompletionInputMessage | ChatCompletionOutputMessage] = []
system_prompt_count = sum(
1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same :)

)
for message in messages:
if isinstance(message, ModelRequest):
async for item in self._map_user_message(message):
Expand Down Expand Up @@ -361,7 +364,7 @@ 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
hf_messages.insert(system_prompt_count, ChatCompletionInputMessage(content=instructions, role='system')) # type: ignore
return hf_messages

@staticmethod
Expand Down
5 changes: 4 additions & 1 deletion pydantic_ai_slim/pydantic_ai/models/mistral.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,9 @@ def _map_messages(
) -> list[MistralMessages]:
"""Just maps a `pydantic_ai.Message` to a `MistralMessage`."""
mistral_messages: list[MistralMessages] = []
system_prompt_count = sum(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same!

1 for m in messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart)
)
for message in messages:
if isinstance(message, ModelRequest):
mistral_messages.extend(self._map_user_message(message))
Expand Down Expand Up @@ -557,7 +560,7 @@ def _map_messages(
else:
assert_never(message)
if instructions := self._get_instructions(messages, model_request_parameters):
mistral_messages.insert(0, MistralSystemMessage(content=instructions))
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.
Expand Down
15 changes: 13 additions & 2 deletions pydantic_ai_slim/pydantic_ai/models/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,11 @@ async def _map_messages(
) -> list[chat.ChatCompletionMessageParam]:
"""Just maps a `pydantic_ai.Message` to a `openai.types.ChatCompletionMessageParam`."""
openai_messages: list[chat.ChatCompletionMessageParam] = []
system_prompt_count = 0
for message in messages:
for part in message.parts:
if isinstance(part, SystemPromptPart):
system_prompt_count += 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And same :)

if isinstance(message, ModelRequest):
async for item in self._map_user_message(message):
openai_messages.append(item)
Expand All @@ -840,7 +844,9 @@ 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'))
openai_messages.insert(
system_prompt_count, chat.ChatCompletionSystemMessageParam(content=instructions, role='system')
)
return openai_messages

@staticmethod
Expand Down Expand Up @@ -1313,7 +1319,12 @@ 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 messages if isinstance(m, ModelRequest) for p in m.parts if isinstance(p, SystemPromptPart)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here as well. We should find the right index in the actual openai_messages list

)
openai_messages.insert(
system_prompt_count, responses.EasyInputMessageParam(role='system', content=instructions)
)
instructions = OMIT

if verbosity := model_settings.get('openai_text_verbosity'):
Expand Down