agno 2.0.0rc1__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. agno/agent/agent.py +101 -140
  2. agno/db/mongo/mongo.py +8 -3
  3. agno/eval/accuracy.py +12 -5
  4. agno/knowledge/chunking/strategy.py +14 -14
  5. agno/knowledge/knowledge.py +156 -120
  6. agno/knowledge/reader/arxiv_reader.py +5 -5
  7. agno/knowledge/reader/csv_reader.py +6 -77
  8. agno/knowledge/reader/docx_reader.py +5 -5
  9. agno/knowledge/reader/firecrawl_reader.py +5 -5
  10. agno/knowledge/reader/json_reader.py +5 -5
  11. agno/knowledge/reader/markdown_reader.py +31 -9
  12. agno/knowledge/reader/pdf_reader.py +10 -123
  13. agno/knowledge/reader/reader_factory.py +65 -72
  14. agno/knowledge/reader/s3_reader.py +44 -114
  15. agno/knowledge/reader/text_reader.py +5 -5
  16. agno/knowledge/reader/url_reader.py +75 -31
  17. agno/knowledge/reader/web_search_reader.py +6 -29
  18. agno/knowledge/reader/website_reader.py +5 -5
  19. agno/knowledge/reader/wikipedia_reader.py +5 -5
  20. agno/knowledge/reader/youtube_reader.py +6 -6
  21. agno/knowledge/reranker/__init__.py +9 -0
  22. agno/knowledge/utils.py +10 -10
  23. agno/media.py +269 -268
  24. agno/models/aws/bedrock.py +3 -7
  25. agno/models/base.py +50 -54
  26. agno/models/google/gemini.py +11 -10
  27. agno/models/message.py +4 -4
  28. agno/models/ollama/chat.py +1 -1
  29. agno/models/openai/chat.py +33 -14
  30. agno/models/response.py +5 -5
  31. agno/os/app.py +40 -29
  32. agno/os/mcp.py +39 -59
  33. agno/os/router.py +547 -16
  34. agno/os/routers/evals/evals.py +197 -12
  35. agno/os/routers/knowledge/knowledge.py +428 -14
  36. agno/os/routers/memory/memory.py +250 -28
  37. agno/os/routers/metrics/metrics.py +125 -7
  38. agno/os/routers/session/session.py +393 -25
  39. agno/os/schema.py +55 -2
  40. agno/run/agent.py +37 -28
  41. agno/run/base.py +9 -19
  42. agno/run/team.py +110 -19
  43. agno/run/workflow.py +41 -28
  44. agno/team/team.py +808 -1080
  45. agno/tools/brightdata.py +3 -3
  46. agno/tools/cartesia.py +3 -5
  47. agno/tools/dalle.py +7 -4
  48. agno/tools/desi_vocal.py +2 -2
  49. agno/tools/e2b.py +6 -6
  50. agno/tools/eleven_labs.py +3 -3
  51. agno/tools/fal.py +4 -4
  52. agno/tools/function.py +7 -7
  53. agno/tools/giphy.py +2 -2
  54. agno/tools/lumalab.py +3 -3
  55. agno/tools/mcp.py +1 -2
  56. agno/tools/models/azure_openai.py +2 -2
  57. agno/tools/models/gemini.py +3 -3
  58. agno/tools/models/groq.py +3 -5
  59. agno/tools/models/nebius.py +2 -2
  60. agno/tools/models_labs.py +5 -5
  61. agno/tools/openai.py +4 -9
  62. agno/tools/opencv.py +3 -3
  63. agno/tools/replicate.py +7 -7
  64. agno/utils/events.py +5 -5
  65. agno/utils/gemini.py +1 -1
  66. agno/utils/log.py +52 -2
  67. agno/utils/mcp.py +57 -5
  68. agno/utils/models/aws_claude.py +1 -1
  69. agno/utils/models/claude.py +0 -8
  70. agno/utils/models/cohere.py +1 -1
  71. agno/utils/models/watsonx.py +1 -1
  72. agno/utils/openai.py +1 -1
  73. agno/utils/print_response/team.py +177 -73
  74. agno/utils/streamlit.py +27 -0
  75. agno/vectordb/lancedb/lance_db.py +82 -25
  76. agno/workflow/step.py +7 -7
  77. agno/workflow/types.py +13 -13
  78. agno/workflow/workflow.py +37 -28
  79. {agno-2.0.0rc1.dist-info → agno-2.0.1.dist-info}/METADATA +140 -1
  80. {agno-2.0.0rc1.dist-info → agno-2.0.1.dist-info}/RECORD +83 -84
  81. agno-2.0.1.dist-info/licenses/LICENSE +201 -0
  82. agno/knowledge/reader/gcs_reader.py +0 -67
  83. agno-2.0.0rc1.dist-info/licenses/LICENSE +0 -375
  84. {agno-2.0.0rc1.dist-info → agno-2.0.1.dist-info}/WHEEL +0 -0
  85. {agno-2.0.0rc1.dist-info → agno-2.0.1.dist-info}/top_level.txt +0 -0
