Skip to content

Commit d4307c6

Browse files
authored
[fix] Make models stateless (#3140)
## Summary Remove state like `response_format` and `tools` from model state to avoid issues where models are shared between teams/agents. Fixes #3095 ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [ ] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [ ] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [ ] Tested in clean environment - [ ] Tests added/updated (if applicable) --- ## Additional Notes Add any important context (deployment instructions, screenshots, security considerations, etc.)
1 parent 17c17da commit d4307c6

File tree

36 files changed

+1497
-1283
lines changed

36 files changed

+1497
-1283
lines changed

cookbook/models/azure/ai_foundry/async_tool_use.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@
1515
tools=[DuckDuckGoTools()],
1616
show_tool_calls=True,
1717
markdown=True,
18+
debug_mode=True,
1819
)
1920

20-
response: RunResponse = asyncio.run(agent.arun("Whats happening in France?"))
21-
22-
print(response.content)
23-
print()
24-
print(response.metrics)
21+
asyncio.run(agent.aprint_response("Whats happening in France?"))

cookbook/models/azure/ai_foundry/structured_output.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class MovieScript(BaseModel):
2626

2727

2828
agent = Agent(
29-
model=AzureAIFoundry(id="Phi-4"),
29+
model=AzureAIFoundry(id="gpt-4o"),
3030
description="You help people write movie scripts.",
3131
response_model=MovieScript,
3232
# debug_mode=True,

cookbook/models/ollama/tool_use.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from agno.tools.duckduckgo import DuckDuckGoTools
66

77
agent = Agent(
8-
model=Ollama(id="llama3.1:8b"),
8+
model=Ollama(id="llama3.2:latest"),
99
tools=[DuckDuckGoTools()],
1010
show_tool_calls=True,
1111
markdown=True,

cookbook/models/ollama/tool_use_stream.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from agno.tools.duckduckgo import DuckDuckGoTools
66

77
agent = Agent(
8-
model=Ollama(id="llama3.1:8b"),
8+
model=Ollama(id="llama3.2:latest"),
99
tools=[DuckDuckGoTools()],
1010
show_tool_calls=True,
1111
markdown=True,

cookbook/models/ollama_tools/tool_use.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from agno.tools.duckduckgo import DuckDuckGoTools
66

77
agent = Agent(
8-
model=OllamaTools(id="llama3.1:8b"),
8+
model=OllamaTools(id="llama3.2:latest"),
99
tools=[DuckDuckGoTools()],
1010
show_tool_calls=True,
1111
markdown=True,

libs/agno/agno/agent/agent.py

Lines changed: 99 additions & 95 deletions
Large diffs are not rendered by default.

libs/agno/agno/memory/manager.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,7 @@ def update_model(self) -> None:
4040
exit(1)
4141
self.model = OpenAIChat(id="gpt-4o")
4242

43-
# Add tools to the Model
44-
self.add_tools_to_model(model=self.model)
45-
46-
def add_tools_to_model(self, model: Model) -> None:
43+
def determine_tools_for_model(self) -> None:
4744
if self._tools_for_model is None:
4845
self._tools_for_model = []
4946
if self._functions_for_model is None:
@@ -64,10 +61,6 @@ def add_tools_to_model(self, model: Model) -> None:
6461
log_debug(f"Added function {func.name}")
6562
except Exception as e:
6663
logger.warning(f"Could not add function {tool}: {e}")
67-
# Set tools on the model
68-
model.set_tools(tools=self._tools_for_model)
69-
# Set functions on the model
70-
model.set_functions(functions=self._functions_for_model)
7164

7265
def get_existing_memories(self) -> Optional[List[MemoryRow]]:
7366
if self.db is None:
@@ -175,6 +168,7 @@ def run(
175168

176169
# Update the Model (set defaults, add logit etc.)
177170
self.update_model()
171+
self.determine_tools_for_model()
178172

179173
# Prepare the List of messages to send to the Model
180174
messages_for_model: List[Message] = [self.get_system_message()]
@@ -189,7 +183,9 @@ def run(
189183

190184
# Generate a response from the Model (includes running function calls)
191185
self.model = cast(Model, self.model)
192-
response = self.model.response(messages=messages_for_model)
186+
response = self.model.response(
187+
messages=messages_for_model, tools=self._tools_for_model, functions=self._functions_for_model
188+
)
193189
log_debug("*********** MemoryManager End ***********")
194190
return response.content
195191

@@ -215,6 +211,8 @@ async def arun(
215211

216212
# Generate a response from the Model (includes running function calls)
217213
self.model = cast(Model, self.model)
218-
response = await self.model.aresponse(messages=messages_for_model)
214+
response = await self.model.aresponse(
215+
messages=messages_for_model, tools=self._tools_for_model, functions=self._functions_for_model
216+
)
219217
log_debug("*********** Async MemoryManager End ***********")
220218
return response.content

libs/agno/agno/memory/summarizer.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,6 @@ def update_model(self) -> None:
2626
exit(1)
2727
self.model = OpenAIChat(id="gpt-4o")
2828

29-
# Set response_format if it is not set on the Model
30-
if self.use_structured_outputs:
31-
self.model.response_format = SessionSummary
32-
self.model.structured_outputs = True
33-
else:
34-
self.model.response_format = {"type": "json_object"}
35-
3629
def get_system_message(self, messages_for_summarization: List[Dict[str, str]]) -> Message:
3730
# -*- Return a system message for summarization
3831
system_prompt = dedent("""\
@@ -95,6 +88,12 @@ def run(
9588
# Update the Model (set defaults, add logit etc.)
9689
self.update_model()
9790

91+
# Set response_format if it is not set on the Model
92+
if self.use_structured_outputs:
93+
response_format: Any = SessionSummary
94+
else:
95+
response_format = {"type": "json_object"}
96+
9897
# Convert the message pairs to a list of dictionaries
9998
messages_for_summarization: List[Dict[str, str]] = []
10099
for message_pair in message_pairs:
@@ -115,7 +114,7 @@ def run(
115114

116115
# Generate a response from the Model (includes running function calls)
117116
self.model = cast(Model, self.model)
118-
response = self.model.response(messages=messages_for_model)
117+
response = self.model.response(messages=messages_for_model, response_format=response_format)
119118
log_debug("*********** MemorySummarizer End ***********")
120119

121120
# If the model natively supports structured outputs, the parsed value is already in the structured format

libs/agno/agno/memory/v2/manager.py

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from copy import deepcopy
22
from dataclasses import dataclass
33
from textwrap import dedent
4-
from typing import Any, Callable, Dict, List, Optional, cast
4+
from typing import Any, Callable, Dict, List, Optional
55

66
from agno.memory.v2.db.base import MemoryDb
77
from agno.memory.v2.db.schema import MemoryRow
@@ -44,30 +44,25 @@ def __init__(
4444
self.system_message = system_message
4545
self.memory_capture_instructions = memory_capture_instructions
4646
self.additional_instructions = additional_instructions
47-
48-
def add_tools_to_model(self, model: Model, tools: List[Callable]) -> None:
49-
model = cast(Model, model)
50-
model.reset_tools_and_functions()
51-
52-
_tools_for_model = []
53-
_functions_for_model = {}
54-
55-
for tool in tools:
56-
try:
57-
function_name = tool.__name__
58-
if function_name not in _functions_for_model:
59-
func = Function.from_callable(tool, strict=True) # type: ignore
60-
func.strict = True
61-
_functions_for_model[func.name] = func
62-
_tools_for_model.append({"type": "function", "function": func.to_dict()})
63-
log_debug(f"Added function {func.name}")
64-
except Exception as e:
65-
log_warning(f"Could not add function {tool}: {e}")
66-
67-
# Set tools on the model
68-
model.set_tools(tools=_tools_for_model)
69-
# Set functions on the model
70-
model.set_functions(functions=_functions_for_model)
47+
self._tools_for_model: Optional[List[Dict[str, Any]]] = None
48+
self._functions_for_model: Optional[Dict[str, Function]] = None
49+
50+
def determine_tools_for_model(self, tools: List[Callable]) -> None:
51+
if self._tools_for_model is None:
52+
self._tools_for_model = []
53+
self._functions_for_model = {}
54+
55+
for tool in tools:
56+
try:
57+
function_name = tool.__name__
58+
if function_name not in self._functions_for_model:
59+
func = Function.from_callable(tool, strict=True) # type: ignore
60+
func.strict = True
61+
self._functions_for_model[func.name] = func
62+
self._tools_for_model.append({"type": "function", "function": func.to_dict()})
63+
log_debug(f"Added function {func.name}")
64+
except Exception as e:
65+
log_warning(f"Could not add function {tool}: {e}")
7166

7267
def get_system_message(
7368
self,
@@ -166,8 +161,7 @@ def create_or_update_memories(
166161

167162
model_copy = deepcopy(self.model)
168163
# Update the Model (set defaults, add logit etc.)
169-
self.add_tools_to_model(
170-
model_copy,
164+
self.determine_tools_for_model(
171165
self._get_db_tools(
172166
user_id, db, input_string, enable_delete_memory=delete_memories, enable_clear_memory=clear_memories
173167
),
@@ -184,7 +178,9 @@ def create_or_update_memories(
184178
]
185179

186180
# Generate a response from the Model (includes running function calls)
187-
response = model_copy.response(messages=messages_for_model)
181+
response = model_copy.response(
182+
messages=messages_for_model, tools=self._tools_for_model, functions=self._functions_for_model
183+
)
188184

189185
if response.tool_calls is not None and len(response.tool_calls) > 0:
190186
self.memories_updated = True
@@ -214,8 +210,7 @@ async def acreate_or_update_memories(
214210

215211
model_copy = deepcopy(self.model)
216212
# Update the Model (set defaults, add logit etc.)
217-
self.add_tools_to_model(
218-
model_copy,
213+
self.determine_tools_for_model(
219214
self._get_db_tools(
220215
user_id, db, input_string, enable_delete_memory=delete_memories, enable_clear_memory=clear_memories
221216
),
@@ -232,7 +227,9 @@ async def acreate_or_update_memories(
232227
]
233228

234229
# Generate a response from the Model (includes running function calls)
235-
response = await model_copy.aresponse(messages=messages_for_model)
230+
response = await model_copy.aresponse(
231+
messages=messages_for_model, tools=self._tools_for_model, functions=self._functions_for_model
232+
)
236233

237234
if response.tool_calls is not None and len(response.tool_calls) > 0:
238235
self.memories_updated = True
@@ -257,8 +254,7 @@ def run_memory_task(
257254

258255
model_copy = deepcopy(self.model)
259256
# Update the Model (set defaults, add logit etc.)
260-
self.add_tools_to_model(
261-
model_copy,
257+
self.determine_tools_for_model(
262258
self._get_db_tools(
263259
user_id, db, task, enable_delete_memory=delete_memories, enable_clear_memory=clear_memories
264260
),
@@ -274,7 +270,9 @@ def run_memory_task(
274270
]
275271

276272
# Generate a response from the Model (includes running function calls)
277-
response = model_copy.response(messages=messages_for_model)
273+
response = model_copy.response(
274+
messages=messages_for_model, tools=self._tools_for_model, functions=self._functions_for_model
275+
)
278276

279277
if response.tool_calls is not None and len(response.tool_calls) > 0:
280278
self.memories_updated = True
@@ -299,8 +297,7 @@ async def arun_memory_task(
299297

300298
model_copy = deepcopy(self.model)
301299
# Update the Model (set defaults, add logit etc.)
302-
self.add_tools_to_model(
303-
model_copy,
300+
self.determine_tools_for_model(
304301
self._get_db_tools(
305302
user_id, db, task, enable_delete_memory=delete_memories, enable_clear_memory=clear_memories
306303
),
@@ -316,7 +313,9 @@ async def arun_memory_task(
316313
]
317314

318315
# Generate a response from the Model (includes running function calls)
319-
response = await model_copy.aresponse(messages=messages_for_model)
316+
response = await model_copy.aresponse(
317+
messages=messages_for_model, tools=self._tools_for_model, functions=self._functions_for_model
318+
)
320319

321320
if response.tool_calls is not None and len(response.tool_calls) > 0:
322321
self.memories_updated = True

libs/agno/agno/memory/v2/memory.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from dataclasses import dataclass, field
44
from datetime import datetime
55
from os import getenv
6-
from typing import Any, Dict, List, Literal, Optional, Union
6+
from typing import Any, Dict, List, Literal, Optional, Type, Union
77

88
from pydantic import BaseModel, Field
99

@@ -789,24 +789,21 @@ def search_user_memories(
789789
else: # Default to last_n
790790
return self._get_last_n_memories(user_id=user_id, limit=limit)
791791

792-
def _update_model_for_agentic_search(self) -> None:
792+
def get_response_format(self) -> Union[Dict[str, Any], Type[BaseModel]]:
793793
model = self.get_model()
794794
if model.supports_native_structured_outputs:
795-
model.response_format = MemorySearchResponse
796-
model.structured_outputs = True
795+
return MemorySearchResponse
797796

798797
elif model.supports_json_schema_outputs:
799-
model.response_format = {
798+
return {
800799
"type": "json_schema",
801800
"json_schema": {
802801
"name": MemorySearchResponse.__name__,
803802
"schema": MemorySearchResponse.model_json_schema(),
804803
},
805804
}
806-
model.structured_outputs = False
807805
else:
808-
model.response_format = {"type": "json_object"}
809-
model.structured_outputs = False
806+
return {"type": "json_object"}
810807

811808
def _search_user_memories_agentic(self, user_id: str, query: str, limit: Optional[int] = None) -> List[UserMemory]:
812809
"""Search through user memories using agentic search."""
@@ -815,7 +812,7 @@ def _search_user_memories_agentic(self, user_id: str, query: str, limit: Optiona
815812

816813
model = self.get_model()
817814

818-
self._update_model_for_agentic_search()
815+
response_format = self.get_response_format()
819816

820817
log_debug("Searching for memories", center=True)
821818

@@ -833,7 +830,7 @@ def _search_user_memories_agentic(self, user_id: str, query: str, limit: Optiona
833830
system_message_str += "\n</user_memories>\n\n"
834831
system_message_str += "REMEMBER: Only return the IDs of the memories that are related to the query."
835832

836-
if model.response_format == {"type": "json_object"}:
833+
if response_format == {"type": "json_object"}:
837834
system_message_str += "\n" + get_json_output_prompt(MemorySearchResponse) # type: ignore
838835

839836
messages_for_model = [
@@ -845,7 +842,7 @@ def _search_user_memories_agentic(self, user_id: str, query: str, limit: Optiona
845842
]
846843

847844
# Generate a response from the Model (includes running function calls)
848-
response = model.response(messages=messages_for_model)
845+
response = model.response(messages=messages_for_model, response_format=response_format)
849846
log_debug("Search for memories complete", center=True)
850847

851848
memory_search: Optional[MemorySearchResponse] = None

0 commit comments

Comments
 (0)