agno 2.0.1__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 (314) hide show
  1. agno/agent/agent.py +6015 -2823
  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 +594 -186
  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 +2 -8
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +72 -0
  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 +999 -519
  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 +103 -31
  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 +139 -0
  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 +59 -5
  142. agno/models/openai/chat.py +69 -29
  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 +77 -1
  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 -178
  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 +248 -94
  205. agno/run/base.py +44 -5
  206. agno/run/team.py +238 -97
  207. agno/run/workflow.py +144 -33
  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 -1610
  213. agno/tools/dalle.py +2 -4
  214. agno/tools/decorator.py +4 -2
  215. agno/tools/duckduckgo.py +15 -11
  216. agno/tools/e2b.py +14 -7
  217. agno/tools/eleven_labs.py +23 -25
  218. agno/tools/exa.py +21 -16
  219. agno/tools/file.py +153 -23
  220. agno/tools/file_generation.py +350 -0
  221. agno/tools/firecrawl.py +4 -4
  222. agno/tools/function.py +250 -30
  223. agno/tools/gmail.py +238 -14
  224. agno/tools/google_drive.py +270 -0
  225. agno/tools/googlecalendar.py +36 -8
  226. agno/tools/googlesheets.py +20 -5
  227. agno/tools/jira.py +20 -0
  228. agno/tools/knowledge.py +3 -3
  229. agno/tools/mcp/__init__.py +10 -0
  230. agno/tools/mcp/mcp.py +331 -0
  231. agno/tools/mcp/multi_mcp.py +347 -0
  232. agno/tools/mcp/params.py +24 -0
  233. agno/tools/mcp_toolbox.py +284 -0
  234. agno/tools/mem0.py +11 -17
  235. agno/tools/memori.py +1 -53
  236. agno/tools/memory.py +419 -0
  237. agno/tools/models/nebius.py +5 -5
  238. agno/tools/models_labs.py +20 -10
  239. agno/tools/notion.py +204 -0
  240. agno/tools/parallel.py +314 -0
  241. agno/tools/scrapegraph.py +58 -31
  242. agno/tools/searxng.py +2 -2
  243. agno/tools/serper.py +2 -2
  244. agno/tools/slack.py +18 -3
  245. agno/tools/spider.py +2 -2
  246. agno/tools/tavily.py +146 -0
  247. agno/tools/whatsapp.py +1 -1
  248. agno/tools/workflow.py +278 -0
  249. agno/tools/yfinance.py +12 -11
  250. agno/utils/agent.py +820 -0
  251. agno/utils/audio.py +27 -0
  252. agno/utils/common.py +90 -1
  253. agno/utils/events.py +217 -2
  254. agno/utils/gemini.py +180 -22
  255. agno/utils/hooks.py +57 -0
  256. agno/utils/http.py +111 -0
  257. agno/utils/knowledge.py +12 -5
  258. agno/utils/log.py +1 -0
  259. agno/utils/mcp.py +92 -2
  260. agno/utils/media.py +188 -10
  261. agno/utils/merge_dict.py +22 -1
  262. agno/utils/message.py +60 -0
  263. agno/utils/models/claude.py +40 -11
  264. agno/utils/print_response/agent.py +105 -21
  265. agno/utils/print_response/team.py +103 -38
  266. agno/utils/print_response/workflow.py +251 -34
  267. agno/utils/reasoning.py +22 -1
  268. agno/utils/serialize.py +32 -0
  269. agno/utils/streamlit.py +16 -10
  270. agno/utils/string.py +41 -0
  271. agno/utils/team.py +98 -9
  272. agno/utils/tools.py +1 -1
  273. agno/vectordb/base.py +23 -4
  274. agno/vectordb/cassandra/cassandra.py +65 -9
  275. agno/vectordb/chroma/chromadb.py +182 -38
  276. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  277. agno/vectordb/couchbase/couchbase.py +105 -10
  278. agno/vectordb/lancedb/lance_db.py +124 -133
  279. agno/vectordb/langchaindb/langchaindb.py +25 -7
  280. agno/vectordb/lightrag/lightrag.py +17 -3
  281. agno/vectordb/llamaindex/__init__.py +3 -0
  282. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  283. agno/vectordb/milvus/milvus.py +126 -9
  284. agno/vectordb/mongodb/__init__.py +7 -1
  285. agno/vectordb/mongodb/mongodb.py +112 -7
  286. agno/vectordb/pgvector/pgvector.py +142 -21
  287. agno/vectordb/pineconedb/pineconedb.py +80 -8
  288. agno/vectordb/qdrant/qdrant.py +125 -39
  289. agno/vectordb/redis/__init__.py +9 -0
  290. agno/vectordb/redis/redisdb.py +694 -0
  291. agno/vectordb/singlestore/singlestore.py +111 -25
  292. agno/vectordb/surrealdb/surrealdb.py +31 -5
  293. agno/vectordb/upstashdb/upstashdb.py +76 -8
  294. agno/vectordb/weaviate/weaviate.py +86 -15
  295. agno/workflow/__init__.py +2 -0
  296. agno/workflow/agent.py +299 -0
  297. agno/workflow/condition.py +112 -18
  298. agno/workflow/loop.py +69 -10
  299. agno/workflow/parallel.py +266 -118
  300. agno/workflow/router.py +110 -17
  301. agno/workflow/step.py +638 -129
  302. agno/workflow/steps.py +65 -6
  303. agno/workflow/types.py +61 -23
  304. agno/workflow/workflow.py +2085 -272
  305. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/METADATA +182 -58
  306. agno-2.3.0.dist-info/RECORD +577 -0
  307. agno/knowledge/reader/url_reader.py +0 -128
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -610
  310. agno/utils/models/aws_claude.py +0 -170
  311. agno-2.0.1.dist-info/RECORD +0 -515
  312. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  313. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,350 @@