agno/media.py CHANGED
@@ -1,335 +1,336 @@
1
1
  from pathlib import Path
2
2
  from typing import Any, Dict, List, Optional, Tuple, Union
3
+ from uuid import uuid4
3
4
 
4
5
  from pydantic import BaseModel, field_validator, model_validator
5
6
 
6
7
 
7
- class Media(BaseModel):
8
- id: str
9
- original_prompt: Optional[str] = None
10
- revised_prompt: Optional[str] = None
11
-
12
-
13
- class VideoArtifact(Media):
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
17
- eta: Optional[str] = None
18
- length: Optional[str] = None
19
-
20
- def to_dict(self) -> Dict[str, Any]:
21
- response_dict = {
22
- "id": self.id,
23
- "url": self.url,
24
- "content": self.content
25
- if isinstance(self.content, str)
26
- else self.content.decode("utf-8")
27
- if self.content
28
- else None,
29
- "mime_type": self.mime_type,
30
- "eta": self.eta,
31
- }
32
- return {k: v for k, v in response_dict.items() if v is not None}
8
+ class Image(BaseModel):
9
+ """Unified Image class for all use cases (input, output, artifacts)"""
33
10
 
11
+ # Core content fields (exactly one required)
12
+ url: Optional[str] = None # Remote location
13
+ filepath: Optional[Union[Path, str]] = None # Local file path
14
+ content: Optional[bytes] = None # Raw image bytes (standardized to bytes)
34
15
 
35
- class ImageArtifact(Media):
36
- url: Optional[str] = None # Remote location for file
37
- content: Optional[bytes] = None # Actual image bytes content
38
- mime_type: Optional[str] = None
39
- alt_text: Optional[str] = None
40
-
41
- def _normalise_content(self) -> Optional[Union[str, bytes]]:
42
- if self.content is None:
43
- return None
44
- content_normalised: Union[str, bytes] = self.content
45
- if content_normalised and isinstance(content_normalised, bytes):
46
- from base64 import b64encode
47
-
48
- try:
49
- # First try to decode as UTF-8
50
- content_normalised = content_normalised.decode("utf-8") # type: ignore
51
- except UnicodeDecodeError:
52
- # Fallback to base64 encoding for binary content
53
- content_normalised = b64encode(bytes(content_normalised)).decode("utf-8") # type: ignore
54
- except Exception:
55
- # Last resort: try to convert to base64
56
- try:
57
- content_normalised = b64encode(bytes(content_normalised)).decode("utf-8") # type: ignore
58
- except Exception:
59
- pass
60
- return content_normalised
61
-
62
- def to_dict(self) -> Dict[str, Any]:
63
- content_normalised = self._normalise_content()
64
-
65
- response_dict = {
66
- "id": self.id,
67
- "url": self.url,
68
- "content": content_normalised,
69
- "mime_type": self.mime_type,
70
- "alt_text": self.alt_text,
71
- }
72
- return {k: v for k, v in response_dict.items() if v is not None}
16
+ # Metadata fields
17
+ id: Optional[str] = None # For tracking/referencing
18
+ format: Optional[str] = None # E.g. 'png', 'jpeg', 'webp', 'gif'
19
+ mime_type: Optional[str] = None # E.g. 'image/png', 'image/jpeg'
73
20
 
21
+ # Input-specific fields
22
+ detail: Optional[str] = (
23
+ None # low, medium, high or auto (per OpenAI spec https://platform.openai.com/docs/guides/vision?lang=node#low-or-high-fidelity-image-understanding)
24
+ )
74
25
 
