agno 2.0.0rc2__py3-none-any.whl → 2.3.0__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 (331) hide show
  1. agno/agent/agent.py +6009 -2874
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/culture/__init__.py +3 -0
  5. agno/culture/manager.py +956 -0
  6. agno/db/async_postgres/__init__.py +3 -0
  7. agno/db/base.py +385 -6
  8. agno/db/dynamo/dynamo.py +388 -81
  9. agno/db/dynamo/schemas.py +47 -10
  10. agno/db/dynamo/utils.py +63 -4
  11. agno/db/firestore/firestore.py +435 -64
  12. agno/db/firestore/schemas.py +11 -0
  13. agno/db/firestore/utils.py +102 -4
  14. agno/db/gcs_json/gcs_json_db.py +384 -42
  15. agno/db/gcs_json/utils.py +60 -26
  16. agno/db/in_memory/in_memory_db.py +351 -66
  17. agno/db/in_memory/utils.py +60 -2
  18. agno/db/json/json_db.py +339 -48
  19. agno/db/json/utils.py +60 -26
  20. agno/db/migrations/manager.py +199 -0
  21. agno/db/migrations/v1_to_v2.py +510 -37
  22. agno/db/migrations/versions/__init__.py +0 -0
  23. agno/db/migrations/versions/v2_3_0.py +938 -0
  24. agno/db/mongo/__init__.py +15 -1
  25. agno/db/mongo/async_mongo.py +2036 -0
  26. agno/db/mongo/mongo.py +653 -76
  27. agno/db/mongo/schemas.py +13 -0
  28. agno/db/mongo/utils.py +80 -8
  29. agno/db/mysql/mysql.py +687 -25
  30. agno/db/mysql/schemas.py +61 -37
  31. agno/db/mysql/utils.py +60 -2
  32. agno/db/postgres/__init__.py +2 -1
  33. agno/db/postgres/async_postgres.py +2001 -0
  34. agno/db/postgres/postgres.py +676 -57
  35. agno/db/postgres/schemas.py +43 -18
  36. agno/db/postgres/utils.py +164 -2
  37. agno/db/redis/redis.py +344 -38
  38. agno/db/redis/schemas.py +18 -0
  39. agno/db/redis/utils.py +60 -2
  40. agno/db/schemas/__init__.py +2 -1
  41. agno/db/schemas/culture.py +120 -0
  42. agno/db/schemas/memory.py +13 -0
  43. agno/db/singlestore/schemas.py +26 -1
  44. agno/db/singlestore/singlestore.py +687 -53
  45. agno/db/singlestore/utils.py +60 -2
  46. agno/db/sqlite/__init__.py +2 -1
  47. agno/db/sqlite/async_sqlite.py +2371 -0
  48. agno/db/sqlite/schemas.py +24 -0
  49. agno/db/sqlite/sqlite.py +774 -85
  50. agno/db/sqlite/utils.py +168 -5
  51. agno/db/surrealdb/__init__.py +3 -0
  52. agno/db/surrealdb/metrics.py +292 -0
  53. agno/db/surrealdb/models.py +309 -0
  54. agno/db/surrealdb/queries.py +71 -0
  55. agno/db/surrealdb/surrealdb.py +1361 -0
  56. agno/db/surrealdb/utils.py +147 -0
  57. agno/db/utils.py +50 -22
  58. agno/eval/accuracy.py +50 -43
  59. agno/eval/performance.py +6 -3
  60. agno/eval/reliability.py +6 -3
  61. agno/eval/utils.py +33 -16
  62. agno/exceptions.py +68 -1
  63. agno/filters.py +354 -0
  64. agno/guardrails/__init__.py +6 -0
  65. agno/guardrails/base.py +19 -0
  66. agno/guardrails/openai.py +144 -0
  67. agno/guardrails/pii.py +94 -0
  68. agno/guardrails/prompt_injection.py +52 -0
  69. agno/integrations/discord/client.py +1 -0
  70. agno/knowledge/chunking/agentic.py +13 -10
  71. agno/knowledge/chunking/fixed.py +1 -1
  72. agno/knowledge/chunking/semantic.py +40 -8
  73. agno/knowledge/chunking/strategy.py +59 -15
  74. agno/knowledge/embedder/aws_bedrock.py +9 -4
  75. agno/knowledge/embedder/azure_openai.py +54 -0
  76. agno/knowledge/embedder/base.py +2 -0
  77. agno/knowledge/embedder/cohere.py +184 -5
  78. agno/knowledge/embedder/fastembed.py +1 -1
  79. agno/knowledge/embedder/google.py +79 -1
  80. agno/knowledge/embedder/huggingface.py +9 -4
  81. agno/knowledge/embedder/jina.py +63 -0
  82. agno/knowledge/embedder/mistral.py +78 -11
  83. agno/knowledge/embedder/nebius.py +1 -1
  84. agno/knowledge/embedder/ollama.py +13 -0
  85. agno/knowledge/embedder/openai.py +37 -65
  86. agno/knowledge/embedder/sentence_transformer.py +8 -4
  87. agno/knowledge/embedder/vllm.py +262 -0
  88. agno/knowledge/embedder/voyageai.py +69 -16
  89. agno/knowledge/knowledge.py +595 -187
  90. agno/knowledge/reader/base.py +9 -2
  91. agno/knowledge/reader/csv_reader.py +8 -10
  92. agno/knowledge/reader/docx_reader.py +5 -6
  93. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  94. agno/knowledge/reader/json_reader.py +6 -5
  95. agno/knowledge/reader/markdown_reader.py +13 -13
  96. agno/knowledge/reader/pdf_reader.py +43 -68
  97. agno/knowledge/reader/pptx_reader.py +101 -0
  98. agno/knowledge/reader/reader_factory.py +51 -6
  99. agno/knowledge/reader/s3_reader.py +3 -15
  100. agno/knowledge/reader/tavily_reader.py +194 -0
  101. agno/knowledge/reader/text_reader.py +13 -13
  102. agno/knowledge/reader/web_search_reader.py +2 -43
  103. agno/knowledge/reader/website_reader.py +43 -25
  104. agno/knowledge/reranker/__init__.py +3 -0
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +339 -266
  108. agno/memory/manager.py +336 -82
  109. agno/models/aimlapi/aimlapi.py +2 -2
  110. agno/models/anthropic/claude.py +183 -37
  111. agno/models/aws/bedrock.py +52 -112
  112. agno/models/aws/claude.py +33 -1
  113. agno/models/azure/ai_foundry.py +33 -15
  114. agno/models/azure/openai_chat.py +25 -8
  115. agno/models/base.py +1011 -566
  116. agno/models/cerebras/cerebras.py +19 -13
  117. agno/models/cerebras/cerebras_openai.py +8 -5
  118. agno/models/cohere/chat.py +27 -1
  119. agno/models/cometapi/__init__.py +5 -0
  120. agno/models/cometapi/cometapi.py +57 -0
  121. agno/models/dashscope/dashscope.py +1 -0
  122. agno/models/deepinfra/deepinfra.py +2 -2
  123. agno/models/deepseek/deepseek.py +2 -2
  124. agno/models/fireworks/fireworks.py +2 -2
  125. agno/models/google/gemini.py +110 -37
  126. agno/models/groq/groq.py +28 -11
  127. agno/models/huggingface/huggingface.py +2 -1
  128. agno/models/internlm/internlm.py +2 -2
  129. agno/models/langdb/langdb.py +4 -4
  130. agno/models/litellm/chat.py +18 -1
  131. agno/models/litellm/litellm_openai.py +2 -2
  132. agno/models/llama_cpp/__init__.py +5 -0
  133. agno/models/llama_cpp/llama_cpp.py +22 -0
  134. agno/models/message.py +143 -4
  135. agno/models/meta/llama.py +27 -10
  136. agno/models/meta/llama_openai.py +5 -17
  137. agno/models/nebius/nebius.py +6 -6
  138. agno/models/nexus/__init__.py +3 -0
  139. agno/models/nexus/nexus.py +22 -0
  140. agno/models/nvidia/nvidia.py +2 -2
  141. agno/models/ollama/chat.py +60 -6
  142. agno/models/openai/chat.py +102 -43
  143. agno/models/openai/responses.py +103 -106
  144. agno/models/openrouter/openrouter.py +41 -3
  145. agno/models/perplexity/perplexity.py +4 -5
  146. agno/models/portkey/portkey.py +3 -3
  147. agno/models/requesty/__init__.py +5 -0
  148. agno/models/requesty/requesty.py +52 -0
  149. agno/models/response.py +81 -5
  150. agno/models/sambanova/sambanova.py +2 -2
  151. agno/models/siliconflow/__init__.py +5 -0
  152. agno/models/siliconflow/siliconflow.py +25 -0
  153. agno/models/together/together.py +2 -2
  154. agno/models/utils.py +254 -8
  155. agno/models/vercel/v0.py +2 -2
  156. agno/models/vertexai/__init__.py +0 -0
  157. agno/models/vertexai/claude.py +96 -0
  158. agno/models/vllm/vllm.py +1 -0
  159. agno/models/xai/xai.py +3 -2
  160. agno/os/app.py +543 -175
  161. agno/os/auth.py +24 -14
  162. agno/os/config.py +1 -0
  163. agno/os/interfaces/__init__.py +1 -0
  164. agno/os/interfaces/a2a/__init__.py +3 -0
  165. agno/os/interfaces/a2a/a2a.py +42 -0
  166. agno/os/interfaces/a2a/router.py +250 -0
  167. agno/os/interfaces/a2a/utils.py +924 -0
  168. agno/os/interfaces/agui/agui.py +23 -7
  169. agno/os/interfaces/agui/router.py +27 -3
  170. agno/os/interfaces/agui/utils.py +242 -142
  171. agno/os/interfaces/base.py +6 -2
  172. agno/os/interfaces/slack/router.py +81 -23
  173. agno/os/interfaces/slack/slack.py +29 -14
  174. agno/os/interfaces/whatsapp/router.py +11 -4
  175. agno/os/interfaces/whatsapp/whatsapp.py +14 -7
  176. agno/os/mcp.py +111 -54
  177. agno/os/middleware/__init__.py +7 -0
  178. agno/os/middleware/jwt.py +233 -0
  179. agno/os/router.py +556 -139
  180. agno/os/routers/evals/evals.py +71 -34
  181. agno/os/routers/evals/schemas.py +31 -31
  182. agno/os/routers/evals/utils.py +6 -5
  183. agno/os/routers/health.py +31 -0
  184. agno/os/routers/home.py +52 -0
  185. agno/os/routers/knowledge/knowledge.py +185 -38
  186. agno/os/routers/knowledge/schemas.py +82 -22
  187. agno/os/routers/memory/memory.py +158 -53
  188. agno/os/routers/memory/schemas.py +20 -16
  189. agno/os/routers/metrics/metrics.py +20 -8
  190. agno/os/routers/metrics/schemas.py +16 -16
  191. agno/os/routers/session/session.py +499 -38
  192. agno/os/schema.py +308 -198
  193. agno/os/utils.py +401 -41
  194. agno/reasoning/anthropic.py +80 -0
  195. agno/reasoning/azure_ai_foundry.py +2 -2
  196. agno/reasoning/deepseek.py +2 -2
  197. agno/reasoning/default.py +3 -1
  198. agno/reasoning/gemini.py +73 -0
  199. agno/reasoning/groq.py +2 -2
  200. agno/reasoning/ollama.py +2 -2
  201. agno/reasoning/openai.py +7 -2
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +266 -112
  205. agno/run/base.py +53 -24
  206. agno/run/team.py +252 -111
  207. agno/run/workflow.py +156 -45
  208. agno/session/agent.py +105 -89
  209. agno/session/summary.py +65 -25
  210. agno/session/team.py +176 -96
  211. agno/session/workflow.py +406 -40
  212. agno/team/team.py +3854 -1692
  213. agno/tools/brightdata.py +3 -3
  214. agno/tools/cartesia.py +3 -5
  215. agno/tools/dalle.py +9 -8
  216. agno/tools/decorator.py +4 -2
  217. agno/tools/desi_vocal.py +2 -2
  218. agno/tools/duckduckgo.py +15 -11
  219. agno/tools/e2b.py +20 -13
  220. agno/tools/eleven_labs.py +26 -28
  221. agno/tools/exa.py +21 -16
  222. agno/tools/fal.py +4 -4
  223. agno/tools/file.py +153 -23
  224. agno/tools/file_generation.py +350 -0
  225. agno/tools/firecrawl.py +4 -4
  226. agno/tools/function.py +257 -37
  227. agno/tools/giphy.py +2 -2
  228. agno/tools/gmail.py +238 -14
  229. agno/tools/google_drive.py +270 -0
  230. agno/tools/googlecalendar.py +36 -8
  231. agno/tools/googlesheets.py +20 -5
  232. agno/tools/jira.py +20 -0
  233. agno/tools/knowledge.py +3 -3
  234. agno/tools/lumalab.py +3 -3
  235. agno/tools/mcp/__init__.py +10 -0
  236. agno/tools/mcp/mcp.py +331 -0
  237. agno/tools/mcp/multi_mcp.py +347 -0
  238. agno/tools/mcp/params.py +24 -0
  239. agno/tools/mcp_toolbox.py +284 -0
  240. agno/tools/mem0.py +11 -17
  241. agno/tools/memori.py +1 -53
  242. agno/tools/memory.py +419 -0
  243. agno/tools/models/azure_openai.py +2 -2
  244. agno/tools/models/gemini.py +3 -3
  245. agno/tools/models/groq.py +3 -5
  246. agno/tools/models/nebius.py +7 -7
  247. agno/tools/models_labs.py +25 -15
  248. agno/tools/notion.py +204 -0
  249. agno/tools/openai.py +4 -9
  250. agno/tools/opencv.py +3 -3
  251. agno/tools/parallel.py +314 -0
  252. agno/tools/replicate.py +7 -7
  253. agno/tools/scrapegraph.py +58 -31
  254. agno/tools/searxng.py +2 -2
  255. agno/tools/serper.py +2 -2
  256. agno/tools/slack.py +18 -3
  257. agno/tools/spider.py +2 -2
  258. agno/tools/tavily.py +146 -0
  259. agno/tools/whatsapp.py +1 -1
  260. agno/tools/workflow.py +278 -0
  261. agno/tools/yfinance.py +12 -11
  262. agno/utils/agent.py +820 -0
  263. agno/utils/audio.py +27 -0
  264. agno/utils/common.py +90 -1
  265. agno/utils/events.py +222 -7
  266. agno/utils/gemini.py +181 -23
  267. agno/utils/hooks.py +57 -0
  268. agno/utils/http.py +111 -0
  269. agno/utils/knowledge.py +12 -5
  270. agno/utils/log.py +1 -0
  271. agno/utils/mcp.py +95 -5
  272. agno/utils/media.py +188 -10
  273. agno/utils/merge_dict.py +22 -1
  274. agno/utils/message.py +60 -0
  275. agno/utils/models/claude.py +40 -11
  276. agno/utils/models/cohere.py +1 -1
  277. agno/utils/models/watsonx.py +1 -1
  278. agno/utils/openai.py +1 -1
  279. agno/utils/print_response/agent.py +105 -21
  280. agno/utils/print_response/team.py +103 -38
  281. agno/utils/print_response/workflow.py +251 -34
  282. agno/utils/reasoning.py +22 -1
  283. agno/utils/serialize.py +32 -0
  284. agno/utils/streamlit.py +16 -10
  285. agno/utils/string.py +41 -0
  286. agno/utils/team.py +98 -9
  287. agno/utils/tools.py +1 -1
  288. agno/vectordb/base.py +23 -4
  289. agno/vectordb/cassandra/cassandra.py +65 -9
  290. agno/vectordb/chroma/chromadb.py +182 -38
  291. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  292. agno/vectordb/couchbase/couchbase.py +105 -10
  293. agno/vectordb/lancedb/lance_db.py +183 -135
  294. agno/vectordb/langchaindb/langchaindb.py +25 -7
  295. agno/vectordb/lightrag/lightrag.py +17 -3
  296. agno/vectordb/llamaindex/__init__.py +3 -0
  297. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  298. agno/vectordb/milvus/milvus.py +126 -9
  299. agno/vectordb/mongodb/__init__.py +7 -1
  300. agno/vectordb/mongodb/mongodb.py +112 -7
  301. agno/vectordb/pgvector/pgvector.py +142 -21
  302. agno/vectordb/pineconedb/pineconedb.py +80 -8
  303. agno/vectordb/qdrant/qdrant.py +125 -39
  304. agno/vectordb/redis/__init__.py +9 -0
  305. agno/vectordb/redis/redisdb.py +694 -0
  306. agno/vectordb/singlestore/singlestore.py +111 -25
  307. agno/vectordb/surrealdb/surrealdb.py +31 -5
  308. agno/vectordb/upstashdb/upstashdb.py +76 -8
  309. agno/vectordb/weaviate/weaviate.py +86 -15
  310. agno/workflow/__init__.py +2 -0
  311. agno/workflow/agent.py +299 -0
  312. agno/workflow/condition.py +112 -18
  313. agno/workflow/loop.py +69 -10
  314. agno/workflow/parallel.py +266 -118
  315. agno/workflow/router.py +110 -17
  316. agno/workflow/step.py +645 -136
  317. agno/workflow/steps.py +65 -6
  318. agno/workflow/types.py +71 -33
  319. agno/workflow/workflow.py +2113 -300
  320. agno-2.3.0.dist-info/METADATA +618 -0
  321. agno-2.3.0.dist-info/RECORD +577 -0
  322. agno-2.3.0.dist-info/licenses/LICENSE +201 -0
  323. agno/knowledge/reader/url_reader.py +0 -128
  324. agno/tools/googlesearch.py +0 -98
  325. agno/tools/mcp.py +0 -610
  326. agno/utils/models/aws_claude.py +0 -170
  327. agno-2.0.0rc2.dist-info/METADATA +0 -355
  328. agno-2.0.0rc2.dist-info/RECORD +0 -515
  329. agno-2.0.0rc2.dist-info/licenses/LICENSE +0 -375
  330. {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  331. {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
agno/media.py CHANGED
@@ -1,343 +1,349 @@
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
8
+ class Image(BaseModel):
9
+ """Unified Image class for all use cases (input, output, artifacts)"""
11
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)
12
15
 
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
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'
19
20
 
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}
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
+ )
33
25
 
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
34
30
 
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
31
+ @model_validator(mode="before")
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())
40
49
 
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
50
+ return data
47
51
 
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
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
61
58
 