1
+ import csv
2
+ import io
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional, Union
6
+ from uuid import uuid4
7
+
8
+ from agno.media import File
9
+ from agno.tools import Toolkit
10
+ from agno.tools.function import ToolResult
11
+ from agno.utils.log import log_debug, logger
12
+
13
+ try:
14
+ from reportlab.lib.pagesizes import letter
15
+ from reportlab.lib.styles import getSampleStyleSheet
16
+ from reportlab.lib.units import inch
17
+ from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
18
+
19
+ PDF_AVAILABLE = True
20
+ except ImportError:
21
+ PDF_AVAILABLE = False
22
+ logger.warning("reportlab not installed. PDF generation will not be available. Install with: pip install reportlab")
23
+
24
+
25
+ class FileGenerationTools(Toolkit):
26
+ def __init__(
27
+ self,
28
+ enable_json_generation: bool = True,
29
+ enable_csv_generation: bool = True,
30
+ enable_pdf_generation: bool = True,
31
+ enable_txt_generation: bool = True,
32
+ output_directory: Optional[str] = None,
33
+ all: bool = False,
34
+ **kwargs,
35
+ ):
36
+ self.enable_json_generation = enable_json_generation
37
+ self.enable_csv_generation = enable_csv_generation
38
+ self.enable_pdf_generation = enable_pdf_generation and PDF_AVAILABLE
39
+ self.enable_txt_generation = enable_txt_generation
40
+ self.output_directory = Path(output_directory) if output_directory else None
41
+
42
+ # Create output directory if specified
43
+ if self.output_directory:
44
+ self.output_directory.mkdir(parents=True, exist_ok=True)
45
+ log_debug(f"Files will be saved to: {self.output_directory}")
46
+
47
+ if enable_pdf_generation and not PDF_AVAILABLE:
48
+ logger.warning("PDF generation requested but reportlab is not installed. Disabling PDF generation.")
49
+ self.enable_pdf_generation = False
50
+
51
+ tools: List[Any] = []
52
+ if all or enable_json_generation:
53
+ tools.append(self.generate_json_file)
54
+ if all or enable_csv_generation:
55
+ tools.append(self.generate_csv_file)
56
+ if all or (enable_pdf_generation and PDF_AVAILABLE):
57
+ tools.append(self.generate_pdf_file)
58
+ if all or enable_txt_generation:
59
+ tools.append(self.generate_text_file)
60
+
61
+ super().__init__(name="file_generation", tools=tools, **kwargs)
62
+
63
+ def _save_file_to_disk(self, content: Union[str, bytes], filename: str) -> Optional[str]:
64
+ """Save file to disk if output_directory is set. Return file path or None."""
65
+ if not self.output_directory:
66
+ return None
67
+
68
+ file_path = self.output_directory / filename
69
+
70
+ if isinstance(content, str):
71
+ file_path.write_text(content, encoding="utf-8")
72
+ else:
73
+ file_path.write_bytes(content)
74
+
75
+ log_debug(f"File saved to: {file_path}")
76
+ return str(file_path)
77
+
78
+ def generate_json_file(self, data: Union[Dict, List, str], filename: Optional[str] = None) -> ToolResult:
79
+ """Generate a JSON file from the provided data.
80
+
81
+ Args:
82
+ data: The data to write to the JSON file. Can be a dictionary, list, or JSON string.
83
+ filename: Optional filename for the generated file. If not provided, a UUID will be used.
84
+
85
+ Returns:
86
+ ToolResult: Result containing the generated JSON file as a FileArtifact.
87
+ """
88
+ try:
89
+ log_debug(f"Generating JSON file with data: {type(data)}")
90
+
91
+ # Handle different input types
92
+ if isinstance(data, str):
93
+ try:
94
+ json.loads(data)
95
+ json_content = data # Use the original string if it's valid JSON
96
+ except json.JSONDecodeError:
97
+ # If it's not valid JSON, treat as plain text and wrap it
98
+ json_content = json.dumps({"content": data}, indent=2)
99
+ else:
100
+ json_content = json.dumps(data, indent=2, ensure_ascii=False)
101
+
102
+ # Generate filename if not provided
103
+ if not filename:
104
+ filename = f"generated_file_{str(uuid4())[:8]}.json"
105
+ elif not filename.endswith(".json"):
106
+ filename += ".json"
107
+
108
+ # Save file to disk (if output_directory is set)
109
+ file_path = self._save_file_to_disk(json_content, filename)
110
+
111
+ # Create FileArtifact
112
+ file_artifact = File(
113
+ id=str(uuid4()),
114
+ content=json_content,
115
+ mime_type="application/json",
116
+ file_type="json",
117
+ filename=filename,
118
+ size=len(json_content.encode("utf-8")),
119
+ filepath=file_path if file_path else None,
120
+ )
121
+
122
+ log_debug("JSON file generated successfully")
123
+ success_msg = f"JSON file '{filename}' has been generated successfully with {len(json_content)} characters."
124
+ if file_path:
125
+ success_msg += f" File saved to: {file_path}"
126
+ else:
127
+ success_msg += " File is available in response."
128
+
129
+ return ToolResult(content=success_msg, files=[file_artifact])
130
+
131
+ except Exception as e:
132
+ logger.error(f"Failed to generate JSON file: {e}")
133
+ return ToolResult(content=f"Error generating JSON file: {e}")
134
+
135
+ def generate_csv_file(
136
+ self,
137
+ data: Union[List[List], List[Dict], str],
138
+ filename: Optional[str] = None,
139
+ headers: Optional[List[str]] = None,
140
+ ) -> ToolResult:
141
+ """Generate a CSV file from the provided data.
142
+
143
+ Args:
144
+ data: The data to write to the CSV file. Can be a list of lists, list of dictionaries, or CSV string.
145
+ filename: Optional filename for the generated file. If not provided, a UUID will be used.
146
+ headers: Optional headers for the CSV. Used when data is a list of lists.
147
+
148
+ Returns:
149
+ ToolResult: Result containing the generated CSV file as a FileArtifact.
150
+ """
151
+ try:
152
+ log_debug(f"Generating CSV file with data: {type(data)}")
153
+
154
+ # Create CSV content
155
+ output = io.StringIO()
156
+
157
+ if isinstance(data, str):
158
+ # If it's already a CSV string, use it directly
159
+ csv_content = data
160
+ elif isinstance(data, list) and len(data) > 0:
161
+ writer = csv.writer(output)
162
+
163
+ if isinstance(data[0], dict):
164
+ # List of dictionaries - use keys as headers
165
+ if data:
166
+ fieldnames = list(data[0].keys())
167
+ writer.writerow(fieldnames)
168
+ for row in data:
169
+ if isinstance(row, dict):
170
+ writer.writerow([row.get(field, "") for field in fieldnames])
171
+ else:
172
+ writer.writerow([str(row)] + [""] * (len(fieldnames) - 1))
173
+ elif isinstance(data[0], list):
174
+ # List of lists
175
+ if headers:
176
+ writer.writerow(headers)
177
+ writer.writerows(data)
178
+ else:
179
+ # List of other types
180
+ if headers:
181
+ writer.writerow(headers)
182
+ for item in data:
183
+ writer.writerow([str(item)])
184
+
185
+ csv_content = output.getvalue()
186
+ else:
187
+ csv_content = ""
188
+
189
+ # Generate filename if not provided
190
+ if not filename:
191
+ filename = f"generated_file_{str(uuid4())[:8]}.csv"
192
+ elif not filename.endswith(".csv"):
193
+ filename += ".csv"
194
+
195
+ # Save file to disk (if output_directory is set)
196
+ file_path = self._save_file_to_disk(csv_content, filename)
197
+
198
+ # Create FileArtifact
199
+ file_artifact = File(
200
+ id=str(uuid4()),
201
+ content=csv_content,
202
+ mime_type="text/csv",
203
+ file_type="csv",
204
+ filename=filename,
205
+ size=len(csv_content.encode("utf-8")),
206
+ filepath=file_path if file_path else None,
207
+ )
208
+
209
+ log_debug("CSV file generated successfully")
210
+ success_msg = f"CSV file '{filename}' has been generated successfully with {len(csv_content)} characters."
211
+ if file_path:
212
+ success_msg += f" File saved to: {file_path}"
213
+ else:
214
+ success_msg += " File is available in response."
215
+
216
+ return ToolResult(content=success_msg, files=[file_artifact])
217
+
218
+ except Exception as e:
219
+ logger.error(f"Failed to generate CSV file: {e}")
220
+ return ToolResult(content=f"Error generating CSV file: {e}")
221
+
222
+ def generate_pdf_file(
223
+ self, content: str, filename: Optional[str] = None, title: Optional[str] = None
224
+ ) -> ToolResult:
225
+ """Generate a PDF file from the provided content.
226
+
227
+ Args:
228
+ content: The text content to write to the PDF file.
229
+ filename: Optional filename for the generated file. If not provided, a UUID will be used.
230
+ title: Optional title for the PDF document.
231
+
232
+ Returns:
233
+ ToolResult: Result containing the generated PDF file as a FileArtifact.
234
+ """
235
+ if not PDF_AVAILABLE:
236
+ return ToolResult(
237
+ content="PDF generation is not available. Please install reportlab: pip install reportlab"
238
+ )
239
+
240
+ try:
241
+ log_debug(f"Generating PDF file with content length: {len(content)}")
242
+
243
+ # Create PDF content in memory
244
+ buffer = io.BytesIO()
245
+ doc = SimpleDocTemplate(buffer, pagesize=letter, topMargin=1 * inch)
246
+
247
+ # Get styles
248
+ styles = getSampleStyleSheet()
249
+ title_style = styles["Title"]
250
+ normal_style = styles["Normal"]
251
+
252
+ # Build story (content elements)
253
+ story = []
254
+
255
+ if title:
256
+ story.append(Paragraph(title, title_style))
257
+ story.append(Spacer(1, 20))
258
+
259
+ # Split content into paragraphs and add to story
260
+ paragraphs = content.split("\n\n")
261
+ for para in paragraphs:
262
+ if para.strip():
263
+ # Clean the paragraph text for PDF
264
+ clean_para = para.strip().replace("<", "&lt;").replace(">", "&gt;")
265
+ story.append(Paragraph(clean_para, normal_style))
266
+ story.append(Spacer(1, 10))
267
+
268
+ # Build PDF
269
+ doc.build(story)
270
+ pdf_content = buffer.getvalue()
271
+ buffer.close()
272
+
273
+ # Generate filename if not provided
274
+ if not filename:
275
+ filename = f"generated_file_{str(uuid4())[:8]}.pdf"
276
+ elif not filename.endswith(".pdf"):
277
+ filename += ".pdf"
278
+
279
+ # Save file to disk (if output_directory is set)
280
+ file_path = self._save_file_to_disk(pdf_content, filename)
281
+
282
+ # Create FileArtifact
283
+ file_artifact = File(
284
+ id=str(uuid4()),
285
+ content=pdf_content,
286
+ mime_type="application/pdf",
287
+ file_type="pdf",
288
+ filename=filename,
289
+ size=len(pdf_content),
290
+ filepath=file_path if file_path else None,
291
+ )
292
+
293
+ log_debug("PDF file generated successfully")
294
+ success_msg = f"PDF file '{filename}' has been generated successfully with {len(pdf_content)} bytes."
295
+ if file_path:
296
+ success_msg += f" File saved to: {file_path}"
297
+ else:
298
+ success_msg += " File is available in response."
299
+
300
+ return ToolResult(content=success_msg, files=[file_artifact])
301
+
302
+ except Exception as e:
303
+ logger.error(f"Failed to generate PDF file: {e}")
304
+ return ToolResult(content=f"Error generating PDF file: {e}")
305
+
306
+ def generate_text_file(self, content: str, filename: Optional[str] = None) -> ToolResult:
307
+ """Generate a text file from the provided content.
308
+
309
+ Args:
310
+ content: The text content to write to the file.
311
+ filename: Optional filename for the generated file. If not provided, a UUID will be used.
312
+
313
+ Returns:
314
+ ToolResult: Result containing the generated text file as a FileArtifact.
315
+ """
316
+ try:
317
+ log_debug(f"Generating text file with content length: {len(content)}")
318
+
319
+ # Generate filename if not provided
320
+ if not filename:
321
+ filename = f"generated_file_{str(uuid4())[:8]}.txt"
322
+ elif not filename.endswith(".txt"):
323
+ filename += ".txt"
324
+
325
+ # Save file to disk (if output_directory is set)
326
+ file_path = self._save_file_to_disk(content, filename)
327
+
328
+ # Create FileArtifact
329
+ file_artifact = File(
330
+ id=str(uuid4()),
331
+ content=content,
332
+ mime_type="text/plain",
333
+ file_type="txt",
334
+ filename=filename,
335
+ size=len(content.encode("utf-8")),
336
+ filepath=file_path if file_path else None,
337
+ )
338
+
339
+ log_debug("Text file generated successfully")
340
+ success_msg = f"Text file '{filename}' has been generated successfully with {len(content)} characters."
341
+ if file_path:
342
+ success_msg += f" File saved to: {file_path}"
343
+ else:
344
+ success_msg += " File is available in response."
345
+
346
+ return ToolResult(content=success_msg, files=[file_artifact])
347
+
348
+ except Exception as e:
349
+ logger.error(f"Failed to generate text file: {e}")
350
+ return ToolResult(content=f"Error generating text file: {e}")
agno/tools/firecrawl.py CHANGED
@@ -3,7 +3,7 @@ from os import getenv
3
3
  from typing import Any, Dict, List, Optional
