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.
- agno/agent/agent.py +53 -17
- agno/db/dynamo/dynamo.py +7 -5
- agno/db/firestore/firestore.py +4 -2
- agno/db/gcs_json/gcs_json_db.py +4 -2
- agno/db/json/json_db.py +8 -4
- agno/db/mongo/mongo.py +6 -4
- agno/db/mysql/mysql.py +2 -1
- agno/db/postgres/postgres.py +2 -1
- agno/db/redis/redis.py +1 -1
- agno/db/singlestore/singlestore.py +2 -2
- agno/db/sqlite/sqlite.py +1 -1
- agno/knowledge/embedder/openai.py +19 -11
- agno/knowledge/knowledge.py +4 -3
- agno/knowledge/reader/website_reader.py +33 -16
- agno/media.py +70 -0
- agno/models/aimlapi/aimlapi.py +2 -2
- agno/models/base.py +31 -4
- agno/models/cerebras/cerebras_openai.py +2 -2
- agno/models/deepinfra/deepinfra.py +2 -2
- agno/models/deepseek/deepseek.py +2 -2
- agno/models/fireworks/fireworks.py +2 -2
- agno/models/internlm/internlm.py +2 -2
- agno/models/langdb/langdb.py +4 -4
- agno/models/litellm/litellm_openai.py +2 -2
- agno/models/message.py +26 -0
- agno/models/meta/llama_openai.py +2 -2
- agno/models/nebius/nebius.py +2 -2
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +25 -0
- agno/models/nvidia/nvidia.py +2 -2
- agno/models/openrouter/openrouter.py +2 -2
- agno/models/perplexity/perplexity.py +2 -2
- agno/models/portkey/portkey.py +3 -3
- agno/models/response.py +2 -1
- agno/models/sambanova/sambanova.py +2 -2
- agno/models/together/together.py +2 -2
- agno/models/vercel/v0.py +2 -2
- agno/models/xai/xai.py +2 -2
- agno/os/router.py +3 -1
- agno/os/utils.py +1 -1
- agno/run/agent.py +16 -0
- agno/run/team.py +15 -0
- agno/run/workflow.py +10 -0
- agno/team/team.py +37 -7
- agno/tools/e2b.py +14 -7
- agno/tools/file_generation.py +350 -0
- agno/tools/function.py +2 -0
- agno/utils/gemini.py +24 -4
- agno/vectordb/chroma/chromadb.py +66 -25
- agno/vectordb/lancedb/lance_db.py +15 -4
- agno/vectordb/milvus/milvus.py +6 -0
- agno/workflow/workflow.py +4 -0
- {agno-2.0.5.dist-info → agno-2.0.6.dist-info}/METADATA +4 -1
- {agno-2.0.5.dist-info → agno-2.0.6.dist-info}/RECORD +57 -54
- {agno-2.0.5.dist-info → agno-2.0.6.dist-info}/WHEEL +0 -0
- {agno-2.0.5.dist-info → agno-2.0.6.dist-info}/licenses/LICENSE +0 -0
- {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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
#
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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("<", "<").replace(">", ">")
|
|
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
|