Skip to content

Commit 554a8d3

Browse files
authored
chore: Release 1.7.5 (#3885)
# Changelog ## Improvements: - **Session Caching:** Added `cache_session` attribute to allow users to switch off session caching, which improves on memory management. ## Bug Fixes: - **Nested Tool Hooks**: Fixed bug with nested tool hooks.
1 parent 789993b commit 554a8d3

File tree

12 files changed

+166
-23
lines changed

12 files changed

+166
-23
lines changed

.github/workflows/test_on_release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ jobs:
252252
253253
# Run tests for Mistral
254254
test-mistral:
255+
if: false # Disable mistral tests until we get a production account
255256
runs-on: ubuntu-latest
256257
strategy:
257258
matrix:
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import asyncio
2+
import json
3+
from typing import AsyncIterator
4+
5+
import httpx
6+
from agno.agent import Agent
7+
from agno.tools import tool
8+
9+
10+
class DemoTools:
11+
@tool(description="Get the top hackernews stories")
12+
@staticmethod
13+
async def get_top_hackernews_stories(agent: Agent) -> str:
14+
num_stories = agent.context.get("num_stories", 5) if agent.context else 5
15+
16+
# Fetch top story IDs
17+
response = httpx.get("https://hacker-news.firebaseio.com/v0/topstories.json")
18+
story_ids = response.json()
19+
20+
# Get story details
21+
for story_id in story_ids[:num_stories]:
22+
async with httpx.AsyncClient() as client:
23+
story_response = await client.get(
24+
f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json"
25+
)
26+
story = story_response.json()
27+
if "text" in story:
28+
story.pop("text", None)
29+
return json.dumps(story)
30+
31+
@tool(
32+
description="Get the current weather for a city using the MetaWeather public API"
33+
)
34+
async def get_current_weather(agent: Agent) -> str:
35+
city = (
36+
agent.context.get("city", "San Francisco")
37+
if agent.context
38+
else "San Francisco"
39+
)
40+
41+
async with httpx.AsyncClient() as client:
42+
# Geocode city to get latitude and longitude
43+
geo_resp = await client.get(
44+
"https://geocoding-api.open-meteo.com/v1/search",
45+
params={"name": city, "count": 1, "language": "en", "format": "json"},
46+
)
47+
geo_data = geo_resp.json()
48+
if not geo_data.get("results"):
49+
return json.dumps({"error": f"City '{city}' not found."})
50+
location = geo_data["results"][0]
51+
lat, lon = location["latitude"], location["longitude"]
52+
53+
# Get current weather
54+
weather_resp = await client.get(
55+
"https://api.open-meteo.com/v1/forecast",
56+
params={
57+
"latitude": lat,
58+
"longitude": lon,
59+
"current_weather": True,
60+
"timezone": "auto",
61+
},
62+
)
63+
weather_data = weather_resp.json()
64+
current_weather = weather_data.get("current_weather")
65+
if not current_weather:
66+
return json.dumps({"error": f"No weather data found for '{city}'."})
67+
68+
result = {
69+
"city": city,
70+
"weather_state": f"{current_weather['weathercode']}", # Open-Meteo uses weather codes
71+
"temp_celsius": current_weather["temperature"],
72+
"humidity": None, # Open-Meteo current_weather does not provide humidity
73+
"date": current_weather["time"],
74+
}
75+
return json.dumps(result)
76+
77+
78+
agent = Agent(
79+
name="HackerNewsAgent",
80+
context={
81+
"num_stories": 2,
82+
},
83+
tools=[DemoTools.get_top_hackernews_stories],
84+
)
85+
asyncio.run(agent.aprint_response("What are the top hackernews stories?"))
86+
87+
88+
agent = Agent(
89+
name="WeatherAgent",
90+
context={
91+
"city": "San Francisco",
92+
},
93+
tools=[DemoTools().get_current_weather],
94+
)
95+
asyncio.run(agent.aprint_response("What is the weather like?"))

libs/agno/agno/agent/agent.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,10 +1001,10 @@ def run(
10011001
session_id, user_id = self._initialize_session(
10021002
session_id=session_id, user_id=user_id, session_state=session_state
10031003
)
1004-
1004+
10051005
# Initialize the Agent
10061006
self.initialize_agent()
1007-
1007+
10081008
# Read existing session from storage
10091009
self.read_from_storage(session_id=session_id)
10101010

@@ -1377,16 +1377,15 @@ async def arun(
13771377
) -> Any:
13781378
"""Async Run the Agent and return the response."""
13791379

1380-
13811380
session_id, user_id = self._initialize_session(
13821381
session_id=session_id, user_id=user_id, session_state=session_state
13831382
)
13841383

13851384
log_debug(f"Session ID: {session_id}", center=True)
1386-
1385+
13871386
# Initialize the Agent
13881387
self.initialize_agent()
1389-
1388+
13901389
# Read existing session from storage
13911390
self.read_from_storage(session_id=session_id)
13921391

libs/agno/agno/app/fastapi/async_router.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -406,9 +406,7 @@ async def run_agent_or_team_or_workflow(
406406
)
407407
else:
408408
return StreamingResponse(
409-
workflow_response_streamer(
410-
workflow, workflow_input, session_id=session_id, user_id=user_id
411-
), # type: ignore
409+
workflow_response_streamer(workflow, workflow_input, session_id=session_id, user_id=user_id), # type: ignore
412410
media_type="text/event-stream",
413411
)
414412
else:

libs/agno/agno/app/fastapi/sync_router.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -391,11 +391,11 @@ def run_agent_or_team_or_workflow(
391391
if isinstance(workflow_input, dict):
392392
return StreamingResponse(
393393
(json.dumps(asdict(result)) for result in workflow_instance.run(**workflow_input)),
394-
media_type="text/event-stream",
395-
)
394+
media_type="text/event-stream",
395+
)
396396
else:
397397
return StreamingResponse(
398-
(json.dumps(asdict(result)) for result in workflow_instance.run(workflow_input)),
398+
(json.dumps(asdict(result)) for result in workflow_instance.run(workflow_input)), # type: ignore
399399
media_type="text/event-stream",
400400
)
401401
else:
@@ -438,7 +438,7 @@ def run_agent_or_team_or_workflow(
438438
if isinstance(workflow_input, dict):
439439
return workflow_instance.run(**workflow_input).to_dict()
440440
else:
441-
return workflow_instance.run(workflow_input).to_dict()
441+
return workflow_instance.run(workflow_input).to_dict() # type: ignore
442442
else:
443443
if isinstance(workflow_input, dict):
444444
return workflow.run(**workflow_input, session_id=session_id, user_id=user_id).to_dict()

libs/agno/agno/knowledge/combined.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ async def async_document_lists(self) -> AsyncIterator[List[Document]]:
3232

3333
for kb in self.sources:
3434
log_debug(f"Loading documents from {kb.__class__.__name__}")
35-
async for document in kb.async_document_lists:
35+
async for document in kb.async_document_lists: # type: ignore
3636
yield document

libs/agno/agno/team/team.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,7 @@ def run(
800800

801801
# Initialize Team
802802
self.initialize_team(session_id=session_id)
803-
803+
804804
# Read existing session from storage
805805
self.read_from_storage(session_id=session_id)
806806

@@ -1200,10 +1200,10 @@ async def arun(
12001200
session_id=session_id, user_id=user_id, session_state=session_state
12011201
)
12021202
log_debug(f"Session ID: {session_id}", center=True)
1203-
1203+
12041204
# Initialize Team
12051205
self.initialize_team(session_id=session_id)
1206-
1206+
12071207
# Read existing session from storage
12081208
self.read_from_storage(session_id=session_id)
12091209

libs/agno/agno/tools/decorator.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,49 @@
99
ToolConfig = TypeVar("ToolConfig", bound=Dict[str, Any])
1010

1111

12+
def _is_async_function(func: Callable) -> bool:
13+
"""
14+
Check if a function is async, even when wrapped by decorators like @staticmethod.
15+
16+
This function tries to detect async functions by:
17+
1. Checking the function directly with inspect functions
18+
2. Looking at the original function if it's wrapped
19+
3. Checking the function's code object for async indicators
20+
"""
21+
from inspect import iscoroutine, iscoroutinefunction
22+
23+
# First, try the standard inspect functions
24+
if iscoroutinefunction(func) or iscoroutine(func):
25+
return True
26+
27+
# If the function has a __wrapped__ attribute, check the original function
28+
if hasattr(func, "__wrapped__"):
29+
original_func = func.__wrapped__
30+
if iscoroutinefunction(original_func) or iscoroutine(original_func):
31+
return True
32+
33+
# Check if the function has CO_COROUTINE flag in its code object
34+
try:
35+
if hasattr(func, "__code__") and func.__code__.co_flags & 0x80: # CO_COROUTINE flag
36+
return True
37+
except (AttributeError, TypeError):
38+
pass
39+
40+
# For static methods, try to get the original function
41+
try:
42+
if hasattr(func, "__func__"):
43+
original_func = func.__func__
44+
if iscoroutinefunction(original_func) or iscoroutine(original_func):
45+
return True
46+
# Check the code object of the original function
47+
if hasattr(original_func, "__code__") and original_func.__code__.co_flags & 0x80:
48+
return True
49+
except (AttributeError, TypeError):
50+
pass
51+
52+
return False
53+
54+
1255
@overload
1356
def tool() -> Callable[[F], Function]: ...
1457

@@ -125,7 +168,7 @@ async def my_async_function():
125168
)
126169

127170
def decorator(func: F) -> Function:
128-
from inspect import isasyncgenfunction, iscoroutine, iscoroutinefunction
171+
from inspect import isasyncgenfunction
129172

130173
@wraps(func)
131174
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
@@ -163,7 +206,7 @@ async def async_gen_wrapper(*args: Any, **kwargs: Any) -> Any:
163206
# Choose appropriate wrapper based on function type
164207
if isasyncgenfunction(func):
165208
wrapper = async_gen_wrapper
166-
elif iscoroutinefunction(func) or iscoroutine(func):
209+
elif _is_async_function(func):
167210
wrapper = async_wrapper
168211
else:
169212
wrapper = sync_wrapper

libs/agno/agno/tools/function.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def from_callable(cls, c: Callable, name: Optional[str] = None, strict: bool = F
151151
param_type_hints = {
152152
name: type_hints.get(name)
153153
for name in sig.parameters
154-
if name != "return" and name not in ["agent", "team"]
154+
if name != "return" and name not in ["agent", "team", "self"]
155155
}
156156

157157
# Parse docstring for parameters
@@ -177,7 +177,9 @@ def from_callable(cls, c: Callable, name: Optional[str] = None, strict: bool = F
177177
# If strict=True mark all fields as required
178178
# See: https://platform.openai.com/docs/guides/structured-outputs/supported-schemas#all-fields-must-be-required
179179
if strict:
180-
parameters["required"] = [name for name in parameters["properties"] if name not in ["agent", "team"]]
180+
parameters["required"] = [
181+
name for name in parameters["properties"] if name not in ["agent", "team", "self"]
182+
]
181183
else:
182184
# Mark a field as required if it has no default value (this would include optional fields)
183185
parameters["required"] = [
@@ -235,7 +237,7 @@ def process_entrypoint(self, strict: bool = False):
235237
# log_info(f"Type hints for {self.name}: {type_hints}")
236238

237239
# Filter out return type and only process parameters
238-
excluded_params = ["return", "agent", "team"]
240+
excluded_params = ["return", "agent", "team", "self"]
239241
if self.requires_user_input and self.user_input_fields:
240242
if len(self.user_input_fields) == 0:
241243
excluded_params.extend(list(type_hints.keys()))
@@ -337,7 +339,9 @@ def _wrap_callable(func: Callable) -> Callable:
337339

338340
def process_schema_for_strict(self):
339341
self.parameters["additionalProperties"] = False
340-
self.parameters["required"] = [name for name in self.parameters["properties"] if name not in ["agent", "team"]]
342+
self.parameters["required"] = [
343+
name for name in self.parameters["properties"] if name not in ["agent", "team", "self"]
344+
]
341345

342346
def _get_cache_key(self, entrypoint_args: Dict[str, Any], call_args: Optional[Dict[str, Any]] = None) -> str:
343347
"""Generate a cache key based on function name and arguments."""

libs/agno/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "agno"
3-
version = "1.7.4"
3+
version = "1.7.5"
44
description = "Agno: a lightweight library for building Multi-Agent Systems"
55
requires-python = ">=3.7,<4"
66
readme = "README.md"

0 commit comments

Comments
 (0)