Skip to content

Commit aefde3a

Browse files
committed
Mcerge brant libs/agno/tests/integration/agent/test_agent_with_storage_and_memory.py::test_multi_user_multi_session_chaterge branch 'main' of https://github.com/agno-agi/agno into release-1.4.5
2 parents 44c0ef8 + 300bb0d commit aefde3a

File tree

9 files changed

+218
-45
lines changed

9 files changed

+218
-45
lines changed

cookbook/storage/mongodb_storage/mongodb_storage_for_team.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class Article(BaseModel):
5757
markdown=True,
5858
debug_mode=True,
5959
show_members_responses=True,
60-
add_member_tools_to_system_message=False
60+
add_member_tools_to_system_message=False,
6161
)
6262

6363
hn_team.print_response("Write an article about the top 2 stories on hackernews")

cookbook/tools/models/__init__.py

Whitespace-only changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""🔧 Example: Using the GeminiTools Toolkit for Video Generation
2+
3+
An Agent using the Gemini video generation tool.
4+
5+
Video generation only works with Vertex AI.
6+
Make sure you have set the GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.
7+
8+
Example prompts to try:
9+
- "Generate a 5-second video of a kitten playing a piano"
10+
- "Create a short looping animation of a neon city skyline at dusk"
11+
12+
Run `pip install google-genai agno` to install the necessary dependencies.
13+
"""
14+
15+
from agno.agent import Agent
16+
from agno.models.openai import OpenAIChat
17+
from agno.tools.models.gemini import GeminiTools
18+
from agno.utils.media import save_base64_data
19+
20+
agent = Agent(
21+
model=OpenAIChat(id="gpt-4o"),
22+
tools=[GeminiTools(vertexai=True)], # Video Generation only works on VertexAI mode
23+
show_tool_calls=True,
24+
debug_mode=True,
25+
)
26+
27+
agent.print_response(
28+
"create a video of a cat driving at top speed",
29+
)
30+
response = agent.run_response
31+
if response.videos:
32+
for video in response.videos:
33+
save_base64_data(video.content, f"tmp/cat_driving_{video.id}.mp4")

libs/agno/agno/document/reader/firecrawl_reader.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,26 @@
1111
except ImportError:
1212
raise ImportError("The `firecrawl` package is not installed. Please install it via `pip install firecrawl-py`.")
1313

14+
1415
@dataclass
1516
class FirecrawlReader(Reader):
1617
api_key: Optional[str] = None
1718
params: Optional[Dict] = None
1819
mode: Literal["scrape", "crawl"] = "scrape"
19-
20-
def __init__(self, api_key: Optional[str] = None, params: Optional[Dict] = None, mode: Literal["scrape", "crawl"] = "scrape", *args, **kwargs) -> None:
20+
21+
def __init__(
22+
self,
23+
api_key: Optional[str] = None,
24+
params: Optional[Dict] = None,
25+
mode: Literal["scrape", "crawl"] = "scrape",
26+
*args,
27+
**kwargs,
28+
) -> None:
2129
super().__init__(*args, **kwargs)
2230
self.api_key = api_key
2331
self.params = params
2432
self.mode = mode
2533

26-
2734
def scrape(self, url: str) -> List[Document]:
2835
"""
2936
Scrapes a website and returns a list of documents.

libs/agno/agno/media.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ class Media(BaseModel):
1111

1212

1313
class VideoArtifact(Media):
14-
url: str # Remote location for file
14+
url: Optional[str] = None # Remote location for file (if no inline content)
15+
content: Optional[Union[str, bytes]] = None # type: ignore
16+
mime_type: Optional[str] = None # MIME type of the video content
1517
eta: Optional[str] = None
1618
length: Optional[str] = None
1719

libs/agno/agno/models/meta/llama.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def request_kwargs(self) -> Dict[str, Any]:
152152
# Add tools
153153
if self._tools is not None and len(self._tools) > 0:
154154
request_params["tools"] = self._tools
155-
155+
156156
# Fix optional parameters where the "type" is [<type>, null]
157157
for tool in request_params["tools"]: # type: ignore
158158
if "parameters" in tool["function"] and "properties" in tool["function"]["parameters"]: # type: ignore