62
- def to_dict(self) -> Dict[str, Any]:
63
- content_normalised = self._normalise_content()
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:
69
+ import base64
64
70
 
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}
71
+ return base64.b64encode(content_bytes).decode("utf-8")
72
+ return None
73
73
 
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
74
85
 
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
86
+ try:
87
+ content_bytes = base64.b64decode(base64_content)
88
+ except Exception:
89
+ content_bytes = base64_content.encode("utf-8")
80
90
 
81
- @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
+ return cls(content=content_bytes, id=id or str(uuid4()), mime_type=mime_type, format=format, **kwargs)
91
92
 
92
- def to_dict(self) -> Dict[str, Any]:
93
- response_dict = {
93
+ def to_dict(self, include_base64_content: bool = True) -> Dict[str, Any]:
94
+ """Convert to dict, optionally including base64-encoded content"""
95
+ result = {
94
96
  "id": self.id,
95
97
  "url": self.url,
96
- "content": self.base64_audio,
98
+ "filepath": str(self.filepath) if self.filepath else None,
99
+ "format": self.format,
97
100
  "mime_type": self.mime_type,
98
- "length": self.length,
101
+ "detail": self.detail,
102
+ "original_prompt": self.original_prompt,
103
+ "revised_prompt": self.revised_prompt,
104
+ "alt_text": self.alt_text,
99
105
  }
100
- return {k: v for k, v in response_dict.items() if v is not None}
101
-
102
-
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`
108
-
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):
122
- import base64
123
-
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
132
-
133
- # Count how many fields are set (not None)
134
- count = len([field for field in [filepath, content, url] if field is not None])
135
-
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.")
140
-
141
- return data
142
-
143
- def to_dict(self) -> Dict[str, Any]:
144
- import base64
145
- import zlib
146
106
 
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,
154
- "format": self.format,
155
- }
156
- return {k: v for k, v in response_dict.items() if v is not None}
107
+ if include_base64_content and self.content:
108
+ result["content"] = self.to_base64()
157
109
 
158
- @classmethod
159
- def from_artifact(cls, artifact: VideoArtifact) -> "Video":
160
- return cls(url=artifact.url, content=artifact.content, format=artifact.mime_type)
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
305
 
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,
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):
337
+ id: Optional[str] = None
336
338
  url: Optional[str] = None
337
339
  filepath: Optional[Union[Path, str]] = None
338
340
  # Raw bytes content of a file
339
341
  content: Optional[Any] = None
340
342
  mime_type: Optional[str] = None
343
+
344
+ file_type: Optional[str] = None
345
+ filename: Optional[str] = None
346
+ size: Optional[int] = None
341
347
  # External file object (e.g. GeminiFile, must be a valid object as expected by the model you are using)
342
348
  external: Optional[Any] = None
343
349
  format: Optional[str] = None # E.g. `pdf`, `txt`, `csv`, `xml`, etc.
@@ -363,7 +369,10 @@ class File(BaseModel):
363
369
  def valid_mime_types(cls) -> List[str]:
364
370
  return [
365
371
  "application/pdf",
372
+ "application/json",
366
373
  "application/x-javascript",
374
+ "application/json",
375
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
367
376
  "text/javascript",
368
377
  "application/x-python",
369
378
  "text/x-python",
@@ -376,6 +385,29 @@ class File(BaseModel):
376
385
  "text/rtf",
377
386
  ]
378
387
 
388
+ @classmethod
389
+ def from_base64(
390
+ cls,
391
+ base64_content: str,
392
+ id: Optional[str] = None,
393
+ mime_type: Optional[str] = None,
394
+ filename: Optional[str] = None,
395
+ name: Optional[str] = None,
396
+ format: Optional[str] = None,
397
+ ) -> "File":
398
+ """Create File from base64 encoded content"""
399
+ import base64
400
+
401
+ content_bytes = base64.b64decode(base64_content)
402
+ return cls(
403
+ content=content_bytes,
404
+ id=id,
405
+ mime_type=mime_type,
406
+ filename=filename,
407
+ name=name,
408
+ format=format,
409
+ )
410
+
379
411
  @property
380
412
  def file_url_content(self) -> Optional[Tuple[bytes, str]]:
381
413
  import httpx
@@ -387,3 +419,44 @@ class File(BaseModel):
387
419
  return content, mime_type
388
420
  else:
389
421
  return None
422
+
423
+ def _normalise_content(self) -> Optional[Union[str, bytes]]:
424
+ if self.content is None:
425
+ return None
426
+ content_normalised: Union[str, bytes] = self.content
427
+ if content_normalised and isinstance(content_normalised, bytes):
428
+ from base64 import b64encode
429
+
430
+ try:
431
+ if self.mime_type and self.mime_type.startswith("text/"):
432
+ content_normalised = content_normalised.decode("utf-8")
433
+ else:
434
+ content_normalised = b64encode(content_normalised).decode("utf-8")
435
+ except UnicodeDecodeError:
436
+ if isinstance(self.content, bytes):
437
+ content_normalised = b64encode(self.content).decode("utf-8")
438
+ except Exception:
439
+ try:
440
+ if isinstance(self.content, bytes):
441
+ content_normalised = b64encode(self.content).decode("utf-8")
442
+ except Exception:
443
+ pass
444
+ return content_normalised
445
+
446
+ def to_dict(self) -> Dict[str, Any]:
447
+ content_normalised = self._normalise_content()
448
+
449
+ response_dict = {
450
+ "id": self.id,
451
+ "url": self.url,
452
+ "filepath": str(self.filepath) if self.filepath else None,
453
+ "content": content_normalised,
454
+ "mime_type": self.mime_type,
455
+ "file_type": self.file_type,
456
+ "filename": self.filename,
457
+ "size": self.size,
458
+ "external": self.external,
459
+ "format": self.format,
460
+ "name": self.name,
461
+ }
462
+ return {k: v for k, v in response_dict.items() if v is not None}