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.
- agno/agent/agent.py +67 -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/chunking/semantic.py +33 -6
- agno/knowledge/embedder/openai.py +19 -11
- agno/knowledge/knowledge.py +4 -3
- agno/knowledge/reader/website_reader.py +33 -16
- agno/media.py +72 -0
- agno/models/aimlapi/aimlapi.py +2 -2
- agno/models/base.py +68 -12
- 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/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- 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 +22 -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/app.py +4 -10
- agno/os/router.py +3 -2
- agno/os/routers/evals/evals.py +1 -1
- agno/os/routers/memory/memory.py +1 -1
- agno/os/schema.py +3 -4
- agno/os/utils.py +47 -12
- agno/run/agent.py +20 -0
- agno/run/team.py +18 -1
- agno/run/workflow.py +10 -0
- agno/team/team.py +58 -18
- agno/tools/decorator.py +4 -2
- agno/tools/e2b.py +14 -7
- agno/tools/file_generation.py +350 -0
- agno/tools/function.py +2 -0
- agno/tools/mcp.py +1 -1
- agno/tools/memori.py +1 -53
- agno/utils/events.py +7 -1
- 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 +32 -0
- {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/METADATA +4 -1
- {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/RECORD +68 -63
- {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/WHEEL +0 -0
- {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/licenses/LICENSE +0 -0
- {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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
5063
|
-
|
|
5094
|
+
all_chats = session.get_messages_from_last_n_runs(
|
|
5095
|
+
team_id=self.id,
|
|
5096
|
+
)
|
|
5064
5097
|
|
|
5065
|
-
|
|
5066
|
-
|
|
5098
|
+
if len(all_chats) == 0:
|
|
5099
|
+
return ""
|
|
5067
5100
|
|
|
5068
|
-
|
|
5069
|
-
|
|
5101
|
+
for chat in all_chats[::-1]: # type: ignore
|
|
5102
|
+
history.insert(0, chat.to_dict()) # type: ignore
|
|
5070
5103
|
|
|
5071
|
-
|
|
5072
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
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 =
|
|
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,
|