4
4
 
5
5
  from agno.tools import Toolkit
6
- from agno.utils.log import logger
6
+ from agno.utils.log import log_error
7
7
 
8
8
  try:
9
9
  from firecrawl import FirecrawlApp # type: ignore[attr-defined]
@@ -57,7 +57,7 @@ class FirecrawlTools(Toolkit):
57
57
  ):
58
58
  self.api_key: Optional[str] = api_key or getenv("FIRECRAWL_API_KEY")
59
59
  if not self.api_key:
60
- logger.error("FIRECRAWL_API_KEY not set. Please set the FIRECRAWL_API_KEY environment variable.")
60
+ log_error("FIRECRAWL_API_KEY not set. Please set the FIRECRAWL_API_KEY environment variable.")
61
61
 
62
62
  self.formats: Optional[List[str]] = formats
63
63
  self.limit: int = limit
@@ -73,7 +73,7 @@ class FirecrawlTools(Toolkit):
73
73
  if all or enable_mapping:
74
74
  tools.append(self.map_website)
75
75
  if all or enable_search:
76
- tools.append(self.search)
76
+ tools.append(self.search_web)
77
77
 
78
78
  super().__init__(name="firecrawl_tools", tools=tools, **kwargs)
79
79
 
@@ -121,7 +121,7 @@ class FirecrawlTools(Toolkit):
121
121
  map_result = self.app.map(url)
122
122
  return json.dumps(map_result.model_dump(), cls=CustomJSONEncoder)
123
123
 
124
- def search(self, query: str, limit: Optional[int] = None):
124
+ def search_web(self, query: str, limit: Optional[int] = None):
125
125
  """Use this function to search for the web using Firecrawl.
126
126
 
127
127
  Args: