File size: 13,150 Bytes
0238379
 
 
 
 
 
 
7ea279c
b85f76a
 
0238379
 
 
 
 
7ea279c
 
 
 
 
 
 
0238379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7ea279c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0238379
7ea279c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0238379
 
 
 
 
 
 
 
7ea279c
 
 
 
 
 
 
 
 
 
 
0238379
 
 
 
7ea279c
0238379
7ea279c
 
 
 
0238379
 
 
 
 
 
 
 
 
7ea279c
0238379
7ea279c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0238379
 
 
 
 
 
7ea279c
 
 
0238379
7ea279c
 
 
 
 
0238379
 
 
 
 
 
 
 
 
7ea279c
 
0238379
7ea279c
 
 
 
 
 
 
0238379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7ea279c
 
 
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
"""Chat assistant: LLM-backed via HF Inference, with a rule-based fallback.

If the HF_TOKEN environment variable is set (a Space secret), replies come
from a hosted chat model (CHAT_MODEL env var, default Gemma 3 27B). On any
failure β€” missing token, exhausted quota, provider error β€” the assistant
falls back to the built-in keyword knowledge base, so the tab always works.
"""

from __future__ import annotations

import os
import re

CHAT_MODEL = os.environ.get("CHAT_MODEL", "google/gemma-3-27b-it")

WELCOME = (
    "Hi! I'm your ML analysis assistant. Ask me anything about machine learning "
    "models (e.g. *'What is a random forest?'*, *'When should I use logistic "
    "regression?'*), or upload a dataset in the **Data & Preprocessing** tab.\n\n"
    "**So β€” what do you want to analyse today?**"
)

SYSTEM_PROMPT = (
    "You are the built-in assistant of 'ML Data Analysis Studio', a Gradio app for "
    "no-code machine learning analysis. The app has four tabs:\n"
    "1. ML Assistant β€” this chat.\n"
    "2. Data & Preprocessing β€” upload CSV/TSV/Excel/JSON/Parquet (delimiter/encoding "
    "auto-detected), automatic profiling, then cleaning: mean/median imputation or "
    "row-dropping, duplicate removal, IQR outlier clipping, one-hot encoding, and "
    "standard or min-max scaling. Placeholders ('N/A', '?'), numeric-looking text "
    "('$1,234', '45%'), and empty/constant columns are handled automatically.\n"
    "3. Model Training β€” regression (Linear, Decision Tree, Random Forest, Gradient "
    "Boosting, SVR, KNN) or classification (Logistic, Decision Tree, Random Forest, "
    "Gradient Boosting, SVC, KNN), with metrics, plots, feature importance, and the "
    "equivalent scikit-learn code shown in a dropdown.\n"
    "4. Reports & Downloads β€” PDF/HTML report and cleaned CSV export.\n\n"
    "Answer questions about machine learning, statistics, and data analysis clearly "
    "and concisely (a few short paragraphs at most, markdown allowed). When relevant, "
    "point the user to the right tab and to the models this app actually offers. "
    "If asked about something unrelated to ML or data analysis, answer briefly and "
    "steer the conversation back to their data."
)


def _llm_reply(message: str, history: list | None, uploaded_file_name: str | None) -> str:
    from huggingface_hub import InferenceClient

    client = InferenceClient(model=CHAT_MODEL, token=os.environ.get("HF_TOKEN"))
    system = SYSTEM_PROMPT
    if uploaded_file_name:
        system += (
            f"\n\nThe user has already uploaded the dataset '{uploaded_file_name}' "
            "in the Data & Preprocessing tab."
        )
    messages = [{"role": "system", "content": system}]
    for m in (history or [])[-8:]:
        if (
            isinstance(m, dict)
            and m.get("role") in ("user", "assistant")
            and isinstance(m.get("content"), str)
        ):
            messages.append({"role": m["role"], "content": m["content"]})
    messages.append({"role": "user", "content": message})
    out = client.chat_completion(messages=messages, max_tokens=512, temperature=0.4)
    reply = out.choices[0].message.content
    if not reply or not reply.strip():
        raise ValueError("empty LLM reply")
    return reply.strip()


# ------------------------------------------------- rule-based fallback ----

