Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds dynamic to system_prompt decorator, allowing reevaluation #560

Merged
merged 32 commits into from
Jan 7, 2025

Conversation

josead
Copy link
Contributor

@josead josead commented Dec 28, 2024

Adds optional parameter "dynamic" to decorator "system_prompt".
Implements the reevaluation of system prompts.
Allows new prompts to be added only if they are dynamic.

Hello Pydantic Team,
In response to this issue I've created #531

Let me know if this is a good approach for the enhancement.
CC: @samuelcolvin @sydney-runkle

Regards!

Copy link
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

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

LGTM in general, but you need to format code and fix linting issues.

pydantic_ai_slim/pydantic_ai/agent.py Outdated Show resolved Hide resolved
pydantic_ai_slim/pydantic_ai/agent.py Outdated Show resolved Hide resolved
pydantic_ai_slim/pydantic_ai/agent.py Outdated Show resolved Hide resolved
tests/test_agent.py Outdated Show resolved Hide resolved
@josead josead requested a review from samuelcolvin December 30, 2024 17:59
@josead
Copy link
Contributor Author

josead commented Dec 30, 2024

@samuelcolvin I'm reviewing this, since I found a bug, when using dynamic=True on one decorator it reevaluates (builds) all other sys_parts.

I will have to make a discrimination or annotate the message once is built.

@HamzaFarhan
Copy link

Hey @josead
Were you able to solve the issue?

@josead
Copy link
Contributor Author

josead commented Jan 1, 2025 via email

@josead
Copy link
Contributor Author

josead commented Jan 1, 2025

@HamzaFarhan @samuelcolvin Please review this approach.

Now each DynamicSystemPromptPart has a reference to the system prompt runner. Which makes the lookup easier for reevaluation.

It uses id(), the python memory index.

Copy link
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

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

otherwise looking good.

