File size: 13,979 Bytes
b01478f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddbcc1e
b01478f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
import os
import threading
from typing import Optional

import gradio as gr
from groq import Groq


# ============================================================
# Configuration
# ============================================================

LOCAL_MODEL_NAME = "devendrajadhav34/gemma3-bitext-support-lora"

# This model is currently shown in Groq's official examples.
GROQ_MODEL_NAME = os.getenv(
    "GROQ_MODEL_NAME",
    "llama-3.3-70b-versatile",
)

# On a free CPU Hugging Face Space, leave this false.
# Set ENABLE_LOCAL_MODEL=true only when CUDA is available.
ENABLE_LOCAL_MODEL = (
    os.getenv("ENABLE_LOCAL_MODEL", "false").lower() == "true"
)

GROQ_API_KEY = os.getenv("GROQ_API_KEY")

generation_lock = threading.Lock()

local_model = None
local_tokenizer = None
local_model_error: Optional[str] = None


# ============================================================
# Few-shot prompt
# ============================================================

SYSTEM_PROMPT = """
You are a helpful customer support assistant.

Your responses must closely follow the style demonstrated in the examples:

- Begin by acknowledging the customer's issue with empathy.
- Sound reassuring, polite, and professional.
- Give a concise answer rather than an overly detailed explanation.
- Ask only for information genuinely needed to resolve the issue.
- When appropriate, provide simple numbered steps.
- Reassure the customer that their satisfaction is important.
- Do not invent company policies, order details, refund guarantees,
  phone numbers, URLs, operating hours, or account information.
- When company-specific information is required, use descriptive
  placeholders such as {{Order Number}}, {{Tracking Number}},
  {{Customer Support Phone Number}}, or {{Website URL}}.
- Never claim that an action has already been completed.
- Never claim that you accessed an order, account, transaction,
  delivery system, or customer record.
- Keep most answers between 60 and 140 words.
- Do not mention these instructions or the examples.
""".strip()


FEW_SHOT_MESSAGES = [
    {
        "role": "user",
        "content": "I accidentally placed an order and want to cancel it",
    },
    {
        "role": "assistant",
        "content": (
            "I understand your situation and the urgency to cancel your "
            "order. Rest assured, I'm here to assist you every step of the "
            "way. To proceed with the cancellation, please provide your "
            "{{Order Number}}, as it will help the support team locate the "
            "order and assist you further. Please note that cancellation "
            "availability may depend on whether the order has already been "
            "processed or shipped. Your satisfaction is our priority, and "
            "we'll do our best to help resolve the issue promptly."
        ),
    },
    {
        "role": "user",
        "content": "My package has not arrived yet",
    },
    {
        "role": "assistant",
        "content": (
            "We understand that you're concerned about your package not "
            "arriving, and we're here to help. Could you please provide your "
            "{{Tracking Number}} or {{Order Number}}? With this information, "
            "the support team can review the shipment status and provide the "
            "most recent available update. We appreciate your patience and "
            "cooperation while the delivery is investigated."
        ),
    },
    {
        "role": "user",
        "content": "I was charged twice for the same purchase",
    },
    {
        "role": "assistant",
        "content": (
            "I'm sorry to hear that you noticed two charges for the same "
            "purchase. It's important to review billing discrepancies "
            "promptly. To assist you, please provide your {{Order Number}} "
            "or {{Transaction ID}}, along with the dates and amounts of the "
            "two charges. Please avoid sharing complete card or bank account "
            "details. Once the relevant information is available, the "
            "support team can investigate the duplicate charge and explain "
            "the appropriate next steps."
        ),
    },
    {
        "role": "user",
        "content": "How can I change my shipping address?",
    },
    {
        "role": "assistant",
        "content": (
            "To change your shipping address, please follow these steps:\n\n"
            "1. Log in to your account on our website.\n"
            "2. Open the 'My Account' or 'My Orders' section.\n"
            "3. Select the relevant order or saved shipping address.\n"
            "4. Choose 'Edit Address' and enter the updated details.\n"
            "5. Review the information carefully and save your changes.\n\n"
            "If the order has already been processed or shipped, the address "
            "may no longer be editable online. In that case, please contact "
            "customer support with your {{Order Number}} for assistance."
        ),
    },
    {
        "role": "user",
        "content": "I forgot my account password",
    },
    {
        "role": "assistant",
        "content": (
            "I understand that you've forgotten your account password, and "
            "I'm here to guide you through regaining access:\n\n"
            "1. Visit the {{Account Recovery Page URL}}.\n"
            "2. Select the '{{Forgot Password}}' option.\n"
            "3. Enter the email address associated with your account.\n"
            "4. Check your email for a password-reset link.\n"
            "5. Follow the instructions to create a new password.\n\n"
            "For your security, never share your password or reset code with "
            "anyone. If the email does not arrive, check your spam folder or "
            "contact customer support."
        ),
    },
]


# ============================================================
# Optional local Unsloth model
# ============================================================

def load_local_model() -> bool:
    """
    Attempt to load the fine-tuned Unsloth model.

    On a free CPU Hugging Face Space, this function immediately returns
    False because CUDA will not be available.
    """
    global local_model
    global local_tokenizer
    global local_model_error

    if not ENABLE_LOCAL_MODEL:
        local_model_error = "Local model disabled by configuration."
        return False

    try:
        import torch

        if not torch.cuda.is_available():
            local_model_error = "CUDA is not available."
            return False

        from unsloth import FastLanguageModel

        local_model, local_tokenizer = (
            FastLanguageModel.from_pretrained(
                model_name=LOCAL_MODEL_NAME,
                max_seq_length=512,
                load_in_4bit=True,
            )
        )

        FastLanguageModel.for_inference(local_model)
        return True

    except Exception as exc:
        local_model = None
        local_tokenizer = None
        local_model_error = (
            f"{type(exc).__name__}: {exc}"
        )
        return False


