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):
|
Implemented in actual NODE1 stack (`services/router/*` + gateway):
|
||||||
|
|
||||||
### Added actions
|
### Added actions
|
||||||
|
- `excel_create`
|
||||||
|
- `excel_update`
|
||||||
- `csv_create`
|
- `csv_create`
|
||||||
- `csv_update`
|
- `csv_update`
|
||||||
- `json_export`
|
- `json_export`
|
||||||
@@ -90,6 +92,7 @@ For file-producing tool calls, router now propagates:
|
|||||||
|
|
||||||
## Smoke Tests
|
## Smoke Tests
|
||||||
Run inside `dagi-router-node1` to validate actions deterministically:
|
Run inside `dagi-router-node1` to validate actions deterministically:
|
||||||
|
- Excel create/update
|
||||||
- CSV create/update
|
- CSV create/update
|
||||||
- JSON/YAML export
|
- JSON/YAML export
|
||||||
- ZIP bundle
|
- 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_step2_tool_manager.py.bak_20260215_012029`
|
||||||
- `rollback_backups/file_tool_step3_tool_manager.py.bak_20260215_012200`
|
- `rollback_backups/file_tool_step3_tool_manager.py.bak_20260215_012200`
|
||||||
- `rollback_backups/file_tool_step4_tool_manager.py.bak_20260215_012309`
|
- `rollback_backups/file_tool_step4_tool_manager.py.bak_20260215_012309`
|
||||||
|
- `services/router/tool_manager.py.bak_20260215_020902`
|
||||||
|
|
||||||
## Rollback (NODE1)
|
## Rollback (NODE1)
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -588,11 +588,22 @@ class ToolManager:
|
|||||||
out.append([row])
|
out.append([row])
|
||||||
return out
|
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:
|
async def _file_tool(self, args: Dict[str, Any]) -> ToolResult:
|
||||||
action = str((args or {}).get("action") or "").strip().lower()
|
action = str((args or {}).get("action") or "").strip().lower()
|
||||||
if not action:
|
if not action:
|
||||||
return ToolResult(success=False, result=None, error="Missing 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":
|
if action == "csv_create":
|
||||||
return self._file_csv_create(args)
|
return self._file_csv_create(args)
|
||||||
if action == "csv_update":
|
if action == "csv_update":
|
||||||
@@ -751,6 +762,128 @@ class ToolManager:
|
|||||||
file_mime="application/zip",
|
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:
|
def _file_docx_create(self, args: Dict[str, Any]) -> ToolResult:
|
||||||
from docx import Document
|
from docx import Document
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user