agno 2.4.2__py3-none-any.whl → 2.4.4__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 +13 -0
- agno/db/firestore/firestore.py +53 -30
- agno/db/surrealdb/models.py +5 -5
- agno/db/surrealdb/surrealdb.py +13 -1
- agno/knowledge/chunking/markdown.py +112 -11
- agno/knowledge/embedder/openai.py +8 -4
- agno/knowledge/knowledge.py +59 -6
- agno/knowledge/reader/csv_reader.py +48 -216
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +13 -179
- agno/knowledge/reader/reader_factory.py +22 -5
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/models/base.py +6 -0
- agno/models/moonshot/__init__.py +3 -0
- agno/models/moonshot/moonshot.py +57 -0
- agno/models/openrouter/responses.py +2 -2
- agno/models/response.py +4 -0
- agno/models/utils.py +5 -0
- agno/os/routers/knowledge/knowledge.py +5 -3
- agno/run/base.py +4 -0
- agno/tools/decorator.py +3 -0
- agno/tools/function.py +3 -0
- agno/tools/unsplash.py +341 -0
- agno/utils/print_response/agent.py +8 -5
- agno/utils/response.py +38 -28
- agno/utils/string.py +2 -1
- agno/vectordb/lancedb/lance_db.py +29 -7
- agno/workflow/workflow.py +16 -6
- {agno-2.4.2.dist-info → agno-2.4.4.dist-info}/METADATA +7 -5
- {agno-2.4.2.dist-info → agno-2.4.4.dist-info}/RECORD +34 -28
- {agno-2.4.2.dist-info → agno-2.4.4.dist-info}/WHEEL +1 -1
- {agno-2.4.2.dist-info → agno-2.4.4.dist-info}/licenses/LICENSE +0 -0
- {agno-2.4.2.dist-info → agno-2.4.4.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
import csv
|
|
3
3
|
import io
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import IO, Any,
|
|
5
|
+
from typing import IO, Any, List, Optional, Union
|
|
6
6
|
|
|
7
7
|
try:
|
|
8
8
|
import aiofiles
|
|
@@ -12,12 +12,7 @@ except ImportError:
|
|
|
12
12
|
from agno.knowledge.chunking.strategy import ChunkingStrategyType
|
|
13
13
|
from agno.knowledge.document.base import Document
|
|
14
14
|
from agno.knowledge.reader.base import Reader
|
|
15
|
-
from agno.knowledge.reader.
|
|
16
|
-
_convert_xls_cell_value,
|
|
17
|
-
_get_workbook_name,
|
|
18
|
-
_infer_file_extension,
|
|
19
|
-
_stringify_spreadsheet_cell_value,
|
|
20
|
-
)
|
|
15
|
+
from agno.knowledge.reader.utils import stringify_cell_value
|
|
21
16
|
from agno.knowledge.types import ContentType
|
|
22
17
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
23
18
|
|
|
@@ -47,7 +42,7 @@ class FieldLabeledCSVReader(Reader):
|
|
|
47
42
|
@classmethod
|
|
48
43
|
def get_supported_content_types(cls) -> List[ContentType]:
|
|
49
44
|
"""Get the list of supported content types."""
|
|
50
|
-
return [ContentType.CSV
|
|
45
|
+
return [ContentType.CSV]
|
|
51
46
|
|
|
52
47
|
def _format_field_name(self, field_name: str) -> str:
|
|
53
48
|
"""Format field name to be more readable."""
|
|
@@ -72,17 +67,7 @@ class FieldLabeledCSVReader(Reader):
|
|
|
72
67
|
return None
|
|
73
68
|
|
|
74
69
|
def _convert_row_to_labeled_text(self, headers: List[str], row: List[str], entry_index: int) -> str:
|
|
75
|
-
"""
|
|
76
|
-
Convert a CSV row to field-labeled text format.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
headers: Column headers
|
|
80
|
-
row: Data row values
|
|
81
|
-
entry_index: Index of this entry (for title rotation)
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
Formatted text with field labels
|
|
85
|
-
"""
|
|
70
|
+
"""Convert a CSV row to field-labeled text format."""
|
|
86
71
|
lines = []
|
|
87
72
|
|
|
88
73
|
title = self._get_title_for_entry(entry_index)
|
|
@@ -91,7 +76,7 @@ class FieldLabeledCSVReader(Reader):
|
|
|
91
76
|
|
|
92
77
|
for i, (header, value) in enumerate(zip(headers, row)):
|
|
93
78
|
# Normalize line endings before stripping to handle embedded newlines
|
|
94
|
-
clean_value =
|
|
79
|
+
clean_value = stringify_cell_value(value).strip() if value else ""
|
|
95
80
|
|
|
96
81
|
if self.skip_empty_fields and not clean_value:
|
|
97
82
|
continue
|
|
@@ -105,155 +90,11 @@ class FieldLabeledCSVReader(Reader):
|
|
|
105
90
|
|
|
106
91
|
return "\n".join(lines)
|
|
107
92
|
|
|
108
|
-
def _excel_rows_to_field_labeled_documents(
|
|
109
|
-
self,
|
|
110
|
-
*,
|
|
111
|
-
workbook_name: str,
|
|
112
|
-
sheets: Iterable[Tuple[str, Iterable[Sequence[Any]]]],
|
|
113
|
-
) -> List[Document]:
|
|
114
|
-
"""Convert Excel rows to field-labeled documents (one document per data row).
|
|
115
|
-
|
|
116
|
-
For each sheet: first row = headers, subsequent rows = data.
|
|
117
|
-
Each data row becomes a Document with field-labeled content.
|
|
118
|
-
"""
|
|
119
|
-
documents = []
|
|
120
|
-
global_row_index = 0
|
|
121
|
-
|
|
122
|
-
for sheet_index, (sheet_name, rows) in enumerate(sheets, start=1):
|
|
123
|
-
rows_list = list(rows)
|
|
124
|
-
|
|
125
|
-
if not rows_list:
|
|
126
|
-
log_debug(f"Sheet '{sheet_name}' is empty, skipping")
|
|
127
|
-
continue
|
|
128
|
-
|
|
129
|
-
# First row is headers
|
|
130
|
-
headers = [_stringify_spreadsheet_cell_value(h).strip() for h in rows_list[0]]
|
|
131
|
-
if not any(headers):
|
|
132
|
-
log_debug(f"Sheet '{sheet_name}' has no valid headers, skipping")
|
|
133
|
-
continue
|
|
134
|
-
|
|
135
|
-
data_rows = rows_list[1:]
|
|
136
|
-
if not data_rows:
|
|
137
|
-
log_debug(f"Sheet '{sheet_name}' has only headers, skipping")
|
|
138
|
-
continue
|
|
139
|
-
|
|
140
|
-
log_debug(f"Processing sheet '{sheet_name}' with {len(headers)} headers and {len(data_rows)} rows")
|
|
141
|
-
|
|
142
|
-
for row_in_sheet, row in enumerate(data_rows):
|
|
143
|
-
# Convert cell values to strings
|
|
144
|
-
str_row = [_stringify_spreadsheet_cell_value(v) for v in row]
|
|
145
|
-
|
|
146
|
-
# Normalize row length
|
|
147
|
-
normalized_row = str_row[: len(headers)]
|
|
148
|
-
while len(normalized_row) < len(headers):
|
|
149
|
-
normalized_row.append("")
|
|
150
|
-
|
|
151
|
-
# Skip entirely empty rows
|
|
152
|
-
if not any(v.strip() for v in normalized_row):
|
|
153
|
-
continue
|
|
154
|
-
|
|
155
|
-
labeled_text = self._convert_row_to_labeled_text(headers, normalized_row, global_row_index)
|
|
156
|
-
|
|
157
|
-
if labeled_text.strip():
|
|
158
|
-
doc_id = f"{workbook_name}_{sheet_name}_row_{row_in_sheet + 1}"
|
|
159
|
-
documents.append(
|
|
160
|
-
Document(
|
|
161
|
-
id=doc_id,
|
|
162
|
-
name=workbook_name,
|
|
163
|
-
meta_data={
|
|
164
|
-
"sheet_name": sheet_name,
|
|
165
|
-
"sheet_index": sheet_index,
|
|
166
|
-
"row_index": row_in_sheet,
|
|
167
|
-
"headers": headers,
|
|
168
|
-
"source": "field_labeled_csv_reader",
|
|
169
|
-
},
|
|
170
|
-
content=labeled_text,
|
|
171
|
-
)
|
|
172
|
-
)
|
|
173
|
-
global_row_index += 1
|
|
174
|
-
|
|
175
|
-
return documents
|
|
176
|
-
|
|
177
|
-
def _read_xlsx(self, file: Union[Path, IO[Any]], *, workbook_name: str) -> List[Document]:
|
|
178
|
-
"""Read .xlsx file and convert rows to field-labeled documents."""
|
|
179
|
-
try:
|
|
180
|
-
import openpyxl # type: ignore
|
|
181
|
-
except ImportError as e:
|
|
182
|
-
raise ImportError(
|
|
183
|
-
"`openpyxl` not installed. Please install it via `pip install agno[csv]` or `pip install openpyxl`."
|
|
184
|
-
) from e
|
|
185
|
-
|
|
186
|
-
if isinstance(file, Path):
|
|
187
|
-
workbook = openpyxl.load_workbook(filename=str(file), read_only=True, data_only=True)
|
|
188
|
-
else:
|
|
189
|
-
file.seek(0)
|
|
190
|
-
raw = file.read()
|
|
191
|
-
if isinstance(raw, str):
|
|
192
|
-
raw = raw.encode("utf-8", errors="replace")
|
|
193
|
-
workbook = openpyxl.load_workbook(filename=io.BytesIO(raw), read_only=True, data_only=True)
|
|
194
|
-
|
|
195
|
-
try:
|
|
196
|
-
return self._excel_rows_to_field_labeled_documents(
|
|
197
|
-
workbook_name=workbook_name,
|
|
198
|
-
sheets=[(worksheet.title, worksheet.iter_rows(values_only=True)) for worksheet in workbook.worksheets],
|
|
199
|
-
)
|
|
200
|
-
finally:
|
|
201
|
-
workbook.close()
|
|
202
|
-
|
|
203
|
-
def _read_xls(self, file: Union[Path, IO[Any]], *, workbook_name: str) -> List[Document]:
|
|
204
|
-
"""Read .xls file and convert rows to field-labeled documents."""
|
|
205
|
-
try:
|
|
206
|
-
import xlrd # type: ignore
|
|
207
|
-
except ImportError as e:
|
|
208
|
-
raise ImportError(
|
|
209
|
-
"`xlrd` not installed. Please install it via `pip install agno[csv]` or `pip install xlrd`."
|
|
210
|
-
) from e
|
|
211
|
-
|
|
212
|
-
if isinstance(file, Path):
|
|
213
|
-
workbook = xlrd.open_workbook(filename=str(file))
|
|
214
|
-
else:
|
|
215
|
-
file.seek(0)
|
|
216
|
-
raw = file.read()
|
|
217
|
-
if isinstance(raw, str):
|
|
218
|
-
raw = raw.encode("utf-8", errors="replace")
|
|
219
|
-
workbook = xlrd.open_workbook(file_contents=raw)
|
|
220
|
-
|
|
221
|
-
sheets: List[Tuple[str, Iterable[Sequence[Any]]]] = []
|
|
222
|
-
for sheet_index in range(workbook.nsheets):
|
|
223
|
-
sheet = workbook.sheet_by_index(sheet_index)
|
|
224
|
-
|
|
225
|
-
def _iter_sheet_rows(_sheet: Any = sheet, _datemode: int = workbook.datemode) -> Iterable[Sequence[Any]]:
|
|
226
|
-
for row_index in range(_sheet.nrows):
|
|
227
|
-
yield [
|
|
228
|
-
_convert_xls_cell_value(
|
|
229
|
-
_sheet.cell_value(row_index, col_index),
|
|
230
|
-
_sheet.cell_type(row_index, col_index),
|
|
231
|
-
_datemode,
|
|
232
|
-
)
|
|
233
|
-
for col_index in range(_sheet.ncols)
|
|
234
|
-
]
|
|
235
|
-
|
|
236
|
-
sheets.append((sheet.name, _iter_sheet_rows()))
|
|
237
|
-
|
|
238
|
-
return self._excel_rows_to_field_labeled_documents(workbook_name=workbook_name, sheets=sheets)
|
|
239
|
-
|
|
240
93
|
def read(
|
|
241
94
|
self, file: Union[Path, IO[Any]], delimiter: str = ",", quotechar: str = '"', name: Optional[str] = None
|
|
242
95
|
) -> List[Document]:
|
|
96
|
+
"""Read a CSV file and convert each row to a field-labeled document."""
|
|
243
97
|
try:
|
|
244
|
-
file_extension = _infer_file_extension(file, name)
|
|
245
|
-
|
|
246
|
-
# Handle Excel files
|
|
247
|
-
if file_extension in {ContentType.XLSX, ContentType.XLS}:
|
|
248
|
-
workbook_name = _get_workbook_name(file, name)
|
|
249
|
-
log_debug(f"Reading Excel file: {workbook_name}{file_extension}")
|
|
250
|
-
|
|
251
|
-
if file_extension == ContentType.XLSX:
|
|
252
|
-
return self._read_xlsx(file, workbook_name=workbook_name)
|
|
253
|
-
else:
|
|
254
|
-
return self._read_xls(file, workbook_name=workbook_name)
|
|
255
|
-
|
|
256
|
-
# Handle CSV files
|
|
257
98
|
if isinstance(file, Path):
|
|
258
99
|
if not file.exists():
|
|
259
100
|
raise FileNotFoundError(f"Could not find file: {file}")
|
|
@@ -318,6 +159,8 @@ class FieldLabeledCSVReader(Reader):
|
|
|
318
159
|
log_debug(f"Successfully created {len(documents)} labeled documents from CSV")
|
|
319
160
|
return documents
|
|
320
161
|
|
|
162
|
+
except FileNotFoundError:
|
|
163
|
+
raise
|
|
321
164
|
except Exception as e:
|
|
322
165
|
log_error(f"Error reading: {getattr(file, 'name', str(file)) if isinstance(file, IO) else file}: {e}")
|
|
323
166
|
return []
|
|
@@ -330,20 +173,8 @@ class FieldLabeledCSVReader(Reader):
|
|
|
330
173
|
page_size: int = 1000,
|
|
331
174
|
name: Optional[str] = None,
|
|
332
175
|
) -> List[Document]:
|
|
176
|
+
"""Read a CSV file asynchronously and convert each row to a field-labeled document."""
|
|
333
177
|
try:
|
|
334
|
-
file_extension = _infer_file_extension(file, name)
|
|
335
|
-
|
|
336
|
-
# Handle Excel files (use asyncio.to_thread for sync openpyxl/xlrd)
|
|
337
|
-
if file_extension in {ContentType.XLSX, ContentType.XLS}:
|
|
338
|
-
workbook_name = _get_workbook_name(file, name)
|
|
339
|
-
log_debug(f"Reading Excel file async: {workbook_name}{file_extension}")
|
|
340
|
-
|
|
341
|
-
if file_extension == ContentType.XLSX:
|
|
342
|
-
return await asyncio.to_thread(self._read_xlsx, file, workbook_name=workbook_name)
|
|
343
|
-
else:
|
|
344
|
-
return await asyncio.to_thread(self._read_xls, file, workbook_name=workbook_name)
|
|
345
|
-
|
|
346
|
-
# Handle CSV files
|
|
347
178
|
if isinstance(file, Path):
|
|
348
179
|
if not file.exists():
|
|
349
180
|
raise FileNotFoundError(f"Could not find file: {file}")
|
|
@@ -399,12 +230,13 @@ class FieldLabeledCSVReader(Reader):
|
|
|
399
230
|
)
|
|
400
231
|
documents.append(document)
|
|
401
232
|
else:
|
|
233
|
+
# Large files: paginate and process in parallel
|
|
402
234
|
pages = []
|
|
403
235
|
for i in range(0, total_rows, page_size):
|
|
404
236
|
pages.append(data_rows[i : i + page_size])
|
|
405
237
|
|
|
406
238
|
async def _process_page(page_number: int, page_rows: List[List[str]]) -> List[Document]:
|
|
407
|
-
"""Process a page of rows into documents"""
|
|
239
|
+
"""Process a page of rows into documents."""
|
|
408
240
|
page_documents = []
|
|
409
241
|
start_row_index = (page_number - 1) * page_size
|
|
410
242
|
|
|
@@ -443,6 +275,8 @@ class FieldLabeledCSVReader(Reader):
|
|
|
443
275
|
log_debug(f"Successfully created {len(documents)} labeled documents from CSV")
|
|
444
276
|
return documents
|
|
445
277
|
|
|
278
|
+
except FileNotFoundError:
|
|
279
|
+
raise
|
|
446
280
|
except Exception as e:
|
|
447
281
|
log_error(f"Error reading async: {getattr(file, 'name', str(file)) if isinstance(file, IO) else file}: {e}")
|
|
448
282
|
return []
|
|
@@ -18,7 +18,11 @@ class ReaderFactory:
|
|
|
18
18
|
},
|
|
19
19
|
"csv": {
|
|
20
20
|
"name": "CsvReader",
|
|
21
|
-
"description": "Parses CSV
|
|
21
|
+
"description": "Parses CSV files with custom delimiter support",
|
|
22
|
+
},
|
|
23
|
+
"excel": {
|
|
24
|
+
"name": "ExcelReader",
|
|
25
|
+
"description": "Processes Excel workbooks (.xlsx and .xls) with sheet filtering and row-based chunking",
|
|
22
26
|
},
|
|
23
27
|
"field_labeled_csv": {
|
|
24
28
|
"name": "FieldLabeledCsvReader",
|
|
@@ -93,11 +97,23 @@ class ReaderFactory:
|
|
|
93
97
|
|
|
94
98
|
config: Dict[str, Any] = {
|
|
95
99
|
"name": "CSV Reader",
|
|
96
|
-
"description": "Parses CSV
|
|
100
|
+
"description": "Parses CSV files with custom delimiter support",
|
|
97
101
|
}
|
|
98
102
|
config.update(kwargs)
|
|
99
103
|
return CSVReader(**config)
|
|
100
104
|
|
|
105
|
+
@classmethod
|
|
106
|
+
def _get_excel_reader(cls, **kwargs) -> Reader:
|
|
107
|
+
"""Get Excel reader instance."""
|
|
108
|
+
from agno.knowledge.reader.excel_reader import ExcelReader
|
|
109
|
+
|
|
110
|
+
config: Dict[str, Any] = {
|
|
111
|
+
"name": "Excel Reader",
|
|
112
|
+
"description": "Processes Excel workbooks (.xlsx and .xls) with sheet filtering and row-based chunking",
|
|
113
|
+
}
|
|
114
|
+
config.update(kwargs)
|
|
115
|
+
return ExcelReader(**config)
|
|
116
|
+
|
|
101
117
|
@classmethod
|
|
102
118
|
def _get_field_labeled_csv_reader(cls, **kwargs) -> Reader:
|
|
103
119
|
"""Get Field Labeled CSV reader instance."""
|
|
@@ -288,6 +304,7 @@ class ReaderFactory:
|
|
|
288
304
|
reader_class_map: Dict[str, tuple] = {
|
|
289
305
|
"pdf": ("agno.knowledge.reader.pdf_reader", "PDFReader"),
|
|
290
306
|
"csv": ("agno.knowledge.reader.csv_reader", "CSVReader"),
|
|
307
|
+
"excel": ("agno.knowledge.reader.excel_reader", "ExcelReader"),
|
|
291
308
|
"field_labeled_csv": ("agno.knowledge.reader.field_labeled_csv_reader", "FieldLabeledCSVReader"),
|
|
292
309
|
"docx": ("agno.knowledge.reader.docx_reader", "DocxReader"),
|
|
293
310
|
"pptx": ("agno.knowledge.reader.pptx_reader", "PPTXReader"),
|
|
@@ -335,15 +352,15 @@ class ReaderFactory:
|
|
|
335
352
|
|
|
336
353
|
if extension in [".pdf", "application/pdf"]:
|
|
337
354
|
return cls.create_reader("pdf")
|
|
355
|
+
elif extension in [".csv", "text/csv"]:
|
|
356
|
+
return cls.create_reader("csv")
|
|
338
357
|
elif extension in [
|
|
339
|
-
".csv",
|
|
340
358
|
".xlsx",
|
|
341
359
|
".xls",
|
|
342
|
-
"text/csv",
|
|
343
360
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
344
361
|
"application/vnd.ms-excel",
|
|
345
362
|
]:
|
|
346
|
-
return cls.create_reader("
|
|
363
|
+
return cls.create_reader("excel")
|
|
347
364
|
elif extension in [".docx", ".doc", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]:
|
|
348
365
|
return cls.create_reader("docx")
|
|
349
366
|
elif extension == ".pptx":
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from agno.knowledge.reader.utils.spreadsheet import (
|
|
2
|
+
convert_xls_cell_value,
|
|
3
|
+
excel_rows_to_documents,
|
|
4
|
+
get_workbook_name,
|
|
5
|
+
infer_file_extension,
|
|
6
|
+
row_to_csv_line,
|
|
7
|
+
stringify_cell_value,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"convert_xls_cell_value",
|
|
12
|
+
"excel_rows_to_documents",
|
|
13
|
+
"get_workbook_name",
|
|
14
|
+
"infer_file_extension",
|
|
15
|
+
"row_to_csv_line",
|
|
16
|
+
"stringify_cell_value",
|
|
17
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import IO, Any, Iterable, List, Optional, Sequence, Tuple, Union
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from agno.knowledge.document.base import Document
|
|
7
|
+
from agno.utils.log import log_debug
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def stringify_cell_value(value: Any) -> str:
|
|
11
|
+
"""Convert cell value to string, normalizing dates and line endings."""
|
|
12
|
+
if value is None:
|
|
13
|
+
return ""
|
|
14
|
+
|
|
15
|
+
if isinstance(value, datetime):
|
|
16
|
+
return value.isoformat()
|
|
17
|
+
if isinstance(value, date):
|
|
18
|
+
return value.isoformat()
|
|
19
|
+
|
|
20
|
+
if isinstance(value, float) and value.is_integer():
|
|
21
|
+
return str(int(value))
|
|
22
|
+
|
|
23
|
+
result = str(value)
|
|
24
|
+
# Normalize all line endings to space to preserve row integrity in CSV-like output
|
|
25
|
+
# Must handle CRLF first before individual CR/LF to avoid double-spacing
|
|
26
|
+
result = result.replace("\r\n", " ") # Windows (CRLF)
|
|
27
|
+
result = result.replace("\r", " ") # Old Mac (CR)
|
|
28
|
+
result = result.replace("\n", " ") # Unix (LF)
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_workbook_name(file: Union[Path, IO[Any]], name: Optional[str]) -> str:
|
|
33
|
+
"""Extract workbook name from file path or name parameter."""
|
|
34
|
+
if name:
|
|
35
|
+
return Path(name).stem
|
|
36
|
+
if isinstance(file, Path):
|
|
37
|
+
return file.stem
|
|
38
|
+
# getattr returns None when attribute exists but is None, so check explicitly
|
|
39
|
+
file_name = getattr(file, "name", None)
|
|
40
|
+
if file_name:
|
|
41
|
+
return Path(file_name).stem
|
|
42
|
+
return "workbook"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def infer_file_extension(file: Union[Path, IO[Any]], name: Optional[str]) -> str:
|
|
46
|
+
"""Infer file extension from Path, IO object, or explicit name."""
|
|
47
|
+
if isinstance(file, Path):
|
|
48
|
+
return file.suffix.lower()
|
|
49
|
+
|
|
50
|
+
file_name = getattr(file, "name", None)
|
|
51
|
+
if isinstance(file_name, str) and file_name:
|
|
52
|
+
return Path(file_name).suffix.lower()
|
|
53
|
+
|
|
54
|
+
if name:
|
|
55
|
+
return Path(name).suffix.lower()
|
|
56
|
+
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def convert_xls_cell_value(cell_value: Any, cell_type: int, datemode: int) -> Any:
|
|
61
|
+
"""Convert xlrd cell value to Python type (dates and booleans need conversion)."""
|
|
62
|
+
try:
|
|
63
|
+
import xlrd
|
|
64
|
+
except ImportError:
|
|
65
|
+
return cell_value
|
|
66
|
+
|
|
67
|
+
if cell_type == xlrd.XL_CELL_DATE:
|
|
68
|
+
try:
|
|
69
|
+
date_tuple = xlrd.xldate_as_tuple(cell_value, datemode)
|
|
70
|
+
return datetime(*date_tuple)
|
|
71
|
+
except Exception:
|
|
72
|
+
return cell_value
|
|
73
|
+
if cell_type == xlrd.XL_CELL_BOOLEAN:
|
|
74
|
+
return bool(cell_value)
|
|
75
|
+
return cell_value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def row_to_csv_line(row_values: Sequence[Any]) -> str:
|
|
79
|
+
"""Convert row values to CSV-like string, trimming trailing empty cells."""
|
|
80
|
+
values = [stringify_cell_value(v) for v in row_values]
|
|
81
|
+
while values and values[-1] == "":
|
|
82
|
+
values.pop()
|
|
83
|
+
|
|
84
|
+
return ", ".join(values)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def excel_rows_to_documents(
|
|
88
|
+
*,
|
|
89
|
+
workbook_name: str,
|
|
90
|
+
sheets: Iterable[Tuple[str, int, Iterable[Sequence[Any]]]],
|
|
91
|
+
) -> List[Document]:
|
|
92
|
+
"""Convert Excel sheet rows to Documents (one per sheet)."""
|
|
93
|
+
documents = []
|
|
94
|
+
for sheet_name, sheet_index, rows in sheets:
|
|
95
|
+
lines = []
|
|
96
|
+
for row in rows:
|
|
97
|
+
line = row_to_csv_line(row)
|
|
98
|
+
if line:
|
|
99
|
+
lines.append(line)
|
|
100
|
+
|
|
101
|
+
if not lines:
|
|
102
|
+
log_debug(f"Sheet '{sheet_name}' is empty, skipping")
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
documents.append(
|
|
106
|
+
Document(
|
|
107
|
+
name=workbook_name,
|
|
108
|
+
id=str(uuid4()),
|
|
109
|
+
meta_data={"sheet_name": sheet_name, "sheet_index": sheet_index},
|
|
110
|
+
content="\n".join(lines),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return documents
|
agno/models/base.py
CHANGED
|
@@ -2109,6 +2109,7 @@ class Model(ABC):
|
|
|
2109
2109
|
tool_name=fc.function.name,
|
|
2110
2110
|
tool_args=fc.arguments,
|
|
2111
2111
|
requires_confirmation=True,
|
|
2112
|
+
external_execution_silent=fc.function.external_execution_silent,
|
|
2112
2113
|
)
|
|
2113
2114
|
)
|
|
2114
2115
|
|
|
@@ -2128,6 +2129,7 @@ class Model(ABC):
|
|
|
2128
2129
|
tool_args=fc.arguments,
|
|
2129
2130
|
requires_user_input=True,
|
|
2130
2131
|
user_input_schema=user_input_schema,
|
|
2132
|
+
external_execution_silent=fc.function.external_execution_silent,
|
|
2131
2133
|
)
|
|
2132
2134
|
)
|
|
2133
2135
|
|
|
@@ -2176,6 +2178,7 @@ class Model(ABC):
|
|
|
2176
2178
|
tool_name=fc.function.name,
|
|
2177
2179
|
tool_args=fc.arguments,
|
|
2178
2180
|
external_execution_required=True,
|
|
2181
|
+
external_execution_silent=fc.function.external_execution_silent,
|
|
2179
2182
|
)
|
|
2180
2183
|
)
|
|
2181
2184
|
|
|
@@ -2270,6 +2273,7 @@ class Model(ABC):
|
|
|
2270
2273
|
tool_name=fc.function.name,
|
|
2271
2274
|
tool_args=fc.arguments,
|
|
2272
2275
|
requires_confirmation=True,
|
|
2276
|
+
external_execution_silent=fc.function.external_execution_silent,
|
|
2273
2277
|
)
|
|
2274
2278
|
)
|
|
2275
2279
|
# If the function requires user input, we yield a message to the user
|
|
@@ -2288,6 +2292,7 @@ class Model(ABC):
|
|
|
2288
2292
|
tool_args=fc.arguments,
|
|
2289
2293
|
requires_user_input=True,
|
|
2290
2294
|
user_input_schema=user_input_schema,
|
|
2295
|
+
external_execution_silent=fc.function.external_execution_silent,
|
|
2291
2296
|
)
|
|
2292
2297
|
)
|
|
2293
2298
|
# If the function is from the user control flow tools, we handle it here
|
|
@@ -2340,6 +2345,7 @@ class Model(ABC):
|
|
|
2340
2345
|
tool_name=fc.function.name,
|
|
2341
2346
|
tool_args=fc.arguments,
|
|
2342
2347
|
external_execution_required=True,
|
|
2348
|
+
external_execution_silent=fc.function.external_execution_silent,
|
|
2343
2349
|
)
|
|
2344
2350
|
)
|
|
2345
2351
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from os import getenv
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from agno.exceptions import ModelAuthenticationError
|
|
6
|
+
from agno.models.openai.like import OpenAILike
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class MoonShot(OpenAILike):
|
|
11
|
+
"""
|
|
12
|
+
A class for interacting with MoonShot models.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
id (str): The model id. Defaults to "kimi-k2-thinking".
|
|
16
|
+
name (str): The model name. Defaults to "Moonshot".
|
|
17
|
+
provider (str): The provider name. Defaults to "Moonshot".
|
|
18
|
+
api_key (Optional[str]): The API key.
|
|
19
|
+
base_url (str): The base URL. Defaults to "https://api.moonshot.ai/v1".
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
id: str = "kimi-k2-thinking"
|
|
23
|
+
name: str = "Moonshot"
|
|
24
|
+
provider: str = "Moonshot"
|
|
25
|
+
|
|
26
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("MOONSHOT_API_KEY"))
|
|
27
|
+
base_url: str = "https://api.moonshot.ai/v1"
|
|
28
|
+
|
|
29
|
+
def _get_client_params(self) -> Dict[str, Any]:
|
|
30
|
+
# Fetch API key from env if not already set
|
|
31
|
+
if not self.api_key:
|
|
32
|
+
self.api_key = getenv("MOONSHOT_API_KEY")
|
|
33
|
+
if not self.api_key:
|
|
34
|
+
# Raise error immediately if key is missing
|
|
35
|
+
raise ModelAuthenticationError(
|
|
36
|
+
message="MOONSHOT_API_KEY not set. Please set the MOONSHOT_API_KEY environment variable.",
|
|
37
|
+
model_name=self.name,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Define base client params
|
|
41
|
+
base_params = {
|
|
42
|
+
"api_key": self.api_key,
|
|
43
|
+
"organization": self.organization,
|
|
44
|
+
"base_url": self.base_url,
|
|
45
|
+
"timeout": self.timeout,
|
|
46
|
+
"max_retries": self.max_retries,
|
|
47
|
+
"default_headers": self.default_headers,
|
|
48
|
+
"default_query": self.default_query,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Create client_params dict with non-None values
|
|
52
|
+
client_params = {k: v for k, v in base_params.items() if v is not None}
|
|
53
|
+
|
|
54
|
+
# Add additional client params if provided
|
|
55
|
+
if self.client_params:
|
|
56
|
+
client_params.update(self.client_params)
|
|
57
|
+
return client_params
|
|
@@ -5,8 +5,8 @@ from typing import Any, Dict, List, Optional, Type, Union
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
7
7
|
from agno.exceptions import ModelAuthenticationError
|
|
8
|
-
from agno.models.openai.open_responses import OpenResponses
|
|
9
8
|
from agno.models.message import Message
|
|
9
|
+
from agno.models.openai.open_responses import OpenResponses
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@dataclass
|
|
@@ -143,4 +143,4 @@ class OpenRouterResponses(OpenResponses):
|
|
|
143
143
|
# Check for OpenAI reasoning models hosted on OpenRouter
|
|
144
144
|
if self.id.startswith("openai/o3") or self.id.startswith("openai/o4"):
|
|
145
145
|
return True
|
|
146
|
-
return False
|
|
146
|
+
return False
|
agno/models/response.py
CHANGED
|
@@ -52,6 +52,9 @@ class ToolExecution:
|
|
|
52
52
|
|
|
53
53
|
external_execution_required: Optional[bool] = None
|
|
54
54
|
|
|
55
|
+
# If True (and external_execution_required=True), suppresses verbose paused messages
|
|
56
|
+
external_execution_silent: Optional[bool] = None
|
|
57
|
+
|
|
55
58
|
@property
|
|
56
59
|
def is_paused(self) -> bool:
|
|
57
60
|
return bool(self.requires_confirmation or self.requires_user_input or self.external_execution_required)
|
|
@@ -84,6 +87,7 @@ class ToolExecution:
|
|
|
84
87
|
if "user_input_schema" in data
|
|
85
88
|
else None,
|
|
86
89
|
external_execution_required=data.get("external_execution_required"),
|
|
90
|
+
external_execution_silent=data.get("external_execution_silent"),
|
|
87
91
|
metrics=Metrics(**(data.get("metrics", {}) or {})),
|
|
88
92
|
**{"created_at": data["created_at"]} if "created_at" in data else {},
|
|
89
93
|
)
|
agno/models/utils.py
CHANGED
|
@@ -139,6 +139,11 @@ def _get_model_class(model_id: str, model_provider: str) -> Model:
|
|
|
139
139
|
|
|
140
140
|
return MistralChat(id=model_id)
|
|
141
141
|
|
|
142
|
+
elif model_provider == "moonshot":
|
|
143
|
+
from agno.models.moonshot import MoonShot
|
|
144
|
+
|
|
145
|
+
return MoonShot(id=model_id)
|
|
146
|
+
|
|
142
147
|
elif model_provider == "nebius":
|
|
143
148
|
from agno.models.nebius import Nebius
|
|
144
149
|
|
|
@@ -989,9 +989,11 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Union[Knowledge,
|
|
|
989
989
|
"text": ["web_search"],
|
|
990
990
|
"topic": ["arxiv"],
|
|
991
991
|
"file": ["csv", "gcs"],
|
|
992
|
-
".csv": ["csv"],
|
|
993
|
-
".xlsx": ["
|
|
994
|
-
".xls": ["
|
|
992
|
+
".csv": ["csv", "field_labeled_csv"],
|
|
993
|
+
".xlsx": ["excel"],
|
|
994
|
+
".xls": ["excel"],
|
|
995
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ["excel"],
|
|
996
|
+
"application/vnd.ms-excel": ["excel"],
|
|
995
997
|
".docx": ["docx"],
|
|
996
998
|
".doc": ["docx"],
|
|
997
999
|
".json": ["json"],
|
agno/run/base.py
CHANGED
|
@@ -246,6 +246,10 @@ class BaseRunOutputEvent:
|
|
|
246
246
|
data["requirements"] = requirements_list if requirements_list else None
|
|
247
247
|
|
|
248
248
|
# Filter data to only include fields that are actually defined in the target class
|
|
249
|
+
# CustomEvent accepts arbitrary fields, so skip filtering for it
|
|
250
|
+
if cls.__name__ == "CustomEvent":
|
|
251
|
+
return cls(**data)
|
|
252
|
+
|
|
249
253
|
from dataclasses import fields
|
|
250
254
|
|
|
251
255
|
supported_fields = {f.name for f in fields(cls)}
|
agno/tools/decorator.py
CHANGED
|
@@ -70,6 +70,7 @@ def tool(
|
|
|
70
70
|
requires_user_input: Optional[bool] = None,
|
|
71
71
|
user_input_fields: Optional[List[str]] = None,
|
|
72
72
|
external_execution: Optional[bool] = None,
|
|
73
|
+
external_execution_silent: Optional[bool] = None,
|
|
73
74
|
pre_hook: Optional[Callable] = None,
|
|
74
75
|
post_hook: Optional[Callable] = None,
|
|
75
76
|
tool_hooks: Optional[List[Callable]] = None,
|
|
@@ -98,6 +99,7 @@ def tool(*args, **kwargs) -> Union[Function, Callable[[F], Function]]:
|
|
|
98
99
|
requires_user_input: Optional[bool] - If True, the function will require user input before execution
|
|
99
100
|
user_input_fields: Optional[List[str]] - List of fields that will be provided to the function as user input
|
|
100
101
|
external_execution: Optional[bool] - If True, the function will be executed outside of the agent's context
|
|
102
|
+
external_execution_silent: Optional[bool] - If True (and external_execution=True), suppresses verbose paused messages (e.g., "I have tools to execute...")
|
|
101
103
|
pre_hook: Optional[Callable] - Hook that runs before the function is executed.
|
|
102
104
|
post_hook: Optional[Callable] - Hook that runs after the function is executed.
|
|
103
105
|
tool_hooks: Optional[List[Callable]] - List of hooks that run before and after the function is executed.
|
|
@@ -135,6 +137,7 @@ def tool(*args, **kwargs) -> Union[Function, Callable[[F], Function]]:
|
|
|
135
137
|
"requires_user_input",
|
|
136
138
|
"user_input_fields",
|
|
137
139
|
"external_execution",
|
|
140
|
+
"external_execution_silent",
|
|
138
141
|
"pre_hook",
|
|
139
142
|
"post_hook",
|
|
140
143
|
"tool_hooks",
|