agno 2.0.5__py3-none-any.whl → 2.0.7__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 (68) hide show
  1. agno/agent/agent.py +67 -17
  2. agno/db/dynamo/dynamo.py +7 -5
  3. agno/db/firestore/firestore.py +4 -2
  4. agno/db/gcs_json/gcs_json_db.py +4 -2
  5. agno/db/json/json_db.py +8 -4
  6. agno/db/mongo/mongo.py +6 -4
  7. agno/db/mysql/mysql.py +2 -1
  8. agno/db/postgres/postgres.py +2 -1
  9. agno/db/redis/redis.py +1 -1
  10. agno/db/singlestore/singlestore.py +2 -2
  11. agno/db/sqlite/sqlite.py +1 -1
  12. agno/knowledge/chunking/semantic.py +33 -6
  13. agno/knowledge/embedder/openai.py +19 -11
  14. agno/knowledge/knowledge.py +4 -3
  15. agno/knowledge/reader/website_reader.py +33 -16
  16. agno/media.py +72 -0
  17. agno/models/aimlapi/aimlapi.py +2 -2
  18. agno/models/base.py +68 -12
  19. agno/models/cerebras/cerebras_openai.py +2 -2
  20. agno/models/deepinfra/deepinfra.py +2 -2
  21. agno/models/deepseek/deepseek.py +2 -2
  22. agno/models/fireworks/fireworks.py +2 -2
  23. agno/models/internlm/internlm.py +2 -2
  24. agno/models/langdb/langdb.py +4 -4
  25. agno/models/litellm/litellm_openai.py +2 -2
  26. agno/models/llama_cpp/__init__.py +5 -0
  27. agno/models/llama_cpp/llama_cpp.py +22 -0
  28. agno/models/message.py +26 -0
  29. agno/models/meta/llama_openai.py +2 -2
  30. agno/models/nebius/nebius.py +2 -2
  31. agno/models/nexus/__init__.py +3 -0
  32. agno/models/nexus/nexus.py +22 -0
  33. agno/models/nvidia/nvidia.py +2 -2
  34. agno/models/openrouter/openrouter.py +2 -2
  35. agno/models/perplexity/perplexity.py +2 -2
  36. agno/models/portkey/portkey.py +3 -3
  37. agno/models/response.py +2 -1
  38. agno/models/sambanova/sambanova.py +2 -2
  39. agno/models/together/together.py +2 -2
  40. agno/models/vercel/v0.py +2 -2
  41. agno/models/xai/xai.py +2 -2
  42. agno/os/app.py +4 -10
  43. agno/os/router.py +3 -2
  44. agno/os/routers/evals/evals.py +1 -1
  45. agno/os/routers/memory/memory.py +1 -1
  46. agno/os/schema.py +3 -4
  47. agno/os/utils.py +47 -12
  48. agno/run/agent.py +20 -0
  49. agno/run/team.py +18 -1
  50. agno/run/workflow.py +10 -0
  51. agno/team/team.py +58 -18
  52. agno/tools/decorator.py +4 -2
  53. agno/tools/e2b.py +14 -7
  54. agno/tools/file_generation.py +350 -0
  55. agno/tools/function.py +2 -0
  56. agno/tools/mcp.py +1 -1
  57. agno/tools/memori.py +1 -53
  58. agno/utils/events.py +7 -1
  59. agno/utils/gemini.py +24 -4
  60. agno/vectordb/chroma/chromadb.py +66 -25
  61. agno/vectordb/lancedb/lance_db.py +15 -4
  62. agno/vectordb/milvus/milvus.py +6 -0
  63. agno/workflow/workflow.py +32 -0
  64. {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/METADATA +4 -1
  65. {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/RECORD +68 -63
  66. {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/WHEEL +0 -0
  67. {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/licenses/LICENSE +0 -0
  68. {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/top_level.txt +0 -0
agno/team/team.py CHANGED
@@ -829,6 +829,7 @@ class Team:
829
829
  functions=self._functions_for_model,
830
830
  tool_choice=self.tool_choice,
831
831
  tool_call_limit=self.tool_call_limit,
832
+ send_media_to_model=self.send_media_to_model,
832
833
  )
833
834
 
834
835
  # Check for cancellation after model call
@@ -1121,13 +1122,17 @@ class Team:
1121
1122
  # Initialize Team
1122
1123
  self.initialize_team(debug_mode=debug_mode)
1123
1124
 
1124
- image_artifacts, video_artifacts, audio_artifacts = self._validate_media_object_id(
1125
- images=images, videos=videos, audios=audio
1125
+ image_artifacts, video_artifacts, audio_artifacts, file_artifacts = self._validate_media_object_id(
1126
+ images=images, videos=videos, audios=audio, files=files
1126
1127
  )
1127
1128
 
1128
1129
  # Create RunInput to capture the original user input
1129
1130
  run_input = TeamRunInput(
1130
- input_content=input, images=image_artifacts, videos=video_artifacts, audios=audio_artifacts, files=files
1131
+ input_content=input,
1132
+ images=image_artifacts,
1133
+ videos=video_artifacts,
1134
+ audios=audio_artifacts,
1135
+ files=file_artifacts,
1131
1136
  )
1132
1137
 
1133
1138
  # Read existing session from database
@@ -1411,6 +1416,7 @@ class Team:
1411
1416
  tool_choice=self.tool_choice,
1412
1417
  tool_call_limit=self.tool_call_limit,
1413
1418
  response_format=response_format,
1419
+ send_media_to_model=self.send_media_to_model,
1414
1420
  ) # type: ignore
1415
1421
 
1416
1422
  # Check for cancellation after model call
@@ -1739,13 +1745,17 @@ class Team:
1739
1745
  # Initialize Team
1740
1746
  self.initialize_team(debug_mode=debug_mode)
1741
1747
 
1742
- image_artifacts, video_artifacts, audio_artifacts = self._validate_media_object_id(
1743
- images=images, videos=videos, audios=audio
1748
+ image_artifacts, video_artifacts, audio_artifacts, file_artifacts = self._validate_media_object_id(
1749
+ images=images, videos=videos, audios=audio, files=files
1744
1750
  )
1745
1751
 
1746
1752
  # Create RunInput to capture the original user input
1747
1753
  run_input = TeamRunInput(
1748
- input_content=input, images=image_artifacts, videos=video_artifacts, audios=audio_artifacts, files=files
1754
+ input_content=input,
1755
+ images=image_artifacts,
1756
+ videos=video_artifacts,
1757
+ audios=audio_artifacts,
1758
+ files=file_artifacts,
1749
1759
  )
1750
1760
 
1751
1761
  team_session = self._read_or_create_session(session_id=session_id, user_id=user_id)
@@ -1963,6 +1973,10 @@ class Team:
1963
1973
  for audio in model_response.audios:
1964
1974
  self._add_audio(audio, run_response) # Generated audio go to run_response.audio
1965
1975
 
1976
+ if model_response.files is not None:
1977
+ for file in model_response.files:
1978
+ self._add_file(file, run_response) # Generated files go to run_response.files
1979
+
1966
1980
  def _update_run_response(
1967
1981
  self, model_response: ModelResponse, run_response: TeamRunOutput, run_messages: RunMessages
1968
1982
  ):
@@ -1985,7 +1999,9 @@ class Team:
1985
1999
  run_response.reasoning_content = model_response.reasoning_content
1986
2000
  else:
1987
2001
  run_response.reasoning_content += model_response.reasoning_content
1988
-
2002
+ # Update provider data
2003
+ if model_response.provider_data is not None:
2004
+ run_response.model_provider_data = model_response.provider_data
1989
2005
  # Update citations
1990
2006
  if model_response.citations is not None:
1991
2007
  run_response.citations = model_response.citations
@@ -2049,6 +2065,7 @@ class Team:
2049
2065
  tool_choice=self.tool_choice,
2050
2066
  tool_call_limit=self.tool_call_limit,
2051
2067
  stream_model_response=stream_model_response,
2068
+ send_media_to_model=self.send_media_to_model,
2052
2069
  ):
2053
2070
  yield from self._handle_model_response_chunk(
2054
2071
  session=session,
@@ -2071,6 +2088,8 @@ class Team:
2071
2088
  run_response.response_audio = full_model_response.audio
2072
2089
  if full_model_response.citations is not None:
2073
2090
  run_response.citations = full_model_response.citations
2091
+ if full_model_response.provider_data is not None:
2092
+ run_response.model_provider_data = full_model_response.provider_data
2074
2093
 
2075
2094
  if stream_intermediate_steps and reasoning_state["reasoning_started"]:
2076
2095
  all_reasoning_steps: List[ReasoningStep] = []
@@ -2129,6 +2148,7 @@ class Team:
2129
2148
  tool_choice=self.tool_choice,
2130
2149
  tool_call_limit=self.tool_call_limit,
2131
2150
  stream_model_response=stream_model_response,
2151
+ send_media_to_model=self.send_media_to_model,
2132
2152
  ) # type: ignore
2133
2153
  async for model_response_event in model_stream:
2134
2154
  for event in self._handle_model_response_chunk(
@@ -2158,6 +2178,8 @@ class Team:
2158
2178
  run_response.response_audio = full_model_response.audio
2159
2179
  if full_model_response.citations is not None:
2160
2180
  run_response.citations = full_model_response.citations
2181
+ if full_model_response.provider_data is not None:
2182
+ run_response.model_provider_data = full_model_response.provider_data
2161
2183
 
2162
2184
  # Build a list of messages that should be added to the RunOutput
2163
2185
  messages_for_run_response = [m for m in run_messages.messages if m.add_to_agent_memory]
@@ -2297,6 +2319,7 @@ class Team:
2297
2319
  redacted_reasoning_content=model_response_event.redacted_reasoning_content,
2298
2320
  response_audio=full_model_response.audio,
2299
2321
  citations=model_response_event.citations,
2322
+ model_provider_data=model_response_event.provider_data,
2300
2323
  image=model_response_event.images[-1] if model_response_event.images else None,
2301
2324
  ),
2302
2325
  run_response,
@@ -3118,6 +3141,7 @@ class Team:
3118
3141
  images: Optional[Sequence[Image]] = None,
3119
3142
  videos: Optional[Sequence[Video]] = None,
3120
3143
  audios: Optional[Sequence[Audio]] = None,
3144
+ files: Optional[Sequence[File]] = None,
3121
3145
  ) -> tuple:
3122
3146
  image_list = None
3123
3147
  if images:
@@ -3149,7 +3173,17 @@ class Team:
3149
3173
  aud.id = str(uuid4())
3150
3174
  audio_list.append(aud)
3151
3175
 
3152
- return image_list, video_list, audio_list
3176
+ file_list = None
3177
+ if files:
3178
+ file_list = []
3179
+ for file in files:
3180
+ if not file.id:
3181
+ from uuid import uuid4
3182
+
3183
+ file.id = str(uuid4())
3184
+ file_list.append(file)
3185
+
3186
+ return image_list, video_list, audio_list, file_list
3153
3187
 
3154
3188
  def cli_app(
3155
3189
  self,
@@ -5056,20 +5090,19 @@ class Team:
5056
5090
  import json
5057
5091
 
5058
5092
  history: List[Dict[str, Any]] = []
5059
- if session is not None:
5060
- all_chats = self.get_messages_for_session(session_id=session.session_id)
5061
5093
 
5062
- if len(all_chats) == 0:
5063
- return ""
5094
+ all_chats = session.get_messages_from_last_n_runs(
5095
+ team_id=self.id,
5096
+ )
5064
5097
 
5065
- for chat in all_chats[::-1]: # type: ignore
5066
- history.insert(0, chat.to_dict()) # type: ignore
5098
+ if len(all_chats) == 0:
5099
+ return ""
5067
5100
 
5068
- if num_chats is not None:
5069
- history = history[:num_chats]
5101
+ for chat in all_chats[::-1]: # type: ignore
5102
+ history.insert(0, chat.to_dict()) # type: ignore
5070
5103
 
5071
- else:
5072
- return ""
5104
+ if num_chats is not None:
5105
+ history = history[:num_chats]
5073
5106
 
5074
5107
  return json.dumps(history)
5075
5108
 
@@ -6259,6 +6292,13 @@ class Team:
6259
6292
  run_response.audio = []
6260
6293
  run_response.audio.append(audio)
6261
6294
 
6295
+ def _add_file(self, file: File, run_response: TeamRunOutput) -> None:
6296
+ """Add file to both the agent's stateful storage and the current run response"""
6297
+ # Add to run response
6298
+ if run_response.files is None:
6299
+ run_response.files = []
6300
+ run_response.files.append(file)
6301
+
6262
6302
  def _update_reasoning_content_from_tool_call(
6263
6303
  self, run_response: TeamRunOutput, tool_name: str, tool_args: Dict[str, Any]
6264
6304
  ) -> Optional[ReasoningStep]:
agno/tools/decorator.py CHANGED
@@ -250,8 +250,10 @@ def tool(*args, **kwargs) -> Union[Function, Callable[[F], Function]]:
250
250
  if kwargs.get("stop_after_tool_call") is True:
251
251
  if "show_result" not in kwargs or kwargs.get("show_result") is None:
252
252
  tool_config["show_result"] = True
253
-
254
- return Function(**tool_config)
253
+ function = Function(**tool_config)
254
+ # Determine parameters for the function
255
+ function.process_entrypoint()
256
+ return function
255
257
 
256
258
  # Handle both @tool and @tool() cases
257
259
  if len(args) == 1 and callable(args[0]) and not kwargs:
agno/tools/e2b.py CHANGED
@@ -464,7 +464,7 @@ class E2BTools(Toolkit):
464
464
 
465
465
  result = f"Contents of {directory_path}:\n"
466
466
  for file in files:
467
- file_type = "Directory" if file.is_dir else "File"
467
+ file_type = "Directory" if file.type == "directory" else "File"
468
468
  size = f"{file.size} bytes" if file.size is not None else "Unknown size"
469
469
  result += f"- {file.name} ({file_type}, {size})\n"
470
470
 
@@ -486,12 +486,19 @@ class E2BTools(Toolkit):
486
486
  try:
487
487
  content = self.sandbox.files.read(file_path)
488
488
 
489
- # Try to decode as text if encoding is provided
490
- try:
491
- text_content = content.decode(encoding)
492
- return text_content
493
- except UnicodeDecodeError:
494
- return f"File read successfully but contains binary data ({len(content)} bytes). Use download_file_from_sandbox to save it."
489
+ # Check if content is already a string or if it's bytes that need decoding
490
+ if isinstance(content, str):
491
+ return content
492
+ elif isinstance(content, bytes):
493
+ # Try to decode as text if encoding is provided
494
+ try:
495
+ text_content = content.decode(encoding)
496
+ return text_content
497
+ except UnicodeDecodeError:
498
+ return f"File read successfully but contains binary data ({len(content)} bytes). Use download_file_from_sandbox to save it."
499
+ else:
500
+ # Handle unexpected content type
501
+ return f"Unexpected content type: {type(content)}. Expected str or bytes."
495
502
 
496
503
  except Exception as e:
497
504
  return json.dumps({"status": "error", "message": f"Error reading file: {str(e)}"})
@@ -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
+ url=f"file://{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
+ url=f"file://{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
+ url=f"file://{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
+ url=f"file://{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/function.py CHANGED
@@ -485,6 +485,7 @@ class FunctionExecutionResult(BaseModel):
485
485
  images: Optional[List[Image]] = None
486
486
  videos: Optional[List[Video]] = None
487
487
  audios: Optional[List[Audio]] = None
488
+ files: Optional[List[File]] = None
488
489
 
489
490
 
490
491
  class FunctionCall(BaseModel):
@@ -965,3 +966,4 @@ class ToolResult(BaseModel):
965
966
  images: Optional[List[Image]] = None
966
967
  videos: Optional[List[Video]] = None
967
968
  audios: Optional[List[Audio]] = None
969
+ files: Optional[List[File]] = None
agno/tools/mcp.py CHANGED
@@ -102,7 +102,7 @@ class MCPTools(Toolkit):
102
102
  transport: Literal["stdio", "sse", "streamable-http"] = "stdio",
103
103
  server_params: Optional[Union[StdioServerParameters, SSEClientParams, StreamableHTTPClientParams]] = None,
104
104
  session: Optional[ClientSession] = None,
105
- timeout_seconds: int = 5,
105
+ timeout_seconds: int = 10,
106
106
  client=None,
107
107
  include_tools: Optional[list[str]] = None,
108
108
  exclude_tools: Optional[list[str]] = None,