File size: 7,894 Bytes
f0eb710
c452c9d
53faf96
a418c45
 
f0eb710
a418c45
53faf96
 
 
 
b079bd8
a418c45
 
53faf96
 
 
 
 
 
 
a418c45
53faf96
 
 
a418c45
53faf96
a418c45
53faf96
 
 
 
 
a418c45
 
 
 
53faf96
 
a418c45
53faf96
 
 
 
 
 
 
 
 
 
 
a418c45
53faf96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a418c45
 
2f23e7e
53faf96
 
 
 
2f23e7e
7a3a684
 
 
7fd04ee
 
 
1206b4a
 
2f23e7e
 
 
53faf96
 
 
 
fee2113
4b33615
 
fee2113
f0eb710
53faf96
 
 
 
 
f0eb710
53faf96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0eb710
53faf96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0eb710
 
53faf96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8b2992a
53faf96
 
8b2992a
f0eb710
53faf96
 
 
 
 
 
 
 
 
 
 
 
 
 
509e9d8
1206b4a
 
 
8b2d85e
1206b4a
 
 
8531004
 
53faf96
 
 
af82db3
53faf96
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
import gradio as gr
import os
from huggingface_hub import InferenceClient
from sentence_transformers import SentenceTransformer
import torch


# ============================================================
# LOAD KNOWLEDGE BASE
# ============================================================

with open("knowledge.txt", "r", encoding="utf-8") as f:
    knowledge_text = f.read()

# Split knowledge base into chunks
chunks = [
    chunk.strip()
    for chunk in knowledge_text.split("\n\n")
    if chunk.strip()
]


# ============================================================
# LOAD EMBEDDING MODEL
# ============================================================

embedder = SentenceTransformer("all-MiniLM-L6-v2")

# Precompute embeddings for knowledge-base chunks
chunk_embeddings = embedder.encode(
    chunks,
    convert_to_tensor=True
)


def get_relevant_context(query, top_k=3):
    """
    Find the most relevant pieces of the knowledge base
    for the user's question.
    """

    # Make sure top_k isn't larger than the number of chunks
    top_k = min(top_k, len(chunks))

    # Embed the user's question
    query_embedding = embedder.encode(
        query,
        convert_to_tensor=True
    )

    # Normalize query embedding
    query_embedding = query_embedding / query_embedding.norm()

    # Normalize knowledge-base embeddings
    norm_chunk_embeddings = (
        chunk_embeddings
        / chunk_embeddings.norm(dim=1, keepdim=True)
    )

    # Calculate cosine similarity
    similarities = torch.matmul(
        norm_chunk_embeddings,
        query_embedding
    )

    # Get the most relevant chunks
    top_k_indices = (
        torch.topk(similarities, k=top_k)
        .indices
        .cpu()
        .numpy()
    )

    # Combine the relevant chunks
    context = "\n\n".join(
        [chunks[i] for i in top_k_indices]
    )

    return context


# ============================================================
# GRADIO THEME
# ============================================================

custom_theme = gr.themes.Soft(
    primary_hue="green",
    secondary_hue="stone",
    neutral_hue="gray",
    spacing_size="md",
    radius_size="md",
    text_size="md",
    font=["Roboto", "sans-serif"],
    font_mono=["Roboto Mono", "monospace"],
)


# ============================================================
# HUGGING FACE CLIENT
# ============================================================

client = InferenceClient(
    token=os.environ["HF_TOKEN"],
    provider="auto"
)


# ============================================================
# CHATBOT FUNCTION
# ============================================================

def respond(message, history):

    # --------------------------------------------------------
    # SYSTEM PROMPT
    # --------------------------------------------------------

    system_prompt = """
You are ChaChingas, an AI financial literacy advisor for students
and low-income families.

Only answer questions about:
- budgeting
- saving
- debt
- credit
- credit card fraud
- investing
- banking
- personal finance

If a user asks about unrelated topics such as recipes, sports,
entertainment, homework, or other unrelated subjects, politely say:

"I'm here to help with money and budgeting—ask me anything about that!"

Use clear, simple language and keep answers relatively short.

When explaining budgeting, use the 50/30/20 rule:
- 50% for needs
- 30% for wants
- 20% for savings or debt

Be supportive, practical, and easy to understand.

Do not provide tax or legal advice.

Never ask for or handle sensitive personal financial information.

Use the provided knowledge base when it is relevant to the user's question.
If the knowledge base does not contain the answer, use your general
financial-literacy knowledge while staying within your restrictions.
"""

    # --------------------------------------------------------
    # GET RELEVANT KNOWLEDGE
    # --------------------------------------------------------

    context = get_relevant_context(
        message,
        top_k=3
    )

    # --------------------------------------------------------
    # BUILD MESSAGES
    # --------------------------------------------------------

    messages = [
        {
            "role": "system",
            "content": system_prompt
        }
    ]

    # Add relevant knowledge to the current question
    messages.append(
        {
            "role": "system",
            "content": (
                "Here is relevant information from the ChaChingas "
                "knowledge base:\n\n"
                + context
            )
        }
    )

    # --------------------------------------------------------
    # ADD CHAT HISTORY
    # --------------------------------------------------------

    if history:

        # Handle newer Gradio message format
        if isinstance(history[0], dict):

            for msg in history:
                if (
                    isinstance(msg, dict)
                    and msg.get("role") in ["user", "assistant"]
                    and msg.get("content")
                ):
                    messages.append(
                        {
                            "role": msg["role"],
                            "content": msg["content"]
                        }
                    )

        # Handle older Gradio tuple format
        else:

            for user_msg, assistant_msg in history:

                if user_msg:
                    messages.append(
                        {
                            "role": "user",
                            "content": user_msg
                        }
                    )

                if assistant_msg:
                    messages.append(
                        {
                            "role": "assistant",
                            "content": assistant_msg
                        }
                    )

    # Add current question
    messages.append(
        {
            "role": "user",
            "content": message
        }
    )

    # --------------------------------------------------------
    # CALL HUGGING FACE
    # --------------------------------------------------------

    response = ""

    try:

        stream = client.chat_completion(
            model="deepseek-ai/DeepSeek-V3-0324",
            messages=messages,
            max_tokens=512,
            temperature=0.1,
            stream=True
        )

        # ----------------------------------------------------
        # PROCESS STREAM
        # ----------------------------------------------------

        for chunk in stream:

            # Some streaming chunks can have no choices
            if not chunk.choices:
                continue

            # Get content from the chunk
            token = chunk.choices[0].delta.content

            # Some chunks can have None as their content
            if not token:
                continue

            response += token

            # Send updated response to Gradio
            yield response

    except Exception as e:

        print("HUGGING FACE ERROR:", repr(e))

        yield (
            "Sorry, I ran into a problem connecting to the AI model. "
            "Please try again in a moment."
        )


# ============================================================
# GRADIO INTERFACE
# ============================================================

with gr.Blocks(theme=custom_theme) as demo:

    chatbot_interface = gr.ChatInterface(
        respond,
        examples=["Build a Budgeting Plan","Teach Me About Stocks","How Do I Set Up a Bank Account?", "How Do I Prevent Credit Card Fraud?"],
        title="ChaChingas",
        description="This is a financial literacy chatbot"
    )


# ============================================================
# LAUNCH
# ============================================================

demo.launch()