CoolFace
Apppublic

ali27x/TendersREF

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
app.py401 linesDownload Raw Back to root
1from __future__ import annotations2 3from io import BytesIO4from pathlib import Path5from datetime import datetime6 7import pandas as pd8import streamlit as st9 10import tce.classifier as classifier11from tce import ui_labels, workflow12from tce.classifier import ClassificationResult13from tce.spec_loader import load_spec14from tce.storage import fetch_references, upsert_reference15 16 17ROOT = Path(__file__).parent18SPEC_PATH = ROOT / "TCS_TCE_Master_Spec_v1.0.xlsx"19BENCHMARK_PATH = ROOT / "Gold_Dataset_v3.0_Final_All_clean_v13.xlsx"20DB_PATH = ROOT / "tce.db"21BUILD_ID = "2026-07-14-14"22CLASSIFIER_VERSION = "TCE v1.0 Beta"23APPROVED_BY_DEFAULT = "Manual Reviewer"24 25 26@st.cache_resource27def get_spec():28    return load_spec(SPEC_PATH)29 30 31@st.cache_data32def get_benchmark_metrics() -> dict | None:33    if BENCHMARK_PATH.exists():34        from tce.evaluation import evaluate_gold_set35 36        return evaluate_gold_set(BENCHMARK_PATH, get_spec())37    return None38 39 40def load_approved_references() -> list[dict]:41    return fetch_references(DB_PATH)42 43 44def result_to_dict(result: ClassificationResult) -> dict:45    return result.__dict__.copy()46 47 48def render_bilingual_value(en: str, ar: str) -> str:49    en = (en or "").strip() or ui_labels.BLANK_LABEL50    ar = (ar or "").strip() or ui_labels.BLANK_LABEL51    if en == ui_labels.BLANK_LABEL and ar == ui_labels.BLANK_LABEL:52        return ui_labels.BLANK_LABEL53    return f"{en} | {ar}"54 55 56def title_payload(title: str) -> tuple[str, str, str]:57    return workflow.select_classification_inputs(title.strip())58 59 60def classify_single_title(title: str, spec, refs: list[dict]) -> tuple[workflow.SuggestionBundle, str]:61    bundle = workflow.classify_with_approved_learning(title, spec, refs)62    detected_language = workflow.detect_input_language(title)63    return bundle, detected_language64 65 66def classify_dataframe(df: pd.DataFrame, spec, refs: list[dict]) -> pd.DataFrame:67    rows = []68    for _, row in df.iterrows():69        bundle = workflow.classify_row_with_learning(row.to_dict(), spec, refs)70        r = bundle.result71        out = row.to_dict()72        out.update(result_to_dict(r))73        out["classification_title_used"] = r.classification_title_used74        out["classification_language"] = r.classification_language75        out["approval_status"] = r.approval_status76        out["suggestion_source"] = bundle.suggestion_source77        out["detected_language"] = workflow.detect_input_language(78            str(row.get("name_ar", "") or row.get("name_en", "") or "")79        )80        rows.append(out)81    return pd.DataFrame(rows)82 83 84def bilingual_options(values: list[str], mapping: dict[str, dict[str, str]]) -> list[str]:85    return values86 87 88def format_bilingual_option(value: str, mapping: dict[str, dict[str, str]]) -> str:89    return ui_labels.pair_label(value, mapping)90 91 92def save_approved_result(93    *,94    original_title: str,95    detected_language: str,96    suggested: ClassificationResult,97    final_nature: str,98    final_sector: str,99    final_scope: str,100    approved_by: str,101    correction_note: str,102    suggestion_source: str,103    approved_at: str | None = None,104):105    title_norm = workflow.normalize_input_title(original_title)106    upsert_reference(107        DB_PATH,108        {109            "title_key": title_norm,110            "title_original": original_title,111            "title_normalized": title_norm,112            "detected_language": detected_language,113            "title_ar": original_title if detected_language in {"ar", "mixed"} else "",114            "title_en": original_title if detected_language == "en" else "",115            "suggested_nature": suggested.nature_en,116            "suggested_sector": suggested.market_sector_en,117            "suggested_scope": suggested.scope_en,118            "approved_nature": final_nature,119            "approved_sector": final_sector,120            "approved_scope": final_scope,121            "nature_en": final_nature,122            "nature_ar": ui_labels.NATURE_LABELS.get(final_nature, {}).get("ar", ""),123            "market_sector_en": final_sector,124            "market_sector_ar": ui_labels.SECTOR_LABELS.get(final_sector, {}).get("ar", ""),125            "scope_en": final_scope,126            "scope_ar": ui_labels.SCOPE_LABELS.get(final_scope, {}).get("ar", ""),127            "confidence": int(suggested.confidence or 0),128            "needs_review": "No",129            "classification_evidence": suggested.classification_evidence,130            "classification_note": suggested.classification_note,131            "suggestion_source": suggestion_source,132            "nature_changed": int(final_nature != suggested.nature_en),133            "sector_changed": int(final_sector != suggested.market_sector_en),134            "scope_changed": int(final_scope != suggested.scope_en),135            "correction_note": correction_note,136            "approved_by": approved_by,137            "approved_at": approved_at or datetime.utcnow().isoformat(sep=" ", timespec="seconds"),138            "classifier_version": CLASSIFIER_VERSION,139            "gold_dataset_version": "Gold Dataset v3.0 Final Clean v13",140            "tcs_version": spec.version,141            "approval_status": "Approved",142        },143    )144 145 146def set_pending_result(bundle: workflow.SuggestionBundle, original_title: str, detected_language: str):147    st.session_state["pending_result"] = result_to_dict(bundle.result)148    st.session_state["pending_title"] = original_title149    st.session_state["pending_language"] = detected_language150    st.session_state["pending_source"] = bundle.suggestion_source151    st.session_state["pending_similar_ref"] = bundle.similar_reference152    st.session_state["edit_single"] = False153 154 155def clear_pending_result():156    for key in ["pending_result", "pending_title", "pending_language", "pending_source", "pending_similar_ref", "edit_single"]:157        st.session_state.pop(key, None)158 159 160def request_single_form_reset(message: str) -> None:161    st.session_state["single_flash_message"] = message162    st.session_state["single_reset_requested"] = True163    st.session_state["single_title_widget_version"] = st.session_state.get("single_title_widget_version", 0) + 1164 165 166def apply_single_form_reset() -> None:167    st.session_state.setdefault("single_title_widget_version", 0)168    if st.session_state.pop("single_reset_requested", False):169        for key in ["pending_result", "pending_title", "pending_language", "pending_source", "pending_similar_ref", "edit_single"]:170            st.session_state.pop(key, None)171 172 173st.set_page_config(page_title="Tender Classification Engine", layout="wide")174spec = get_spec()175 176st.title("Tender Classification Engine")177st.caption(178    f"Classifier Version: {CLASSIFIER_VERSION} | Build: {BUILD_ID} | Latest TCS: {spec.version} | "179    f"Official benchmark: Gold Dataset v3.0 Final Clean v13"180)181 182if flash := st.session_state.pop("single_flash_message", ""):183    st.success(flash)184 185apply_single_form_reset()186 187with st.expander("Benchmark Status", expanded=False):188    if BENCHMARK_PATH.exists():189        if st.button("Load benchmark metrics"):190            bench = get_benchmark_metrics()191            if bench:192                st.write(193                    {194                        "Nature Accuracy": f'{bench["nature_accuracy"]:.1f}%',195                        "Market Sector Accuracy": f'{bench["market_sector_accuracy"]:.1f}%',196                        "Scope Accuracy": f'{bench["scope_accuracy"]:.1f}%',197                        "Needs Review %": f'{bench["needs_review_pct"]:.1f}%',198                        "Average Confidence": f'{bench["avg_confidence"]:.1f}%',199                        "Rows": bench["rows"],200                    }201                )202            else:203                st.warning("Benchmark file not found.")204    else:205        st.warning("Benchmark file not found.")206 207single_tab, batch_tab = st.tabs(["Single Classification | تصنيف مفرد", "Excel Upload | رفع ملف إكسل"])208 209with single_tab:210    st.subheader("Single Classification | تصنيف مفرد")211    title_widget_key = f"single_title_{st.session_state['single_title_widget_version']}"212    title = st.text_input("Tender Title | عنوان المناقصة", key=title_widget_key, placeholder="توريد أجهزة كمبيوتر / Supply of Computer Equipment")213    classify_clicked = st.button("Classify | تصنيف", type="primary")214 215    if classify_clicked:216        if title.strip():217            refs = load_approved_references()218            bundle, detected_language = classify_single_title(title, spec, refs)219            set_pending_result(bundle, title, detected_language)220        else:221            st.error("Tender Title | عنوان المناقصة is required.")222 223    if "pending_result" in st.session_state:224        res = st.session_state["pending_result"]225        source = st.session_state.get("pending_source", "rule_engine")226        st.markdown("### Suggested Classification | التصنيف المقترح")227        c1, c2, c3, c4 = st.columns(4)228        c1.metric("Nature", render_bilingual_value(res["nature_en"], res["nature_ar"]))229        c2.metric("Market Sector", render_bilingual_value(res["market_sector_en"], res["market_sector_ar"]))230        c3.metric("Scope", render_bilingual_value(res["scope_en"], res["scope_ar"]))231        c4.metric("Confidence", f'{res["confidence"]}%')232 233        st.write(234            {235                "Nature": render_bilingual_value(res["nature_en"], res["nature_ar"]),236                "Market Sector": render_bilingual_value(res["market_sector_en"], res["market_sector_ar"]),237                "Scope": render_bilingual_value(res["scope_en"], res["scope_ar"]),238                "Confidence": res["confidence"],239                "Evidence": res.get("classification_evidence", ""),240                "Suggestion Source": ui_labels.source_label(source),241                "Needs Review": res.get("needs_review", ""),242                "Final Decision": res.get("final_decision", ""),243            }244        )245 246        with st.expander("Technical Details | التفاصيل الفنية", expanded=False):247            st.write(248                {249                    "Main Action": res.get("main_action", ""),250                    "Main Subject": res.get("main_subject", ""),251                    "Supporting Evidence": res.get("supporting_evidence", []),252                    "Conflicting Evidence": res.get("conflicting_evidence", []),253                    "Matched Rule": res.get("matched_rule", ""),254                    "Review Reason": res.get("review_reason", ""),255                    "Classification Note": res.get("classification_note", ""),256                    "Classification Evidence": res.get("classification_evidence", ""),257                    "TCS Version": res.get("tcs_version", ""),258                }259            )260 261        col_a, col_b = st.columns(2)262        if col_a.button("Approve | موافقة"):263            save_approved_result(264                original_title=st.session_state.get("pending_title", title),265                detected_language=st.session_state.get("pending_language", ""),266                suggested=ClassificationResult(**res),267                final_nature=res["nature_en"],268                final_sector=res["market_sector_en"],269                final_scope=res["scope_en"],270                approved_by=APPROVED_BY_DEFAULT,271                correction_note="",272                suggestion_source=source,273                approved_at=None,274            )275            request_single_form_reset("Classification approved and saved | تم اعتماد التصنيف وحفظه")276            st.rerun()277 278        if col_b.button("Edit | تعديل"):279            st.session_state["edit_single"] = True280 281        if st.session_state.get("edit_single"):282            st.markdown("#### Edit Approved Classification | تعديل واعتماد التصنيف")283            with st.form("single_edit_form"):284                nature = st.selectbox(285                    "Nature | الطبيعة",286                    list(ui_labels.NATURE_LABELS.keys()),287                    index=list(ui_labels.NATURE_LABELS.keys()).index(res["nature_en"]) if res["nature_en"] in ui_labels.NATURE_LABELS else 0,288                    format_func=lambda v: ui_labels.pair_label(v, ui_labels.NATURE_LABELS),289                )290                sector = st.selectbox(291                    "Market Sector | قطاع السوق",292                    list(ui_labels.SECTOR_LABELS.keys()),293                    index=list(ui_labels.SECTOR_LABELS.keys()).index(res["market_sector_en"]) if res["market_sector_en"] in ui_labels.SECTOR_LABELS else 0,294                    format_func=lambda v: ui_labels.pair_label(v, ui_labels.SECTOR_LABELS),295                )296                scope = st.selectbox(297                    "Scope | النطاق",298                    list(ui_labels.SCOPE_LABELS.keys()),299                    index=list(ui_labels.SCOPE_LABELS.keys()).index(res["scope_en"]) if res["scope_en"] in ui_labels.SCOPE_LABELS else 0,300                    format_func=lambda v: ui_labels.pair_label(v, ui_labels.SCOPE_LABELS),301                )302                approved_by = st.text_input("Approved by | المعتمد بواسطة", value=APPROVED_BY_DEFAULT)303                correction_note = st.text_area("Correction note | ملاحظة التعديل", value="")304                save = st.form_submit_button("Save Approved Correction | حفظ واعتماد التعديل")305                if save:306                    save_approved_result(307                        original_title=st.session_state.get("pending_title", title),308                        detected_language=st.session_state.get("pending_language", ""),309                        suggested=ClassificationResult(**res),310                        final_nature=nature,311                        final_sector=sector,312                        final_scope=scope,313                        approved_by=approved_by,314                        correction_note=correction_note,315                        suggestion_source=source,316                        approved_at=None,317                    )318                    request_single_form_reset("Classification approved and saved | تم اعتماد التصنيف وحفظه")319                    st.rerun()320 321with batch_tab:322    st.subheader("Excel Upload | رفع ملف إكسل")323    uploaded = st.file_uploader("Upload Excel", type=["xlsx"])324    if uploaded is not None:325        df = pd.read_excel(uploaded)326        required = {"id", "publisher_ar", "name_ar", "name_en"}327        missing = required - set(df.columns)328        if missing:329            st.error(f"Missing columns: {', '.join(sorted(missing))}")330        else:331            if st.button("Classify | تصنيف", key="batch_classify_btn"):332                refs = load_approved_references()333                st.session_state["batch_df"] = classify_dataframe(df, spec, refs)334                st.session_state["batch_source_df"] = df335 336    if "batch_df" in st.session_state:337        st.markdown("### Review Results | مراجعة النتائج")338        options_nature = list(ui_labels.NATURE_LABELS.keys())339        options_sector = list(ui_labels.SECTOR_LABELS.keys())340        options_scope = list(ui_labels.SCOPE_LABELS.keys())341        edited_df = st.data_editor(342            st.session_state["batch_df"],343            use_container_width=True,344            num_rows="dynamic",345            hide_index=True,346            column_config={347                "nature_en": st.column_config.SelectboxColumn(348                    "Nature | الطبيعة",349                    options=options_nature,350                    required=True,351                    format_func=lambda v: ui_labels.pair_label(v, ui_labels.NATURE_LABELS),352                ),353                "market_sector_en": st.column_config.SelectboxColumn(354                    "Market Sector | قطاع السوق",355                    options=options_sector,356                    required=False,357                    format_func=lambda v: ui_labels.pair_label(v, ui_labels.SECTOR_LABELS),358                ),359                "scope_en": st.column_config.SelectboxColumn(360                    "Scope | النطاق",361                    options=options_scope,362                    required=False,363                    format_func=lambda v: ui_labels.pair_label(v, ui_labels.SCOPE_LABELS),364                ),365            },366            key="batch_editor",367        )368 369        selected_ids = st.multiselect("Approve selected row IDs", options=edited_df["id"].astype(str).tolist())370        if st.button("Approve Selected Rows"):371            refs = load_approved_references()372            for _, row in edited_df[edited_df["id"].astype(str).isin(selected_ids)].iterrows():373                original = st.session_state["batch_source_df"][st.session_state["batch_source_df"]["id"] == row["id"]].iloc[0]374                bundle = workflow.classify_row_with_learning(original.to_dict(), spec, refs)375                save_approved_result(376                    original_title=workflow.select_classification_inputs(str(original.get("name_ar", "") or original.get("name_en", "") or ""))[0]377                    or str(original.get("name_ar", "") or original.get("name_en", "") or ""),378                    detected_language=workflow.detect_input_language(str(original.get("name_ar", "") or original.get("name_en", "") or "")),379                    suggested=bundle.result,380                    final_nature=row.get("nature_en", ""),381                    final_sector=row.get("market_sector_en", ""),382                    final_scope=row.get("scope_en", ""),383                    approved_by=APPROVED_BY_DEFAULT,384                    correction_note=str(row.get("correction_note", "")) if "correction_note" in row else "",385                    suggestion_source=bundle.suggestion_source,386                    approved_at=None,387                )388            st.success("Selected rows approved. | تم اعتماد الصفوف المحددة")389 390        if st.button("Export Excel"):391            export_df = edited_df.copy()392            buf = BytesIO()393            with pd.ExcelWriter(buf, engine="openpyxl") as writer:394                export_df.to_excel(writer, index=False, sheet_name="classified")395            st.download_button(396                "Download exported file",397                data=buf.getvalue(),398                file_name="tce_classified.xlsx",399                mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",400            )401