feat(router): implement file_tool excel actions on NODE1 stack
This commit is contained in:
@@ -48,6 +48,8 @@ Critical files were drifted against NODE1 runtime (15 files):
|
||||
Implemented in actual NODE1 stack (`services/router/*` + gateway):
|
||||
|
||||
### Added actions
|
||||
- `excel_create`
|
||||
- `excel_update`
|
||||
- `csv_create`
|
||||
- `csv_update`
|
||||
- `json_export`
|
||||
@@ -90,6 +92,7 @@ For file-producing tool calls, router now propagates:
|
||||
|
||||
## Smoke Tests
|
||||
Run inside `dagi-router-node1` to validate actions deterministically:
|
||||
- Excel create/update
|
||||
- CSV create/update
|
||||
- JSON/YAML export
|
||||
- ZIP bundle
|
||||
@@ -104,6 +107,7 @@ Also verify infer endpoint still works:
|
||||
- `rollback_backups/file_tool_step2_tool_manager.py.bak_20260215_012029`
|
||||
- `rollback_backups/file_tool_step3_tool_manager.py.bak_20260215_012200`
|
||||
- `rollback_backups/file_tool_step4_tool_manager.py.bak_20260215_012309`
|
||||
- `services/router/tool_manager.py.bak_20260215_020902`
|
||||
|
||||
## Rollback (NODE1)
|
||||
```bash
|
||||
|
||||
@@ -588,11 +588,22 @@ class ToolManager:
|
||||
out.append([row])
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _append_sheet_data(ws: Any, headers: List[str], rows: List[List[Any]]) -> None:
|
||||
if headers:
|
||||
ws.append(headers)
|
||||
for row in rows:
|
||||
ws.append(row)
|
||||
|
||||
async def _file_tool(self, args: Dict[str, Any]) -> ToolResult:
|
||||
action = str((args or {}).get("action") or "").strip().lower()
|
||||
if not action:
|
||||
return ToolResult(success=False, result=None, error="Missing action")
|
||||
|
||||
if action == "excel_create":
|
||||
return self._file_excel_create(args)
|
||||
if action == "excel_update":
|
||||
return self._file_excel_update(args)
|
||||
if action == "csv_create":
|
||||
return self._file_csv_create(args)
|
||||
if action == "csv_update":
|
||||
@@ -751,6 +762,128 @@ class ToolManager:
|
||||
file_mime="application/zip",
|
||||
)
|
||||
|
||||
def _file_excel_create(self, args: Dict[str, Any]) -> ToolResult:
|
||||
import openpyxl
|
||||
|
||||
file_name = self._sanitize_file_name(args.get("file_name"), "report.xlsx", force_ext=".xlsx")
|
||||
sheets = args.get("sheets")
|
||||
|
||||
wb = openpyxl.Workbook()
|
||||
wb.remove(wb.active)
|
||||
created = False
|
||||
|
||||
if isinstance(sheets, list) and sheets:
|
||||
for idx, sheet in enumerate(sheets, start=1):
|
||||
if not isinstance(sheet, dict):
|
||||
return ToolResult(success=False, result=None, error=f"sheets[{idx-1}] must be object")
|
||||
sheet_name = str(sheet.get("name") or f"Sheet{idx}")[:31]
|
||||
headers = sheet.get("headers") or []
|
||||
rows_raw = sheet.get("rows") or []
|
||||
rows = self._normalize_rows(rows_raw, headers=headers if headers else None)
|
||||
if rows and not headers and isinstance(rows_raw[0], dict):
|
||||
headers = list(rows_raw[0].keys())
|
||||
rows = self._normalize_rows(rows_raw, headers=headers)
|
||||
ws = wb.create_sheet(title=sheet_name)
|
||||
self._append_sheet_data(ws, headers, rows)
|
||||
created = True
|
||||
else:
|
||||
sheet_name = str(args.get("sheet_name") or "Sheet1")[:31]
|
||||
headers = args.get("headers") or []
|
||||
rows_raw = args.get("rows") or []
|
||||
rows = self._normalize_rows(rows_raw, headers=headers if headers else None)
|
||||
if rows and not headers and isinstance(rows_raw[0], dict):
|
||||
headers = list(rows_raw[0].keys())
|
||||
rows = self._normalize_rows(rows_raw, headers=headers)
|
||||
ws = wb.create_sheet(title=sheet_name)
|
||||
self._append_sheet_data(ws, headers, rows)
|
||||
created = True
|
||||
|
||||
if not created:
|
||||
wb.create_sheet(title="Sheet1")
|
||||
|
||||
out = BytesIO()
|
||||
wb.save(out)
|
||||
return ToolResult(
|
||||
success=True,
|
||||
result={"message": f"Excel created: {file_name}"},
|
||||
file_base64=self._b64_from_bytes(out.getvalue()),
|
||||
file_name=file_name,
|
||||
file_mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
)
|
||||
|
||||
def _file_excel_update(self, args: Dict[str, Any]) -> ToolResult:
|
||||
import openpyxl
|
||||
|
||||
src_b64 = args.get("file_base64")
|
||||
operations = args.get("operations") or []
|
||||
if not src_b64:
|
||||
return ToolResult(success=False, result=None, error="file_base64 is required for excel_update")
|
||||
if not isinstance(operations, list) or not operations:
|
||||
return ToolResult(success=False, result=None, error="operations must be non-empty array")
|
||||
|
||||
file_name = self._sanitize_file_name(args.get("file_name"), "updated.xlsx", force_ext=".xlsx")
|
||||
wb = openpyxl.load_workbook(filename=BytesIO(self._bytes_from_b64(src_b64)))
|
||||
|
||||
for op in operations:
|
||||
if not isinstance(op, dict):
|
||||
return ToolResult(success=False, result=None, error="Each operation must be object")
|
||||
op_type = str(op.get("type") or "").strip().lower()
|
||||
|
||||
if op_type == "append_rows":
|
||||
sheet = str(op.get("sheet") or wb.sheetnames[0])[:31]
|
||||
if sheet not in wb.sheetnames:
|
||||
wb.create_sheet(title=sheet)
|
||||
ws = wb[sheet]
|
||||
rows_raw = op.get("rows") or []
|
||||
header_row = [c.value for c in ws[1]] if ws.max_row >= 1 else []
|
||||
rows = self._normalize_rows(rows_raw, headers=header_row if header_row else None)
|
||||
if rows and not header_row and isinstance(rows_raw[0], dict):
|
||||
header_row = list(rows_raw[0].keys())
|
||||
ws.append(header_row)
|
||||
rows = self._normalize_rows(rows_raw, headers=header_row)
|
||||
for row in rows:
|
||||
ws.append(row)
|
||||
elif op_type == "set_cell":
|
||||
sheet = str(op.get("sheet") or wb.sheetnames[0])[:31]
|
||||
cell = op.get("cell")
|
||||
if not cell:
|
||||
return ToolResult(success=False, result=None, error="set_cell operation requires 'cell'")
|
||||
if sheet not in wb.sheetnames:
|
||||
wb.create_sheet(title=sheet)
|
||||
wb[sheet][str(cell)] = op.get("value", "")
|
||||
elif op_type == "replace_sheet":
|
||||
sheet = str(op.get("sheet") or wb.sheetnames[0])[:31]
|
||||
if sheet in wb.sheetnames:
|
||||
wb.remove(wb[sheet])
|
||||
ws = wb.create_sheet(title=sheet)
|
||||
headers = op.get("headers") or []
|
||||
rows_raw = op.get("rows") or []
|
||||
rows = self._normalize_rows(rows_raw, headers=headers if headers else None)
|
||||
if rows and not headers and isinstance(rows_raw[0], dict):
|
||||
headers = list(rows_raw[0].keys())
|
||||
rows = self._normalize_rows(rows_raw, headers=headers)
|
||||
self._append_sheet_data(ws, headers, rows)
|
||||
elif op_type == "rename_sheet":
|
||||
src = str(op.get("from") or "")
|
||||
dst = str(op.get("to") or "").strip()
|
||||
if not src or not dst:
|
||||
return ToolResult(success=False, result=None, error="rename_sheet requires 'from' and 'to'")
|
||||
if src not in wb.sheetnames:
|
||||
return ToolResult(success=False, result=None, error=f"Sheet not found: {src}")
|
||||
wb[src].title = dst[:31]
|
||||
else:
|
||||
return ToolResult(success=False, result=None, error=f"Unsupported excel_update operation: {op_type}")
|
||||
|
||||
out = BytesIO()
|
||||
wb.save(out)
|
||||
return ToolResult(
|
||||
success=True,
|
||||
result={"message": f"Excel updated: {file_name}"},
|
||||
file_base64=self._b64_from_bytes(out.getvalue()),
|
||||
file_name=file_name,
|
||||
file_mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
)
|
||||
|
||||
def _file_docx_create(self, args: Dict[str, Any]) -> ToolResult:
|
||||
from docx import Document
|
||||
|
||||
|
||||
Reference in New Issue
Block a user