agno 2.0.5__py3-none-any.whl → 2.0.6__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 (57) hide show
  1. agno/agent/agent.py +53 -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/embedder/openai.py +19 -11
  13. agno/knowledge/knowledge.py +4 -3
  14. agno/knowledge/reader/website_reader.py +33 -16
  15. agno/media.py +70 -0
  16. agno/models/aimlapi/aimlapi.py +2 -2
  17. agno/models/base.py +31 -4
  18. agno/models/cerebras/cerebras_openai.py +2 -2
  19. agno/models/deepinfra/deepinfra.py +2 -2
  20. agno/models/deepseek/deepseek.py +2 -2
  21. agno/models/fireworks/fireworks.py +2 -2
  22. agno/models/internlm/internlm.py +2 -2
  23. agno/models/langdb/langdb.py +4 -4
  24. agno/models/litellm/litellm_openai.py +2 -2
  25. agno/models/message.py +26 -0
  26. agno/models/meta/llama_openai.py +2 -2
  27. agno/models/nebius/nebius.py +2 -2
  28. agno/models/nexus/__init__.py +3 -0
  29. agno/models/nexus/nexus.py +25 -0
  30. agno/models/nvidia/nvidia.py +2 -2
  31. agno/models/openrouter/openrouter.py +2 -2
  32. agno/models/perplexity/perplexity.py +2 -2
  33. agno/models/portkey/portkey.py +3 -3
  34. agno/models/response.py +2 -1
  35. agno/models/sambanova/sambanova.py +2 -2
  36. agno/models/together/together.py +2 -2
  37. agno/models/vercel/v0.py +2 -2
  38. agno/models/xai/xai.py +2 -2
  39. agno/os/router.py +3 -1
  40. agno/os/utils.py +1 -1
  41. agno/run/agent.py +16 -0
  42. agno/run/team.py +15 -0
  43. agno/run/workflow.py +10 -0
  44. agno/team/team.py +37 -7
  45. agno/tools/e2b.py +14 -7
  46. agno/tools/file_generation.py +350 -0
  47. agno/tools/function.py +2 -0
  48. agno/utils/gemini.py +24 -4
  49. agno/vectordb/chroma/chromadb.py +66 -25
  50. agno/vectordb/lancedb/lance_db.py +15 -4
  51. agno/vectordb/milvus/milvus.py +6 -0
  52. agno/workflow/workflow.py +4 -0
  53. {agno-2.0.5.dist-info → agno-2.0.6.dist-info}/METADATA +4 -1
  54. {agno-2.0.5.dist-info → agno-2.0.6.dist-info}/RECORD +57 -54
  55. {agno-2.0.5.dist-info → agno-2.0.6.dist-info}/WHEEL +0 -0
  56. {agno-2.0.5.dist-info → agno-2.0.6.dist-info}/licenses/LICENSE +0 -0
  57. {agno-2.0.5.dist-info → agno-2.0.6.dist-info}/top_level.txt +0 -0
agno/run/agent.py CHANGED
@@ -337,6 +337,8 @@ class RunInput:
337
337
  result["videos"] = [vid.to_dict() for vid in self.videos]
338
338
  if self.audios:
339
339
  result["audios"] = [aud.to_dict() for aud in self.audios]
340
+ if self.files:
341
+ result["files"] = [file.to_dict() for file in self.files]
340
342
 
341
343
  return result
342
344
 
@@ -392,6 +394,7 @@ class RunOutput:
392
394
  images: Optional[List[Image]] = None # Images attached to the response
393
395
  videos: Optional[List[Video]] = None # Videos attached to the response
394
396
  audio: Optional[List[Audio]] = None # Audio attached to the response
397
+ files: Optional[List[File]] = None # Files attached to the response
395
398
  response_audio: Optional[Audio] = None # Model audio response
396
399
 
397
400
  # Input media and messages from user
@@ -446,6 +449,7 @@ class RunOutput:
446
449
  "images",
447
450
  "videos",
448
451
  "audio",
452
+ "files",
449
453
  "response_audio",
450
454
  "input",
451
455
  "citations",
@@ -508,6 +512,14 @@ class RunOutput:
508
512
  else:
509
513
  _dict["audio"].append(aud)
510
514
 
515
+ if self.files is not None:
516
+ _dict["files"] = []
517
+ for file in self.files:
518
+ if isinstance(file, File):
519
+ _dict["files"].append(file.to_dict())
520
+ else:
521
+ _dict["files"].append(file)
522
+
511
523
  if self.response_audio is not None:
512
524
  if isinstance(self.response_audio, Audio):
513
525
  _dict["response_audio"] = self.response_audio.to_dict()
@@ -576,6 +588,9 @@ class RunOutput:
576
588
  audio = data.pop("audio", [])
