CI / test (push) Canceled after 0s
## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
"""Build a REAL .xlsx from text content so an agent can CREATE Excel by calling
|
|
save_file/write_file('report.xlsx', <table text>).
|
|
|
|
A .xlsx is a binary ZIP package — writing the model's text straight to a .xlsx
|
|
corrupts it (the file won't open). This turns the content the model produces
|
|
(CSV / TSV / a Markdown table / JSON rows) into a genuine workbook via openpyxl
|
|
(already a dependency). Pure logic (no Qt) → unit-testable.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
|
|
def _openpyxl():
|
|
"""Import openpyxl, auto-installing it on first use if it isn't present —
|
|
same self-healing path the Excel VIEWER and doc_style_extract use
|
|
(``deps.ensure_module``). openpyxl is a declared dependency, so this only
|
|
matters for a from-source run whose venv is missing it; a normal install /
|
|
frozen build already bundles it. Returns the module or None."""
|
|
try:
|
|
from .deps import ensure_module
|
|
return ensure_module("openpyxl", "openpyxl")
|
|
except Exception: # noqa: BLE001 - fall back to a plain import
|
|
try:
|
|
import openpyxl # noqa: F401
|
|
return openpyxl
|
|
except Exception: # noqa: BLE001
|
|
return None
|
|
|
|
|
|
def is_available() -> bool:
|
|
"""Máy đã cài ``openpyxl`` chưa — không có thì mọi tính năng Excel tắt."""
|
|
return _openpyxl() is not None
|
|
|
|
|
|
def _rows_from_text(content: str) -> List[list]:
|
|
"""Parse table content into a list of rows. Accepts JSON (list-of-lists or
|
|
list-of-dicts), a Markdown table, or CSV/TSV (delimiter sniffed)."""
|
|
content = content or ""
|
|
s = content.strip()
|
|
# JSON: [[...],[...]] or [{...},{...}] or {"rows"/"data": [...]}
|
|
if s[:1] in ("[", "{"):
|
|
try:
|
|
data = json.loads(s)
|
|
if isinstance(data, dict):
|
|
data = data.get("rows") or data.get("data") or [data]
|
|
rows: List[list] = []
|
|
if isinstance(data, list):
|
|
if data and isinstance(data[0], dict):
|
|
headers = list(dict.fromkeys(k for d in data if isinstance(d, dict) for k in d))
|
|
rows.append(headers)
|
|
for d in data:
|
|
rows.append([d.get(h, "") for h in headers] if isinstance(d, dict) else [d])
|
|
else:
|
|
for r in data:
|
|
rows.append(list(r) if isinstance(r, (list, tuple)) else [r])
|
|
if rows:
|
|
return rows
|
|
except (ValueError, TypeError):
|
|
pass
|
|
lines = [ln for ln in content.splitlines() if ln.strip()]
|
|
# Markdown table: rows delimited by '|', a --- separator row skipped.
|
|
if lines and lines[0].lstrip().startswith("|"):
|
|
rows = []
|
|
for ln in lines:
|
|
body = ln.strip()
|
|
if set(body) <= set("|-: "): # separator row like |---|---|
|
|
continue
|
|
rows.append([c.strip() for c in body.strip("|").split("|")])
|
|
if rows:
|
|
return rows
|
|
# CSV / TSV — sniff which delimiter dominates.
|
|
delim = "\t" if content.count("\t") > content.count(",") else ","
|
|
return list(csv.reader(io.StringIO(content), delimiter=delim))
|
|
|
|
|
|
def build_xlsx_from_text(path, content: str) -> bool:
|
|
"""Write a genuine .xlsx at ``path`` from CSV/TSV/Markdown-table/JSON
|
|
``content``. Returns True on success, False if openpyxl is unavailable or the
|
|
write fails (caller can then fall back). Never raises."""
|
|
openpyxl = _openpyxl()
|
|
if openpyxl is None:
|
|
return False
|
|
try:
|
|
rows = _rows_from_text(content)
|
|
wb = openpyxl.Workbook()
|
|
ws = wb.active
|
|
for r in rows:
|
|
ws.append(["" if v is None else v for v in r])
|
|
p = Path(path)
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
wb.save(str(p))
|
|
return True
|
|
except Exception: # noqa: BLE001
|
|
return False
|