libs/agno/agno/models/meta/llama_openai.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class LlamaOpenAI(OpenAILike):
3333

3434
api_key: Optional[str] = getenv("LLAMA_API_KEY")
3535
base_url: Optional[str] = "https://api.llama.com/compat/v1/"
36-
36+
3737
# Request parameters
3838
max_completion_tokens: Optional[int] = None
3939
repetition_penalty: Optional[float] = None
@@ -47,8 +47,7 @@ class LlamaOpenAI(OpenAILike):
4747

4848
supports_native_structured_outputs: bool = False
4949
supports_json_schema_outputs: bool = True
50-
51-
50+
5251
@property
5352
def request_kwargs(self) -> Dict[str, Any]:
5453
"""
@@ -76,7 +75,7 @@ def request_kwargs(self) -> Dict[str, Any]:
7675
# Add tools
7776
if self._tools is not None and len(self._tools) > 0:
7877
request_params["tools"] = self._tools
79-
78+
8079
# Fix optional parameters where the "type" is [<type>, null]
8180
for tool in request_params["tools"]: # type: ignore
8281
if "parameters" in tool["function"] and "properties" in tool["function"]["parameters"]: # type: ignore
Lines changed: 106 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,67 @@
11
import base64
2+
import time
23
from os import getenv
3-
from typing import Optional
4+
from typing import Any, Optional
45
from uuid import uuid4
56

67
from agno.agent import Agent
7-
from agno.media import ImageArtifact
8+
from agno.media import ImageArtifact, VideoArtifact
89
from agno.tools import Toolkit
9-
from agno.utils.log import log_debug, log_error
10+
from agno.utils.log import log_debug, log_error, log_info
1011

1112
try:
1213
from google.genai import Client
14+
from google.genai.types import GenerateImagesResponse, GenerateVideosOperation
1315
except (ModuleNotFoundError, ImportError):
1416
raise ImportError("`google-genai` not installed. Please install using `pip install google-genai`")
1517

1618

1719
class GeminiTools(Toolkit):
18-
"""Tools for interacting with Google Gemini API (including Imagen for images)"""
20+
"""Tools for interacting with Google Gemini API"""
1921

2022
def __init__(
2123
self,
2224
api_key: Optional[str] = None,
25+
vertexai: bool = False,
26+
project_id: Optional[str] = None,
27+
location: Optional[str] = None,
2328
image_generation_model: str = "imagen-3.0-generate-002",
29+
video_generation_model: str = "veo-2.0-generate-001",
2430
**kwargs,
2531
):
26-
super().__init__(name="gemini_tools", tools=[self.generate_image], **kwargs)
32+
super().__init__(name="gemini_tools", tools=[self.generate_image, self.generate_video], **kwargs)
2733

34+
# Set mode and credentials: use only provided vertexai parameter
35+
self.vertexai = vertexai
36+
self.project_id = project_id
37+
self.location = location
38+
39+
# Load API key from argument or environment
2840
self.api_key = api_key or getenv("GOOGLE_API_KEY")
29-
if not self.api_key:
30-
raise ValueError(
31-
"GOOGLE_API_KEY not set. Please provide api_key or set the GOOGLE_API_KEY environment variable."
32-
)
41+
if not self.vertexai and not self.api_key:
42+
log_error("GOOGLE_API_KEY not set. Please set the GOOGLE_API_KEY environment variable.")
43+
raise ValueError("GOOGLE_API_KEY not set. Please provide api_key or set the environment variable.")
44+
45+
# Prepare client parameters
46+
client_params: dict[str, Any] = {}
47+
if self.vertexai:
48+
log_info("Using Vertex AI API")
49+
client_params["vertexai"] = True
50+
client_params["project"] = self.project_id or getenv("GOOGLE_CLOUD_PROJECT")
51+
client_params["location"] = self.location or getenv("GOOGLE_CLOUD_LOCATION")
52+
else:
53+
log_info("Using Gemini API")
54+
client_params["api_key"] = self.api_key
3355

3456
try:
35-
self.client = Client(api_key=self.api_key)
57+
self.client = Client(**client_params)
3658
log_debug("Google GenAI Client created successfully.")
3759
except Exception as e:
3860
log_error(f"Failed to create Google GenAI Client: {e}", exc_info=True)
3961
raise ValueError(f"Failed to create Google GenAI Client. Error: {e}")
4062

4163
self.image_model = image_generation_model
64+
self.video_model = video_generation_model
4265

4366
def generate_image(
4467
self,
@@ -54,40 +77,89 @@ def generate_image(
5477
"""
5578