KNOWLEDGE_BASE = {
    ("linear regression",): (
        "**Linear Regression** fits a straight line (or hyperplane) that minimizes the "
        "squared error between predictions and actual values. Use it when the target is "
        "continuous and roughly linearly related to the features. Pros: fast, interpretable "
        "coefficients. Cons: can't capture non-linear patterns, sensitive to outliers."
    ),
    ("logistic regression",): (
        "**Logistic Regression** is a classification model that predicts class "
        "probabilities via the sigmoid function. Great baseline for binary classification. "
        "Pros: fast, well-calibrated probabilities, interpretable. Cons: linear decision "
        "boundary only."
    ),
    ("random forest",): (
        "**Random Forest** builds many decision trees on bootstrapped samples with random "
        "feature subsets, then averages (regression) or votes (classification). Pros: "
        "handles non-linearity, robust to outliers/overfitting, gives feature importance. "
        "Cons: slower and less interpretable than a single tree."
    ),
    ("decision tree",): (
        "**Decision Trees** split data by feature thresholds to form a flowchart of rules. "
        "Pros: highly interpretable, no scaling needed, handles mixed data. Cons: prone to "
        "overfitting β€” usually better inside an ensemble (Random Forest / Gradient Boosting)."
    ),
    ("gradient boosting", "xgboost", "boosting"): (
        "**Gradient Boosting** builds trees sequentially, each one correcting the errors of "
        "the previous ones. Often the top performer on tabular data (XGBoost/LightGBM are "
        "popular variants). Pros: high accuracy. Cons: more hyperparameters, slower to train."
    ),
    ("svm", "svr", "svc", "support vector"): (
        "**Support Vector Machines** find the boundary that maximizes the margin between "
        "classes (SVC) or fit within an error tube (SVR). Kernels enable non-linear "
        "boundaries. Pros: effective in high dimensions. Cons: slow on large datasets, "
        "needs feature scaling."
    ),
    ("knn", "k-nearest", "nearest neighbor"): (
        "**K-Nearest Neighbors** predicts from the K closest training points β€” majority "
        "vote for classification, average for regression. Pros: simple, no training phase. "
        "Cons: slow at prediction time, sensitive to scaling and irrelevant features."
    ),
    ("overfitting", "overfit"): (
        "**Overfitting** is when a model memorizes training data (including noise) and "
        "performs poorly on new data. Signs: high train accuracy, low test accuracy. "
        "Remedies: more data, simpler models, regularization, cross-validation, early stopping."
    ),
    ("which model", "best model", "what model", "choose a model", "recommend",
     "model should i use", "which algorithm", "what algorithm"): (
        "Quick guide: predicting a **number** β†’ start with Linear Regression, then Random "
        "Forest / Gradient Boosting. Predicting a **category** β†’ start with Logistic "
        "Regression, then Random Forest / Gradient Boosting. Small dataset with scaled "
        "features β†’ SVM or KNN are worth a try. Upload your data and I can suggest a task "
        "type automatically from your target column."
    ),
    ("classification", "classify"): (
        "**Classification** predicts a discrete category (spam/not-spam, species, churn). "
        "Models here: Logistic Regression, Decision Tree, Random Forest, Gradient Boosting, "
        "SVC, and KNN. Metrics: accuracy, precision, recall, F1."
    ),
    ("regression", "predict a number", "continuous"): (
        "**Regression** predicts a continuous number (price, temperature, sales). Models "
        "here: Linear Regression, Decision Tree, Random Forest, Gradient Boosting, SVR, and "
        "KNN. Metrics: RΒ², MAE, RMSE."
    ),
    ("preprocessing", "cleaning", "clean data", "preprocess"): (
        "**Preprocessing** in this app: fix messy headers, convert placeholder values and "
        "numeric-looking text, drop empty/constant columns, remove duplicates, impute or "
        "drop missing values, clip outliers, one-hot encode categoricals, and optionally "
        "scale numeric features. Head to the **Data & Preprocessing** tab to run it."
    ),
    ("missing value", "missing values", "impute", "imputation", "nan"): (
        "**Missing values** can be dropped (safe if few) or imputed β€” mean/median for "
        "numeric, mode for categorical. This app supports both strategies in the "
        "preprocessing step."
    ),
    ("scaling", "standardize", "normalize", "standardscaler", "min-max"): (
        "**Feature scaling** puts features on comparable ranges. Standard scaling (mean 0, "
        "std 1) or Min-Max (0-1) matters a lot for SVM and KNN; tree-based models don't "
        "need it. Choose a scaler in the preprocessing options."
    ),
    ("outlier", "outliers", "iqr"): (
        "**Outliers** are extreme values that can distort model fits, especially for linear "
        "models and KNN. This app can clip values beyond 1.5Γ—IQR (the boxplot whisker rule) "
        "β€” enable it in the preprocessing options."
    ),
    ("metric", "metrics", "accuracy", "precision", "recall", "f1"): (
        "**Classification metrics**: accuracy = overall correct rate; precision = of "
        "predicted positives, how many were right; recall = of actual positives, how many "
        "were found; F1 = harmonic mean of precision and recall (good for imbalanced data)."
    ),
    ("r2", "rΒ²", "rmse", "mae"): (
        "**Regression metrics**: RΒ² = share of variance explained (1.0 is perfect); "
        "MAE = average absolute error in target units; RMSE = like MAE but penalizes "
        "large errors more."
    ),
    ("train test split", "test size", "split"): (
        "**Train/test split** holds out part of the data (default 20% here) to evaluate the "
        "model on unseen examples. Adjust it with the slider in the Model Training tab."
    ),
    ("feature importance",): (
        "**Feature importance** shows which columns most influence predictions. Tree models "
        "expose `feature_importances_`; linear models use coefficient magnitudes. This app "
        "plots the top 15 automatically after training."
    ),
    ("cross validation", "cross-validation", "k-fold"): (
        "**Cross-validation** splits data into K folds, trains K times each holding out one "
        "fold, and averages the scores β€” a more reliable estimate than a single split, at "
        "the cost of KΓ— training time."
    ),
}

