From cf6ac778bb7e850103d67a4a36bfab7215d5e6c3 Mon Sep 17 00:00:00 2001 From: Apple Date: Sun, 15 Feb 2026 02:24:11 -0800 Subject: [PATCH] feat(file-tool): add text markdown xml html actions --- docs/runbooks/NODE1_FILE_TOOL_SYNC_RUNBOOK.md | 9 ++ services/router/tool_manager.py | 117 +++++++++++++++++- 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/docs/runbooks/NODE1_FILE_TOOL_SYNC_RUNBOOK.md b/docs/runbooks/NODE1_FILE_TOOL_SYNC_RUNBOOK.md index b9ded408..5ef6e6f3 100644 --- a/docs/runbooks/NODE1_FILE_TOOL_SYNC_RUNBOOK.md +++ b/docs/runbooks/NODE1_FILE_TOOL_SYNC_RUNBOOK.md @@ -60,6 +60,12 @@ Implemented in actual NODE1 stack (`services/router/*` + gateway): - `pdf_merge` - `pdf_split` - `pdf_fill` +- `text_create` +- `text_update` +- `markdown_create` +- `markdown_update` +- `xml_export` +- `html_export` ### Standard output contract For file-producing tool calls, router now propagates: @@ -93,6 +99,8 @@ For file-producing tool calls, router now propagates: ## Smoke Tests Run inside `dagi-router-node1` to validate actions deterministically: - Excel create/update +- Text/Markdown create/update +- XML/HTML export - CSV create/update - JSON/YAML export - ZIP bundle @@ -108,6 +116,7 @@ Also verify infer endpoint still works: - `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` +- `services/router/tool_manager.py.bak_20260215_112313` ## Rollback (NODE1) ```bash diff --git a/services/router/tool_manager.py b/services/router/tool_manager.py index 00029061..a8e99b81 100644 --- a/services/router/tool_manager.py +++ b/services/router/tool_manager.py @@ -325,7 +325,9 @@ TOOL_DEFINITIONS = [ "enum": [ "excel_create", "excel_update", "docx_create", "docx_update", "csv_create", "csv_update", "pdf_fill", "pdf_merge", "pdf_split", - "json_export", "yaml_export", "zip_bundle" + "json_export", "yaml_export", "zip_bundle", + "text_create", "text_update", "markdown_create", "markdown_update", + "xml_export", "html_export" ], "description": "Дія file tool" }, @@ -608,6 +610,18 @@ class ToolManager: return self._file_csv_create(args) if action == "csv_update": return self._file_csv_update(args) + if action == "text_create": + return self._file_text_create(args) + if action == "text_update": + return self._file_text_update(args) + if action == "markdown_create": + return self._file_markdown_create(args) + if action == "markdown_update": + return self._file_markdown_update(args) + if action == "xml_export": + return self._file_xml_export(args) + if action == "html_export": + return self._file_html_export(args) if action == "json_export": return self._file_json_export(args) if action == "yaml_export": @@ -711,6 +725,107 @@ class ToolManager: file_mime="application/json", ) + @staticmethod + def _stringify_text_payload(args: Dict[str, Any], key: str = "text") -> str: + value = args.get(key) + if value is None and key != "content": + value = args.get("content") + if value is None: + return "" + if isinstance(value, (dict, list)): + return json.dumps(value, ensure_ascii=False, indent=2) + return str(value) + + def _file_text_create(self, args: Dict[str, Any]) -> ToolResult: + file_name = self._sanitize_file_name(args.get("file_name"), "note.txt", force_ext=".txt") + text = self._stringify_text_payload(args, key="text") + data = text.encode("utf-8") + return ToolResult( + success=True, + result={"message": f"Text file created: {file_name}"}, + file_base64=self._b64_from_bytes(data), + file_name=file_name, + file_mime="text/plain", + ) + + def _file_text_update(self, args: Dict[str, Any]) -> ToolResult: + src_b64 = args.get("file_base64") + if not src_b64: + return ToolResult(success=False, result=None, error="file_base64 is required for text_update") + file_name = self._sanitize_file_name(args.get("file_name"), "updated.txt", force_ext=".txt") + operation = str(args.get("operation") or "append").strip().lower() + if operation not in {"append", "replace"}: + return ToolResult(success=False, result=None, error="operation must be append|replace") + + incoming = self._stringify_text_payload(args, key="text") + existing = self._bytes_from_b64(src_b64).decode("utf-8") + updated = incoming if operation == "replace" else f"{existing}{incoming}" + data = updated.encode("utf-8") + return ToolResult( + success=True, + result={"message": f"Text file updated: {file_name}"}, + file_base64=self._b64_from_bytes(data), + file_name=file_name, + file_mime="text/plain", + ) + + def _file_markdown_create(self, args: Dict[str, Any]) -> ToolResult: + file_name = self._sanitize_file_name(args.get("file_name"), "document.md", force_ext=".md") + text = self._stringify_text_payload(args, key="text") + data = text.encode("utf-8") + return ToolResult( + success=True, + result={"message": f"Markdown created: {file_name}"}, + file_base64=self._b64_from_bytes(data), + file_name=file_name, + file_mime="text/markdown", + ) + + def _file_markdown_update(self, args: Dict[str, Any]) -> ToolResult: + src_b64 = args.get("file_base64") + if not src_b64: + return ToolResult(success=False, result=None, error="file_base64 is required for markdown_update") + file_name = self._sanitize_file_name(args.get("file_name"), "updated.md", force_ext=".md") + operation = str(args.get("operation") or "append").strip().lower() + if operation not in {"append", "replace"}: + return ToolResult(success=False, result=None, error="operation must be append|replace") + + incoming = self._stringify_text_payload(args, key="text") + existing = self._bytes_from_b64(src_b64).decode("utf-8") + updated = incoming if operation == "replace" else f"{existing}{incoming}" + data = updated.encode("utf-8") + return ToolResult( + success=True, + result={"message": f"Markdown updated: {file_name}"}, + file_base64=self._b64_from_bytes(data), + file_name=file_name, + file_mime="text/markdown", + ) + + def _file_xml_export(self, args: Dict[str, Any]) -> ToolResult: + file_name = self._sanitize_file_name(args.get("file_name"), "export.xml", force_ext=".xml") + xml = self._stringify_text_payload(args, key="xml") + data = xml.encode("utf-8") + return ToolResult( + success=True, + result={"message": f"XML exported: {file_name}"}, + file_base64=self._b64_from_bytes(data), + file_name=file_name, + file_mime="application/xml", + ) + + def _file_html_export(self, args: Dict[str, Any]) -> ToolResult: + file_name = self._sanitize_file_name(args.get("file_name"), "export.html", force_ext=".html") + html = self._stringify_text_payload(args, key="html") + data = html.encode("utf-8") + return ToolResult( + success=True, + result={"message": f"HTML exported: {file_name}"}, + file_base64=self._b64_from_bytes(data), + file_name=file_name, + file_mime="text/html", + ) + def _file_yaml_export(self, args: Dict[str, Any]) -> ToolResult: file_name = self._sanitize_file_name(args.get("file_name"), "export.yaml", force_ext=".yaml") content = args.get("content")