-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Hi, thanks for the great framework - it's been very productive for me so far. I've been hitting an issue implementing an agentic pattern using some tools that I was hoping to get some direction on. I'm not sure if it can be handled through the type system, prompting, or the agent/model arguments. I've tried variations on all of them with seemingly little effect.
High level, I have one top level Agent that has tools that call out to sub-agents, - a data agent and a news agent. These agents have tools that get data and news and summarize what they find as they relate to the query.
I'm hitting an issue with the news agent get_articles tool where it seems to get placed into an endless loop, calling the news API over and over (getting the same results) and eventually leading to getting 429 (Resource Exhausted) responses from the Gemini API. Using the debugger, I can see that news_agent.run(...) is only getting invoked once, so the issue seems to be with how news_agent is converting its tool return type into its agent return type (list[WPArticle] -> list[NewsSummary]). I think understanding how this works and being able to manage the control flow of agent tools on some level will be pretty important for developers of agentic applications like this.
I've tried various configurations on NewsAgent, including
end_strategy="early", defer_model_check=False, which don't seem to have any effect on the behavior. Every tool has the retries=1 parameter set too, and explicit system prompting to only invoke said tool one time.
from pydantic_ai import Agent, RunContext
import yaml
from pydantic import BaseModel, Field
from pydantic_ai.models.vertexai import VertexAIModel
from datetime import datetime
from typing import Optional
from io import StringIO
class WPArticle(BaseModel):
# omitted for brevity
pass
class NewsRequest(BaseModel):
query: str = Field(
...,
description="(individual components of the user's query with locations, topics)",
)
start_date: Optional[datetime] = Field(
None,
description="The Start Date for the user's query. Only include this if it is explicitly requested.",
)
end_date: Optional[datetime] = Field(
None,
description="The End Date for the user's query. Only include this if it is explicitly requested.",
)
class NewsSummary(BaseModel):
date: datetime = Field(..., description="The date the article was published on")
summary: str = Field(
...,
description="A brief summary of the article, and how it may relate to the user's query",
)
title: str = Field(..., description="The title of the article")
link: str = Field(..., description="The link to the article")
def to_yaml(self) -> str:
json_body = self.model_dump(mode="json")
return yaml.dump(json_body)
news_agent = Agent(
"google-vertex:gemini-1.5-flash",
system_prompt=[
"You are an expert at searching ___ news articles, using sematic search terms are included or are related to their query, and summarizing their content as they relate to user queries. You call your tool exactly one time to find relevant results - the responses to your tool calls will not change.",
],
model_settings={"temperature": 0, "timeout": 15},
result_type=list[NewsSummary],
# end_strategy="early",
defer_model_check=False,
)
@news_agent.tool_plain(retries=1)
async def get_articles(search: NewsRequest) -> list[WPArticle]:
"""
Fetch articles from the ___ API
* Use this tool ONE TIME PER RUN to retrieve relevant articles for a search string.
Args:
search: NewsRequest.
object to search news articles with, including:
query: str (individual components of the user's query with locations, topics)
start_date: date (optional)
end_date: date (optional)
Returns:
A list of WPArticle objects representing the article contents, date, and title. This list may be empty if no records were found.
"""
logging.info("Running get_articles...")
# return []
query = search.query.replace('""', "")
async with NewsApiClient() as client:
res = await client.get_articles(
search=query, start_date=search.start_date, end_date=search.end_date
)
return res # I can see that this returns the top 5 articles as WPArticles (pydantic models) related to the user query when debugging - but the tool evaluates over and over, having the effect of spamming the API I'm interacting with. It's the same search and response every time.
response_agent = Agent("google-vertex:gemini-1.5-pro")
@response_agent.tool(retries=1)
async def get_data(ctx: RunContext, query: str):
# ... longer running op than news
pass
@response_agent.tool(retries=1)
async def get_news(ctx: RunContext, query: str) -> str:
"""
Tool to page our org's news API and summarize articles relevant to a user's query.
"""
response = await news_agent.run(query)
with StringIO() as buffer:
for item in response.data:
buffer.write(item.to_yaml())
return buffer.getvalue()Is there some way that I can better indicate to Gemini through the Agent/tool that the results of a tool call are idempotent, given the same input, and that it should only invoke that tool once while attempting to converge?