Skip to content
Draft
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
543d414
initial commit
adtyavrdhn Dec 3, 2025
c5686de
Removing from here for the moment
adtyavrdhn Dec 3, 2025
8d9d9b9
Adding prompt_templates to public APIs in Agent.run family
adtyavrdhn Dec 4, 2025
4968c1f
lint
adtyavrdhn Dec 4, 2025
5d04126
docstring
adtyavrdhn Dec 4, 2025
b901be7
fix
adtyavrdhn Dec 4, 2025
ea4a9b8
remove test
adtyavrdhn Dec 4, 2025
933022f
removing unused part
adtyavrdhn Dec 4, 2025
6cc9b1d
fixing test
adtyavrdhn Dec 4, 2025
c8ebcea
format
adtyavrdhn Dec 4, 2025
7eaa90b
fix
adtyavrdhn Dec 5, 2025
16c4d92
Adding return kind to ToolReturnPart
adtyavrdhn Dec 6, 2025
edb115a
adding docstring
adtyavrdhn Dec 6, 2025
c1d77cf
lint
adtyavrdhn Dec 6, 2025
e8de0b3
Fix tests + dbos and temporal implementation of runs with prompt_temp…
adtyavrdhn Dec 6, 2025
086e035
Fix tests + dbos and temporal implementation of runs with prompt_temp…
adtyavrdhn Dec 6, 2025
f23b841
Fix tests
adtyavrdhn Dec 6, 2025
1ef50f3
merge
adtyavrdhn Dec 6, 2025
acc5420
useless diff
adtyavrdhn Dec 6, 2025
a34c391
useless diff
adtyavrdhn Dec 6, 2025
5920092
lint
adtyavrdhn Dec 6, 2025
4ac181f
fix prefect
adtyavrdhn Dec 6, 2025
ef8cc54
lint
adtyavrdhn Dec 6, 2025
874d70e
fix
adtyavrdhn Dec 6, 2025
c4ef9ba
rolling back vercel adapter return kind
adtyavrdhn Dec 6, 2025
71608af
fix test
adtyavrdhn Dec 6, 2025
e368175
RunContext type
adtyavrdhn Dec 6, 2025
e7fc0c9
RunContext type
adtyavrdhn Dec 6, 2025
eefe430
fix test
adtyavrdhn Dec 6, 2025
d2d0498
fix test
adtyavrdhn Dec 6, 2025
d4a0c2d
fix test + coverage
adtyavrdhn Dec 6, 2025
2e8a1f2
fix lint
adtyavrdhn Dec 6, 2025
f8b5026
fix test
adtyavrdhn Dec 6, 2025
b3632b7
fix test
adtyavrdhn Dec 6, 2025
9bebf4f
lint
adtyavrdhn Dec 6, 2025
59981c1
renaming variable
adtyavrdhn Dec 6, 2025
987293e
removing useless comment
adtyavrdhn Dec 9, 2025
400b34e
Merge branch 'main' of https://github.com/pydantic/pydantic-ai into c…
adtyavrdhn Dec 9, 2025
3c6ea8e
Merge branch 'main' of https://github.com/pydantic/pydantic-ai into c…
adtyavrdhn Dec 12, 2025
def1747
rolling back from __repr__
adtyavrdhn Dec 12, 2025
74c6e23
removing mutating of message history without copy(ruining history)
adtyavrdhn Dec 12, 2025
9aadb71
moving prompt_templates to a diff file
adtyavrdhn Dec 12, 2025
41f4f2b
Using class default values for init of content
adtyavrdhn Dec 12, 2025
8253d8f
Moving tool call denied
adtyavrdhn Dec 12, 2025
09b2597
removing prompt_templates from messages.py
adtyavrdhn Dec 12, 2025
0477465
lint
adtyavrdhn Dec 12, 2025
f5fb994
keep prompt_templates non-able, read default values off of the class …
adtyavrdhn Dec 12, 2025
b6415aa
fixing ToolDenied
adtyavrdhn Dec 12, 2025
339ea74
Moving to a default instance instead of reading class variables
adtyavrdhn Dec 12, 2025
8141c3a
fixing tooldenied overwritten by prompt_template
adtyavrdhn Dec 12, 2025
fee446d
fixing string in tool denied message
adtyavrdhn Dec 12, 2025
45dff51
tool return kind in google
adtyavrdhn Dec 12, 2025
0f729f0
Adding handling for retry prompt templates
adtyavrdhn Dec 12, 2025
3570d40
Removing retry_prompt for more granular controls
adtyavrdhn Dec 12, 2025
946a20b
fixing test snapshots
adtyavrdhn Dec 12, 2025
454bda1
better test string
adtyavrdhn Dec 12, 2025
da87aa5
lint fix
adtyavrdhn Dec 12, 2025
539be42
Merge branch 'main' of https://github.com/pydantic/pydantic-ai into c…
adtyavrdhn Dec 12, 2025
65e1321
fix test
adtyavrdhn Dec 12, 2025
1ef3ddc
lint fix
adtyavrdhn Dec 12, 2025
05d031e
fixing test for retry prompt part, adding default value
adtyavrdhn Dec 12, 2025
6723457
fixing test for retry prompt part, adding default value
adtyavrdhn Dec 12, 2025
e28a4ff
fixing test for retry prompt part, adding default value
adtyavrdhn Dec 12, 2025
a31598b
adding PromptOutput
adtyavrdhn Dec 12, 2025
04b6f14
fixing docs
adtyavrdhn Dec 12, 2025
c979029
coverage for tool-denied message
adtyavrdhn Dec 12, 2025
201d7f6
coverage for prompted output
adtyavrdhn Dec 12, 2025
cdc477d
cleanup
adtyavrdhn Dec 12, 2025
81755bb
lint cleanup, skeptical about cov after refactor
adtyavrdhn Dec 12, 2025
7df0a25
coverage
adtyavrdhn Dec 12, 2025
165e795
adding comment
adtyavrdhn Dec 13, 2025
f39cc66
Adding PromptConfig, composition templates inside PromptConfig, can a…
adtyavrdhn Dec 13, 2025
90bfab2
Revamping of PreparedToolSet to allow using tool_config as well
adtyavrdhn Dec 13, 2025
c91b2ab
merge
adtyavrdhn Dec 13, 2025
12c3ad6
merge conflicts ughh
adtyavrdhn Dec 13, 2025
4069ad8
fixes
adtyavrdhn Dec 13, 2025
378d0e6
test fixes
adtyavrdhn Dec 13, 2025
2c1fe89
docs
adtyavrdhn Dec 13, 2025
2c74c5a
fixing not passing prompt_config via iter
adtyavrdhn Dec 13, 2025
c36ee12
better test for tool config overriding check
adtyavrdhn Dec 13, 2025
2bde52a
lint cleanup
adtyavrdhn Dec 13, 2025
18b2fa8
fixing order
adtyavrdhn Dec 13, 2025
ca0c29c
fixing doc
adtyavrdhn Dec 13, 2025
e5285f0
fixing doc
adtyavrdhn Dec 13, 2025
ecf13ce
changes for coverage
adtyavrdhn Dec 13, 2025
60c1f89
changes for coverage
adtyavrdhn Dec 13, 2025
b0aa837
toolconfig could be none
adtyavrdhn Dec 14, 2025
aeef8ba
Adding ToolConfig for tool_args description change in runtime
adtyavrdhn Dec 15, 2025
d9a8ff9
updating docstring
adtyavrdhn Dec 15, 2025
5b66710
Merge branch 'main' of https://github.com/pydantic/pydantic-ai into c…
adtyavrdhn Dec 15, 2025
bdd2ea1
docstring example
adtyavrdhn Dec 15, 2025
65c5f14
docstring example
adtyavrdhn Dec 15, 2025
6aa80f9
fixing test example skipping it for now
adtyavrdhn Dec 15, 2025
38f7d0d
removing unused public methods
adtyavrdhn Dec 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/deferred-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ print(result.all_messages())
content="File 'README.md' updated: 'Hello, world!'",
tool_call_id='update_file_readme',
timestamp=datetime.datetime(...),
return_kind='tool-executed',
)
],
run_id='...',
Expand All @@ -161,12 +162,14 @@ print(result.all_messages())
content="File '.env' updated: ''",
tool_call_id='update_file_dotenv',
timestamp=datetime.datetime(...),
return_kind='tool-executed',
),
ToolReturnPart(
tool_name='delete_file',
content='Deleting files is not allowed',
tool_call_id='delete_file',
timestamp=datetime.datetime(...),
return_kind='tool-denied',
),
UserPromptPart(
content='Now create a backup of README.md',
Expand Down Expand Up @@ -195,6 +198,7 @@ print(result.all_messages())
content="File 'README.md.bak' updated: 'Hello, world!'",
tool_call_id='update_file_backup',
timestamp=datetime.datetime(...),
return_kind='tool-executed',
)
],
run_id='...',
Expand Down Expand Up @@ -348,6 +352,7 @@ async def main():
content=42,
tool_call_id='pyd_ai_tool_call_id',
timestamp=datetime.datetime(...),
return_kind='tool-executed',
)
],
run_id='...',
Expand Down
1 change: 1 addition & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ async def test_forecast():
content='Sunny with a chance of rain',
tool_call_id=IsStr(),
timestamp=IsNow(tz=timezone.utc),
return_kind='tool-executed',
),
],
run_id=IsStr(),
Expand Down
2 changes: 2 additions & 0 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ print(dice_result.all_messages())
content='4',
tool_call_id='pyd_ai_tool_call_id',
timestamp=datetime.datetime(...),
return_kind='tool-executed',
)
],
run_id='...',
Expand All @@ -130,6 +131,7 @@ print(dice_result.all_messages())
content='Anne',
tool_call_id='pyd_ai_tool_call_id',
timestamp=datetime.datetime(...),
return_kind='tool-executed',
)
],
run_id='...',
Expand Down
25 changes: 25 additions & 0 deletions pydantic_ai_slim/pydantic_ai/_agent_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ class GraphAgentDeps(Generic[DepsT, OutputDataT]):