577
589
  audio = [Audio.model_validate(audio) for audio in audio] if audio else None
578
590
 
591
+ files = data.pop("files", [])
592
+ files = [File.model_validate(file) for file in files] if files else None
593
+
579
594
  response_audio = data.pop("response_audio", None)
580
595
  response_audio = Audio.model_validate(response_audio) if response_audio else None
581
596
 
@@ -613,6 +628,7 @@ class RunOutput:
613
628
  images=images,
614
629
  audio=audio,
615
630
  videos=videos,
631
+ files=files,
616
632
  response_audio=response_audio,
617
633
  input=input_obj,
618
634
  events=events,
agno/run/team.py CHANGED
@@ -321,6 +321,8 @@ class TeamRunInput:
321
321
  result["videos"] = [vid.to_dict() for vid in self.videos]
322
322
  if self.audios:
323
323
  result["audios"] = [aud.to_dict() for aud in self.audios]
324
+ if self.files:
325
+ result["files"] = [file.to_dict() for file in self.files]
324
326
 
325
327
  return result
326
328
 
@@ -370,6 +372,7 @@ class TeamRunOutput:
370
372
  images: Optional[List[Image]] = None # Images from member runs
371
373
  videos: Optional[List[Video]] = None # Videos from member runs
372
374
  audio: Optional[List[Audio]] = None # Audio from member runs
375
+ files: Optional[List[File]] = None # Files from member runs
373
376
 
374
377
  response_audio: Optional[Audio] = None # Model audio response
375
378
 
@@ -419,6 +422,7 @@ class TeamRunOutput:
419
422
  "images",
420
423
  "videos",
421
424
  "audio",
425
+ "files",
422
426
  "response_audio",
423
427
  "citations",
424
428
  "events",
@@ -461,6 +465,9 @@ class TeamRunOutput:
461
465
  if self.audio is not None:
462
466
  _dict["audio"] = [aud.to_dict() for aud in self.audio]
463
467
 
468
+ if self.files is not None:
469
+ _dict["files"] = [file.to_dict() for file in self.files]
470
+
464
471
  if self.response_audio is not None:
465
472
  _dict["response_audio"] = self.response_audio.to_dict()
466
473
 
@@ -555,6 +562,9 @@ class TeamRunOutput:
555
562
  audio = data.pop("audio", [])
556
563
  audio = [Audio.model_validate(audio) for audio in audio] if audio else None
557
564
 
565
+ files = data.pop("files", [])
566
+ files = [File.model_validate(file) for file in files] if files else None
567
+
558
568
  tools = data.pop("tools", [])
559
569
  tools = [ToolExecution.from_dict(tool) for tool in tools] if tools else None
560
570
 
@@ -584,6 +594,7 @@ class TeamRunOutput:
584
594
  images=images,
585
595
  videos=videos,
586
596
  audio=audio,
597
+ files=files,
587
598
  response_audio=response_audio,
588
599
  input=input_obj,
589
600
  citations=citations,
@@ -618,3 +629,7 @@ class TeamRunOutput:
618
629
  if self.audio is None:
619
630
  self.audio = []
620
631
  self.audio.extend(run_response.audio)
632
+ if run_response.files is not None:
633
+ if self.files is None:
634
+ self.files = []
635
+ self.files.extend(run_response.files)
agno/run/workflow.py CHANGED
@@ -462,6 +462,7 @@ def workflow_run_output_event_from_dict(data: dict) -> BaseWorkflowRunOutputEven
462
462
  class WorkflowRunOutput:
463
463
  """Response returned by Workflow.run() functions - kept for backwards compatibility"""
464
464
 
465
+ input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel]] = None
465
466
  content: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, Any]] = None
466
467
  content_type: str = "str"
467
468
 
@@ -553,6 +554,12 @@ class WorkflowRunOutput:
553
554
  if self.metrics is not None:
554
555
  _dict["metrics"] = self.metrics.to_dict()
555
556
 
557
+ if self.input is not None:
558
+ if isinstance(self.input, BaseModel):
559
+ _dict["input"] = self.input.model_dump(exclude_none=True)
560
+ else:
561
+ _dict["input"] = self.input
562
+
556
563
  if self.content and isinstance(self.content, BaseModel):
557
564
  _dict["content"] = self.content.model_dump(exclude_none=True)
558
565
 
@@ -624,6 +631,8 @@ class WorkflowRunOutput:
624
631
  final_events.append(event)
625
632
  events = final_events
626
633
 
634
+ input_data = data.pop("input", None)
635
+
627
636
  return cls(
628
637
  step_results=parsed_step_results,
629
638
  metadata=metadata,
@@ -634,6 +643,7 @@ class WorkflowRunOutput:
634
643
  events=events,
635
644
  metrics=workflow_metrics,
636
645
  step_executor_runs=step_executor_runs,
646
+ input=input_data,
637
647
  **data,
638
648
  )