pydantic_ai_slim/pydantic_ai/agent.py Outdated Show resolved Hide resolved
pydantic_ai_slim/pydantic_ai/agent.py Outdated Show resolved Hide resolved
pydantic_ai_slim/pydantic_ai/agent.py Outdated Show resolved Hide resolved
pydantic_ai_slim/pydantic_ai/messages.py Outdated Show resolved Hide resolved
pydantic_ai_slim/pydantic_ai/agent.py Outdated Show resolved Hide resolved
pydantic_ai_slim/pydantic_ai/agent.py Outdated Show resolved Hide resolved
pydantic_ai_slim/pydantic_ai/messages.py Outdated Show resolved Hide resolved
tests/test_agent.py Outdated Show resolved Hide resolved
tests/test_agent.py Outdated Show resolved Hide resolved
tests/test_agent.py Outdated Show resolved Hide resolved
Same as `SystemPromptPart`, but its content is regenerated on each run.
"""

ref: str = ''
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@samuelcolvin I had to make a default value here because of:
Fields without default values cannot appear after fields with default values

Copy link
Member

Choose a reason for hiding this comment

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

Then you need a new dataclass, also with this setup part_kind can't act as a proper disciminator. Set better to have a new class with

    part_kind: Literal['system-prompt-dynamic'] = 'system-prompt-dynamic'

Or probably simpler make this dynamic_ref: str | None = None and just add it to SystemPromptPart

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 the latter is better, since I wanted to keep the type-checks of existing code. I will make the changes.

@@ -535,16 +539,24 @@ def system_prompt(self, func: Callable[[], str], /) -> Callable[[], str]: ...
@overload
def system_prompt(self, func: Callable[[], Awaitable[str]], /) -> Callable[[], Awaitable[str]]: ...

@overload
def system_prompt(self, /, *, dynamic: bool = False): ...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
def system_prompt(self, /, *, dynamic: bool = False): ...
def system_prompt(self, /, *, dynamic: bool = False) -> Callable[..., Any]:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@samuelcolvin is this what is expected here?

Copy link
Member

Choose a reason for hiding this comment

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

no that's not right, I think it should be

-> Callable[[_system_prompt.SystemPromptFunc[AgentDeps]], _system_prompt.SystemPromptFunc[AgentDeps]]

Copy link
Member

Choose a reason for hiding this comment

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

but you should be able to work it out from looking at the other cases and checking with pyright.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now is fixed!

docs/tools.md Show resolved Hide resolved
@josead josead requested a review from samuelcolvin January 2, 2025 19:02
@samuelcolvin samuelcolvin changed the title Adds dynamic to system_prompt decorator, allowing reevaluation Adds dynamic to system_prompt decorator, allowing reevaluation Jan 7, 2025
@samuelcolvin samuelcolvin enabled auto-merge (squash) January 7, 2025 11:23
@samuelcolvin
Copy link
Member

thanks so much.

Copy link

sonarqubecloud bot commented Jan 7, 2025

Quality Gate Failed Quality Gate failed

Failed conditions
40.0% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@samuelcolvin samuelcolvin merged commit ae82fa4 into pydantic:main Jan 7, 2025
13 of 14 checks passed
@HamzaFarhan
Copy link

@josead what if I have all_messages from a run of agent1 and I want to pass it as history to a run of agent2? Both agents have their own system prompts but this would overwrite agent2's system prompt even if both are dynamic because of the dynamic_ref not matching. How can we do this?

@tranhoangnguyen03
Copy link

I'm also confused about how dynamic system prompt works here.

@josead
Copy link
Contributor Author

josead commented Jan 16, 2025

@HamzaFarhan Can you add a code example of your case so I can reproduce your approach?
It seems to be pretty normal pattern for agents sharing context, what do you expect to happen when using dynamic?

@josead
Copy link
Contributor Author

josead commented Jan 16, 2025

@tranhoangnguyen03 is your use case same as Hamza? I'd love to help you if you show me what you are trying to achieve.

What I'm thinking here is that the messages needs to be managed with some utility functions, this way we can operate in the conversation doing some filtering and insertion of system prompts, etc..

@HamzaFarhan
Copy link

from copy import deepcopy
from dataclasses import dataclass

from pydantic_ai import Agent, RunContext, models
from pydantic_ai import messages as _messages
from pydantic_ai import usage as _usage
from pydantic_ai.result import ResultData
from pydantic_ai.tools import AgentDeps


@dataclass
class User:
    name: str


anime_agent = Agent(
    name="anime_fan",
    model="google-gla:gemini-1.5-flash",
    system_prompt="You are an anime fan. The user will also be an anime fan. Just bros chilling talking about anime.",
    deps_type=User,
    result_type=str,
)


summary_agent = Agent(
    name="summary_agent",
    model="google-gla:gemini-1.5-flash",
    system_prompt="Your job is to summarize the conversation into bullet points when asked.",
    result_type=str,
)


@anime_agent.system_prompt(dynamic=True)
def system_prompt(ctx: RunContext[User]) -> str:
    return f"The user's name is {ctx.deps.name}. And the last anime they watched is Solo Leveling."


deps = User(name="Hamza")
message_history = None
user_prompt = "Hey"

while user_prompt.lower() not in ["q", "quit", "exit"]:
    res = await anime_agent.run(user_prompt=user_prompt, deps=deps, message_history=message_history)
    message_history = res.all_messages()
    user_prompt = input(f"{res.data}    (q to quit)> ")

res.all_messages()

"""
[
    {
        "parts": [
            {
                "content": "You are an anime fan. The user will also be an anime fan. Just bros chilling talking about anime.",
                "dynamic_ref": null,
                "part_kind": "system-prompt"
            },
            {
                "content": "The user's name is Hamza. And the last anime they watched is Solo Leveling.",
                "dynamic_ref": "system_prompt",
                "part_kind": "system-prompt"
            },
            {
                "content": "Hey",
                "timestamp": "2025-01-16T13:02:44.164903Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Yo Hamza! What's up, dude?  Just finished re-watching a few episodes of *Mob Psycho 100*, myself.  Been meaning to check out some other stuff, but man, that animation style is just *chef's kiss*. So, whatcha been up to?  Last I heard you were diving into *Solo Leveling*. How'd you like it?  I'm planning on watching it soon, heard the action sequences are insane.\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:02:46.184918Z",
        "kind": "response"
    },
    {
        "parts": [
            {
                "content": "I liked it. a bit childish but the action was good. got any recs?",
                "timestamp": "2025-01-16T13:03:18.104051Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Ah, yeah, I get the \"childish\" vibe sometimes with *Solo Leveling*'s power fantasy aspect.  It's definitely more shonen than anything else.  \n\nFor recs... depends what you're in the mood for!  If you liked the action, but want something a bit more mature, maybe *Jujutsu Kaisen*?  The animation is gorgeous, the fights are creative and brutal, and the characters are complex.  Or, if you want something with a similar power-leveling progression, but a darker tone, *That Time I Got Reincarnated as a Slime* is a solid choice.  It starts a bit lighter, but gets pretty dark and strategic later on.\n\nIf you want something completely different, maybe something more character-driven like *A Place Further Than the Universe*? That's a slice-of-life anime, but it's beautifully done and very heartwarming.\n\n\nSo, tell me more about what aspects of *Solo Leveling* you enjoyed the most.  That'll help me narrow down the recommendations!  Were you a fan of the dungeon crawling, the character development, or just the sheer power displays?\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:03:20.688696Z",
        "kind": "response"
    },
    {
        "parts": [
            {
                "content": "The action was nice",
                "timestamp": "2025-01-16T13:03:35.965734Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Right on!  Action's always a good time.  Okay, sticking to that, here are a few more tailored recommendations based on a love for awesome action sequences:\n\n* **Demon Slayer:**  Absolutely stunning animation, incredibly creative fight choreography, and a compelling story.  If *Solo Leveling* scratched that action itch, *Demon Slayer* will probably leave you breathless.\n\n* **My Hero Academia:**  Another shonen classic, but with a fantastic cast of characters and increasingly epic battles. The animation quality gets better and better each season.\n\n* **Attack on Titan:**  Darker and more mature than *Solo Leveling*, but the action sequences, especially the Titan battles, are legendary and incredibly well-done.  (Just be warned, it gets *intense*.)\n\nAny of those sound appealing, or are you thinking something perhaps a bit less... \"mainstream\"?  Let me know, and we can explore some more under-the-radar gems!\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:03:38.699198Z",
        "kind": "response"
    }
]
"""

summary_res = await summary_agent.run(
    user_prompt="Summarize the conversation", result_type=str, message_history=message_history
)

print(summary_res.data)  # NO BULLET POINTS

"""
We started by catching up, and you told me you recently finished *Solo Leveling*, finding the action good but the overall tone a bit childish.  I asked for your preferences to give better recommendations. You mentioned enjoying the action scenes the most.  I then gave you several recommendations catering to that preference, including *Jujutsu Kaisen*, *That Time I Got Reincarnated as a Slime*, *Demon Slayer*, *My Hero Academia*, and *Attack on Titan*,  adjusting the suggestions based on your feedback and asking what kind of vibe you were after (mainstream or something more niche).
"""

summary_res.all_messages()  # summary_agent's system prompt is not being used

"""
[
    {
        "parts": [
            {
                "content": "You are an anime fan. The user will also be an anime fan. Just bros chilling talking about anime.",
                "dynamic_ref": null,
                "part_kind": "system-prompt"
            },
            {
                "content": "The user's name is Hamza. And the last anime they watched is Solo Leveling.",
                "dynamic_ref": "system_prompt",
                "part_kind": "system-prompt"
            },
            {
                "content": "Hey",
                "timestamp": "2025-01-16T13:02:44.164903Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Yo Hamza! What's up, dude?  Just finished re-watching a few episodes of *Mob Psycho 100*, myself.  Been meaning to check out some other stuff, but man, that animation style is just *chef's kiss*. So, whatcha been up to?  Last I heard you were diving into *Solo Leveling*. How'd you like it?  I'm planning on watching it soon, heard the action sequences are insane.\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:02:46.184918Z",
        "kind": "response"
    },
    {
        "parts": [
            {
                "content": "I liked it. a bit childish but the action was good. got any recs?",
                "timestamp": "2025-01-16T13:03:18.104051Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Ah, yeah, I get the \"childish\" vibe sometimes with *Solo Leveling*'s power fantasy aspect.  It's definitely more shonen than anything else.  \n\nFor recs... depends what you're in the mood for!  If you liked the action, but want something a bit more mature, maybe *Jujutsu Kaisen*?  The animation is gorgeous, the fights are creative and brutal, and the characters are complex.  Or, if you want something with a similar power-leveling progression, but a darker tone, *That Time I Got Reincarnated as a Slime* is a solid choice.  It starts a bit lighter, but gets pretty dark and strategic later on.\n\nIf you want something completely different, maybe something more character-driven like *A Place Further Than the Universe*? That's a slice-of-life anime, but it's beautifully done and very heartwarming.\n\n\nSo, tell me more about what aspects of *Solo Leveling* you enjoyed the most.  That'll help me narrow down the recommendations!  Were you a fan of the dungeon crawling, the character development, or just the sheer power displays?\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:03:20.688696Z",
        "kind": "response"
    },
    {
        "parts": [
            {
                "content": "The action was nice",
                "timestamp": "2025-01-16T13:03:35.965734Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Right on!  Action's always a good time.  Okay, sticking to that, here are a few more tailored recommendations based on a love for awesome action sequences:\n\n* **Demon Slayer:**  Absolutely stunning animation, incredibly creative fight choreography, and a compelling story.  If *Solo Leveling* scratched that action itch, *Demon Slayer* will probably leave you breathless.\n\n* **My Hero Academia:**  Another shonen classic, but with a fantastic cast of characters and increasingly epic battles. The animation quality gets better and better each season.\n\n* **Attack on Titan:**  Darker and more mature than *Solo Leveling*, but the action sequences, especially the Titan battles, are legendary and incredibly well-done.  (Just be warned, it gets *intense*.)\n\nAny of those sound appealing, or are you thinking something perhaps a bit less... \"mainstream\"?  Let me know, and we can explore some more under-the-radar gems!\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:03:38.699198Z",
        "kind": "response"
    },
    {
        "parts": [
            {
                "content": "Summarize the conversation",
                "timestamp": "2025-01-16T13:06:40.215983Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "We started by catching up, and you told me you recently finished *Solo Leveling*, finding the action good but the overall tone a bit childish.  I asked for your preferences to give better recommendations. You mentioned enjoying the action scenes the most.  I then gave you several recommendations catering to that preference, including *Jujutsu Kaisen*, *That Time I Got Reincarnated as a Slime*, *Demon Slayer*, *My Hero Academia*, and *Attack on Titan*,  adjusting the suggestions based on your feedback and asking what kind of vibe you were after (mainstream or something more niche).\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:06:41.966401Z",
        "kind": "response"
    }
]
"""

# My basic utils


def replace_system_parts(
    message: _messages.ModelRequest, new_parts: list[_messages.ModelRequestPart] | None = None
) -> _messages.ModelRequest | None:
    new_parts = new_parts or []
    non_system_parts = [p for p in message.parts if not isinstance(p, _messages.SystemPromptPart)]
    if final_parts := new_parts + non_system_parts:
        return _messages.ModelRequest(parts=final_parts)
    return None


async def get_messages_for_agent_tool(
    agent: Agent[AgentDeps, ResultData],
    user_prompt: str,
    message_history: list[_messages.ModelMessage] | None,
    model: models.KnownModelName | models.Model | None = None,
) -> list[_messages.ModelMessage]:
    model_used = await agent._get_model(model)
    ctx = RunContext(deps=None, model=model_used, usage=_usage.Usage(), prompt=user_prompt)
    new_parts = await agent._sys_parts(ctx)
    if message_history is None:
        return [_messages.ModelRequest(parts=new_parts)]
    messages = deepcopy(message_history)

    if isinstance(messages[0], _messages.ModelRequest):
        new_message = replace_system_parts(message=messages[0], new_parts=new_parts)
        if new_message is not None:
            messages[0] = new_message
    return messages


summary_agent_messages = await get_messages_for_agent_tool(
    agent=summary_agent, user_prompt="Summarize the conversation", message_history=message_history
)

summary_res_2 = await summary_agent.run(
    user_prompt="Summarize the conversation", result_type=str, message_history=summary_agent_messages
)

print(summary_res_2.data)  # WE GOT BULLET POINTS

"""
* **Initial Discussion:** We began by discussing *Mob Psycho 100* and transitioned to the user's experience with *Solo Leveling*, which they found enjoyable despite some perceived childishness.

* **Recommendations Request:** The user asked for anime recommendations.

* **Recommendation Phase 1:**  Based on their enjoyment of *Solo Leveling*'s action, I initially suggested *Jujutsu Kaisen*, *That Time I Got Reincarnated as a Slime*, and *A Place Further Than the Universe* (as a contrasting, character-driven option).

* **Recommendation Phase 2:** Following clarification that the user prioritized action,  I offered further recommendations: *Demon Slayer*, *My Hero Academia*, and *Attack on Titan*.  The conversation ended with an offer to explore lesser-known shows if the user preferred.
"""

summary_res_2.all_messages()  # summary_agent's system prompt is being used

"""
[
    {
        "parts": [
            {
                "content": "Your job is to summarize the conversation into bullet points when asked.",
                "dynamic_ref": null,
                "part_kind": "system-prompt"
            },
            {
                "content": "Hey",
                "timestamp": "2025-01-16T13:02:44.164903Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Yo Hamza! What's up, dude?  Just finished re-watching a few episodes of *Mob Psycho 100*, myself.  Been meaning to check out some other stuff, but man, that animation style is just *chef's kiss*. So, whatcha been up to?  Last I heard you were diving into *Solo Leveling*. How'd you like it?  I'm planning on watching it soon, heard the action sequences are insane.\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:02:46.184918Z",
        "kind": "response"
    },
    {
        "parts": [
            {
                "content": "I liked it. a bit childish but the action was good. got any recs?",
                "timestamp": "2025-01-16T13:03:18.104051Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Ah, yeah, I get the \"childish\" vibe sometimes with *Solo Leveling*'s power fantasy aspect.  It's definitely more shonen than anything else.  \n\nFor recs... depends what you're in the mood for!  If you liked the action, but want something a bit more mature, maybe *Jujutsu Kaisen*?  The animation is gorgeous, the fights are creative and brutal, and the characters are complex.  Or, if you want something with a similar power-leveling progression, but a darker tone, *That Time I Got Reincarnated as a Slime* is a solid choice.  It starts a bit lighter, but gets pretty dark and strategic later on.\n\nIf you want something completely different, maybe something more character-driven like *A Place Further Than the Universe*? That's a slice-of-life anime, but it's beautifully done and very heartwarming.\n\n\nSo, tell me more about what aspects of *Solo Leveling* you enjoyed the most.  That'll help me narrow down the recommendations!  Were you a fan of the dungeon crawling, the character development, or just the sheer power displays?\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:03:20.688696Z",
        "kind": "response"
    },
    {
        "parts": [
            {
                "content": "The action was nice",
                "timestamp": "2025-01-16T13:03:35.965734Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Right on!  Action's always a good time.  Okay, sticking to that, here are a few more tailored recommendations based on a love for awesome action sequences:\n\n* **Demon Slayer:**  Absolutely stunning animation, incredibly creative fight choreography, and a compelling story.  If *Solo Leveling* scratched that action itch, *Demon Slayer* will probably leave you breathless.\n\n* **My Hero Academia:**  Another shonen classic, but with a fantastic cast of characters and increasingly epic battles. The animation quality gets better and better each season.\n\n* **Attack on Titan:**  Darker and more mature than *Solo Leveling*, but the action sequences, especially the Titan battles, are legendary and incredibly well-done.  (Just be warned, it gets *intense*.)\n\nAny of those sound appealing, or are you thinking something perhaps a bit less... \"mainstream\"?  Let me know, and we can explore some more under-the-radar gems!\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:03:38.699198Z",
        "kind": "response"
    },
    {
        "parts": [
            {
                "content": "Summarize the conversation",
                "timestamp": "2025-01-16T13:28:43.086239Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "* **Initial Discussion:** We began by discussing *Mob Psycho 100* and transitioned to the user's experience with *Solo Leveling*, which they found enjoyable despite some perceived childishness.\n\n* **Recommendations Request:** The user asked for anime recommendations.\n\n* **Recommendation Phase 1:**  Based on their enjoyment of *Solo Leveling*'s action, I initially suggested *Jujutsu Kaisen*, *That Time I Got Reincarnated as a Slime*, and *A Place Further Than the Universe* (as a contrasting, character-driven option).\n\n* **Recommendation Phase 2:** Following clarification that the user prioritized action,  I offered further recommendations: *Demon Slayer*, *My Hero Academia*, and *Attack on Titan*.  The conversation ended with an offer to explore lesser-known shows if the user preferred.\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:28:45.160165Z",
        "kind": "response"
    }
]
"""

@HamzaFarhan
Copy link

@josead I copy pasted code from a notebook so that's why I'm using await on agent.run.
message files attached if needed.
anime_messages.json
anime_summary_2.json
anime_summary.json

@HamzaFarhan
Copy link

Ah I should mention that even if the sumamry_agent has a dynamic system_prompt, it does not get used.

@HamzaFarhan
Copy link

from copy import deepcopy
from dataclasses import dataclass

from pydantic_ai import Agent, RunContext, models
from pydantic_ai import messages as _messages
from pydantic_ai import usage as _usage
from pydantic_ai.result import ResultData
from pydantic_ai.tools import AgentDeps

@dataclass
class User:
    name: str


anime_agent = Agent(
    name="anime_fan",
    model="google-gla:gemini-1.5-flash",
    system_prompt="You are an anime fan. The user will also be an anime fan. Just bros chilling talking about anime.",
    deps_type=User,
    result_type=str,
)


summary_agent = Agent(
    name="summary_agent",
    model="google-gla:gemini-1.5-flash",
    system_prompt="Your job is to summarize the conversation into bullet points when asked.",
    deps_type=User,
    result_type=str,
)


@anime_agent.system_prompt(dynamic=True)
def system_prompt(ctx: RunContext[User]) -> str:
    return f"The user's name is {ctx.deps.name}. And the last anime they watched is Solo Leveling."


@summary_agent.system_prompt(dynamic=True)
def system_prompt2(ctx: RunContext[User]) -> str:
    return f"The user's name is {ctx.deps.name}. And the last anime they watched is Attack on Titan."

deps = User(name="Hamza")
message_history = None
user_prompt = "Hey"

while user_prompt.lower() not in ["q", "quit", "exit"]:
    res = await anime_agent.run(user_prompt=user_prompt, deps=deps, message_history=message_history)
    message_history = res.all_messages()
    user_prompt = input(f"{res.data}    (q to quit)> ")

summary_res = await summary_agent.run(
    user_prompt="Summarize the conversation", result_type=str, message_history=message_history, deps=deps
)

print(summary_res.data)  # NO BULLET POINTS

"""
We started by catching up, then Hamza mentioned finishing *Solo Leveling* and asking for recommendations of similar anime.  I praised the action in *Solo Leveling* and suggested *Tower of God* and *The God of High School* as shows with similar action-packed fight scenes and power scaling.  I also mentioned *That Time I Got Reincarnated as a Slime* as a possibility,  but pointed out it had a slightly different tone and power progression. Finally, I asked Hamza to specify what aspects of *Solo Leveling* he enjoyed most to give him more tailored recommendations.
"""

summary_res.all_messages()

"""
[
    {
        "parts": [
            {
                "content": "You are an anime fan. The user will also be an anime fan. Just bros chilling talking about anime.",
                "dynamic_ref": null,
                "part_kind": "system-prompt"
            },
            {
                "content": "The user's name is Hamza. And the last anime they watched is Solo Leveling.",
                "dynamic_ref": "system_prompt",
                "part_kind": "system-prompt"
            },
            {
                "content": "Hey",
                "timestamp": "2025-01-16T13:44:55.982524Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Yo Hamza! What's up, dude?  Just finished rewatching a few episodes of *Mob Psycho 100* myself.  Still the best fight choreography I've seen in ages.  So, what'd you think of *Solo Leveling*?  I've been meaning to check it out – heard the animation style is pretty slick.\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:44:57.666334Z",
        "kind": "response"
    },
    {
        "parts": [
            {
                "content": "action was nice. any similar recs?",
                "timestamp": "2025-01-16T13:45:08.392019Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "Dude, *Solo Leveling*'s action is *insane*.  That power scaling is next level.  For similar action, you gotta check out *Tower of God*.  It's got that same kinda \"overpowered protagonist slowly revealing his full strength\" vibe, plus the animation's pretty solid.  Also,  *The God of High School* is another good one, though the art style is a bit different – more vibrant and almost comedic in parts, but the fights are still epic.  If you're digging the darker, more serious tone of *Solo Leveling*, then maybe *That Time I Got Reincarnated as a Slime* (though it's got a different power progression). What are you looking for specifically – more isekai, more straight-up action, something with a similar \"underdog becomes OP\" plotline?\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:45:10.632385Z",
        "kind": "response"
    },
    {
        "parts": [
            {
                "content": "Summarize the conversation",
                "timestamp": "2025-01-16T13:45:12.096573Z",
                "part_kind": "user-prompt"
            }
        ],
        "kind": "request"
    },
    {
        "parts": [
            {
                "content": "We started by catching up, then Hamza mentioned finishing *Solo Leveling* and asking for recommendations of similar anime.  I praised the action in *Solo Leveling* and suggested *Tower of God* and *The God of High School* as shows with similar action-packed fight scenes and power scaling.  I also mentioned *That Time I Got Reincarnated as a Slime* as a possibility,  but pointed out it had a slightly different tone and power progression. Finally, I asked Hamza to specify what aspects of *Solo Leveling* he enjoyed most to give him more tailored recommendations.\n",
                "part_kind": "text"
            }
        ],
        "timestamp": "2025-01-16T13:45:13.639345Z",
        "kind": "response"
    }
]
"""






@HamzaFarhan
Copy link

Notice how the dynamic_ref is still "system_prompt" and not "system_prompt2"

@josead
Copy link
Contributor Author

josead commented Jan 16, 2025

I think this is not how you are supposed to use history_messages, but I see what you are trying to achieve.
Correct me if I'm wrong here, but what you want is to anchor the system prompt of an agent, independently of what message_history you have added to it.
Dynamic behaves differently, let me give you an example of how it would make sense to add dynamic:

import random
from copy import deepcopy
from dataclasses import dataclass

from pydantic_ai import Agent, RunContext, models
from pydantic_ai import messages as _messages
from pydantic_ai import usage as _usage
from pydantic_ai.result import ResultData
from pydantic_ai.tools import AgentDeps

@dataclass
class User:
    name: str


anime_agent = Agent(
    name="anime_fan",
    model="google-gla:gemini-1.5-flash",
    system_prompt="You are an anime fan. The user will also be an anime fan. Just bros chilling talking about anime.",
    deps_type=User,
    result_type=str,
)

# Name is not supposed to change, so we don't need to reevaluate.
@anime_agent.system_prompt()
def system_prompt(ctx: RunContext[User]) -> str:
    return f"The user's name is {ctx.deps.name}. And the last anime they watched is Solo Leveling."

# But, adding a system prompt that enables reevaluation we can do things like
@anime_agent.system_prompt(dynamic=True)
def system_prompt(ctx: RunContext[User]) -> str:
    charisma = random.randint(0,10) # suppose we get this from an API (external environment)
    shyness = random.randint(0,10) # this needs to be evaluated on every run
    curiosity = random.randint(0,10)
    neuroticism = random.randint(0,10)
    return f"""You are allowed to change radically the tone and voice on conversation based on this attributes, 0 means none, and 10 means a lot:
[charisma: {charisma}]
[shyness: {shyness}]
[curiosity: {curiosity]
[neuroticism: {neuroticism}]
"""

deps = User(name="Hamza")
message_history = None
user_prompt = "Hey"

while user_prompt.lower() not in ["q", "quit", "exit"]:
    res = await anime_agent.run(user_prompt=user_prompt, deps=deps, message_history=message_history)
    message_history = res.all_messages()
    user_prompt = input(f"{res.data}    (q to quit)> ")

# -----------

Having this code, you should be able to reevaluate the charisma, etc on each run with the usage of message_history. Without dynamic, and adding message_history, you would end up with a fixed charisma, shyness etc.. based on the first run (the one without message_history).

Now, to your point of summarizing, I'd suggest you to lower down to a simple input, and not share the message_history. AFAIU the simple usecase of summarizing, does not require you to send the message_histrory, you should add the history of conversation as just a simple message of the user like this:


summary_agent = Agent(
    name="summary_agent",
    model="google-gla:gemini-1.5-flash",
    system_prompt="Your job is to summarize the conversation into bullet points.",
    result_type=str,
)

summary_res = await summary_agent.run(
    user_prompt=str(message_history), result_type=str,
)

print(summary_res.data)

This is a simple case of a chain or single purposed agent, you can use Agent because it is simple, but I think you are overusing the Agents here. @samuelcolvin correct me if I'm wrong here with the Agent's usage intention.

@HamzaFarhan
Copy link

Yeah this is just an example.
You got the right idea tho. I want to anchor the system prompt and pass the message_history between agents. So each agent knows the whole history, but retains its system instructions.

@josead
Copy link
Contributor Author

josead commented Jan 16, 2025

Adding this functionality of "anchoring" would be interesting perhaps we can submit an issue and implementing this would be something very similar to what we did with dynamic, but dynamic won't be able to help here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants