QueryQuest / src /queryquest /excel /context.py
mohamedgamal04
QueryQuest — Hugging Face Spaces deploy (Docker)
0cac9cf
Raw
History Blame Contribute Delete
5.5 kB
"""Excel file discovery and context summarization utilities for prompt building."""
from __future__ import annotations
import hashlib
import json
import re
from pathlib import Path
import pandas as pd
from ..config import EXCEL_DIR
SUPPORTED_SUFFIXES = {".xlsx", ".xls"}
def _table_name_from_file(file_name: str) -> str:
"""Normalize a workbook or sheet label into a SQL-safe identifier."""
stem = file_name.rsplit(".", 1)[0]
normalized = re.sub(r"[^0-9A-Za-z]+", "_", stem).strip("_")
return normalized or "excel_data"
def _table_name_from_sheet(file_name: str, sheet_name: str) -> str:
"""Return the SQL table name used by the executor for one workbook sheet."""
workbook_part = _table_name_from_file(file_name)
sheet_part = _table_name_from_file(sheet_name)
return f"{workbook_part}__{sheet_part}"
def normalize_excel_dir(excel_dir: str | Path | None = None) -> Path:
"""Normalize Excel directory input into an absolute resolved path."""
path = Path(excel_dir) if excel_dir is not None else EXCEL_DIR
return path.expanduser().resolve()
def list_excel_files(excel_dir: str | Path | None = None) -> list[Path]:
"""Return supported Excel workbooks in a deterministic sorted order."""
directory = normalize_excel_dir(excel_dir)
if not directory.exists() or not directory.is_dir():
return []
return sorted(
[item for item in directory.iterdir() if item.is_file() and item.suffix.lower() in SUPPORTED_SUFFIXES],
key=lambda item: item.name.lower(),
)
def read_excel_file(file_path: Path) -> pd.ExcelFile:
"""Open an Excel workbook and raise if the expected file is missing."""
if not file_path.exists():
raise FileNotFoundError(f"File {file_path.name} not found in {file_path.parent}")
return pd.ExcelFile(file_path)
def get_col_info(df: pd.DataFrame) -> str:
"""Build compact schema metadata for a DataFrame."""
col_types = df.dtypes
rows = len(df)
cols = len(df.columns)
info = "\n".join([f"{col}: {dtype}" for col, dtype in col_types.items()])
if not info:
info = "No columns found"
info += f"\nColumns: {cols}"
info += f"\nRows: {rows}"
return info
def get_sample_rows(df: pd.DataFrame, sample_size: int = 1) -> str:
"""Return a small human-readable table preview used in LLM context text."""
if df.empty:
return "Sample rows: No rows found"
sample_df = df.head(sample_size)
headers = [str(column).strip() for column in sample_df.columns]
lines = [" | ".join(headers)]
for row in sample_df.itertuples(index=False, name=None):
values = [str(value).strip() for value in row]
lines.append(" | ".join(values))
return "Sample rows:\n" + "\n".join(lines)
def _snapshot_file(file_path: Path) -> dict[str, str | int]:
"""Return file metadata used to detect workbook set/content changes."""
stat = file_path.stat()
return {
"name": file_path.name,
"size": stat.st_size,
"mtime_ns": stat.st_mtime_ns,
}
def get_excel_snapshot(excel_dir: str | Path | None = None) -> tuple[str, list[dict[str, str | int]]]:
"""Return snapshot digest and entries for all tracked Excel files."""
files = list_excel_files(excel_dir)
snapshot = [_snapshot_file(file_path) for file_path in files]
digest = hashlib.sha256(json.dumps(snapshot, sort_keys=True).encode("utf-8")).hexdigest()
return digest, snapshot
def describe_excel_snapshot_changes(
previous_snapshot: list[dict[str, str | int]] | None,
current_snapshot: list[dict[str, str | int]] | None,
) -> str:
"""Describe added/removed workbook names between two snapshots."""
previous_names = {
str(item.get("name"))
for item in (previous_snapshot or [])
if isinstance(item, dict) and isinstance(item.get("name"), str)
}
current_names = {
str(item.get("name"))
for item in (current_snapshot or [])
if isinstance(item, dict) and isinstance(item.get("name"), str)
}
added = sorted(current_names - previous_names)
removed = sorted(previous_names - current_names)
if not added and not removed:
return ""
sections: list[str] = ["### EXCEL FILE CHANGES"]
if added:
sections.append(f"Added files: {', '.join(added)}")
if removed:
sections.append(f"Removed files: {', '.join(removed)}")
return "\n".join(sections)
def build_excel_files_info(excel_dir: str | Path | None = None) -> tuple[str, str, list[dict[str, str | int]]]:
"""Build text context, snapshot digest, and snapshot entries for prompts."""
directory = normalize_excel_dir(excel_dir)
files = list_excel_files(directory)
digest, snapshot = get_excel_snapshot(directory)
if not files:
message = f"Excel directory: {directory}\nNo Excel files found."
return message, digest, snapshot
sections: list[str] = [f"Excel directory: {directory}", f"Excel files found: {len(files)}"]
for file_path in files:
xls = read_excel_file(file_path)
sheet_names = [str(sheet_name) for sheet_name in xls.sheet_names]
sheet_sections: list[str] = [f"File: {file_path.name}", f"Sheets: {', '.join(sheet_names)}"]
for sheet_name in xls.sheet_names:
df = xls.parse(sheet_name)
sheet_sections.append(f"Sheet: {sheet_name}")
sheet_sections.append(f"SQL table: {_table_name_from_sheet(file_path.name, str(sheet_name))}")
sheet_sections.append(get_col_info(df))
sheet_sections.append(get_sample_rows(df))
sections.append("\n".join(sheet_sections))
return "\n\n".join(sections), digest, snapshot
def format_excel_context(info: str | None) -> str:
"""Wrap plain Excel metadata text in a structured prompt section."""
if not info:
return ""
return f"### EXCEL FILE CONTEXT\n{info}"