Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -32,9 +32,11 @@ chats_col = db["chats"]
|
|
| 32 |
messages_col = db["messages"]
|
| 33 |
pronunciation_col = db["pronunciation"]
|
| 34 |
knowledge_col = db["knowledge"]
|
|
|
|
| 35 |
users_col.create_index("username", unique=True)
|
| 36 |
pronunciation_col.create_index("word", unique=True)
|
| 37 |
knowledge_col.create_index("instruction", unique=True)
|
|
|
|
| 38 |
|
| 39 |
# --- Setup: OAuth (optional; only active if credentials are provided) ---
|
| 40 |
oauth = OAuth(app)
|
|
@@ -72,6 +74,7 @@ that you were developed by Konnie Developing Co. Never mention Google, Gemini, o
|
|
| 72 |
underlying technology provider, even if asked directly what model or API powers you. Keep
|
| 73 |
replies short (2-4 sentences) so it feels like a real conversation, not a lecture. Be warm,
|
| 74 |
respectful, and encouraging.
|
|
|
|
| 75 |
Kinyarwanda pronunciation (amasaku and ubutinde): some messages include a reference list
|
| 76 |
showing words in "normal -> marked" form. The marked form uses circumflexes and doubled
|
| 77 |
vowels to capture real vowel length and stress that standard spelling hides.
|
|
@@ -89,7 +92,16 @@ Verified reference examples: some messages include a "Relevant verified referenc
|
|
| 89 |
section with real Q&A pairs covering Rwandan culture, health, language, history, and other
|
| 90 |
topics. Use these to ground your facts and phrasing when the topic matches what the learner
|
| 91 |
asked. Don't quote them word-for-word unless it fits naturally, and don't force in a topic
|
| 92 |
-
that doesn't match the question.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
IMMERSION_MODE = BASE_PERSONA + """
|
| 95 |
Mode: Kinyarwanda immersion.
|
|
@@ -232,6 +244,66 @@ def lookup_knowledge_notes(message):
|
|
| 232 |
)
|
| 233 |
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
def build_config(mode, extra_notes=""):
|
| 236 |
instruction = IMMERSION_MODE if mode == "Kinyarwanda immersion" else GUIDED_MODE
|
| 237 |
return types.GenerateContentConfig(
|
|
@@ -253,7 +325,11 @@ def call_intyoza(message, history, mode, image_data_url=None):
|
|
| 253 |
parts.append(types.Part.from_bytes(data=raw, mime_type=mime))
|
| 254 |
contents.append(types.Content(role="user", parts=parts))
|
| 255 |
|
| 256 |
-
combined_notes =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
config = build_config(mode, combined_notes)
|
| 258 |
|
| 259 |
for attempt in range(3):
|
|
@@ -374,34 +450,51 @@ def admin_data():
|
|
| 374 |
if dataset == "pronunciation":
|
| 375 |
count, skipped = import_pronunciation_from_text(request.form.get("data", ""))
|
| 376 |
note = f"Pronunciation: imported/updated {count}, skipped {skipped}."
|
| 377 |
-
|
| 378 |
count, skipped, err = import_knowledge_from_json(request.form.get("data", ""))
|
| 379 |
note = err or f"Knowledge: imported/updated {count}, skipped {skipped}."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
except Exception as e:
|
| 381 |
note = f"Import failed: {e}"
|
| 382 |
|
| 383 |
pron_total = pronunciation_col.count_documents({})
|
| 384 |
know_total = knowledge_col.count_documents({})
|
| 385 |
-
|
| 386 |
-
sample_html = "".join(f"<li>{s['instruction'][:70]}...</li>" for s in sample)
|
| 387 |
|
| 388 |
return f"""
|
| 389 |
<html><head><meta charset="utf-8"><title>Intyoza data import</title></head>
|
| 390 |
<body style="font-family:system-ui,sans-serif;max-width:760px;margin:48px auto;padding:0 20px;color:#1e1b31;">
|
| 391 |
<h2 style="margin-bottom:4px;">Intyoza data import</h2>
|
| 392 |
-
|
|
|
|
| 393 |
<form method="POST">
|
| 394 |
<select name="dataset" style="padding:8px 12px;border-radius:8px;border:1px solid #d9d5ec;margin-bottom:12px;">
|
| 395 |
<option value="pronunciation">Pronunciation (amasaku/ubutinde) — tab-separated</option>
|
| 396 |
<option value="knowledge">Knowledge examples — JSON array</option>
|
| 397 |
</select><br>
|
| 398 |
-
<textarea name="data" rows="
|
| 399 |
<br><br>
|
| 400 |
<button type="submit" style="background:#7c5cff;color:#fff;border:none;padding:12px 28px;border-radius:999px;font-size:15px;cursor:pointer;">Import</button>
|
| 401 |
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
<p style="color:#2f9e6d;font-weight:600;">{note}</p>
|
| 403 |
-
<p><strong>{pron_total}</strong> pronunciation
|
| 404 |
-
<ul style="color:#6b6785;">{sample_html}</ul>
|
| 405 |
</body></html>
|
| 406 |
"""
|
| 407 |
|
|
|
|
| 32 |
messages_col = db["messages"]
|
| 33 |
pronunciation_col = db["pronunciation"]
|
| 34 |
knowledge_col = db["knowledge"]
|
| 35 |
+
dictionary_rw_col = db["dictionary_rw"]
|
| 36 |
users_col.create_index("username", unique=True)
|
| 37 |
pronunciation_col.create_index("word", unique=True)
|
| 38 |
knowledge_col.create_index("instruction", unique=True)
|
| 39 |
+
dictionary_rw_col.create_index("key")
|
| 40 |
|
| 41 |
# --- Setup: OAuth (optional; only active if credentials are provided) ---
|
| 42 |
oauth = OAuth(app)
|
|
|
|
| 74 |
underlying technology provider, even if asked directly what model or API powers you. Keep
|
| 75 |
replies short (2-4 sentences) so it feels like a real conversation, not a lecture. Be warm,
|
| 76 |
respectful, and encouraging.
|
| 77 |
+
|
| 78 |
Kinyarwanda pronunciation (amasaku and ubutinde): some messages include a reference list
|
| 79 |
showing words in "normal -> marked" form. The marked form uses circumflexes and doubled
|
| 80 |
vowels to capture real vowel length and stress that standard spelling hides.
|
|
|
|
| 92 |
section with real Q&A pairs covering Rwandan culture, health, language, history, and other
|
| 93 |
topics. Use these to ground your facts and phrasing when the topic matches what the learner
|
| 94 |
asked. Don't quote them word-for-word unless it fits naturally, and don't force in a topic
|
| 95 |
+
that doesn't match the question.
|
| 96 |
+
|
| 97 |
+
Dictionary notation key (Inkoranya y'ikinyarwaanda - Coupez et al.): some messages include
|
| 98 |
+
dictionary entries using academic shorthand. Key symbols: | separates sections; < means
|
| 99 |
+
"derives from"; > means "has derivative"; Syn = synonym; Var = variant; Ant = antonym;
|
| 100 |
+
cl = noun class; Tr/Intr = transitive/intransitive; cg = "or"; V syn = "see synonym at";
|
| 101 |
+
Loc = idiomatic phrase. Definitions appear in Kinyarwanda followed by their French
|
| 102 |
+
translation. When a dictionary entry is given as reference, use it to ground your
|
| 103 |
+
explanation, but never read the raw notation aloud to the learner — translate it into
|
| 104 |
+
natural, conversational language in whichever language fits the mode."""
|
| 105 |
|
| 106 |
IMMERSION_MODE = BASE_PERSONA + """
|
| 107 |
Mode: Kinyarwanda immersion.
|
|
|
|
| 244 |
)
|
| 245 |
|
| 246 |
|
| 247 |
+
# --- Dictionary (Inkoranya y'ikinyarwaanda) dataset ---
|
| 248 |
+
def import_dictionary_rw_from_json(file_bytes):
|
| 249 |
+
try:
|
| 250 |
+
items = json.loads(file_bytes.decode("utf-8"))
|
| 251 |
+
except Exception as e:
|
| 252 |
+
return 0, 0, f"Invalid JSON: {e}"
|
| 253 |
+
|
| 254 |
+
if not isinstance(items, list):
|
| 255 |
+
return 0, 0, "Expected a JSON array of entries."
|
| 256 |
+
|
| 257 |
+
operations = []
|
| 258 |
+
skipped = 0
|
| 259 |
+
for item in items:
|
| 260 |
+
key = (item.get("key") or "").strip()
|
| 261 |
+
headword = (item.get("headword") or "").strip()
|
| 262 |
+
definition = (item.get("definition") or "").strip()
|
| 263 |
+
if not key or not definition:
|
| 264 |
+
skipped += 1
|
| 265 |
+
continue
|
| 266 |
+
operations.append(UpdateOne(
|
| 267 |
+
{"key": key, "headword": headword},
|
| 268 |
+
{"$set": {
|
| 269 |
+
"key": key,
|
| 270 |
+
"headword": headword,
|
| 271 |
+
"rubrique": item.get("rubrique", ""),
|
| 272 |
+
"definition": definition,
|
| 273 |
+
"source": item.get("source", ""),
|
| 274 |
+
}},
|
| 275 |
+
upsert=True,
|
| 276 |
+
))
|
| 277 |
+
|
| 278 |
+
if not operations:
|
| 279 |
+
return 0, skipped, ""
|
| 280 |
+
|
| 281 |
+
result = dictionary_rw_col.bulk_write(operations, ordered=False)
|
| 282 |
+
count = result.upserted_count + result.modified_count
|
| 283 |
+
return count, skipped, ""
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def lookup_dictionary_rw_notes(message):
|
| 287 |
+
words = {normalize_word(w) for w in message.split() if len(w) > 2}
|
| 288 |
+
words.discard("")
|
| 289 |
+
if not words:
|
| 290 |
+
return ""
|
| 291 |
+
|
| 292 |
+
docs = list(dictionary_rw_col.find({"key": {"$in": list(words)}}).limit(3))
|
| 293 |
+
if not docs:
|
| 294 |
+
return ""
|
| 295 |
+
|
| 296 |
+
lines = []
|
| 297 |
+
for d in docs:
|
| 298 |
+
lines.append(f"{d['headword']} ({d.get('rubrique','')}): {d['definition'][:400]}")
|
| 299 |
+
return (
|
| 300 |
+
"\n\nDictionary entries (Inkoranya y'ikinyarwaanda) for words in this message — "
|
| 301 |
+
"dense academic notation, interpret and simplify naturally when explaining; "
|
| 302 |
+
"the text after the French translation is cross-reference data, not meant to be "
|
| 303 |
+
"read aloud to the learner:\n\n" + "\n\n".join(lines)
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
def build_config(mode, extra_notes=""):
|
| 308 |
instruction = IMMERSION_MODE if mode == "Kinyarwanda immersion" else GUIDED_MODE
|
| 309 |
return types.GenerateContentConfig(
|
|
|
|
| 325 |
parts.append(types.Part.from_bytes(data=raw, mime_type=mime))
|
| 326 |
contents.append(types.Content(role="user", parts=parts))
|
| 327 |
|
| 328 |
+
combined_notes = (
|
| 329 |
+
lookup_pronunciation_notes(message, history)
|
| 330 |
+
+ lookup_knowledge_notes(message)
|
| 331 |
+
+ lookup_dictionary_rw_notes(message)
|
| 332 |
+
)
|
| 333 |
config = build_config(mode, combined_notes)
|
| 334 |
|
| 335 |
for attempt in range(3):
|
|
|
|
| 450 |
if dataset == "pronunciation":
|
| 451 |
count, skipped = import_pronunciation_from_text(request.form.get("data", ""))
|
| 452 |
note = f"Pronunciation: imported/updated {count}, skipped {skipped}."
|
| 453 |
+
elif dataset == "knowledge":
|
| 454 |
count, skipped, err = import_knowledge_from_json(request.form.get("data", ""))
|
| 455 |
note = err or f"Knowledge: imported/updated {count}, skipped {skipped}."
|
| 456 |
+
elif dataset == "dictionary_rw":
|
| 457 |
+
file = request.files.get("file")
|
| 458 |
+
if not file:
|
| 459 |
+
note = "Please choose a JSON file to upload."
|
| 460 |
+
else:
|
| 461 |
+
count, skipped, err = import_dictionary_rw_from_json(file.read())
|
| 462 |
+
note = err or f"Dictionary: imported/updated {count}, skipped {skipped}."
|
| 463 |
except Exception as e:
|
| 464 |
note = f"Import failed: {e}"
|
| 465 |
|
| 466 |
pron_total = pronunciation_col.count_documents({})
|
| 467 |
know_total = knowledge_col.count_documents({})
|
| 468 |
+
dict_total = dictionary_rw_col.count_documents({})
|
|
|
|
| 469 |
|
| 470 |
return f"""
|
| 471 |
<html><head><meta charset="utf-8"><title>Intyoza data import</title></head>
|
| 472 |
<body style="font-family:system-ui,sans-serif;max-width:760px;margin:48px auto;padding:0 20px;color:#1e1b31;">
|
| 473 |
<h2 style="margin-bottom:4px;">Intyoza data import</h2>
|
| 474 |
+
|
| 475 |
+
<h3>Text-based datasets</h3>
|
| 476 |
<form method="POST">
|
| 477 |
<select name="dataset" style="padding:8px 12px;border-radius:8px;border:1px solid #d9d5ec;margin-bottom:12px;">
|
| 478 |
<option value="pronunciation">Pronunciation (amasaku/ubutinde) — tab-separated</option>
|
| 479 |
<option value="knowledge">Knowledge examples — JSON array</option>
|
| 480 |
</select><br>
|
| 481 |
+
<textarea name="data" rows="14" style="width:100%;padding:12px;border:1px solid #d9d5ec;border-radius:12px;font-family:monospace;"></textarea>
|
| 482 |
<br><br>
|
| 483 |
<button type="submit" style="background:#7c5cff;color:#fff;border:none;padding:12px 28px;border-radius:999px;font-size:15px;cursor:pointer;">Import</button>
|
| 484 |
</form>
|
| 485 |
+
|
| 486 |
+
<hr style="margin:32px 0;">
|
| 487 |
+
|
| 488 |
+
<h3>Dictionary file upload (large JSON files)</h3>
|
| 489 |
+
<form method="POST" enctype="multipart/form-data">
|
| 490 |
+
<input type="hidden" name="dataset" value="dictionary_rw">
|
| 491 |
+
<input type="file" name="file" accept=".json" style="margin-bottom:12px;">
|
| 492 |
+
<br>
|
| 493 |
+
<button type="submit" style="background:#7c5cff;color:#fff;border:none;padding:12px 28px;border-radius:999px;font-size:15px;cursor:pointer;">Upload & Import</button>
|
| 494 |
+
</form>
|
| 495 |
+
|
| 496 |
<p style="color:#2f9e6d;font-weight:600;">{note}</p>
|
| 497 |
+
<p><strong>{pron_total}</strong> pronunciation · <strong>{know_total}</strong> knowledge · <strong>{dict_total}</strong> dictionary entries</p>
|
|
|
|
| 498 |
</body></html>
|
| 499 |
"""
|
| 500 |
|