639
649
 
agno/team/team.py CHANGED
@@ -1121,13 +1121,17 @@ class Team:
1121
1121
  # Initialize Team
1122
1122
  self.initialize_team(debug_mode=debug_mode)
1123
1123
 
1124
- image_artifacts, video_artifacts, audio_artifacts = self._validate_media_object_id(
1125
- images=images, videos=videos, audios=audio
1124
+ image_artifacts, video_artifacts, audio_artifacts, file_artifacts = self._validate_media_object_id(
1125
+ images=images, videos=videos, audios=audio, files=files
1126
1126
  )
1127
1127
 
1128
1128
  # Create RunInput to capture the original user input
1129
1129
  run_input = TeamRunInput(
1130
- input_content=input, images=image_artifacts, videos=video_artifacts, audios=audio_artifacts, files=files
1130
+ input_content=input,
1131
+ images=image_artifacts,
1132
+ videos=video_artifacts,
1133
+ audios=audio_artifacts,
1134
+ files=file_artifacts,
1131
1135
  )
1132
1136
 
1133
1137
  # Read existing session from database
@@ -1739,13 +1743,17 @@ class Team:
1739
1743
  # Initialize Team
1740
1744
  self.initialize_team(debug_mode=debug_mode)
1741
1745
 
1742
- image_artifacts, video_artifacts, audio_artifacts = self._validate_media_object_id(
1743
- images=images, videos=videos, audios=audio
1746
+ image_artifacts, video_artifacts, audio_artifacts, file_artifacts = self._validate_media_object_id(
1747
+ images=images, videos=videos, audios=audio, files=files
1744
1748
  )
1745
1749
 
1746
1750
  # Create RunInput to capture the original user input
1747
1751
  run_input = TeamRunInput(
1748
- input_content=input, images=image_artifacts, videos=video_artifacts, audios=audio_artifacts, files=files
1752
+ input_content=input,
1753
+ images=image_artifacts,
1754
+ videos=video_artifacts,
1755
+ audios=audio_artifacts,
1756
+ files=file_artifacts,
1749
1757
  )
1750
1758
 
1751
1759
  team_session = self._read_or_create_session(session_id=session_id, user_id=user_id)
@@ -1963,6 +1971,10 @@ class Team:
1963
1971
  for audio in model_response.audios:
1964
1972
  self._add_audio(audio, run_response) # Generated audio go to run_response.audio
1965
1973
 
1974
+ if model_response.files is not None:
1975
+ for file in model_response.files:
1976
+ self._add_file(file, run_response) # Generated files go to run_response.files
1977
+
1966
1978
  def _update_run_response(
1967
1979
  self, model_response: ModelResponse, run_response: TeamRunOutput, run_messages: RunMessages
1968
1980
  ):
@@ -3118,6 +3130,7 @@ class Team:
3118
3130
  images: Optional[Sequence[Image]] = None,
3119
3131
  videos: Optional[Sequence[Video]] = None,
3120
3132
  audios: Optional[Sequence[Audio]] = None,
3133
+ files: Optional[Sequence[File]] = None,
3121
3134
  ) -> tuple:
3122
3135
  image_list = None
3123
3136
  if images:
@@ -3149,7 +3162,17 @@ class Team:
3149
3162
  aud.id = str(uuid4())
3150
3163
  audio_list.append(aud)
3151
3164
 
3152
- return image_list, video_list, audio_list
3165
+ file_list = None
3166
+ if files:
3167
+ file_list = []
3168
+ for file in files:
3169
+ if not file.id:
3170
+ from uuid import uuid4
3171
+
3172
+ file.id = str(uuid4())
3173
+ file_list.append(file)
3174
+
3175
+ return image_list, video_list, audio_list, file_list
3153
3176
 
3154
3177
  def cli_app(
3155
3178
  self,
@@ -6259,6 +6282,13 @@ class Team:
6259
6282
  run_response.audio = []
6260
6283
  run_response.audio.append(audio)
6261
6284
 
6285
+ def _add_file(self, file: File, run_response: TeamRunOutput) -> None:
6286
+ """Add file to both the agent's stateful storage and the current run response"""
6287
+ # Add to run response
6288
+ if run_response.files is None:
6289
+ run_response.files = []
6290
+ run_response.files.append(file)
6291
+
6262
6292
  def _update_reasoning_content_from_tool_call(
6263
6293
  self, run_response: TeamRunOutput, tool_name: str, tool_args: Dict[str, Any]
6264
6294
  ) -> Optional[ReasoningStep]:
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