75
- class AudioArtifact(Media):
76
- url: Optional[str] = None # Remote location for file
77
- base64_audio: Optional[str] = None # Base64-encoded audio data
78
- length: Optional[str] = None
79
- mime_type: Optional[str] = None
26
+ # Output-specific fields (from tools/LLMs)
27
+ original_prompt: Optional[str] = None # Original generation prompt
28
+ revised_prompt: Optional[str] = None # Revised generation prompt
29
+ alt_text: Optional[str] = None # Alt text description
80
30
 
81
31
  @model_validator(mode="before")
82
- def validate_exclusive_audio(cls, data: Any):
83
- """
84
- Ensure that either `url` or `base64_audio` is provided, but not both.
85
- """
86
- if data.get("url") and data.get("base64_audio"):
87
- raise ValueError("Provide either `url` or `base64_audio`, not both.")
88
- if not data.get("url") and not data.get("base64_audio"):
89
- raise ValueError("Either `url` or `base64_audio` must be provided.")
90
- return data
91
-
92
- def to_dict(self) -> Dict[str, Any]:
93
- response_dict = {
94
- "id": self.id,
95
- "url": self.url,
96
- "content": self.base64_audio,
97
- "mime_type": self.mime_type,
98
- "length": self.length,
99
- }
100
- return {k: v for k, v in response_dict.items() if v is not None}
32
+ def validate_and_normalize_content(cls, data: Any):
33
+ """Ensure exactly one content source and normalize to bytes"""
34
+ if isinstance(data, dict):
35
+ url = data.get("url")
36
+ filepath = data.get("filepath")
37
+ content = data.get("content")
38
+
39
+ # Count non-None sources
40
+ sources = [x for x in [url, filepath, content] if x is not None]
41
+ if len(sources) == 0:
42
+ raise ValueError("One of 'url', 'filepath', or 'content' must be provided")
43
+ elif len(sources) > 1:
44
+ raise ValueError("Only one of 'url', 'filepath', or 'content' should be provided")
45
+
46
+ # Auto-generate ID if not provided
47
+ if data.get("id") is None:
48
+ data["id"] = str(uuid4())
101
49
 
50
+ return data
102
51
 
103
- class Video(BaseModel):
104
- filepath: Optional[Union[Path, str]] = None # Absolute local location for video
105
- content: Optional[Any] = None # Actual video bytes content
106
- url: Optional[str] = None # Remote location for video
107
- format: Optional[str] = None # E.g. `mp4`, `mov`, `avi`, `mkv`, `webm`, `flv`, `mpeg`, `mpg`, `wmv`, `three_gp`
52
+ def get_content_bytes(self) -> Optional[bytes]:
53
+ """Get image content as raw bytes, loading from URL/file if needed"""
54
+ if self.content:
55
+ return self.content
56
+ elif self.url:
57
+ import httpx
108
58
 
109
- @model_validator(mode="before")
110
- def validate_data(cls, data: Any):
111
- """
112
- Ensure that exactly one of `filepath`, or `content` or `url` is provided.
113
- Also converts content to bytes if it's a string.
114
- """
115
- # Extract the values from the input data
116
- filepath = data.get("filepath")
117
- content = data.get("content")
118
- url = data.get("url")
119
-
120
- # Convert and decompress content to bytes if it's a string
121
- if content and isinstance(content, str):
59
+ return httpx.get(self.url).content
60
+ elif self.filepath:
61
+ with open(self.filepath, "rb") as f:
62
+ return f.read()
63
+ return None
64
+
65
+ def to_base64(self) -> Optional[str]:
66
+ """Convert content to base64 string for transmission/storage"""
67
+ content_bytes = self.get_content_bytes()
68
+ if content_bytes:
122
69
  import base64
123
70
 
124
- try:
125
- import zlib
126
-
127
- decoded_content = base64.b64decode(content)
128
- content = zlib.decompress(decoded_content)
129
- except Exception:
130
- content = base64.b64decode(content).decode("utf-8")
131
- data["content"] = content
71
+ return base64.b64encode(content_bytes).decode("utf-8")
72
+ return None
132
73
 
133
- # Count how many fields are set (not None)
134
- count = len([field for field in [filepath, content, url] if field is not None])
74
+ @classmethod
75
+ def from_base64(
76
+ cls,
77
+ base64_content: str,
78
+ id: Optional[str] = None,
79
+ mime_type: Optional[str] = None,
80
+ format: Optional[str] = None,
81
+ **kwargs,
82
+ ) -> "Image":
83
+ """Create Image from base64 content"""
84
+ import base64
135
85
 