DATA_LOADED_PROMPT = (
    "\n\nπŸ“Š I can see you've uploaded **{name}**. Do you want me to analyse this data? "
    "Head to the **Data & Preprocessing** tab to clean it, then train models in "
    "**Model Training**."
)

FALLBACK = (
    "I can help with questions about the models in this app β€” linear/logistic regression, "
    "decision trees, random forests, gradient boosting, SVM, KNN β€” plus preprocessing, "
    "metrics, overfitting, and train/test splits.\n\n"
    "Or just get started: **upload your data in the Data & Preprocessing tab** and I'll "
    "help you analyse it. What do you want to analyse today?"
)

GREETINGS = ("hi", "hello", "hey", "good morning", "good afternoon", "good evening", "yo", "hola")


def _keyword_matches(msg: str, keyword: str) -> bool:
    """Whole-word match, so 'nan' does not fire inside 'financial'."""
    return re.search(r"(?<!\w)" + re.escape(keyword) + r"(?!\w)", msg) is not None


def _rule_based_reply(message: str) -> str:
    msg = message.lower().strip()

    if any(msg == g or msg.startswith(g + " ") or msg.startswith(g + "!") for g in GREETINGS):
        return (
            "Hello! I'm here to help you analyse your data with machine learning. "
            "**What do you want to analyse today?** You can ask about any ML model, "
            "or upload a dataset (CSV, Excel, JSON, Parquet) in the "
            "**Data & Preprocessing** tab."
        )

    # Specific knowledge first, generic workflow guidance after
    for keywords, answer in KNOWLEDGE_BASE.items():
        if any(_keyword_matches(msg, kw) for kw in keywords):
            return answer

    if any(_keyword_matches(msg, kw) for kw in
           ("analyse", "analyze", "analysis", "get started", "start", "upload")):
        return (
            "Great β€” here's the workflow:\n\n"
            "1. **Data & Preprocessing tab** β€” upload your file, review the profile, and "
            "run cleaning (missing values, duplicates, outliers, encoding, scaling).\n"
            "2. **Model Training tab** β€” pick a target column, a task "
            "(regression/classification), and a model, then train.\n"
            "3. Review metrics and charts, open the **code dropdown** to see the exact "
            "Python used, and **download the report** (PDF/HTML) and cleaned data.\n\n"
            "Do you want me to analyse your data? Upload it and I'll take it from there!"
        )

    return FALLBACK


# ---------------------------------------------------------------- entry ----


def respond(message: str, history: list | None = None,
            uploaded_file_name: str | None = None) -> str:
    """Return the assistant's reply: LLM if available, rules otherwise."""
    if os.environ.get("HF_TOKEN"):
        try:
            return _llm_reply(message, history, uploaded_file_name)
        except Exception:
            pass  # quota exhausted, provider down, etc. β€” fall back to rules

    reply = _rule_based_reply(message)
    if uploaded_file_name:
        reply += DATA_LOADED_PROMPT.format(name=uploaded_file_name)
    return reply