5679
try:
57-
response = self.client.models.generate_images(
80+
response: GenerateImagesResponse = self.client.models.generate_images(
5881
model=self.image_model,
5982
prompt=prompt,
6083
)
6184

6285
log_debug("DEBUG: Raw Gemini API response")
6386

64-
image_bytes = None
65-
actual_mime_type = "image/png"
87+
# Extract image bytes
88+
image_bytes = response.generated_images[0].image.image_bytes
89+
for generated_image in response.generated_images:
90+
image_bytes = generated_image.image.image_bytes
91+
if not image_bytes:
92+
log_error("No valid image data extracted.")
93+
return "Failed to generate image: No valid image data extracted."
94+
base64_encoded_image_bytes = base64.b64encode(image_bytes)
95+
actual_mime_type = "image/png"
96+
97+
media_id = str(uuid4())
98+
agent.add_image(
99+
ImageArtifact(
100+
id=media_id,
101+
content=base64_encoded_image_bytes,
102+
original_prompt=prompt,
103+
mime_type=actual_mime_type,
104+
)
105+
)
106+
log_debug(f"Successfully generated image {media_id} with model {self.image_model}")
107+
return "Image generated successfully"
66108

67-
if response.generated_images and response.generated_images[0].image.image_bytes:
68-
image_bytes = response.generated_images[0].image.image_bytes
69-
else:
70-
log_error("No image data found in the response structure.")
71-
return "Failed to generate image: No valid image data extracted."
109+
except Exception as e:
110+
log_error(f"Failed to generate image: Client or method not available ({e})")
111+
return f"Failed to generate image: Client or method not available ({e})"
72112

73-
if image_bytes is None:
74-
log_error("image_bytes is None after extraction.")
75-
return "Failed to generate image: No valid image data extracted."
113+
def generate_video(
114+
self,
115+
agent: Agent,
116+
prompt: str,
117+
) -> str:
118+
"""Generate a video based on a text prompt.
119+
Args:
120+
prompt (str): The text prompt to generate the video from.
121+
Returns:
122+
str: A message indicating success or failure.
123+
"""
124+
# Video generation requires Vertex AI mode.
125+
if not self.vertexai:
126+
log_error("Video generation requires Vertex AI mode. Please enable Vertex AI mode.")
127+
return (
128+
"Video generation requires Vertex AI mode. "
129+
"Please set `vertexai=True` or environment variable `GOOGLE_GENAI_USE_VERTEXAI=true`."
130+
)
76131

77-
base64_encoded_image_bytes = base64.b64encode(image_bytes)
132+
from google.genai.types import GenerateVideosConfig
78133

79-
media_id = str(uuid4())
80-
agent.add_image(
81-
ImageArtifact(
82-
id=media_id,
83-
content=base64_encoded_image_bytes,
84-
original_prompt=prompt,
85-
mime_type=actual_mime_type,
86-
)
134+
try:
135+
operation: GenerateVideosOperation = self.client.models.generate_videos(
136+
model=self.video_model,
137+
prompt=prompt,
138+
config=GenerateVideosConfig(
139+
enhance_prompt=True,
140+
),
87141
)
88-
log_debug(f"Successfully generated image {media_id} with model {self.image_model}")
89-
return f"Image generated successfully with ID: {media_id}"
90142

143+
while not operation.done:
144+
time.sleep(5)
145+
operation = self.client.operations.get(operation=operation)
146+
147+
for video in operation.result.generated_videos:
148+
generated_video = video.video
149+
150+
media_id = str(uuid4())
151+
encoded_video = base64.b64encode(generated_video.video_bytes).decode("utf-8")
152+
153+
agent.add_video(
154+
VideoArtifact(
155+
id=media_id,
156+
content=encoded_video,
157+
original_prompt=prompt,
158+
mime_type=generated_video.mime_type,
159+
)
160+
)
161+
log_debug(f"Successfully generated video {media_id} with model {self.video_model}")
162+
return "Video generated successfully"
91163
except Exception as e:
92-
log_error(f"Failed to generate image: Client or method not available ({e})")
93-
return f"Failed to generate image: Client or method not available ({e})"
164+
log_error(f"Failed to generate video: {e}")
165+
return f"Failed to generate video: {e}"

libs/agno/tests/unit/tools/models/test_gemini.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77

88
from agno.agent import Agent
9-
from agno.media import ImageArtifact
9+
from agno.media import ImageArtifact, VideoArtifact
1010
from agno.tools.models.gemini import GeminiTools
1111

1212

@@ -173,3 +173,63 @@ def test_generate_image_no_image_bytes(mock_gemini_tools, mock_agent, mock_faile
173173
prompt=prompt,
174174
)
175175
mock_agent.add_image.assert_not_called()
176+
177+
178+
# Tests for generate_video method
179+
def test_generate_video_requires_vertexai(mock_gemini_tools, mock_agent):
180+
"""Test video generation when vertexai mode is disabled."""
181+
prompt = "A sample video prompt"
182+
result = mock_gemini_tools.generate_video(mock_agent, prompt)
183+
expected = (
184+
"Video generation requires Vertex AI mode. "
185+
"Please set `vertexai=True` or environment variable `GOOGLE_GENAI_USE_VERTEXAI=true`."
186+
)
187+
assert result == expected
188+
mock_agent.add_video.assert_not_called()
189+
190+
191+
@pytest.fixture
192+
def mock_video_operation():
193+
"""Fixture for a completed video generation operation."""
194+
op = MagicMock()
195+
op.done = True
196+
video = MagicMock()
197+
video.video_bytes = b"fake_video_bytes"
198+
video.mime_type = "video/mp4"
199+
op.result = MagicMock(generated_videos=[MagicMock(video=video)])
200+
return op
201+
202+
203+
def test_generate_video_success(mock_gemini_tools, mock_agent, mock_video_operation):
204+
"""Test successful video generation."""
205+
mock_gemini_tools.vertexai = True
206+
mock_gemini_tools.client.models.generate_videos.return_value = mock_video_operation
207+
prompt = "A sample video prompt"
208+
with patch("agno.tools.models.gemini.uuid4", return_value=UUID("87654321-4321-8765-4321-876543214321")):
209+
result = mock_gemini_tools.generate_video(mock_agent, prompt)
210+
expected_id = "87654321-4321-8765-4321-876543214321"
211+
assert result == f"Video generated successfully with ID: {expected_id}"
212+
assert mock_gemini_tools.client.models.generate_videos.called
213+
call_args = mock_gemini_tools.client.models.generate_videos.call_args
214+
assert call_args.kwargs["model"] == mock_gemini_tools.video_model
215+
assert call_args.kwargs["prompt"] == prompt
216+
mock_agent.add_video.assert_called_once()
217+
added = mock_agent.add_video.call_args[0][0]
218+
assert isinstance(added, VideoArtifact)
219+
assert added.id == expected_id
220+
assert added.original_prompt == prompt
221+
assert added.mime_type == "video/mp4"
222+
import base64
223+
224+
expected_content = base64.b64encode(b"fake_video_bytes").decode("utf-8")
225+
assert added.content == expected_content
226+
227+
228+
def test_generate_video_exception(mock_gemini_tools, mock_agent):
229+
"""Test video generation when API raises exception."""
230+
mock_gemini_tools.vertexai = True
231+
mock_gemini_tools.client.models.generate_videos.side_effect = Exception("API error")
232+
prompt = "A sample video prompt"
233+
result = mock_gemini_tools.generate_video(mock_agent, prompt)
234+
assert result == "Failed to generate video: API error"
235+
mock_agent.add_video.assert_not_called()

0 commit comments

Comments
 (0)