Skip to content

Commit 727c0da

Browse files
kausmeowsdirkbrnd
authored andcommitted
chore: Release 1.5.5 (#3359)
# Changelog ## New Features: - **Claude File Upload:** we can now upload a file to Anthropic directly and then use it as an input to an agent. - **Claude 4 code execution tool:** allows Claude to execute Python code in a secure, sandboxed environment. - **Prompt caching with Anthropic models:** allows resuming from specific prefixes in your prompts. This approach significantly reduces processing time and costs for repetitive tasks or prompts with consistent elements. - **Vercel v0 Model:** Added support for new Vercel v0 models and cookbook examples. - **Qdrant Hybrid Search support** - **Markdown Knowledge Base**: Added native support for Markdown-based knowledge bases. - **AI/ML API platform integration**: introduces integration with [AI/ML API](https://aimlapi.com/models/?utm_source=agno&utm_medium=github&utm_campaign=integration), a platform providing AI/ML models. AI/ML API provides 300+ AI models including Deepseek, Gemini, ChatGPT. The models run at enterprise-grade rate limits and uptimes - **Update pydantic and dataclass in function handling**: Now supports class as input to a function ## Improvements: - **Timeout handling for API calls in ExaTools class:** - Timeout functionality to Exa API calls to prevent indefinite hanging of search operations. The implementation uses Python's `concurrent.futures` module to enforce timeouts on all Exa API operations (search, get contents, find similar, and answer generation). - This change addresses the issue where Exa search functions would hang indefinitely, causing potential service disruptions and resource leaks. - **Fetch messages from last n sessions:** - A tool for the agent, something like `get_previous_session_messages(number_of_sessions: int)` that returns a list of messages that the agent can analyse - Switch on with `search_previous_sessions_history` - **Redis Expiration**: Added expire key to set TTL on Redis keys. -  **Add anthropic cache write to agent session metrics**: adds cache_creation_input_tokens to agent session metrics, to allow for tracking Anthropic cache write statistics ## Bug Fixes: - **HF custom embedder:** - It turns out that HF has changed some things on their API and they've deprecated .post on their InferenceClient()- https://discuss.huggingface.co/t/getting-error-attributeerror-inferenceclient-object-has-no-attribute-post/156682 - Also we can no longer use- `id: str = "jinaai/jina-embeddings-v2-base-code"` as default, because these models are no longer provided by the `HF Inference API`. Changed the default to- `id: str = "intfloat/multilingual-e5-large"` - **Add `role_map` for `OpenAIChat`:** This allows certain models that don’t adhere to OpenAI’s role mapping to be used vir `OpenAILike`. - **Use content hash as id in upsert: in pgvector** use reproducible content_hash in upsert as id - **Insert in vector db passes only last chunk meta_data:** insert in vector db passes only last chunk meta_data. issue link- https://discord.com/channels/965734768803192842/1219054452221153463/1376631140047130649 - **Remove argument sanitization**: a safer way to do this that won't break arguments that shouldn't be sanitized - **Handle async tools when running async agents on playground**: Fixes a regression where using Agents with async tools (e.g. MCP tools) was breaking in the Playground. --------- Co-authored-by: Dirk Brand <dirkbrnd@gmail.com>
1 parent b6dc243 commit 727c0da

File tree

24 files changed

+785
-94
lines changed

24 files changed

+785
-94
lines changed

.github/workflows/test_on_release.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -638,9 +638,10 @@ jobs:
638638
run: |
639639
./scripts/dev_setup.sh
640640
- name: Run embedder tests
641-
working-directory: libs/agno
641+
working-directory: .
642642
run: |
643-
python -m pytest ./tests/integration/embedder
643+
source .venv/bin/activate
644+
python -m pytest ./libs/agno/tests/integration/embedder
644645
645646
test-knowledge:
646647
runs-on: ubuntu-latest
@@ -699,5 +700,4 @@ jobs:
699700
working-directory: .
700701
run: |
701702
source .venv/bin/activate
702-
python -m pytest --ignore=./libs/agno/tests/integration/models --ignore=./libs/agno/tests/integration/agent --ignore=./libs/agno/tests/integration/knowledge --ignore=./libs/agno/tests/integration/teams
703-
--ignore=./libs/agno/tests/integration/embedder ./libs/agno/tests/integration
703+
python -m pytest --ignore=./libs/agno/tests/integration/models --ignore=./libs/agno/tests/integration/agent --ignore=./libs/agno/tests/integration/knowledge --ignore=./libs/agno/tests/integration/teams --ignore=./libs/agno/tests/integration/embedder ./libs/agno/tests/integration
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""
2+
This example shows how to use complex input types with tools.
3+
4+
Recommendations:
5+
- Specify fields with descriptions, these will be used in the JSON schema sent to the model and will increase accuracy.
6+
- Try not to nest the structures too deeply, the model will have a hard time understanding them.
7+
"""
8+
9+
from datetime import datetime
10+
from enum import Enum
11+
from typing import List, Optional
12+
13+
from agno.agent import Agent
14+
from agno.tools.decorator import tool
15+
from pydantic import BaseModel, Field
16+
17+
18+
# Define Pydantic models for our tools
19+
class UserProfile(BaseModel):
20+
"""User profile information."""
21+
22+
name: str = Field(..., description="Full name of the user")
23+
email: str = Field(..., description="Valid email address")
24+
age: int = Field(..., ge=0, le=120, description="Age of the user")
25+
interests: List[str] = Field(
26+
default_factory=list, description="List of user interests"
27+
)
28+
created_at: datetime = Field(
29+
default_factory=datetime.now, description="Account creation timestamp"
30+
)
31+
32+
33+
class TaskPriority(str, Enum):
34+
"""Priority levels for tasks."""
35+
36+
LOW = "low"
37+
MEDIUM = "medium"
38+
HIGH = "high"
39+
URGENT = "urgent"
40+
41+
42+
class Task(BaseModel):
43+
"""Task information."""
44+
45+
title: str = Field(..., min_length=1, max_length=100, description="Task title")
46+
description: Optional[str] = Field(None, description="Detailed task description")
47+
priority: TaskPriority = Field(
48+
default=TaskPriority.MEDIUM, description="Task priority level"
49+
)
50+
due_date: Optional[datetime] = Field(None, description="Task due date")
51+
assigned_to: Optional[UserProfile] = Field(
52+
None, description="User assigned to the task"
53+
)
54+
55+
56+
# Custom tools using Pydantic models
57+
@tool
58+
def create_user(user_data: UserProfile) -> str:
59+
"""Create a new user profile with validated information."""
60+
# In a real application, this would save to a database
61+
return f"Created user profile for {user_data.name} with email {user_data.email}"
62+
63+
64+
@tool
65+
def create_task(task_data: Task) -> str:
66+
"""Create a new task with priority and assignment."""
67+
# In a real application, this would save to a database
68+
return f"Created task '{task_data.title}' with priority {task_data.priority}"
69+
70+
71+
# Create the agent
72+
agent = Agent(
73+
name="task_manager",
74+
description="An agent that manages users and tasks with proper validation",
75+
tools=[create_user, create_task],
76+
)
77+
78+
# Example usage
79+
if __name__ == "__main__":
80+
# Example 1: Create a user
81+
agent.print_response(
82+
"Create a new user named John Doe with email john@example.com, age 30, and interests in Python and AI"
83+
)
84+
85+
# Example 2: Create a task
86+
agent.print_response(
87+
"Create a high priority task titled 'Implement API endpoints' due tomorrow"
88+
)

cookbook/examples/a2a/basic_agent/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Basic Agno A2A Agent example that uses A2A to send and receive messages to/from
2121
4. Run the test client in a different terminal
2222

2323
```bash
24-
python cookbook/examples/a2a/basic_agent/test_client.py
24+
python cookbook/examples/a2a/basic_agent/client.py
2525
```
2626

2727
## Notes

libs/agno/agno/agent/agent.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6157,7 +6157,6 @@ def register_agent(self) -> None:
61576157
if not self.session_id:
61586158
self.session_id = str(uuid4())
61596159

6160-
log_debug(f"Creating Agent on Platform: {self.name}, {self.agent_id}, {self.team_id}, {self.workflow_id}")
61616160
create_agent(
61626161
agent=AgentCreate(
61636162
name=self.name,
@@ -6168,7 +6167,6 @@ def register_agent(self) -> None:
61686167
config=self.get_agent_config_dict(),
61696168
)
61706169
)
6171-
log_debug(f"Agent created: {self.name}, {self.agent_id}, {self.team_id}, {self.workflow_id}")
61726170
except Exception as e:
61736171
log_warning(f"Could not create Agent: {e}")
61746172

@@ -6180,7 +6178,6 @@ async def _aregister_agent(self) -> None:
61806178
from agno.api.agent import AgentCreate, acreate_agent
61816179

61826180
try:
6183-
log_debug(f"Creating Agent on Platform: {self.name}, {self.agent_id}, {self.team_id},")
61846181
await acreate_agent(
61856182
agent=AgentCreate(
61866183
name=self.name,
@@ -6193,7 +6190,6 @@ async def _aregister_agent(self) -> None:
61936190
)
61946191
except Exception as e:
61956192
log_debug(f"Could not create Agent app: {e}")
6196-
log_debug(f"Agent app created: {self.name}, {self.agent_id}, {self.team_id},")
61976193

61986194
def _log_agent_run(self, session_id: str, user_id: Optional[str] = None) -> None:
61996195
self.set_monitoring()

libs/agno/agno/api/agent.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ async def acreate_agent_run(run: AgentRunCreate, monitor: bool = False) -> None:
4848
)
4949
except Exception as e:
5050
log_debug(f"Could not create Agent run: {e}")
51+
return
5152

5253

5354
def create_agent(agent: AgentCreate) -> None:
@@ -60,8 +61,11 @@ def create_agent(agent: AgentCreate) -> None:
6061
ApiRoutes.AGENT_CREATE,
6162
json=agent.model_dump(exclude_none=True),
6263
)
64+
65+
log_debug(f"Created Agent on Platform. ID: {agent.agent_id}")
6366
except Exception as e:
6467
log_debug(f"Could not create Agent: {e}")
68+
return
6569

6670

6771
async def acreate_agent(agent: AgentCreate) -> None:
@@ -74,5 +78,7 @@ async def acreate_agent(agent: AgentCreate) -> None:
7478
ApiRoutes.AGENT_CREATE,
7579
json=agent.model_dump(exclude_none=True),
7680
)
81+
log_debug(f"Created Agent on Platform. ID: {agent.agent_id}")
7782
except Exception as e:
7883
log_debug(f"Could not create Agent: {e}")
84+
return

libs/agno/agno/models/anthropic/claude.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
ContentBlockDeltaEvent,
2424
ContentBlockStartEvent,
2525
ContentBlockStopEvent,
26-
MessageDeltaEvent,
26+
# MessageDeltaEvent, # Broken at the moment
2727
MessageStopEvent,
2828
)
2929
from anthropic.types import Message as AnthropicMessage
@@ -137,16 +137,16 @@ def _prepare_request_kwargs(
137137
Dict[str, Any]: The request keyword arguments.
138138
"""
139139
request_kwargs = self.request_kwargs.copy()
140-
141-
if self.cache_system_prompt:
142-
cache_control = (
143-
{"type": "ephemeral", "ttl": "1h"}
144-
if self.extended_cache_time is not None and self.extended_cache_time is True
145-
else {"type": "ephemeral"}
146-
)
147-
request_kwargs["system"] = [{"text": system_message, "type": "text", "cache_control": cache_control}]
148-
else:
149-
request_kwargs["system"] = [{"text": system_message, "type": "text"}]
140+
if system_message:
141+
if self.cache_system_prompt:
142+
cache_control = (
143+
{"type": "ephemeral", "ttl": "1h"}
144+
if self.extended_cache_time is not None and self.extended_cache_time is True
145+
else {"type": "ephemeral"}
146+
)
147+
request_kwargs["system"] = [{"text": system_message, "type": "text", "cache_control": cache_control}]
148+
else:
149+
request_kwargs["system"] = [{"text": system_message, "type": "text"}]
150150

151151
if tools:
152152
request_kwargs["tools"] = self._format_tools_for_model(tools)
@@ -212,7 +212,6 @@ def invoke(
212212
try:
213213
chat_messages, system_message = format_messages(messages)
214214
request_kwargs = self._prepare_request_kwargs(system_message, tools)
215-
216215
return self.get_client().messages.create(
217216
model=self.id,
218217
messages=chat_messages, # type: ignore
@@ -473,7 +472,7 @@ def parse_provider_response(self, response: AnthropicMessage, **kwargs) -> Model
473472
return model_response
474473

475474
def parse_provider_response_delta(
476-
self, response: Union[ContentBlockDeltaEvent, ContentBlockStopEvent, MessageDeltaEvent]
475+
self, response: Union[ContentBlockStartEvent, ContentBlockDeltaEvent, ContentBlockStopEvent, MessageStopEvent]
477476
) -> ModelResponse:
478477
"""
479478
Parse the Claude streaming response into ModelProviderResponse objects.
@@ -543,12 +542,12 @@ def parse_provider_response_delta(
543542
DocumentCitation(document_title=citation.document_title, cited_text=citation.cited_text)
544543
)
545544

546-
if response.message.usage is not None:
547-
model_response.response_usage = {
548-
"cache_write_tokens": response.usage.cache_creation_input_tokens,
549-
"cached_tokens": response.usage.cache_read_input_tokens,
550-
"input_tokens": response.usage.input_tokens,
551-
"output_tokens": response.usage.output_tokens,
552-
}
545+
if hasattr(response, "usage") and response.usage is not None:
546+
model_response.response_usage = {
547+
"cache_write_tokens": response.usage.cache_creation_input_tokens or 0,
548+
"cached_tokens": response.usage.cache_read_input_tokens or 0,
549+
"input_tokens": response.usage.input_tokens or 0,
550+
"output_tokens": response.usage.output_tokens or 0,
551+
}
553552

554553
return model_response

libs/agno/agno/models/base.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,20 @@ def _add_usage_metrics_to_assistant_message(assistant_message: Message, response
5656

5757
# Standard token metrics
5858
if isinstance(response_usage, dict):
59-
if "input_tokens" in response_usage:
59+
if "input_tokens" in response_usage and response_usage.get("input_tokens") is not None:
6060
assistant_message.metrics.input_tokens = response_usage.get("input_tokens", 0)
61-
if "output_tokens" in response_usage:
61+
if "output_tokens" in response_usage and response_usage.get("output_tokens") is not None:
6262
assistant_message.metrics.output_tokens = response_usage.get("output_tokens", 0)
63-
if "prompt_tokens" in response_usage:
63+
if "prompt_tokens" in response_usage and response_usage.get("prompt_tokens") is not None:
6464
assistant_message.metrics.input_tokens = response_usage.get("prompt_tokens", 0)
65-
if "completion_tokens" in response_usage:
65+
if "completion_tokens" in response_usage and response_usage.get("completion_tokens") is not None:
6666
assistant_message.metrics.output_tokens = response_usage.get("completion_tokens", 0)
67-
if "total_tokens" in response_usage:
68-
assistant_message.metrics.total_tokens = response_usage.get("total_tokens", 0)
69-
if "cached_tokens" in response_usage:
67+
if "cached_tokens" in response_usage and response_usage.get("cached_tokens") is not None:
7068
assistant_message.metrics.cached_tokens = response_usage.get("cached_tokens", 0)
71-
if "cache_write_tokens" in response_usage:
69+
if "cache_write_tokens" in response_usage and response_usage.get("cache_write_tokens") is not None:
7270
assistant_message.metrics.cache_write_tokens = response_usage.get("cache_write_tokens", 0)
71+
if "total_tokens" in response_usage and response_usage.get("total_tokens") is not None:
72+
assistant_message.metrics.total_tokens = response_usage.get("total_tokens", 0)
7373
else:
7474
assistant_message.metrics.total_tokens = (
7575
assistant_message.metrics.input_tokens + assistant_message.metrics.output_tokens
@@ -92,6 +92,17 @@ def _add_usage_metrics_to_assistant_message(assistant_message: Message, response
9292
if hasattr(response_usage, "cache_write_tokens") and response_usage.cache_write_tokens is not None:
9393
assistant_message.metrics.cache_write_tokens = response_usage.cache_write_tokens
9494

95+
# If you didn't capture any total tokens
96+
if not assistant_message.metrics.total_tokens:
97+
if assistant_message.metrics.input_tokens is None:
98+
assistant_message.metrics.input_tokens = 0
99+
if assistant_message.metrics.output_tokens is None:
100+
assistant_message.metrics.output_tokens = 0
101+
102+
assistant_message.metrics.total_tokens = (
103+
assistant_message.metrics.input_tokens + assistant_message.metrics.output_tokens
104+
)
105+
95106
# Additional metrics (e.g., from Groq, Ollama)
96107
if isinstance(response_usage, dict) and "additional_metrics" in response_usage:
97108
assistant_message.metrics.additional_metrics = response_usage["additional_metrics"]

libs/agno/agno/models/openai/like.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class OpenAILike(OpenAIChat):
1919
name: str = "OpenAILike"
2020
api_key: Optional[str] = "not-provided"
2121

22-
role_map = {
22+
default_role_map = {
2323
"system": "system",
2424
"user": "user",
2525
"assistant": "assistant",

libs/agno/agno/tools/knowledge.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def __init__(
3434
self.instructions += "\n" + few_shot_examples
3535
else:
3636
self.instructions += "\n" + self.FEW_SHOT_EXAMPLES
37+
else:
38+
self.instructions = instructions
3739

3840
# The knowledge to search
3941
self.knowledge: AgentKnowledge = knowledge
@@ -48,7 +50,7 @@ def __init__(
4850

4951
super().__init__(
5052
name="knowledge_tools",
51-
instructions=instructions,
53+
instructions=self.instructions,
5254
add_instructions=add_instructions,
5355
tools=tools,
5456
**kwargs,

0 commit comments

Comments
 (0)