File size: 5,660 Bytes
341ba0d
 
 
 
 
 
f310003
 
341ba0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f310003
 
 
341ba0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f310003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341ba0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f310003
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import os
import gc

import numpy as np
import pandas as pd
import faiss
import gradio as gr
from fastapi import HTTPException, Query
from fastapi.responses import HTMLResponse

# Your existing helper functions for attribute-based scoring.
from attributes import extract_attributes, attribute_score

# ---------------------------------------------------------------------------
# Paths to pre-built artifacts. Build these ONCE, offline, in a notebook
# (where you have torch/sentence-transformers), then only ship the three
# files below to the Space. Never load a sentence-transformer model here —
# find_company_items only needs precomputed embeddings + a prebuilt FAISS
# index, not a live model.
# ---------------------------------------------------------------------------
DATA_DIR = os.environ.get("DATA_DIR", "data")
MASTER_PATH = os.path.join(DATA_DIR, "master.parquet")
EMB_PATH = os.path.join(DATA_DIR, "embeddings.npy")
INDEX_PATH = os.path.join(DATA_DIR, "faiss_index.bin")

COMPANIES = ["FFL", "PFL", "FFT"]

master: pd.DataFrame | None = None
embeddings = None  # np.memmap, not a full in-RAM copy
index = None  # faiss.Index


def load_resources():
    global master, embeddings, index

    master = pd.read_parquet(
        MASTER_PATH,
        columns=["Item Code", "Company", "Description"],
    )
    master["Item Code"] = master["Item Code"].astype(str).str.strip()
    master["Company"] = master["Company"].astype("category")

    embeddings = np.load(EMB_PATH, mmap_mode="r")
    index = faiss.read_index(INDEX_PATH)

    gc.collect()


# Gradio SDK Spaces have no startup-event hook of their own, so load before
# building/launching the demo, same as the original script did.
load_resources()


def find_company_items(item_code: str, threshold: float = 0.80, top_k: int = 2000):
    source = master[master["Item Code"] == str(item_code).strip()]

    if source.empty:
        return [
            {
                "Company": c,
                "Item Code": "Not Found",
                "Description": "-",
                "Semantic Score": 0,
                "Attribute Score": 0,
                "Final Score": 0,
            }
            for c in COMPANIES
        ]

    idx = source.index[0]
    query_embedding = np.asarray(embeddings[idx]).reshape(1, -1).astype("float32")

    distances, indices = index.search(query_embedding, top_k)

    source_attrs = extract_attributes(source.iloc[0]["Description"])
    results = []

    for company in COMPANIES:
        best = None
        best_score = -1

        for score, i in zip(distances[0], indices[0]):
            if i == -1:
                continue
            row = master.iloc[i]
            if row["Company"] != company:
                continue

            semantic = float(score)
            try:
                attr = attribute_score(source_attrs, extract_attributes(row["Description"]))
            except Exception:
                attr = 1.0

            final = semantic * 0.70 + attr * 0.30

            if final > best_score:
                best_score = final
                best = {
                    "Company": company,
                    "Item Code": row["Item Code"],
                    "Description": row["Description"],
                    "Semantic Score": round(semantic, 4),
                    "Attribute Score": round(attr, 4),
                    "Final Score": round(final, 4),
                }

        if best is None or best_score < threshold:
            results.append(
                {
                    "Company": company,
                    "Item Code": "Not Found",
                    "Description": "-",
                    "Semantic Score": 0,
                    "Attribute Score": 0,
                    "Final Score": 0,
                }
            )
        else:
            results.append(best)

    return sorted(results, key=lambda r: r["Final Score"], reverse=True)


# ---------------------------------------------------------------------------
# Minimal Gradio UI (Gradio SDK Spaces require a `demo` Blocks/Interface to
# launch). This doubles as a simple manual-testing form in the browser.
# ---------------------------------------------------------------------------
def gradio_lookup(item_code, threshold, top_k):
    if master is None:
        return "Server still starting up, try again shortly"
    return find_company_items(item_code, threshold, int(top_k))


demo = gr.Interface(
    fn=gradio_lookup,
    inputs=[
        gr.Textbox(label="Item Code"),
        gr.Slider(0, 1, value=0.80, label="Threshold"),
        gr.Number(value=2000, label="Top K"),
    ],
    outputs=gr.JSON(label="Results"),
    title="Company Item Matcher",
    description="Look up an item code and find matching items across FFL, PFL, and FFT.",
)

# The underlying FastAPI app that Gradio serves. Attaching routes to it is
# the officially supported way to add a plain JSON API alongside the Gradio
# UI on an SDK=gradio Space.
app = demo.app


@app.get("/find-items")
def find_items(
    item_code: str = Query(..., description="Item code to match"),
    threshold: float = 0.80,
    top_k: int = 2000,
):
    if master is None:
        raise HTTPException(status_code=503, detail="Server still starting up, try again shortly")
    return find_company_items(item_code, threshold, top_k)


@app.get("/health")
def health():
    return {"status": "ok"}


@app.get("/api", response_class=HTMLResponse)
async def api_info():
    return "<h2>Company Item Matcher API</h2><p>Try /find-items?item_code=YOUR_CODE or /docs</p>"


if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)