136
- if count == 0:
137
- raise ValueError("One of `filepath` or `content` or `url` must be provided.")
138
- elif count > 1:
139
- raise ValueError("Only one of `filepath` or `content` or `url` should be provided.")
86
+ try:
87
+ content_bytes = base64.b64decode(base64_content)
88
+ except Exception:
89
+ content_bytes = base64_content.encode("utf-8")
140
90
 
141
- return data
91
+ return cls(content=content_bytes, id=id or str(uuid4()), mime_type=mime_type, format=format, **kwargs)
142
92
 
143
- def to_dict(self) -> Dict[str, Any]:
144
- import base64
145
- import zlib
146
-
147
- response_dict = {
148
- "content": base64.b64encode(
149
- zlib.compress(self.content) if isinstance(self.content, bytes) else self.content.encode("utf-8")
150
- ).decode("utf-8")
151
- if self.content
152
- else None,
153
- "filepath": self.filepath,
93
+ def to_dict(self, include_base64_content: bool = True) -> Dict[str, Any]:
94
+ """Convert to dict, optionally including base64-encoded content"""
95
+ result = {
96
+ "id": self.id,
97
+ "url": self.url,
98
+ "filepath": str(self.filepath) if self.filepath else None,
154
99
  "format": self.format,
100
+ "mime_type": self.mime_type,
101
+ "detail": self.detail,
102
+ "original_prompt": self.original_prompt,
103
+ "revised_prompt": self.revised_prompt,
104
+ "alt_text": self.alt_text,
155
105
  }
156
- return {k: v for k, v in response_dict.items() if v is not None}
157
106
 
158
- @classmethod
159
- def from_artifact(cls, artifact: VideoArtifact) -> "Video":
160
- return cls(url=artifact.url, content=artifact.content, format=artifact.mime_type)
107
+ if include_base64_content and self.content:
108
+ result["content"] = self.to_base64()
109
+
110
+ return {k: v for k, v in result.items() if v is not None}
161
111
 
162
112
 
163
113
  class Audio(BaseModel):
164
- content: Optional[Any] = None # Actual audio bytes content
165
- filepath: Optional[Union[Path, str]] = None # Absolute local location for audio
166
- url: Optional[str] = None # Remote location for audio
167
- format: Optional[str] = None
114
+ """Unified Audio class for all use cases (input, output, artifacts)"""
168
115
 
169
- @model_validator(mode="before")
170
- def validate_data(cls, data: Any):
171
- """
172
- Ensure that exactly one of `filepath`, or `content` is provided.
173
- Also converts content to bytes if it's a string.
174
- """
175
- # Extract the values from the input data
176
- filepath = data.get("filepath")
177
- content = data.get("content")
178
- url = data.get("url")
179
-
180
- # Convert and decompress content to bytes if it's a string
181
- if content and isinstance(content, str):
182
- import base64
116
+ # Core content fields (exactly one required)
117
+ url: Optional[str] = None
118
+ filepath: Optional[Union[Path, str]] = None
119
+ content: Optional[bytes] = None # Raw audio bytes (standardized to bytes)
183
120
 
184
- try:
185
- import zlib
121
+ # Metadata fields
122
+ id: Optional[str] = None
123
+ format: Optional[str] = None # E.g. 'mp3', 'wav', 'ogg'
124
+ mime_type: Optional[str] = None # E.g. 'audio/mpeg', 'audio/wav'
186
125
 
187
- decoded_content = base64.b64decode(content)
188
- content = zlib.decompress(decoded_content)
189
- except Exception:
190
- content = base64.b64decode(content).decode("utf-8")
191
- data["content"] = content
126
+ # Audio-specific metadata
127
+ duration: Optional[float] = None # Duration in seconds
128
+ sample_rate: Optional[int] = 24000 # Sample rate in Hz
129
+ channels: Optional[int] = 1 # Number of audio channels
192
130
 
193
- # Count how many fields are set (not None)
194
- count = len([field for field in [filepath, content, url] if field is not None])
131
+ # Output-specific fields (from LLMs)
132
+ transcript: Optional[str] = None # Text transcript of audio
133
+ expires_at: Optional[int] = None # Expiration timestamp for temporary URLs
195
134
 
196
- if count == 0:
197
- raise ValueError("One of `filepath` or `content` or `url` must be provided.")
198
- elif count > 1:
199
- raise ValueError("Only one of `filepath` or `content` or `url` should be provided.")
135
+ @model_validator(mode="before")
136
+ def validate_and_normalize_content(cls, data: Any):
137
+ """Ensure exactly one content source and normalize to bytes"""
138
+ if isinstance(data, dict):
139
+ url = data.get("url")
140
+ filepath = data.get("filepath")
141
+ content = data.get("content")
142
+
143
+ sources = [x for x in [url, filepath, content] if x is not None]
144
+ if len(sources) == 0:
145
+ raise ValueError("One of 'url', 'filepath', or 'content' must be provided")
146
+ elif len(sources) > 1:
147
+ raise ValueError("Only one of 'url', 'filepath', or 'content' should be provided")
148
+
149
+ if data.get("id") is None:
150
+ data["id"] = str(uuid4())
200
151
 
201
152
  return data
202
153
 
203
- @property
204
- def audio_url_content(self) -> Optional[bytes]:
205
- import httpx
154
+ def get_content_bytes(self) -> Optional[bytes]:
155
+ """Get audio content as raw bytes"""
156
+ if self.content:
157
+ return self.content
158
+ elif self.url:
159
+ import httpx
206
160
 
207
- if self.url:
208
161
  return httpx.get(self.url).content
209
- else:
210
- return None
211
-
212
- def to_dict(self) -> Dict[str, Any]:
213
- import base64
214
- import zlib
215
-
216
- response_dict = {
217
- "content": base64.b64encode(
218
- zlib.compress(self.content) if isinstance(self.content, bytes) else self.content.encode("utf-8")
219
- ).decode("utf-8")
220
- if self.content
221
- else None,
222
- "filepath": self.filepath,
223
- "format": self.format,
224
- }
162
+ elif self.filepath:
163
+ with open(self.filepath, "rb") as f:
164
+ return f.read()
165
+ return None
166
+
167
+ def to_base64(self) -> Optional[str]:
168
+ """Convert content to base64 string"""
169
+ content_bytes = self.get_content_bytes()
170
+ if content_bytes:
171
+ import base64
225
172
 
226
- return {k: v for k, v in response_dict.items() if v is not None}
173
+ return base64.b64encode(content_bytes).decode("utf-8")
174
+ return None
227
175
 
228
176
  @classmethod
229
- def from_artifact(cls, artifact: AudioArtifact) -> "Audio":
230
- return cls(url=artifact.url, content=artifact.base64_audio, format=artifact.mime_type)
231
-
232
-
233
- class AudioResponse(BaseModel):
234
- id: Optional[str] = None
235
- content: Optional[str] = None # Base64 encoded
236
- expires_at: Optional[int] = None
237
- transcript: Optional[str] = None
238
-
239
- mime_type: Optional[str] = None
240
- sample_rate: Optional[int] = 24000
241
- channels: Optional[int] = 1
242
-
243
- def to_dict(self) -> Dict[str, Any]:
177
+ def from_base64(
178
+ cls,
179
+ base64_content: str,
180
+ id: Optional[str] = None,
181
+ mime_type: Optional[str] = None,
182
+ transcript: Optional[str] = None,
183
+ expires_at: Optional[int] = None,
184
+ sample_rate: Optional[int] = 24000,
185
+ channels: Optional[int] = 1,
186
+ **kwargs,
187
+ ) -> "Audio":
188
+ """Create Audio from base64 content (useful for API responses)"""
244
189
  import base64
245
190
 
246
- response_dict = {
191
+ try:
192
+ content_bytes = base64.b64decode(base64_content)
193
+ except Exception:
194
+ # If not valid base64, encode as UTF-8 bytes
195
+ content_bytes = base64_content.encode("utf-8")
196
+
197
+ return cls(
198
+ content=content_bytes,
199
+ id=id or str(uuid4()),
200
+ mime_type=mime_type,
201
+ transcript=transcript,
202
+ expires_at=expires_at,
203
+ sample_rate=sample_rate,
204
+ channels=channels,
205
+ **kwargs,
206
+ )
207
+
208
+ def to_dict(self, include_base64_content: bool = True) -> Dict[str, Any]:
209
+ """Convert to dict, optionally including base64-encoded content"""
210
+ result = {
247
211
  "id": self.id,
248
- "content": base64.b64encode(self.content).decode("utf-8")
249
- if isinstance(self.content, bytes)
250
- else self.content,
251
- "expires_at": self.expires_at,
252
- "transcript": self.transcript,
212
+ "url": self.url,
213
+ "filepath": str(self.filepath) if self.filepath else None,
214
+ "format": self.format,
253
215
  "mime_type": self.mime_type,
216
+ "duration": self.duration,
254
217
  "sample_rate": self.sample_rate,
255
218
  "channels": self.channels,
219
+ "transcript": self.transcript,
220
+ "expires_at": self.expires_at,
256
221
  }
257
- return {k: v for k, v in response_dict.items() if v is not None}
258
222
 
223
+ if include_base64_content and self.content:
224
+ result["content"] = self.to_base64()
259
225
 
260
- class Image(BaseModel):
261
- url: Optional[str] = None # Remote location for image
262
- filepath: Optional[Union[Path, str]] = None # Absolute local location for image
263
- content: Optional[Any] = None # Actual image bytes content
264
- format: Optional[str] = None # E.g. `png`, `jpeg`, `webp`, `gif`
265
- detail: Optional[str] = (
266
- None # low, medium, high or auto (per OpenAI spec https://platform.openai.com/docs/guides/vision?lang=node#low-or-high-fidelity-image-understanding)
267
- )
268
- id: Optional[str] = None
226
+ return {k: v for k, v in result.items() if v is not None}
269
227
 
270
- @property
271
- def image_url_content(self) -> Optional[bytes]:
272
- import httpx
273
228
 
274
- if self.url:
275
- return httpx.get(self.url).content
276
- else:
277
- return None
229
+ class Video(BaseModel):
230
+ """Unified Video class for all use cases (input, output, artifacts)"""
278
231
 
279
- @model_validator(mode="before")
280
- def validate_data(cls, data: Any):
281
- """
282
- Ensure that exactly one of `url`, `filepath`, or `content` is provided.
283
- Also converts content to bytes if it's a string.
284
- """
285
- # Extract the values from the input data
286
- url = data.get("url")
287
- filepath = data.get("filepath")
288
- content = data.get("content")
289
-
290
- # Convert and decompress content to bytes if it's a string
291
- if content and isinstance(content, str):
292
- import base64
232
+ # Core content fields (exactly one required)
233
+ url: Optional[str] = None
234
+ filepath: Optional[Union[Path, str]] = None
235
+ content: Optional[bytes] = None # Raw video bytes (standardized to bytes)
293
236
 
294
- try:
295
- import zlib
237
+ # Metadata fields
238
+ id: Optional[str] = None
239
+ format: Optional[str] = None # E.g. 'mp4', 'mov', 'avi', 'webm'
240
+ mime_type: Optional[str] = None # E.g. 'video/mp4', 'video/quicktime'
296
241
 
297
- decoded_content = base64.b64decode(content)
298
- content = zlib.decompress(decoded_content)
299
- except Exception:
300
- content = base64.b64decode(content).decode("utf-8")
301
- data["content"] = content
242
+ # Video-specific metadata
243
+ duration: Optional[float] = None # Duration in seconds
244
+ width: Optional[int] = None # Video width in pixels
245
+ height: Optional[int] = None # Video height in pixels
246
+ fps: Optional[float] = None # Frames per second
302
247
 
303
- # Count how many fields are set (not None)
304
- count = len([field for field in [url, filepath, content] if field is not None])
248
+ # Output-specific fields (from tools)
249
+ eta: Optional[str] = None # Estimated time for generation
250
+ original_prompt: Optional[str] = None
251
+ revised_prompt: Optional[str] = None
305
252
 
306
- if count == 0:
307
- raise ValueError("One of `url`, `filepath`, or `content` must be provided.")
308
- elif count > 1:
309
- raise ValueError("Only one of `url`, `filepath`, or `content` should be provided.")
253
+ @model_validator(mode="before")
254
+ def validate_and_normalize_content(cls, data: Any):
255
+ """Ensure exactly one content source and normalize to bytes"""
256
+ if isinstance(data, dict):
257
+ url = data.get("url")
258
+ filepath = data.get("filepath")
259
+ content = data.get("content")
260
+
261
+ sources = [x for x in [url, filepath, content] if x is not None]
262
+ if len(sources) == 0:
263
+ raise ValueError("One of 'url', 'filepath', or 'content' must be provided")
264
+ elif len(sources) > 1:
265
+ raise ValueError("Only one of 'url', 'filepath', or 'content' should be provided")
266
+
267
+ if data.get("id") is None:
268
+ data["id"] = str(uuid4())
310
269
 
311
270
  return data
312
271
 
313
- def to_dict(self) -> Dict[str, Any]:
272
+ def get_content_bytes(self) -> Optional[bytes]:
273
+ """Get video content as raw bytes"""
274
+ if self.content:
275
+ return self.content
276
+ elif self.url:
277
+ import httpx
278
+
279
+ return httpx.get(self.url).content
280
+ elif self.filepath:
281
+ with open(self.filepath, "rb") as f:
282
+ return f.read()
283
+ return None
284
+
285
+ def to_base64(self) -> Optional[str]:
286
+ """Convert content to base64 string"""
287
+ content_bytes = self.get_content_bytes()
288
+ if content_bytes:
289
+ import base64
290
+
291
+ return base64.b64encode(content_bytes).decode("utf-8")
292
+ return None
293
+
294
+ @classmethod
295
+ def from_base64(
296
+ cls,
297
+ base64_content: str,
298
+ id: Optional[str] = None,
299
+ mime_type: Optional[str] = None,
300
+ format: Optional[str] = None,
301
+ **kwargs,
302
+ ) -> "Video":
303
+ """Create Image from base64 content"""
314
304
  import base64
315
- import zlib
316
-
317
- response_dict = {
318
- "content": base64.b64encode(
319
- zlib.compress(self.content) if isinstance(self.content, bytes) else self.content.encode("utf-8")
320
- ).decode("utf-8")
321
- if self.content
322
- else None,
323
- "filepath": self.filepath,
305
+
306
+ try:
307
+ content_bytes = base64.b64decode(base64_content)
308
+ except Exception:
309
+ content_bytes = base64_content.encode("utf-8")
310
+
311
+ return cls(content=content_bytes, id=id or str(uuid4()), mime_type=mime_type, format=format, **kwargs)
312
+
313
+ def to_dict(self, include_base64_content: bool = True) -> Dict[str, Any]:
314
+ """Convert to dict, optionally including base64-encoded content"""
315
+ result = {
316
+ "id": self.id,
324
317
  "url": self.url,
325
- "detail": self.detail,
318
+ "filepath": str(self.filepath) if self.filepath else None,
319
+ "format": self.format,
320
+ "mime_type": self.mime_type,
321
+ "duration": self.duration,
322
+ "width": self.width,
323
+ "height": self.height,
324
+ "fps": self.fps,
325
+ "eta": self.eta,
326
+ "original_prompt": self.original_prompt,
327
+ "revised_prompt": self.revised_prompt,
326
328
  }
327
329
 
328
- return {k: v for k, v in response_dict.items() if v is not None}
330
+ if include_base64_content and self.content:
331
+ result["content"] = self.to_base64()
329
332
 
330
- @classmethod
331
- def from_artifact(cls, artifact: ImageArtifact) -> "Image":
332
- return cls(url=artifact.url, content=artifact.content, format=artifact.mime_type)
333
+ return {k: v for k, v in result.items() if v is not None}
333
334
 
334
335
 
335
336
  class File(BaseModel):
@@ -181,14 +181,10 @@ class AwsBedrock(Model):
181
181
  required = []
182
182
 
183
183
  for param_name, param_info in func_def.get("parameters", {}).get("properties", {}).items():
184
- param_type = param_info.get("type")
185
- if isinstance(param_type, list):
186
- param_type = [t for t in param_type if t != "null"][0]
184
+ properties[param_name] = param_info.copy()
187
185
 
188
- properties[param_name] = {
189
- "type": param_type or "string",
190
- "description": param_info.get("description") or "",
191
- }
186
+ if "description" not in properties[param_name]:
187
+ properties[param_name]["description"] = ""
192
188
 
193
189
  if "null" not in (
194
190
  param_info.get("type") if isinstance(param_info.get("type"), list) else [param_info.get("type")]