chore: commit local edits before pushing to HF Space
Browse files- app.py +56 -0
- scripts/build_database.py +5 -0
- scripts/ingest_ebm.py +5 -0
- src/parser.py +6 -0
- src/rag_pipeline.py +5 -0
- src/vector_store.py +10 -0
app.py
CHANGED
|
@@ -4,6 +4,10 @@ from pathlib import Path
|
|
| 4 |
|
| 5 |
import gradio as gr
|
| 6 |
import pandas as pd
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from src.parser import filter_df_by_fachgruppe, parse_ebm_xml_to_dataframe
|
| 9 |
from src.rag_pipeline import EbmRAGPipeline, build_pipeline_from_paths
|
|
@@ -20,10 +24,62 @@ PIPELINE: EbmRAGPipeline | None = None
|
|
| 20 |
def get_pipeline() -> EbmRAGPipeline:
|
| 21 |
global PIPELINE
|
| 22 |
if PIPELINE is None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
PIPELINE = build_pipeline_from_paths(DATA_XML, STORE_DIR)
|
| 24 |
return PIPELINE
|
| 25 |
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
def format_retrieved(results: list[dict]) -> str:
|
| 28 |
if not results:
|
| 29 |
return "No retrieved documents."
|
|
|
|
| 4 |
|
| 5 |
import gradio as gr
|
| 6 |
import pandas as pd
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
import traceback
|
| 11 |
|
| 12 |
from src.parser import filter_df_by_fachgruppe, parse_ebm_xml_to_dataframe
|
| 13 |
from src.rag_pipeline import EbmRAGPipeline, build_pipeline_from_paths
|
|
|
|
| 24 |
def get_pipeline() -> EbmRAGPipeline:
|
| 25 |
global PIPELINE
|
| 26 |
if PIPELINE is None:
|
| 27 |
+
# Ensure vector store exists. Try to download full EBM and build; on failure, fall back to demo XML.
|
| 28 |
+
try:
|
| 29 |
+
ensure_vector_store()
|
| 30 |
+
except Exception:
|
| 31 |
+
# Log and continue; build_pipeline_from_paths will attempt to load or build and may raise a clearer error
|
| 32 |
+
print("Warning: ensure_vector_store failed:\n" + traceback.format_exc())
|
| 33 |
PIPELINE = build_pipeline_from_paths(DATA_XML, STORE_DIR)
|
| 34 |
return PIPELINE
|
| 35 |
|
| 36 |
|
| 37 |
+
def ensure_vector_store() -> None:
|
| 38 |
+
"""Try to prepare the FAISS store before the app starts.
|
| 39 |
+
|
| 40 |
+
Steps:
|
| 41 |
+
- If the store already exists, do nothing.
|
| 42 |
+
- Attempt to download the full EBM ZIP (scripts/download_full_ebm.py).
|
| 43 |
+
- Attempt to build the FAISS store (scripts/build_database.py).
|
| 44 |
+
- If building fails due to missing Fachgruppe 001 data, fall back to using the local demo XML by
|
| 45 |
+
disabling the Fachgruppe filter via environment variable `SKIP_FG_FILTER=1`.
|
| 46 |
+
"""
|
| 47 |
+
root = Path(__file__).resolve().parent
|
| 48 |
+
store_dir = STORE_DIR
|
| 49 |
+
if store_dir.exists() and (store_dir / "index.faiss").exists() and (store_dir / "metadata.jsonl").exists():
|
| 50 |
+
return
|
| 51 |
+
|
| 52 |
+
# Try to download full EBM
|
| 53 |
+
download_script = root / "scripts" / "download_full_ebm.py"
|
| 54 |
+
build_script = root / "scripts" / "build_database.py"
|
| 55 |
+
|
| 56 |
+
if download_script.exists():
|
| 57 |
+
try:
|
| 58 |
+
print("Attempting to download full KBV EBM archive...")
|
| 59 |
+
subprocess.run([sys.executable, str(download_script)], check=True, timeout=600)
|
| 60 |
+
except Exception:
|
| 61 |
+
print("Download script failed:\n" + traceback.format_exc())
|
| 62 |
+
|
| 63 |
+
# Try to build the FAISS store
|
| 64 |
+
if build_script.exists():
|
| 65 |
+
try:
|
| 66 |
+
print("Attempting to build FAISS store from XML...")
|
| 67 |
+
subprocess.run([sys.executable, str(build_script), "--xml", str(DATA_XML), "--store", str(STORE_DIR)], check=True, timeout=600)
|
| 68 |
+
except subprocess.CalledProcessError as e:
|
| 69 |
+
print("Build script failed with CalledProcessError:\n" + traceback.format_exc())
|
| 70 |
+
# If the build failed due to empty Fachgruppe 001, allow fallback to demo XML
|
| 71 |
+
# The build scripts raise ValueError in that case; detect it by inspecting output/exception
|
| 72 |
+
# Fallback: disable fachgruppe filter so demo XML (with 2 entries) can be used
|
| 73 |
+
print("Falling back to demo XML: disabling Fachgruppe-001 filter for this run.")
|
| 74 |
+
os.environ["SKIP_FG_FILTER"] = "1"
|
| 75 |
+
except Exception:
|
| 76 |
+
print("Build script failed:\n" + traceback.format_exc())
|
| 77 |
+
print("Falling back to demo XML: disabling Fachgruppe-001 filter for this run.")
|
| 78 |
+
os.environ["SKIP_FG_FILTER"] = "1"
|
| 79 |
+
else:
|
| 80 |
+
print("No build script found; continuing and relying on existing data/ebm.xml")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
def format_retrieved(results: list[dict]) -> str:
|
| 84 |
if not results:
|
| 85 |
return "No retrieved documents."
|
scripts/build_database.py
CHANGED
|
@@ -26,6 +26,11 @@ def main() -> None:
|
|
| 26 |
|
| 27 |
df = parse_ebm_xml_to_dataframe(str(xml_path))
|
| 28 |
df = filter_df_by_fachgruppe(df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
documents = dataframe_to_documents(df)
|
| 30 |
store, embeddings = EbmVectorStore.build(documents, embedding_model=embedding_model)
|
| 31 |
store.save(store_dir, embeddings=embeddings)
|
|
|
|
| 26 |
|
| 27 |
df = parse_ebm_xml_to_dataframe(str(xml_path))
|
| 28 |
df = filter_df_by_fachgruppe(df)
|
| 29 |
+
if df.empty:
|
| 30 |
+
raise ValueError(
|
| 31 |
+
"No Fachgruppe 001 documents found in data/ebm.xml. "
|
| 32 |
+
"Please provide a full KBV EBM XML with Fachgruppe 001 entries."
|
| 33 |
+
)
|
| 34 |
documents = dataframe_to_documents(df)
|
| 35 |
store, embeddings = EbmVectorStore.build(documents, embedding_model=embedding_model)
|
| 36 |
store.save(store_dir, embeddings=embeddings)
|
scripts/ingest_ebm.py
CHANGED
|
@@ -24,6 +24,11 @@ def main() -> None:
|
|
| 24 |
|
| 25 |
df = parse_ebm_xml_to_dataframe(str(xml_path))
|
| 26 |
df = filter_df_by_fachgruppe(df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
df.to_parquet(output_dir / "ebm.parquet", index=False)
|
| 28 |
df.to_json(output_dir / "ebm.jsonl", orient="records", lines=True, force_ascii=False)
|
| 29 |
|
|
|
|
| 24 |
|
| 25 |
df = parse_ebm_xml_to_dataframe(str(xml_path))
|
| 26 |
df = filter_df_by_fachgruppe(df)
|
| 27 |
+
if df.empty:
|
| 28 |
+
raise ValueError(
|
| 29 |
+
"No Fachgruppe 001 documents found in data/ebm.xml. "
|
| 30 |
+
"Please provide a full KBV EBM XML with Fachgruppe 001 entries."
|
| 31 |
+
)
|
| 32 |
df.to_parquet(output_dir / "ebm.parquet", index=False)
|
| 33 |
df.to_json(output_dir / "ebm.jsonl", orient="records", lines=True, force_ascii=False)
|
| 34 |
|
src/parser.py
CHANGED
|
@@ -109,5 +109,11 @@ def parse_ebm_xml_to_dataframe(xml_path: str) -> pd.DataFrame:
|
|
| 109 |
|
| 110 |
|
| 111 |
def filter_df_by_fachgruppe(df: pd.DataFrame, fachgruppe: str = "001") -> pd.DataFrame:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
return df[df["fachgruppen"].apply(lambda x: isinstance(x, list) and fachgruppe in x)].reset_index(drop=True)
|
| 113 |
|
|
|
|
| 109 |
|
| 110 |
|
| 111 |
def filter_df_by_fachgruppe(df: pd.DataFrame, fachgruppe: str = "001") -> pd.DataFrame:
|
| 112 |
+
# Allow skipping the fachgruppe filter (useful for demo XML fallback)
|
| 113 |
+
import os
|
| 114 |
+
|
| 115 |
+
if os.environ.get("SKIP_FG_FILTER") == "1":
|
| 116 |
+
return df.reset_index(drop=True)
|
| 117 |
+
|
| 118 |
return df[df["fachgruppen"].apply(lambda x: isinstance(x, list) and fachgruppe in x)].reset_index(drop=True)
|
| 119 |
|
src/rag_pipeline.py
CHANGED
|
@@ -134,6 +134,11 @@ def build_pipeline_from_paths(xml_path: str | Path, store_dir: str | Path, embed
|
|
| 134 |
else:
|
| 135 |
df = parse_ebm_xml_to_dataframe(str(xml_path))
|
| 136 |
df = filter_df_by_fachgruppe(df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
documents = dataframe_to_documents(df)
|
| 138 |
store, embeddings = EbmVectorStore.build(documents, embedding_model=embedding_model)
|
| 139 |
store.save(store_dir, embeddings=embeddings)
|
|
|
|
| 134 |
else:
|
| 135 |
df = parse_ebm_xml_to_dataframe(str(xml_path))
|
| 136 |
df = filter_df_by_fachgruppe(df)
|
| 137 |
+
if df.empty:
|
| 138 |
+
raise ValueError(
|
| 139 |
+
"No Fachgruppe 001 documents found in data/ebm.xml. "
|
| 140 |
+
"Please provide a full KBV EBM XML with Fachgruppe 001 entries."
|
| 141 |
+
)
|
| 142 |
documents = dataframe_to_documents(df)
|
| 143 |
store, embeddings = EbmVectorStore.build(documents, embedding_model=embedding_model)
|
| 144 |
store.save(store_dir, embeddings=embeddings)
|
src/vector_store.py
CHANGED
|
@@ -42,7 +42,17 @@ class EbmVectorStore:
|
|
| 42 |
document_to_search_text(EbmDocument(**{k: v for k, v in doc.items() if k != "search_text"}))
|
| 43 |
for doc in docs
|
| 44 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
embeddings = embedding_model.encode(texts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
index = faiss.IndexFlatIP(embeddings.shape[1])
|
| 47 |
index.add(embeddings)
|
| 48 |
store = cls(index=index, documents=docs, embedding_model_name=embedding_model.model_name)
|
|
|
|
| 42 |
document_to_search_text(EbmDocument(**{k: v for k, v in doc.items() if k != "search_text"}))
|
| 43 |
for doc in docs
|
| 44 |
]
|
| 45 |
+
if not texts:
|
| 46 |
+
raise ValueError(
|
| 47 |
+
"Cannot build vector store: no documents available. "
|
| 48 |
+
"Check that data/ebm.xml contains Fachgruppe 001 entries or remove the Fachgruppe-001 filter."
|
| 49 |
+
)
|
| 50 |
embeddings = embedding_model.encode(texts)
|
| 51 |
+
if embeddings.ndim != 2 or embeddings.shape[0] == 0:
|
| 52 |
+
raise ValueError(
|
| 53 |
+
"Embedding model returned invalid embeddings. "
|
| 54 |
+
"Expected a 2D array with one embedding per document."
|
| 55 |
+
)
|
| 56 |
index = faiss.IndexFlatIP(embeddings.shape[1])
|
| 57 |
index.add(embeddings)
|
| 58 |
store = cls(index=index, documents=docs, embedding_model_name=embedding_model.model_name)
|