SPB Git forge

spb/uqo-chat

Public
14commits 1branches 0releases
1.4 MBsize
maindefault branch
17 days agolast push
Python 64.6% TypeScript 33.7% CSS 0.8%
4.7 KB · 105 lines python
Raw Blame History
1"""inspect_excel / edit_excel — read and modify an uploaded or generated workbook."""23from __future__ import annotations45import re6from typing import Any78from pydantic import BaseModel, Field910from app.llm.schemas import Artifact, ToolResult11from app.services import files as file_service12from app.tools import excel_ops13from app.tools.registry import ToolContext, registry141516class InspectArgs(BaseModel):17    file_id: str18    sheet: str | None = None19    max_rows: int = Field(60, ge=5, le=300)202122class EditArgs(BaseModel):23    file_id: str24    operations: list[dict[str, Any]] = Field(default_factory=list)25    filename: str | None = None262728async def _load(ctx: ToolContext, file_id: str) -> tuple[Any, bytes] | ToolResult:29    rec = await file_service.get_file(file_id)30    if not rec or rec.user_id != ctx.user_id:31        return ToolResult(content="Fichier introuvable. Utilise un file_id listé dans le contexte "32                          "(fichiers déposés ou classeurs déjà générés).", error=True)33    if file_service.ext_of(rec.filename) not in {"xlsx", "xlsm"}:34        return ToolResult(content=f"« {rec.filename} » n'est pas un classeur .xlsx.", error=True)35    try:36        return rec, file_service.read_bytes(rec)37    except FileNotFoundError:38        return ToolResult(content="Le fichier a expiré ; demande à l'étudiant de le redéposer.", error=True)394041async def inspect(args: dict[str, Any], ctx: ToolContext) -> ToolResult:42    loaded = await _load(ctx, args["file_id"])43    if isinstance(loaded, ToolResult):44        return loaded45    rec, data = loaded46    await ctx.report("running", f"Lecture de {rec.filename}…")47    info = excel_ops.inspect(data, args.get("sheet"), max_rows=args.get("max_rows", 60))48    parts = [f"Classeur « {rec.filename} » (file_id {rec.id}). Feuilles : "49             + ", ".join(s["name"] for s in info["sheets"])]50    for s in info["sheets"]:51        parts.append(f"\n## Feuille « {s['name']} » ({s['dims']}, {s['max_row']} lignes × {s['max_col']} col., "52                     f"{s['charts']} graphique(s))\n" + "\n".join(s["cells"][:200]))53    if info["defined_names"]:54        parts.append("Noms définis : " + ", ".join(info["defined_names"]))55    parts.append("\nLes formules sont affichées telles quelles (non calculées). Pour modifier : edit_excel "56                 "avec des opérations (set_cell, add_column avec formula '=E{r}*0.02', add_row, format, "57                 "add_sheet, add_chart…).")58    return ToolResult(content="\n".join(parts)[:40000],59                      payload={"filename": rec.filename, "file_id": rec.id, "preview": excel_ops.preview(data)},60                      meta={"summary": f"Lecture : {rec.filename}"})616263def _version_name(name: str) -> str:64    stem, ext = (name.rsplit(".", 1) + ["xlsx"])[:2]65    m = re.search(r"_v(\d+)$", stem)66    if m:67        return f"{stem[: m.start()]}_v{int(m.group(1)) + 1}.{ext}"68    return f"{stem}_v2.{ext}"697071async def edit(args: dict[str, Any], ctx: ToolContext) -> ToolResult:72    loaded = await _load(ctx, args["file_id"])73    if isinstance(loaded, ToolResult):74        return loaded75    rec, data = loaded76    ops = args.get("operations") or []77    if not ops:78        return ToolResult(content="Aucune opération fournie. Utilise inspect_excel pour lire, puis edit_excel "79                          "avec `operations`.", error=True)80    await ctx.report("running", f"Modification de {rec.filename} ({len(ops)} opération(s))…")81    try:82        new_data, log = excel_ops.apply_operations(data, ops)83    except excel_ops.ExcelOpError as exc:84        return ToolResult(content=f"Modification refusée : {exc}", error=True)85    filename = args.get("filename") or _version_name(rec.filename)86    if not filename.lower().endswith(".xlsx"):87        filename += ".xlsx"88    new_rec = await file_service.store_artifact(ctx.user_id, ctx.conversation_id, filename, new_data, ftype="xlsx")89    pv = excel_ops.preview(new_data)90    art = Artifact(type="xlsx", file_id=new_rec.id, filename=new_rec.filename, url=f"/api/v1/files/{new_rec.id}",91                   preview=pv)92    return ToolResult(93        content=f"Classeur modifié et enregistré sous « {new_rec.filename} » (file_id {new_rec.id}). Opérations :\n"94                + "\n".join(log) + "\nLe nouveau fichier est affiché à l'étudiant avec un bouton Télécharger ; "95                "l'original est conservé.",96        artifacts=[art],97        payload={"filename": new_rec.filename, "file_id": new_rec.id, "source": rec.filename,98                 "source_file_id": rec.id, "changes": log, "preview": pv},99        meta={"summary": f"Excel modifié : {new_rec.filename} ({len(log)} op.)"},100    )101102103registry.register("inspect_excel", inspect, InspectArgs)104registry.register("edit_excel", edit, EditArgs, heavy=True)105