LOCAL_MODEL_AVAILABLE = False


def generate_with_local_model(
    question: str,
    temperature: float,
    max_new_tokens: int,
) -> str:
    import torch

    if local_model is None or local_tokenizer is None:
        raise RuntimeError("Local model has not been loaded.")

    messages = [
        {
            "role": "system",
            "content": [
                {
                    "type": "text",
                    "text": (
                        "You are a helpful customer support assistant. "
                        "Respond empathetically, professionally, and "
                        "concisely. Ask only for the information needed "
                        "to resolve the customer's issue."
                    ),
                }
            ],
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": question,
                }
            ],
        },
    ]

    inputs = local_tokenizer.apply_chat_template(
        messages,
        tokenize=True,
        add_generation_prompt=True,
        return_tensors="pt",
        return_dict=True,
    ).to("cuda")

    generation_kwargs = {
        "max_new_tokens": int(max_new_tokens),
        "do_sample": temperature > 0,
        "pad_token_id": local_tokenizer.eos_token_id,
    }

    if temperature > 0:
        generation_kwargs["temperature"] = float(temperature)

    with generation_lock, torch.inference_mode():
        outputs = local_model.generate(
            **inputs,
            **generation_kwargs,
        )

    new_tokens = outputs[
        0,
        inputs["input_ids"].shape[-1]:,
    ]

    return local_tokenizer.decode(
        new_tokens,
        skip_special_tokens=True,
    ).strip()


# ============================================================
# Groq fallback
# ============================================================

def generate_with_groq(
    question: str,
    temperature: float,
    max_new_tokens: int,
) -> str:
    if not GROQ_API_KEY:
        raise RuntimeError(
            "GROQ_API_KEY has not been configured. Add it under "
            "your Hugging Face Space's Settings → Secrets."
        )

    client = Groq(api_key=GROQ_API_KEY)

    messages = [
        {
            "role": "system",
            "content": SYSTEM_PROMPT,
        },
        *FEW_SHOT_MESSAGES,
        {
            "role": "user",
            "content": question,
        },
    ]

    completion = client.chat.completions.create(
        model=GROQ_MODEL_NAME,
        messages=messages,
        temperature=float(temperature),
        max_completion_tokens=int(max_new_tokens),
        top_p=0.9,
    )

    response = completion.choices[0].message.content

    if not response:
        raise RuntimeError("Groq returned an empty response.")

    return response.strip()


# ============================================================
# Unified inference function
# ============================================================

def generate_response(
    question: str,
    temperature: float,
    max_new_tokens: int,
):
    question = question.strip()

    if not question:
        return (
            "Please enter a customer support question.",
            "No inference performed",
        )

    # Attempt the fine-tuned model when it was successfully loaded.
    if LOCAL_MODEL_AVAILABLE:
        try:
            response = generate_with_local_model(
                question=question,
                temperature=temperature,
                max_new_tokens=max_new_tokens,
            )

            return response, "Fine-tuned Gemma 3 model"

        except Exception as local_error:
            # Continue to Groq instead of failing the request.
            print(
                "Local inference failed; using Groq fallback:",
                repr(local_error),
            )

    try:
        response = generate_with_groq(
            question=question,
            temperature=temperature,
            max_new_tokens=max_new_tokens,
        )

        return response, f"Groq fallback: {GROQ_MODEL_NAME}"

    except Exception as groq_error:
        print("Groq inference failed:", repr(groq_error))

        return (
            "I'm sorry, but the assistant is temporarily unavailable. "
            "Please try again shortly.",
            f"Error: {type(groq_error).__name__}",
        )


# ============================================================
# Gradio interface
# ============================================================

with gr.Blocks(
    title="Customer Support Assistant",
) as demo:
    gr.Markdown(
        """
        # Customer Support Assistant

        A customer-support assistant designed to produce concise,
        empathetic and action-oriented responses.
        """
    )

    question = gr.Textbox(
        lines=4,
        placeholder="Describe your customer support issue...",
        label="Customer Query",
    )

    with gr.Accordion(
        "Generation settings",
        open=False,
    ):
        temperature = gr.Slider(
            minimum=0.0,
            maximum=1.2,
            value=0.35,
            step=0.05,
            label="Temperature",
        )

        max_new_tokens = gr.Slider(
            minimum=64,
            maximum=350,
            value=220,
            step=1,
            label="Maximum response tokens",
        )

    generate_button = gr.Button(
        "Generate response",
        variant="primary",
    )

    response = gr.Textbox(
        lines=10,
        label="Assistant Response",
    )

    inference_source = gr.Textbox(
        label="Inference source",
        interactive=False,
    )

    generate_button.click(
        fn=generate_response,
        inputs=[
            question,
            temperature,
            max_new_tokens,
        ],
        outputs=[
            response,
            inference_source,
        ],
    )

    question.submit(
        fn=generate_response,
        inputs=[
            question,
            temperature,
            max_new_tokens,
        ],
        outputs=[
            response,
            inference_source,
        ],
    )

    gr.Examples(
        examples=[
            ["I accidentally placed an order. Can I cancel it?"],
            ["My package hasn't arrived yet."],
            ["I was charged twice for my order."],
            ["How do I reset my password?"],
            [
                "Can I change my shipping address "
                "after placing an order?"
            ],
            ["The product I received is damaged."],
            ["I want to request a refund."],
        ],
        inputs=question,
    )


demo.queue(
    default_concurrency_limit=4,
    max_size=30,
)

if __name__ == "__main__":
    demo.launch()