Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
dirkbrnd committed Feb 7, 2025
2 parents d6b58e6 + 77f4e11 commit 3184ef4
Show file tree
Hide file tree
Showing 19 changed files with 99 additions and 88 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Agno is designed with three core principles:

Here's why you should build Agents with Agno:

- **Lightning Fast**: Agent creation is 6000x faster than LangGraph (see [performance](#performance)).
- **Lightning Fast**: Agent creation is ~10,000x faster than LangGraph (see [performance](#performance)).
- **Model Agnostic**: Use any model, any provider, no lock-in.
- **Multi Modal**: Native support for text, image, audio and video.
- **Multi Agent**: Delegate tasks across a team of specialized agents.
Expand Down Expand Up @@ -222,12 +222,12 @@ python agent_team.py

Agno is designed for high performance agentic systems:

- Agent instantiation: <5μs on average (5000x faster than LangGraph).
- Memory footprint: <0.01Mib on average (50x less memory than LangGraph).
- Agent instantiation: <5μs on average (~10,000x faster than LangGraph).
- Memory footprint: <0.01Mib on average (~50x less memory than LangGraph).

> Tested on an Apple M4 Mackbook Pro.
While an Agent's performance is bottlenecked by inference, we must do everything possible to minimize execution time, reduce memory usage, and parallelize tool calls. These numbers are may seem minimal, but they add up even at medium scale.
While an Agent's performance is bottlenecked by inference, we must do everything possible to minimize execution time, reduce memory usage, and parallelize tool calls. These numbers are may seem trivial, but they add up even at medium scale.

### Instantiation time

Expand Down
5 changes: 3 additions & 2 deletions cookbook/models/openai/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
tools=[DuckDuckGoTools()],
add_history_to_messages=True,
)
agent.print_response("How many people live in Canada?")
agent.print_response("What is their national anthem called?")
agent.cli_app()
# agent.print_response("How many people live in Canada?")
# agent.print_response("What is their national anthem called?")
25 changes: 18 additions & 7 deletions libs/agno/agno/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2170,19 +2170,29 @@ def get_run_messages(

# 3. Add history to run_messages
if self.add_history_to_messages:
from copy import deepcopy

history: List[Message] = self.memory.get_messages_from_last_n_runs(
last_n=self.num_history_responses, skip_role=self.get_system_message_role()
)
if len(history) > 0:
logger.debug(f"Adding {len(history)} messages from history")
# Create a deep copy of the history messages to avoid modifying the original messages
history_copy = [deepcopy(msg) for msg in history]

# Tag each message as coming from history
for _msg in history_copy:
_msg.from_history = True

logger.debug(f"Adding {len(history_copy)} messages from history")

if self.run_response.extra_data is None:
self.run_response.extra_data = RunResponseExtraData(history=history)
self.run_response.extra_data = RunResponseExtraData(history=history_copy)
else:
if self.run_response.extra_data.history is None:
self.run_response.extra_data.history = history
self.run_response.extra_data.history = history_copy
else:
self.run_response.extra_data.history.extend(history)
run_messages.messages += history
self.run_response.extra_data.history.extend(history_copy)
run_messages.messages += history_copy

# 4.Add user message to run_messages
user_message: Optional[Message] = None
Expand Down Expand Up @@ -2547,14 +2557,15 @@ def update_run_response_with_reasoning(
def aggregate_metrics_from_messages(self, messages: List[Message]) -> Dict[str, Any]:
aggregated_metrics: Dict[str, Any] = defaultdict(list)
assistant_message_role = self.model.assistant_message_role if self.model is not None else "assistant"
# Use a defaultdict(list) to collect all values for each assistant message
for m in messages:
if m.role == assistant_message_role and m.metrics is not None:
for k, v in asdict(m.metrics).items():
if k in ("timer"):
if k == "timer":
continue
if v is not None:
aggregated_metrics[k].append(v)
if aggregated_metrics is not None:
aggregated_metrics = dict(aggregated_metrics)
return aggregated_metrics

def calculate_session_metrics(self, messages: List[Message]) -> SessionMetrics:
Expand Down
50 changes: 24 additions & 26 deletions libs/agno/agno/memory/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,39 +134,37 @@ def get_messages(self) -> List[Dict[str, Any]]:
def get_messages_from_last_n_runs(
self, last_n: Optional[int] = None, skip_role: Optional[str] = None
) -> List[Message]:
"""Returns the messages from the last_n runs
"""Returns the messages from the last_n runs, excluding previously tagged history messages.
Args:
last_n: The number of runs to return from the end of the conversation.
skip_role: Skip messages with this role.
Returns:
A list of Messages in the last_n runs.
A list of Messages from the specified runs, excluding history messages.
"""
if last_n is None:
logger.debug("Getting messages from all previous runs")
messages_from_all_history = []
for prev_run in self.runs:
if prev_run.response and prev_run.response.messages:
if skip_role:
prev_run_messages = [m for m in prev_run.response.messages if m.role != skip_role]
else:
prev_run_messages = prev_run.response.messages
messages_from_all_history.extend(prev_run_messages)
logger.debug(f"Messages from previous runs: {len(messages_from_all_history)}")
return messages_from_all_history

logger.debug(f"Getting messages from last {last_n} runs")
messages_from_last_n_history = []
for prev_run in self.runs[-last_n:]:
if prev_run.response and prev_run.response.messages:
if skip_role:
prev_run_messages = [m for m in prev_run.response.messages if m.role != skip_role]
else:
prev_run_messages = prev_run.response.messages
messages_from_last_n_history.extend(prev_run_messages)
logger.debug(f"Messages from last {last_n} runs: {len(messages_from_last_n_history)}")
return messages_from_last_n_history
if not self.runs:
return []

runs_to_process = self.runs if last_n is None else self.runs[-last_n:]
messages_from_history = []

for run in runs_to_process:
if not (run.response and run.response.messages):
continue

for message in run.response.messages:
# Skip messages with specified role
if skip_role and message.role == skip_role:
continue
# Skip messages that were tagged as history in previous runs
if hasattr(message, "from_history") and message.from_history:
continue

messages_from_history.append(message)

logger.debug(f"Getting messages from previous runs: {len(messages_from_history)}")
return messages_from_history

def get_message_pairs(
self, user_role: str = "user", assistant_role: Optional[List[str]] = None
Expand Down
2 changes: 2 additions & 0 deletions libs/agno/agno/models/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ class Message(BaseModel):
stop_after_tool_call: bool = False
# When True, the message will be added to the agent's memory.
add_to_agent_memory: bool = True
# This flag is enabled when a message is fetched from the agent's memory.
from_history: bool = False
# Metrics for the message.
metrics: MessageMetrics = Field(default_factory=MessageMetrics)
# The references added to the message for RAG
Expand Down
50 changes: 1 addition & 49 deletions libs/agno/agno/models/together/together.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,9 @@
import json
from dataclasses import dataclass
from os import getenv
from typing import Any, Dict, Iterator, List, Optional
from typing import Optional

from agno.models.message import Message
from agno.models.openai.like import OpenAILike
from agno.models.response import ModelResponse
from agno.tools.function import FunctionCall
from agno.utils.log import logger
from agno.utils.tools import get_function_call_for_tool_call

try:
from openai.types.chat.chat_completion_chunk import (
ChoiceDelta,
ChoiceDeltaToolCall,
)
from openai.types.completion_usage import CompletionUsage
except ImportError:
logger.error("`openai` not installed")
raise


@dataclass
Expand All @@ -39,36 +24,3 @@ class Together(OpenAILike):
provider: str = "Together " + id
api_key: Optional[str] = getenv("TOGETHER_API_KEY")
base_url: str = "https://api.together.xyz/v1"


def parse_provider_response_delta(self, response_delta: Any) -> ModelResponse:
"""
Parse the streaming response from Together into a ModelResponse.
Args:
response: Raw response chunk from Together API
Returns:
ModelResponse: Parsed response data
"""
model_response = ModelResponse()

if response_delta.choices and len(response_delta.choices) > 0:
delta = response_delta.choices[0].delta
# Add content if present
if delta.content is not None:
model_response.content = delta.content

# Add tool calls if present
if delta.tool_calls is not None:
model_response.tool_calls = delta.tool_calls

# Add role if present
if delta.role is not None:
model_response.role = delta.role

# Add usage metrics if present
if response_delta.usage is not None:
model_response.response_usage = response_delta.usage

return model_response
2 changes: 2 additions & 0 deletions libs/agno/agno/tools/local_file_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def write_file(
filename, file_ext = os.path.splitext(filename)
extension = extension or file_ext.lstrip(".")

logger.debug(f"Writing file to local system: {filename}")

extension = (extension or self.default_extension).lstrip(".")

# Create directory if it doesn't exist
Expand Down
2 changes: 2 additions & 0 deletions libs/agno/agno/tools/moviepy_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ def extract_audio(self, video_path: str, output_path: str) -> str:
str: Path to the extracted audio file
"""
try:
logger.debug(f"Extracting audio from {video_path}")
video = VideoFileClip(video_path)
video.audio.write_audiofile(output_path)
logger.info(f"Audio extracted to {output_path}")
Expand All @@ -244,6 +245,7 @@ def create_srt(self, transcription: str, output_path: str) -> str:
str: Path to the created SRT file, or error message if failed
"""
try:
logger.debug(f"Creating SRT file at {output_path}")
# Since we're getting SRT format from Whisper API now,
# we can just write it directly to file
with open(output_path, "w", encoding="utf-8") as f:
Expand Down
2 changes: 2 additions & 0 deletions libs/agno/agno/tools/newspaper.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from agno.tools import Toolkit
from agno.utils.log import logger

try:
from newspaper import Article
Expand Down Expand Up @@ -27,6 +28,7 @@ def get_article_text(self, url: str) -> str:
"""

try:
logger.debug(f"Reading news: {url}")
article = Article(url)
article.download()
article.parse()
Expand Down
1 change: 1 addition & 0 deletions libs/agno/agno/tools/newspaper4k.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def read_article(self, url: str) -> str:
"""

try:
logger.debug(f"Reading news: {url}")
article_data = self.get_article_data(url)
if not article_data:
return f"Error reading article from {url}: No data found."
Expand Down
4 changes: 4 additions & 0 deletions libs/agno/agno/tools/openbb.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def get_stock_price(self, symbol: str) -> str:
str: The current stock prices or error message.
"""
try:
logger.debug(f"Fetching current price for {symbol}")
result = self.obb.equity.price.quote(symbol=symbol, provider=self.provider).to_polars() # type: ignore
clean_results = []
for row in result.to_dicts():
Expand Down Expand Up @@ -109,6 +110,7 @@ def get_price_targets(self, symbol: str) -> str:
str: JSON containing consensus price target and recommendations.
"""
try:
logger.debug(f"Fetching price targets for {symbol}")
result = self.obb.equity.estimates.consensus(symbol=symbol, provider=self.provider).to_polars() # type: ignore
return json.dumps(result.to_dicts(), indent=2, default=str)
except Exception as e:
Expand All @@ -126,6 +128,7 @@ def get_company_news(self, symbol: str, num_stories: int = 10) -> str:
str: JSON containing company news and press releases.
"""
try:
logger.debug(f"Fetching news for {symbol}")
result = self.obb.news.company(symbol=symbol, provider=self.provider, limit=num_stories).to_polars() # type: ignore
clean_results = []
if len(result) > 0:
Expand All @@ -147,6 +150,7 @@ def get_company_profile(self, symbol: str) -> str:
str: JSON containing company profile and overview.
"""
try:
logger.debug(f"Fetching company profile for {symbol}")
result = self.obb.equity.profile(symbol=symbol, provider=self.provider).to_polars() # type: ignore
return json.dumps(result.to_dicts(), indent=2, default=str)
except Exception as e:
Expand Down
2 changes: 2 additions & 0 deletions libs/agno/agno/tools/pubmed.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import httpx

from agno.tools import Toolkit
from agno.utils.log import logger


class PubmedTools(Toolkit):
Expand Down Expand Up @@ -64,6 +65,7 @@ def search_pubmed(self, query: str, max_results: int = 10) -> str:
str: A JSON string containing the search results.
"""
try:
logger.debug(f"Searching PubMed for: {query}")
ids = self.fetch_pubmed_ids(query, self.max_results or max_results, self.email)
details_root = self.fetch_details(ids)
articles = self.parse_details(details_root)
Expand Down
3 changes: 3 additions & 0 deletions libs/agno/agno/tools/reddit.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def get_top_posts(self, subreddit: str, time_filter: str = "week", limit: int =
return "Please provide Reddit API credentials"

try:
logger.debug(f"Getting top posts from r/{subreddit}")
posts = self.reddit.subreddit(subreddit).top(time_filter=time_filter, limit=limit)
top_posts: List[Dict[str, Union[str, int, float]]] = [
{
Expand Down Expand Up @@ -191,6 +192,7 @@ def get_trending_subreddits(self) -> str:
return "Please provide Reddit API credentials"

try:
logger.debug("Getting trending subreddits")
popular_subreddits = self.reddit.subreddits.popular(limit=5)
trending: List[str] = [subreddit.display_name for subreddit in popular_subreddits]
return json.dumps({"trending_subreddits": trending})
Expand All @@ -209,6 +211,7 @@ def get_subreddit_stats(self, subreddit: str) -> str:
return "Please provide Reddit API credentials"

try:
logger.debug(f"Getting stats for r/{subreddit}")
sub = self.reddit.subreddit(subreddit)
stats: Dict[str, Union[str, int, bool, float]] = {
"display_name": sub.display_name,
Expand Down
2 changes: 2 additions & 0 deletions libs/agno/agno/tools/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def list_tables(self) -> str:
return json.dumps(self.tables)

try:
logger.debug("listing tables in the database")
table_names = inspect(self.db_engine).get_table_names()
logger.debug(f"table_names: {table_names}")
return json.dumps(table_names)
Expand All @@ -87,6 +88,7 @@ def describe_table(self, table_name: str) -> str:
"""

try:
logger.debug(f"Describing table: {table_name}")
table_names = inspect(self.db_engine)
table_schema = table_names.get_columns(table_name)
return json.dumps([str(column) for column in table_schema])
Expand Down
1 change: 1 addition & 0 deletions libs/agno/agno/tools/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def send_message(self, message: str) -> str:
:param message: The message to send.
:return: The response from the API.
"""
logger.debug(f"Sending telegram message: {message}")
response = self._call_post_method("sendMessage", json={"chat_id": self.chat_id, "text": message})
try:
response.raise_for_status()
Expand Down
Loading

0 comments on commit 3184ef4

Please sign in to comment.