agno 2.2.4__py3-none-any.whl → 2.2.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 +82 -19
- agno/culture/manager.py +3 -4
- agno/knowledge/chunking/agentic.py +6 -2
- agno/memory/manager.py +9 -4
- agno/models/anthropic/claude.py +1 -2
- agno/models/azure/ai_foundry.py +31 -14
- agno/models/azure/openai_chat.py +12 -4
- agno/models/base.py +44 -11
- agno/models/cerebras/cerebras.py +11 -6
- agno/models/groq/groq.py +7 -4
- agno/models/meta/llama.py +12 -6
- agno/models/meta/llama_openai.py +5 -1
- agno/models/openai/chat.py +20 -12
- agno/models/openai/responses.py +10 -5
- agno/models/utils.py +254 -8
- agno/models/vertexai/claude.py +9 -13
- agno/os/app.py +48 -21
- agno/os/routers/evals/evals.py +8 -8
- agno/os/routers/evals/utils.py +1 -0
- agno/os/schema.py +48 -33
- agno/os/utils.py +27 -0
- agno/run/agent.py +5 -0
- agno/run/team.py +2 -0
- agno/run/workflow.py +39 -0
- agno/session/summary.py +8 -2
- agno/session/workflow.py +4 -3
- agno/team/team.py +50 -14
- agno/tools/file.py +153 -25
- agno/tools/function.py +5 -1
- agno/tools/notion.py +201 -0
- agno/utils/events.py +2 -0
- agno/utils/print_response/workflow.py +115 -16
- agno/vectordb/milvus/milvus.py +5 -0
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +298 -0
- agno/workflow/workflow.py +929 -64
- {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/METADATA +4 -1
- {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/RECORD +41 -39
- {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/WHEEL +0 -0
- {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/top_level.txt +0 -0
agno/team/team.py
CHANGED
|
@@ -45,6 +45,7 @@ from agno.models.base import Model
|
|
|
45
45
|
from agno.models.message import Message, MessageReferences
|
|
46
46
|
from agno.models.metrics import Metrics
|
|
47
47
|
from agno.models.response import ModelResponse, ModelResponseEvent
|
|
48
|
+
from agno.models.utils import get_model
|
|
48
49
|
from agno.reasoning.step import NextAction, ReasoningStep, ReasoningSteps
|
|
49
50
|
from agno.run.agent import RunEvent, RunOutput, RunOutputEvent
|
|
50
51
|
from agno.run.base import RunStatus
|
|
@@ -437,7 +438,7 @@ class Team:
|
|
|
437
438
|
self,
|
|
438
439
|
members: List[Union[Agent, "Team"]],
|
|
439
440
|
id: Optional[str] = None,
|
|
440
|
-
model: Optional[Model] = None,
|
|
441
|
+
model: Optional[Union[Model, str]] = None,
|
|
441
442
|
name: Optional[str] = None,
|
|
442
443
|
role: Optional[str] = None,
|
|
443
444
|
respond_directly: bool = False,
|
|
@@ -498,9 +499,9 @@ class Team:
|
|
|
498
499
|
post_hooks: Optional[Union[List[Callable[..., Any]], List[BaseGuardrail]]] = None,
|
|
499
500
|
input_schema: Optional[Type[BaseModel]] = None,
|
|
500
501
|
output_schema: Optional[Type[BaseModel]] = None,
|
|
501
|
-
parser_model: Optional[Model] = None,
|
|
502
|
+
parser_model: Optional[Union[Model, str]] = None,
|
|
502
503
|
parser_model_prompt: Optional[str] = None,
|
|
503
|
-
output_model: Optional[Model] = None,
|
|
504
|
+
output_model: Optional[Union[Model, str]] = None,
|
|
504
505
|
output_model_prompt: Optional[str] = None,
|
|
505
506
|
use_json_mode: bool = False,
|
|
506
507
|
parse_response: bool = True,
|
|
@@ -514,7 +515,7 @@ class Team:
|
|
|
514
515
|
add_session_summary_to_context: Optional[bool] = None,
|
|
515
516
|
metadata: Optional[Dict[str, Any]] = None,
|
|
516
517
|
reasoning: bool = False,
|
|
517
|
-
reasoning_model: Optional[Model] = None,
|
|
518
|
+
reasoning_model: Optional[Union[Model, str]] = None,
|
|
518
519
|
reasoning_agent: Optional[Agent] = None,
|
|
519
520
|
reasoning_min_steps: int = 1,
|
|
520
521
|
reasoning_max_steps: int = 10,
|
|
@@ -535,7 +536,7 @@ class Team:
|
|
|
535
536
|
):
|
|
536
537
|
self.members = members
|
|
537
538
|
|
|
538
|
-
self.model = model
|
|
539
|
+
self.model = model # type: ignore[assignment]
|
|
539
540
|
|
|
540
541
|
self.name = name
|
|
541
542
|
self.id = id
|
|
@@ -618,9 +619,9 @@ class Team:
|
|
|
618
619
|
|
|
619
620
|
self.input_schema = input_schema
|
|
620
621
|
self.output_schema = output_schema
|
|
621
|
-
self.parser_model = parser_model
|
|
622
|
+
self.parser_model = parser_model # type: ignore[assignment]
|
|
622
623
|
self.parser_model_prompt = parser_model_prompt
|
|
623
|
-
self.output_model = output_model
|
|
624
|
+
self.output_model = output_model # type: ignore[assignment]
|
|
624
625
|
self.output_model_prompt = output_model_prompt
|
|
625
626
|
self.use_json_mode = use_json_mode
|
|
626
627
|
self.parse_response = parse_response
|
|
@@ -637,7 +638,7 @@ class Team:
|
|
|
637
638
|
self.metadata = metadata
|
|
638
639
|
|
|
639
640
|
self.reasoning = reasoning
|
|
640
|
-
self.reasoning_model = reasoning_model
|
|
641
|
+
self.reasoning_model = reasoning_model # type: ignore[assignment]
|
|
641
642
|
self.reasoning_agent = reasoning_agent
|
|
642
643
|
self.reasoning_min_steps = reasoning_min_steps
|
|
643
644
|
self.reasoning_max_steps = reasoning_max_steps
|
|
@@ -694,6 +695,8 @@ class Team:
|
|
|
694
695
|
# Lazy-initialized shared thread pool executor for background tasks (memory, cultural knowledge, etc.)
|
|
695
696
|
self._background_executor: Optional[Any] = None
|
|
696
697
|
|
|
698
|
+
self._resolve_models()
|
|
699
|
+
|
|
697
700
|
@property
|
|
698
701
|
def background_executor(self) -> Any:
|
|
699
702
|
"""Lazy initialization of shared thread pool executor for background tasks.
|
|
@@ -902,6 +905,17 @@ class Team:
|
|
|
902
905
|
"""Return True if the db the team is equipped with is an Async implementation"""
|
|
903
906
|
return self.db is not None and isinstance(self.db, AsyncBaseDb)
|
|
904
907
|
|
|
908
|
+
def _resolve_models(self) -> None:
|
|
909
|
+
"""Resolve model strings to Model instances."""
|
|
910
|
+
if self.model is not None:
|
|
911
|
+
self.model = get_model(self.model)
|
|
912
|
+
if self.reasoning_model is not None:
|
|
913
|
+
self.reasoning_model = get_model(self.reasoning_model)
|
|
914
|
+
if self.parser_model is not None:
|
|
915
|
+
self.parser_model = get_model(self.parser_model)
|
|
916
|
+
if self.output_model is not None:
|
|
917
|
+
self.output_model = get_model(self.output_model)
|
|
918
|
+
|
|
905
919
|
def initialize_team(self, debug_mode: Optional[bool] = None) -> None:
|
|
906
920
|
# Make sure for the team, we are using the team logger
|
|
907
921
|
use_team_logger()
|
|
@@ -1593,6 +1607,7 @@ class Team:
|
|
|
1593
1607
|
tools=_tools,
|
|
1594
1608
|
response_format=response_format,
|
|
1595
1609
|
stream_events=stream_events,
|
|
1610
|
+
session_state=session_state,
|
|
1596
1611
|
):
|
|
1597
1612
|
raise_if_cancelled(run_response.run_id) # type: ignore
|
|
1598
1613
|
yield event
|
|
@@ -1604,6 +1619,7 @@ class Team:
|
|
|
1604
1619
|
tools=_tools,
|
|
1605
1620
|
response_format=response_format,
|
|
1606
1621
|
stream_events=stream_events,
|
|
1622
|
+
session_state=session_state,
|
|
1607
1623
|
):
|
|
1608
1624
|
raise_if_cancelled(run_response.run_id) # type: ignore
|
|
1609
1625
|
from agno.run.team import IntermediateRunContentEvent, RunContentEvent
|
|
@@ -1932,6 +1948,7 @@ class Team:
|
|
|
1932
1948
|
team_id=self.id,
|
|
1933
1949
|
team_name=self.name,
|
|
1934
1950
|
metadata=metadata,
|
|
1951
|
+
session_state=session_state,
|
|
1935
1952
|
input=run_input,
|
|
1936
1953
|
)
|
|
1937
1954
|
|
|
@@ -2439,6 +2456,7 @@ class Team:
|
|
|
2439
2456
|
tools=_tools,
|
|
2440
2457
|
response_format=response_format,
|
|
2441
2458
|
stream_events=stream_events,
|
|
2459
|
+
session_state=session_state,
|
|
2442
2460
|
):
|
|
2443
2461
|
raise_if_cancelled(run_response.run_id) # type: ignore
|
|
2444
2462
|
yield event
|
|
@@ -2450,6 +2468,7 @@ class Team:
|
|
|
2450
2468
|
tools=_tools,
|
|
2451
2469
|
response_format=response_format,
|
|
2452
2470
|
stream_events=stream_events,
|
|
2471
|
+
session_state=session_state,
|
|
2453
2472
|
):
|
|
2454
2473
|
raise_if_cancelled(run_response.run_id) # type: ignore
|
|
2455
2474
|
from agno.run.team import IntermediateRunContentEvent, RunContentEvent
|
|
@@ -2771,6 +2790,7 @@ class Team:
|
|
|
2771
2790
|
team_id=self.id,
|
|
2772
2791
|
team_name=self.name,
|
|
2773
2792
|
metadata=metadata,
|
|
2793
|
+
session_state=session_state,
|
|
2774
2794
|
input=run_input,
|
|
2775
2795
|
)
|
|
2776
2796
|
|
|
@@ -2930,6 +2950,12 @@ class Team:
|
|
|
2930
2950
|
if model_response.audio is not None:
|
|
2931
2951
|
run_response.response_audio = model_response.audio
|
|
2932
2952
|
|
|
2953
|
+
# Update session_state with changes from model response
|
|
2954
|
+
if model_response.updated_session_state is not None and run_response.session_state is not None:
|
|
2955
|
+
from agno.utils.merge_dict import merge_dictionaries
|
|
2956
|
+
|
|
2957
|
+
merge_dictionaries(run_response.session_state, model_response.updated_session_state)
|
|
2958
|
+
|
|
2933
2959
|
# Build a list of messages that should be added to the RunOutput
|
|
2934
2960
|
messages_for_run_response = [m for m in run_messages.messages if m.add_to_agent_memory]
|
|
2935
2961
|
|
|
@@ -2954,6 +2980,7 @@ class Team:
|
|
|
2954
2980
|
tools: Optional[List[Union[Function, dict]]] = None,
|
|
2955
2981
|
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
2956
2982
|
stream_events: bool = False,
|
|
2983
|
+
session_state: Optional[Dict[str, Any]] = None,
|
|
2957
2984
|
) -> Iterator[Union[TeamRunOutputEvent, RunOutputEvent]]:
|
|
2958
2985
|
self.model = cast(Model, self.model)
|
|
2959
2986
|
|
|
@@ -2985,6 +3012,7 @@ class Team:
|
|
|
2985
3012
|
reasoning_state=reasoning_state,
|
|
2986
3013
|
stream_events=stream_events,
|
|
2987
3014
|
parse_structured_output=self.should_parse_structured_output,
|
|
3015
|
+
session_state=session_state,
|
|
2988
3016
|
)
|
|
2989
3017
|
|
|
2990
3018
|
# 3. Update TeamRunOutput
|
|
@@ -3036,6 +3064,7 @@ class Team:
|
|
|
3036
3064
|
tools: Optional[List[Union[Function, dict]]] = None,
|
|
3037
3065
|
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
3038
3066
|
stream_events: bool = False,
|
|
3067
|
+
session_state: Optional[Dict[str, Any]] = None,
|
|
3039
3068
|
) -> AsyncIterator[Union[TeamRunOutputEvent, RunOutputEvent]]:
|
|
3040
3069
|
self.model = cast(Model, self.model)
|
|
3041
3070
|
|
|
@@ -3068,6 +3097,7 @@ class Team:
|
|
|
3068
3097
|
reasoning_state=reasoning_state,
|
|
3069
3098
|
stream_events=stream_events,
|
|
3070
3099
|
parse_structured_output=self.should_parse_structured_output,
|
|
3100
|
+
session_state=session_state,
|
|
3071
3101
|
):
|
|
3072
3102
|
yield event
|
|
3073
3103
|
|
|
@@ -3122,6 +3152,7 @@ class Team:
|
|
|
3122
3152
|
reasoning_state: Optional[Dict[str, Any]] = None,
|
|
3123
3153
|
stream_events: bool = False,
|
|
3124
3154
|
parse_structured_output: bool = False,
|
|
3155
|
+
session_state: Optional[Dict[str, Any]] = None,
|
|
3125
3156
|
) -> Iterator[Union[TeamRunOutputEvent, RunOutputEvent]]:
|
|
3126
3157
|
if isinstance(model_response_event, tuple(get_args(RunOutputEvent))) or isinstance(
|
|
3127
3158
|
model_response_event, tuple(get_args(TeamRunOutputEvent))
|
|
@@ -3299,10 +3330,15 @@ class Team:
|
|
|
3299
3330
|
|
|
3300
3331
|
# If the model response is a tool_call_completed, update the existing tool call in the run_response
|
|
3301
3332
|
elif model_response_event.event == ModelResponseEvent.tool_call_completed.value:
|
|
3302
|
-
if model_response_event.updated_session_state is not None
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3333
|
+
if model_response_event.updated_session_state is not None:
|
|
3334
|
+
# Update the session_state variable that TeamRunOutput references
|
|
3335
|
+
if session_state is not None:
|
|
3336
|
+
merge_dictionaries(session_state, model_response_event.updated_session_state)
|
|
3337
|
+
# Also update the DB session object
|
|
3338
|
+
if session.session_data is not None:
|
|
3339
|
+
merge_dictionaries(
|
|
3340
|
+
session.session_data["session_state"], model_response_event.updated_session_state
|
|
3341
|
+
)
|
|
3306
3342
|
|
|
3307
3343
|
if model_response_event.images is not None:
|
|
3308
3344
|
for image in model_response_event.images:
|
|
@@ -4498,7 +4534,7 @@ class Team:
|
|
|
4498
4534
|
store_events=self.store_events,
|
|
4499
4535
|
)
|
|
4500
4536
|
else:
|
|
4501
|
-
|
|
4537
|
+
log_info(
|
|
4502
4538
|
f"Reasoning model: {reasoning_model.__class__.__name__} is not a native reasoning model, defaulting to manual Chain-of-Thought reasoning"
|
|
4503
4539
|
)
|
|
4504
4540
|
use_default_reasoning = True
|
|
@@ -4779,7 +4815,7 @@ class Team:
|
|
|
4779
4815
|
store_events=self.store_events,
|
|
4780
4816
|
)
|
|
4781
4817
|
else:
|
|
4782
|
-
|
|
4818
|
+
log_info(
|
|
4783
4819
|
f"Reasoning model: {reasoning_model.__class__.__name__} is not a native reasoning model, defaulting to manual Chain-of-Thought reasoning"
|
|
4784
4820
|
)
|
|
4785
4821
|
use_default_reasoning = True
|
agno/tools/file.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Any, List, Optional
|
|
3
|
+
from typing import Any, List, Optional, Tuple
|
|
4
4
|
|
|
5
5
|
from agno.tools import Toolkit
|
|
6
|
-
from agno.utils.log import log_debug, log_error
|
|
6
|
+
from agno.utils.log import log_debug, log_error
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class FileTools(Toolkit):
|
|
@@ -12,14 +12,26 @@ class FileTools(Toolkit):
|
|
|
12
12
|
base_dir: Optional[Path] = None,
|
|
13
13
|
enable_save_file: bool = True,
|
|
14
14
|
enable_read_file: bool = True,
|
|
15
|
+
enable_delete_file: bool = False,
|
|
15
16
|
enable_list_files: bool = True,
|
|
16
17
|
enable_search_files: bool = True,
|
|
18
|
+
enable_read_file_chunk: bool = True,
|
|
19
|
+
enable_replace_file_chunk: bool = True,
|
|
20
|
+
expose_base_directory: bool = False,
|
|
21
|
+
max_file_length: int = 10000000,
|
|
22
|
+
max_file_lines: int = 100000,
|
|
23
|
+
line_separator: str = "\n",
|
|
17
24
|
all: bool = False,
|
|
18
25
|
**kwargs,
|
|
19
26
|
):
|
|
20
27
|
self.base_dir: Path = base_dir or Path.cwd()
|
|
28
|
+
self.base_dir = self.base_dir.resolve()
|
|
21
29
|
|
|
22
30
|
tools: List[Any] = []
|
|
31
|
+
self.max_file_length = max_file_length
|
|
32
|
+
self.max_file_lines = max_file_lines
|
|
33
|
+
self.line_separator = line_separator
|
|
34
|
+
self.expose_base_directory = expose_base_directory
|
|
23
35
|
if all or enable_save_file:
|
|
24
36
|
tools.append(self.save_file)
|
|
25
37
|
if all or enable_read_file:
|
|
@@ -28,10 +40,16 @@ class FileTools(Toolkit):
|
|
|
28
40
|
tools.append(self.list_files)
|
|
29
41
|
if all or enable_search_files:
|
|
30
42
|
tools.append(self.search_files)
|
|
43
|
+
if all or enable_delete_file:
|
|
44
|
+
tools.append(self.delete_file)
|
|
45
|
+
if all or enable_read_file_chunk:
|
|
46
|
+
tools.append(self.read_file_chunk)
|
|
47
|
+
if all or enable_replace_file_chunk:
|
|
48
|
+
tools.append(self.replace_file_chunk)
|
|
31
49
|
|
|
32
50
|
super().__init__(name="file_tools", tools=tools, **kwargs)
|
|
33
51
|
|
|
34
|
-
def save_file(self, contents: str, file_name: str, overwrite: bool = True) -> str:
|
|
52
|
+
def save_file(self, contents: str, file_name: str, overwrite: bool = True, encoding: str = "utf-8") -> str:
|
|
35
53
|
"""Saves the contents to a file called `file_name` and returns the file name if successful.
|
|
36
54
|
|
|
37
55
|
:param contents: The contents to save.
|
|
@@ -40,44 +58,146 @@ class FileTools(Toolkit):
|
|
|
40
58
|
:return: The file name if successful, otherwise returns an error message.
|
|
41
59
|
"""
|
|
42
60
|
try:
|
|
43
|
-
file_path = self.
|
|
61
|
+
safe, file_path = self.check_escape(file_name)
|
|
62
|
+
if not (safe):
|
|
63
|
+
log_error(f"Attempted to save file: {file_name}")
|
|
64
|
+
return "Error saving file"
|
|
44
65
|
log_debug(f"Saving contents to {file_path}")
|
|
45
66
|
if not file_path.parent.exists():
|
|
46
67
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
68
|
if file_path.exists() and not overwrite:
|
|
48
69
|
return f"File {file_name} already exists"
|
|
49
|
-
file_path.write_text(contents)
|
|
50
|
-
|
|
70
|
+
file_path.write_text(contents, encoding=encoding)
|
|
71
|
+
log_debug(f"Saved: {file_path}")
|
|
51
72
|
return str(file_name)
|
|
52
73
|
except Exception as e:
|
|
53
74
|
log_error(f"Error saving to file: {e}")
|
|
54
75
|
return f"Error saving to file: {e}"
|
|
55
76
|
|
|
56
|
-
def
|
|
77
|
+
def read_file_chunk(self, file_name: str, start_line: int, end_line: int, encoding: str = "utf-8") -> str:
|
|
78
|
+
"""Reads the contents of the file `file_name` and returns lines from start_line to end_line.
|
|
79
|
+
|
|
80
|
+
:param file_name: The name of the file to read.
|
|
81
|
+
:param start_line: Number of first line in the returned chunk
|
|
82
|
+
:param end_line: Number of the last line in the returned chunk
|
|
83
|
+
:param encoding: Encoding to use, default - utf-8
|
|
84
|
+
|
|
85
|
+
:return: The contents of the selected chunk
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
log_debug(f"Reading file: {file_name}")
|
|
89
|
+
safe, file_path = self.check_escape(file_name)
|
|
90
|
+
if not (safe):
|
|
91
|
+
log_error(f"Attempted to read file: {file_name}")
|
|
92
|
+
return "Error reading file"
|
|
93
|
+
contents = file_path.read_text(encoding=encoding)
|
|
94
|
+
lines = contents.split(self.line_separator)
|
|
95
|
+
return self.line_separator.join(lines[start_line : end_line + 1])
|
|
96
|
+
except Exception as e:
|
|
97
|
+
log_error(f"Error reading file: {e}")
|
|
98
|
+
return f"Error reading file: {e}"
|
|
99
|
+
|
|
100
|
+
def replace_file_chunk(
|
|
101
|
+
self, file_name: str, start_line: int, end_line: int, chunk: str, encoding: str = "utf-8"
|
|
102
|
+
) -> str:
|
|
103
|
+
"""Reads the contents of the file, replaces lines
|
|
104
|
+
between start_line and end_line with chunk and writes the file
|
|
105
|
+
|
|
106
|
+
:param file_name: The name of the file to process.
|
|
107
|
+
:param start_line: Number of first line in the replaced chunk
|
|
108
|
+
:param end_line: Number of the last line in the replaced chunk
|
|
109
|
+
:param chunk: String to be inserted instead of lines from start_line to end_line. Can have multiple lines.
|
|
110
|
+
:param encoding: Encoding to use, default - utf-8
|
|
111
|
+
|
|
112
|
+
:return: file name if successfull, error message otherwise
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
log_debug(f"Patching file: {file_name}")
|
|
116
|
+
safe, file_path = self.check_escape(file_name)
|
|
117
|
+
if not (safe):
|
|
118
|
+
log_error(f"Attempted to read file: {file_name}")
|
|
119
|
+
return "Error reading file"
|
|
120
|
+
contents = file_path.read_text(encoding=encoding)
|
|
121
|
+
lines = contents.split(self.line_separator)
|
|
122
|
+
start = lines[0:start_line]
|
|
123
|
+
end = lines[end_line + 1 :]
|
|
124
|
+
return self.save_file(
|
|
125
|
+
file_name=file_name, contents=self.line_separator.join(start + [chunk] + end), encoding=encoding
|
|
126
|
+
)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
log_error(f"Error patching file: {e}")
|
|
129
|
+
return f"Error patching file: {e}"
|
|
130
|
+
|
|
131
|
+
def read_file(self, file_name: str, encoding: str = "utf-8") -> str:
|
|
57
132
|
"""Reads the contents of the file `file_name` and returns the contents if successful.
|
|
58
133
|
|
|
59
134
|
:param file_name: The name of the file to read.
|
|
135
|
+
:param encoding: Encoding to use, default - utf-8
|
|
60
136
|
:return: The contents of the file if successful, otherwise returns an error message.
|
|
61
137
|
"""
|
|
62
138
|
try:
|
|
63
|
-
|
|
64
|
-
file_path = self.
|
|
65
|
-
|
|
139
|
+
log_debug(f"Reading file: {file_name}")
|
|
140
|
+
safe, file_path = self.check_escape(file_name)
|
|
141
|
+
if not (safe):
|
|
142
|
+
log_error(f"Attempted to read file: {file_name}")
|
|
143
|
+
return "Error reading file"
|
|
144
|
+
contents = file_path.read_text(encoding=encoding)
|
|
145
|
+
if len(contents) > self.max_file_length:
|
|
146
|
+
return "Error reading file: file too long. Use read_file_chunk instead"
|
|
147
|
+
if len(contents.split(self.line_separator)) > self.max_file_lines:
|
|
148
|
+
return "Error reading file: file too long. Use read_file_chunk instead"
|
|
149
|
+
|
|
66
150
|
return str(contents)
|
|
67
151
|
except Exception as e:
|
|
68
152
|
log_error(f"Error reading file: {e}")
|
|
69
153
|
return f"Error reading file: {e}"
|
|
70
154
|
|
|
71
|
-
def
|
|
72
|
-
"""
|
|
155
|
+
def delete_file(self, file_name: str) -> str:
|
|
156
|
+
"""Deletes a file
|
|
157
|
+
:param file_name: Name of the file to delete
|
|
158
|
+
|
|
159
|
+
:return: Empty string, if operation succeeded, otherwise returns an error message
|
|
160
|
+
"""
|
|
161
|
+
safe, path = self.check_escape(file_name)
|
|
162
|
+
try:
|
|
163
|
+
if safe:
|
|
164
|
+
if path.is_dir():
|
|
165
|
+
path.rmdir()
|
|
166
|
+
return ""
|
|
167
|
+
path.unlink()
|
|
168
|
+
return ""
|
|
169
|
+
else:
|
|
170
|
+
log_error(f"Attempt to delete file outside {self.base_dir}: {file_name}")
|
|
171
|
+
return "Incorrect file_name"
|
|
172
|
+
except Exception as e:
|
|
173
|
+
log_error(f"Error removing {file_name}: {e}")
|
|
174
|
+
return f"Error removing file: {e}"
|
|
175
|
+
|
|
176
|
+
def check_escape(self, relative_path: str) -> Tuple[bool, Path]:
|
|
177
|
+
d = self.base_dir.joinpath(Path(relative_path)).resolve()
|
|
178
|
+
if self.base_dir == d:
|
|
179
|
+
return True, d
|
|
180
|
+
try:
|
|
181
|
+
d.relative_to(self.base_dir)
|
|
182
|
+
except ValueError:
|
|
183
|
+
log_error("Attempted to escape base_dir")
|
|
184
|
+
return False, self.base_dir
|
|
185
|
+
return True, d
|
|
186
|
+
|
|
187
|
+
def list_files(self, **kwargs) -> str:
|
|
188
|
+
"""Returns a list of files in directory
|
|
189
|
+
:param directory: (Optional) name of directory to list.
|
|
73
190
|
|
|
74
191
|
:return: The contents of the file if successful, otherwise returns an error message.
|
|
75
192
|
"""
|
|
193
|
+
directory = kwargs.get("directory", ".")
|
|
76
194
|
try:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
195
|
+
log_debug(f"Reading files in : {self.base_dir}/{directory}")
|
|
196
|
+
safe, d = self.check_escape(directory)
|
|
197
|
+
if safe:
|
|
198
|
+
return json.dumps([str(file_path.relative_to(self.base_dir)) for file_path in d.iterdir()], indent=4)
|
|
199
|
+
else:
|
|
200
|
+
return "{}"
|
|
81
201
|
except Exception as e:
|
|
82
202
|
log_error(f"Error reading files: {e}")
|
|
83
203
|
return f"Error reading files: {e}"
|
|
@@ -94,15 +214,23 @@ class FileTools(Toolkit):
|
|
|
94
214
|
|
|
95
215
|
log_debug(f"Searching files in {self.base_dir} with pattern {pattern}")
|
|
96
216
|
matching_files = list(self.base_dir.glob(pattern))
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
217
|
+
result = None
|
|
218
|
+
if self.expose_base_directory:
|
|
219
|
+
file_paths = [str(file_path) for file_path in matching_files]
|
|
220
|
+
result = {
|
|
221
|
+
"pattern": pattern,
|
|
222
|
+
"matches_found": len(file_paths),
|
|
223
|
+
"base_directory": str(self.base_dir),
|
|
224
|
+
"files": file_paths,
|
|
225
|
+
}
|
|
226
|
+
else:
|
|
227
|
+
file_paths = [str(file_path.relative_to(self.base_dir)) for file_path in matching_files]
|
|
228
|
+
|
|
229
|
+
result = {
|
|
230
|
+
"pattern": pattern,
|
|
231
|
+
"matches_found": len(file_paths),
|
|
232
|
+
"files": file_paths,
|
|
233
|
+
}
|
|
106
234
|
log_debug(f"Found {len(file_paths)} files matching pattern {pattern}")
|
|
107
235
|
return json.dumps(result, indent=2)
|
|
108
236
|
|
agno/tools/function.py
CHANGED
|
@@ -440,7 +440,7 @@ class Function(BaseModel):
|
|
|
440
440
|
@staticmethod
|
|
441
441
|
def _wrap_callable(func: Callable) -> Callable:
|
|
442
442
|
"""Wrap a callable with Pydantic's validate_call decorator, if relevant"""
|
|
443
|
-
from inspect import isasyncgenfunction, iscoroutinefunction
|
|
443
|
+
from inspect import isasyncgenfunction, iscoroutinefunction, signature
|
|
444
444
|
|
|
445
445
|
pydantic_version = Version(version("pydantic"))
|
|
446
446
|
|
|
@@ -458,6 +458,10 @@ class Function(BaseModel):
|
|
|
458
458
|
# Don't wrap callables that are already wrapped with validate_call
|
|
459
459
|
elif getattr(func, "_wrapped_for_validation", False):
|
|
460
460
|
return func
|
|
461
|
+
# Don't wrap functions with session_state parameter
|
|
462
|
+
# session_state needs to be passed by reference, not copied by pydantic's validation
|
|
463
|
+
elif "session_state" in signature(func).parameters:
|
|
464
|
+
return func
|
|
461
465
|
# Wrap the callable with validate_call
|
|
462
466
|
else:
|
|
463
467
|
wrapped = validate_call(func, config=dict(arbitrary_types_allowed=True)) # type: ignore
|