model: models.Model
model_settings: ModelSettings | None
prompt_templates: _messages.PromptTemplates | None
usage_limits: _usage.UsageLimits
max_result_retries: int
end_strategy: EndStrategy
Expand Down Expand Up @@ -509,6 +510,10 @@ async def _prepare_request(
# Update the new message index to ensure `result.new_messages()` returns the correct messages
ctx.deps.new_message_index -= len(original_history) - len(message_history)

prompt_templates = ctx.deps.prompt_templates
if prompt_templates:
_apply_prompt_templates(message_history, prompt_templates, run_context)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should not modify the ModelRequests or parts in the history in place, so please have this method build new object and a new list.

We should make sure the modified final ModelRequest shows up in result.all_messages() etc, so we need ctx.state.message_history[:] = message_history as above

Copy link
Collaborator

Choose a reason for hiding this comment

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

This only applies to message history passed in by the user, that's not our to edit. We can of course modify self.request and other ModelRequests we build ourselves


# Merge possible consecutive trailing `ModelRequest`s into one, with tool call parts before user parts,
# but don't store it in the message history on state. This is just for the benefit of model classes that want clear user/assistant boundaries.
# See `tests/test_tools.py::test_parallel_tool_return_with_deferred` for an example where this is necessary
Expand Down Expand Up @@ -783,6 +788,11 @@ def _handle_final_result(
) -> End[result.FinalResult[NodeRunEndT]]:
messages = ctx.state.message_history

if tool_responses and ctx.deps.prompt_templates:
run_ctx = build_run_context(ctx)
for part in tool_responses:
ctx.deps.prompt_templates.apply_template(part, run_ctx)

# For backwards compatibility, append a new ModelRequest using the tool returns and retries
if tool_responses:
messages.append(_messages.ModelRequest(parts=tool_responses, run_id=ctx.state.run_id))
Expand Down Expand Up @@ -871,13 +881,15 @@ async def process_tool_calls( # noqa: C901
tool_name=call.tool_name,
content='Final result processed.',
tool_call_id=call.tool_call_id,
return_kind='final-result-processed',
)
else:
yield _messages.FunctionToolCallEvent(call)
part = _messages.ToolReturnPart(
tool_name=call.tool_name,
content='Output tool not used - a final result was already processed.',
tool_call_id=call.tool_call_id,
return_kind='output-tool-not-executed',
)
yield _messages.FunctionToolResultEvent(part)

Expand All @@ -902,6 +914,7 @@ async def process_tool_calls( # noqa: C901
tool_name=call.tool_name,
content='Final result processed.',
tool_call_id=call.tool_call_id,
return_kind='final-result-processed',
)
output_parts.append(part)
final_result = result.FinalResult(result_data, call.tool_name, call.tool_call_id)
Expand All @@ -915,6 +928,7 @@ async def process_tool_calls( # noqa: C901
tool_name=call.tool_name,
content='Tool not executed - a final result was already processed.',
tool_call_id=call.tool_call_id,
return_kind='function-tool-not-executed',
)
)
else:
Expand Down Expand Up @@ -973,6 +987,7 @@ async def process_tool_calls( # noqa: C901
tool_name=call.tool_name,
content='Tool not executed - a final result was already processed.',
tool_call_id=call.tool_call_id,
return_kind='function-tool-not-executed',
)
)
elif calls:
Expand Down Expand Up @@ -1129,6 +1144,7 @@ async def _call_tool(
tool_name=tool_call.tool_name,
content=tool_call_result.message,
tool_call_id=tool_call.tool_call_id,
return_kind='tool-denied',
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not too opposed to having this new field, but I wonder if it's strictly necessary. Since we build the RetryPromptParts in this file, would it be an option to explicitly pass something like content=self.prompt_templates.generate(self.prompt_templates.tool_denied, ctx)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we could but I like the idea of this kind in the messages, I think the visibility of the ToolReturnPart's context increases.

), None
elif isinstance(tool_call_result, exceptions.ModelRetry):
m = _messages.RetryPromptPart(
Expand Down Expand Up @@ -1191,6 +1207,7 @@ async def _call_tool(
tool_call_id=tool_call.tool_call_id,
content=tool_return.return_value, # type: ignore
metadata=tool_return.metadata,
return_kind='tool-executed',
)

return return_part, tool_return.content or None
Expand Down Expand Up @@ -1361,3 +1378,11 @@ def _clean_message_history(messages: list[_messages.ModelMessage]) -> list[_mess
else:
clean_messages.append(message)
return clean_messages


def _apply_prompt_templates(
messages: list[_messages.ModelMessage], prompt_templates: _messages.PromptTemplates, ctx: RunContext[Any]
):
for msg in messages:
for msg_part in msg.parts:
prompt_templates.apply_template(msg_part, ctx)
Copy link
Collaborator

Choose a reason for hiding this comment

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

See above; I'm a bit worried about doing this in-place

42 changes: 41 additions & 1 deletion pydantic_ai_slim/pydantic_ai/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
be merged with this value, with the runtime argument taking priority.
"""

prompt_templates: _messages.PromptTemplates | None
"""Optional prompt templates used to customize the system-injected messages for this agent."""

_output_type: OutputSpec[OutputDataT]

instrument: InstrumentationSettings | bool | None
Expand Down Expand Up @@ -167,6 +170,7 @@ def __init__(
deps_type: type[AgentDepsT] = NoneType,
name: str | None = None,
model_settings: ModelSettings | None = None,
prompt_templates: _messages.PromptTemplates | None = None,
retries: int = 1,
validation_context: Any | Callable[[RunContext[AgentDepsT]], Any] = None,
output_retries: int | None = None,
Expand Down Expand Up @@ -219,6 +223,7 @@ def __init__(
deps_type: type[AgentDepsT] = NoneType,
name: str | None = None,
model_settings: ModelSettings | None = None,
prompt_templates: _messages.PromptTemplates | None = None,
retries: int = 1,
validation_context: Any | Callable[[RunContext[AgentDepsT]], Any] = None,
output_retries: int | None = None,
Expand Down Expand Up @@ -252,6 +257,8 @@ def __init__(
name: The name of the agent, used for logging. If `None`, we try to infer the agent name from the call frame
when the agent is first run.
model_settings: Optional model request settings to use for this agent's runs, by default.
prompt_templates: Optional prompt templates to customize how system-injected messages
(like retry prompts or tool return wrappers) are rendered for this agent.
retries: The default number of retries to allow for tool calls and output validation, before raising an error.
For model request retries, see the [HTTP Request Retries](../retries.md) documentation.
validation_context: Pydantic [validation context](https://docs.pydantic.dev/latest/concepts/validators/#validation-context) used to validate tool arguments and outputs.
Expand Down Expand Up @@ -295,6 +302,7 @@ def __init__(
self._name = name
self.end_strategy = end_strategy
self.model_settings = model_settings
self.prompt_templates = prompt_templates

self._output_type = output_type
self.instrument = instrument
Expand Down Expand Up @@ -357,6 +365,9 @@ def __init__(
self._override_instructions: ContextVar[
_utils.Option[list[str | _system_prompt.SystemPromptFunc[AgentDepsT]]]
] = ContextVar('_override_instructions', default=None)
self._override_prompt_templates: ContextVar[_utils.Option[_messages.PromptTemplates]] = ContextVar(
'_override_prompt_templates', default=None
)

self._enter_lock = Lock()
self._entered_count = 0
Expand Down Expand Up @@ -410,7 +421,7 @@ def event_stream_handler(self) -> EventStreamHandler[AgentDepsT] | None:
return self._event_stream_handler

def __repr__(self) -> str:
return f'{type(self).__name__}(model={self.model!r}, name={self.name!r}, end_strategy={self.end_strategy!r}, model_settings={self.model_settings!r}, output_type={self.output_type!r}, instrument={self.instrument!r})'
return f'{type(self).__name__}(model={self.model!r}, name={self.name!r}, end_strategy={self.end_strategy!r}, model_settings={self.model_settings!r}, prompt_templates={self.prompt_templates!r},output_type={self.output_type!r}, instrument={self.instrument!r})'

@overload
def iter(
Expand All @@ -424,6 +435,7 @@ def iter(
instructions: Instructions[AgentDepsT] = None,
deps: AgentDepsT = None,
model_settings: ModelSettings | None = None,
prompt_templates: _messages.PromptTemplates | None = None,
usage_limits: _usage.UsageLimits | None = None,
usage: _usage.RunUsage | None = None,
infer_name: bool = True,
Expand All @@ -443,6 +455,7 @@ def iter(
instructions: Instructions[AgentDepsT] = None,
deps: AgentDepsT = None,
model_settings: ModelSettings | None = None,
prompt_templates: _messages.PromptTemplates | None = None,
usage_limits: _usage.UsageLimits | None = None,
usage: _usage.RunUsage | None = None,
infer_name: bool = True,
Expand All @@ -462,6 +475,7 @@ async def iter(
instructions: Instructions[AgentDepsT] = None,
deps: AgentDepsT = None,
model_settings: ModelSettings | None = None,
prompt_templates: _messages.PromptTemplates | None = None,
usage_limits: _usage.UsageLimits | None = None,
usage: _usage.RunUsage | None = None,
infer_name: bool = True,
Expand Down Expand Up @@ -538,6 +552,8 @@ async def main():
instructions: Optional additional instructions to use for this run.
deps: Optional dependencies to use for this run.
model_settings: Optional settings to use for this model's request.
prompt_templates: Optional prompt templates to override how system-generated parts are
phrased for this specific run, falling back to the agent's defaults if omitted.
usage_limits: Optional limits on model request count or token usage.
usage: Optional usage to start with, useful for resuming a conversation or agents used in tools.
infer_name: Whether to try to infer the agent name from the call frame if it's not set.
Expand Down Expand Up @@ -588,6 +604,7 @@ async def main():
merged_settings = merge_model_settings(model_used.settings, self.model_settings)
model_settings = merge_model_settings(merged_settings, model_settings)
usage_limits = usage_limits or _usage.UsageLimits()
prompt_templates = self._get_prompt_templates(prompt_templates)

instructions_literal, instructions_functions = self._get_instructions(additional_instructions=instructions)

Expand Down Expand Up @@ -615,6 +632,7 @@ async def get_instructions(run_context: RunContext[AgentDepsT]) -> str | None:
new_message_index=len(message_history) if message_history else 0,
model=model_used,
model_settings=model_settings,
prompt_templates=prompt_templates,
usage_limits=usage_limits,
max_result_retries=self._max_result_retries,
end_strategy=self.end_strategy,
Expand Down Expand Up @@ -749,6 +767,7 @@ def override(
toolsets: Sequence[AbstractToolset[AgentDepsT]] | _utils.Unset = _utils.UNSET,
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] | _utils.Unset = _utils.UNSET,
instructions: Instructions[AgentDepsT] | _utils.Unset = _utils.UNSET,
prompt_templates: _messages.PromptTemplates | _utils.Unset = _utils.UNSET,
) -> Iterator[None]:
"""Context manager to temporarily override agent name, dependencies, model, toolsets, tools, or instructions.

Expand All @@ -762,6 +781,7 @@ def override(
toolsets: The toolsets to use instead of the toolsets passed to the agent constructor and agent run.
tools: The tools to use instead of the tools registered with the agent.
instructions: The instructions to use instead of the instructions registered with the agent.
prompt_templates: The prompt templates to use instead of the prompt templates passed to the agent constructor and agent run.
"""
if _utils.is_set(name):
name_token = self._override_name.set(_utils.Some(name))
Expand Down Expand Up @@ -794,6 +814,11 @@ def override(
else:
instructions_token = None

if _utils.is_set(prompt_templates):
prompt_templates_token = self._override_prompt_templates.set(_utils.Some(prompt_templates))
else:
prompt_templates_token = None

try:
yield
finally:
Expand All @@ -809,6 +834,8 @@ def override(
self._override_tools.reset(tools_token)
if instructions_token is not None:
self._override_instructions.reset(instructions_token)
if prompt_templates_token is not None:
self._override_prompt_templates.reset(prompt_templates_token)

@overload
def instructions(
Expand Down Expand Up @@ -1323,6 +1350,19 @@ def _get_deps(self: Agent[T, OutputDataT], deps: T) -> T:
else:
return deps

def _get_prompt_templates(
self, prompt_templates: _messages.PromptTemplates | None
) -> _messages.PromptTemplates | None:
"""Get prompt_templates for a run.

If we've overridden prompt_templates via `_override_prompt_templates`, use that,
otherwise use the prompt_templates passed to the call, falling back to the agent default.
"""
if some_prompt_templates := self._override_prompt_templates.get():
return some_prompt_templates.value
else:
return prompt_templates or self.prompt_templates

def _normalize_instructions(
self,
instructions: Instructions[AgentDepsT],
Expand Down
Loading