Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -32,6 +32,7 @@ class LearningSession:
|
|
| 32 |
end_time: Optional[datetime] = None
|
| 33 |
words_learned: int = 0
|
| 34 |
idioms_learned: int = 0
|
|
|
|
| 35 |
questions_asked: int = 0
|
| 36 |
|
| 37 |
@dataclass
|
|
@@ -57,16 +58,17 @@ class PersonalizedLearningTracker:
|
|
| 57 |
cursor = conn.cursor()
|
| 58 |
|
| 59 |
cursor.execute('''
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
| 70 |
|
| 71 |
cursor.execute('''
|
| 72 |
CREATE TABLE IF NOT EXISTS word_progress (
|
|
@@ -214,12 +216,25 @@ class PersonalizedLearningTracker:
|
|
| 214 |
WHERE user_id = ? AND word = ? AND category = ?
|
| 215 |
''', (now, user_id, word, category))
|
| 216 |
else:
|
| 217 |
-
cursor.execute('''
|
| 218 |
INSERT INTO word_progress
|
| 219 |
(user_id, word, definition, category, first_encountered, last_reviewed)
|
| 220 |
VALUES (?, ?, ?, ?, ?, ?)
|
| 221 |
''', (user_id, word, definition, category, now, now))
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
conn.commit()
|
| 224 |
conn.close()
|
| 225 |
|
|
@@ -327,6 +342,32 @@ class PersonalizedLearningTracker:
|
|
| 327 |
conn.close()
|
| 328 |
return words
|
| 329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
def get_learning_recommendations(self, user_id: str) -> List[str]:
|
| 331 |
"""Get personalized learning recommendations"""
|
| 332 |
progress = self.get_user_progress(user_id)
|
|
@@ -349,6 +390,7 @@ class PersonalizedLearningTracker:
|
|
| 349 |
|
| 350 |
class PersonalizedKazakhAssistant:
|
| 351 |
def __init__(self):
|
|
|
|
| 352 |
self.setup_environment()
|
| 353 |
self.setup_vectorstore()
|
| 354 |
self.setup_llm()
|
|
@@ -358,19 +400,18 @@ class PersonalizedKazakhAssistant:
|
|
| 358 |
|
| 359 |
def setup_environment(self):
|
| 360 |
"""Setup environment and configuration"""
|
| 361 |
-
|
| 362 |
self.google_api_key = os.getenv("GOOGLE_API_KEY")
|
| 363 |
self.MODEL = "gemini-1.5-flash"
|
| 364 |
self.db_name = "vector_db"
|
| 365 |
|
| 366 |
def setup_vectorstore(self):
|
| 367 |
"""Setup document loading and vector store"""
|
| 368 |
-
folders = glob.glob("knowledge-base/*")
|
| 369 |
text_loader_kwargs = {'encoding': 'utf-8'}
|
| 370 |
documents = []
|
| 371 |
|
| 372 |
for folder in folders:
|
| 373 |
-
doc_type = os.path.basename(folder)
|
| 374 |
loader = DirectoryLoader(
|
| 375 |
folder,
|
| 376 |
glob="**/*.txt",
|
|
@@ -382,6 +423,25 @@ class PersonalizedKazakhAssistant:
|
|
| 382 |
doc.metadata["doc_type"] = doc_type
|
| 383 |
documents.append(doc)
|
| 384 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
text_splitter = CharacterTextSplitter(separator=r'\n', chunk_size=2000, chunk_overlap=0)
|
| 386 |
chunks = text_splitter.split_documents(documents)
|
| 387 |
|
|
@@ -394,122 +454,192 @@ class PersonalizedKazakhAssistant:
|
|
| 394 |
|
| 395 |
self.vectorstore = Chroma.from_documents(documents=chunks, embedding=embeddings, persist_directory=self.db_name)
|
| 396 |
print(f"Vectorstore created with {self.vectorstore._collection.count()} documents")
|
| 397 |
-
|
| 398 |
def setup_llm(self):
|
| 399 |
"""Setup LLM with enhanced system prompt"""
|
| 400 |
system_prompt = """
|
| 401 |
-
You are a personalized Kazakh language learning assistant with access to a comprehensive knowledge base and user learning history. Your role is to help users learn Kazakh words and idioms while tracking their progress and providing personalized recommendations.
|
| 402 |
-
|
| 403 |
-
Key capabilities:
|
| 404 |
-
1. **Answer Queries**: Provide accurate definitions and examples for Kazakh words and idioms from your knowledge base
|
| 405 |
-
2. **Track Learning Progress**: Identify and track when users learn new words or idioms
|
| 406 |
-
3. **Personalized Responses**: Adapt responses based on user's learning history and progress
|
| 407 |
-
4. **Progress Reporting**: Provide detailed progress reports when asked
|
| 408 |
-
5. **Learning Recommendations**: Suggest words/idioms to review or learn next
|
| 409 |
-
|
| 410 |
-
Response Guidelines:
|
| 411 |
-
- For word/idiom queries: Provide definition, usage examples, and related information
|
| 412 |
-
- Always identify the main Kazakh word/idiom being discussed for progress tracking
|
| 413 |
-
- Be encouraging and supportive of the user's learning journey
|
| 414 |
-
- Use simple, clear explanations appropriate for language learners
|
| 415 |
-
- When discussing progress, be specific and motivating
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
| 419 |
|
| 420 |
self.llm = ChatGoogleGenerativeAI(
|
| 421 |
model="models/gemini-1.5-flash",
|
| 422 |
temperature=0.7,
|
| 423 |
-
|
| 424 |
)
|
| 425 |
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
if user_id not in self.user_memories:
|
| 430 |
-
self.user_memories[user_id] = ConversationBufferMemory(
|
| 431 |
-
memory_key='chat_history',
|
| 432 |
-
return_messages=True,
|
| 433 |
-
max_token_limit=10000
|
| 434 |
-
)
|
| 435 |
-
return self.user_memories[user_id]
|
| 436 |
-
|
| 437 |
-
def get_user_chain(self, user_id: str):
|
| 438 |
-
"""Get or create conversation chain for a specific user"""
|
| 439 |
-
memory = self.get_user_memory(user_id)
|
| 440 |
-
retriever = self.vectorstore.as_retriever()
|
| 441 |
-
return ConversationalRetrievalChain.from_llm(
|
| 442 |
-
llm=self.llm,
|
| 443 |
-
retriever=retriever,
|
| 444 |
-
memory=memory
|
| 445 |
-
)
|
| 446 |
|
| 447 |
def extract_kazakh_terms(self, message: str, response: str) -> List[Tuple[str, str, str]]:
|
| 448 |
"""Extract meaningful Kazakh terms using document metadata to determine category"""
|
| 449 |
terms = []
|
|
|
|
| 450 |
|
| 451 |
try:
|
| 452 |
retrieved_docs = self.vectorstore.similarity_search(message, k=5)
|
| 453 |
|
| 454 |
-
|
|
|
|
| 455 |
|
| 456 |
-
for
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
'түсіндірілген', 'келтірілген', 'болып', 'табылады', 'ауруы',
|
| 464 |
-
'мынадай', 'тақырыбына', 'тіркестер', 'арналған', 'байланысты']
|
| 465 |
-
|
| 466 |
-
if any(skip in word.lower() for skip in skip_words):
|
| 467 |
-
continue
|
| 468 |
-
|
| 469 |
-
category = "word"
|
| 470 |
-
definition = ""
|
| 471 |
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
except Exception as e:
|
| 488 |
print(f"Error extracting terms: {e}")
|
| 489 |
-
|
| 490 |
return terms
|
| 491 |
|
| 492 |
def extract_clean_definition(self, term: str, doc_content: str, response: str) -> str:
|
| 493 |
-
"""Extract clean definition for a term"""
|
|
|
|
|
|
|
| 494 |
sentences = response.split('.')
|
| 495 |
for sentence in sentences:
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
if
|
| 499 |
-
|
| 500 |
-
return clean_sentence
|
| 501 |
|
| 502 |
doc_sentences = doc_content.split('.')
|
| 503 |
for sentence in doc_sentences:
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
return clean_sentence
|
| 508 |
|
| 509 |
return f"Definition for {term}"
|
| 510 |
|
| 511 |
-
def
|
| 512 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
|
| 514 |
if session_token and not self.tracker.validate_session(user_id, session_token):
|
| 515 |
return "Session expired. Please login again."
|
|
@@ -526,9 +656,14 @@ Format responses naturally in conversational style, not JSON unless specifically
|
|
| 526 |
return self.get_recommendations(user_id)
|
| 527 |
elif message.lower().startswith('/review'):
|
| 528 |
return self.get_review_words(user_id)
|
|
|
|
|
|
|
| 529 |
elif message.lower().startswith('/help'):
|
| 530 |
return self.get_help_message()
|
| 531 |
|
|
|
|
|
|
|
|
|
|
| 532 |
conversation_chain = self.get_user_chain(user_id)
|
| 533 |
result = conversation_chain.invoke({"question": message})
|
| 534 |
response = result["answer"]
|
|
@@ -607,6 +742,25 @@ Format responses naturally in conversational style, not JSON unless specifically
|
|
| 607 |
|
| 608 |
return response
|
| 609 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
def get_help_message(self) -> str:
|
| 611 |
"""Get help message with available commands"""
|
| 612 |
return """
|
|
@@ -616,6 +770,7 @@ Format responses naturally in conversational style, not JSON unless specifically
|
|
| 616 |
- `/progress` - View your detailed learning progress
|
| 617 |
- `/recommendations` - Get personalized learning suggestions
|
| 618 |
- `/review` - See words that need review
|
|
|
|
| 619 |
- `/help` - Show this help message
|
| 620 |
|
| 621 |
**How to Use**:
|
|
@@ -636,22 +791,43 @@ Start learning by asking about any Kazakh term! 🌟
|
|
| 636 |
"""Create a session token for user authentication"""
|
| 637 |
session_token = self.tracker.create_user_session(user_id)
|
| 638 |
return session_token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
assistant = PersonalizedKazakhAssistant()
|
| 641 |
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
"""Chat interface for Gradio - uses consistent user for web interface"""
|
| 645 |
try:
|
| 646 |
-
# Use a consistent user_id for the web interface session
|
| 647 |
-
# In a real app, you'd use proper session management
|
| 648 |
web_user_id = "web_user_default" # Consistent ID
|
| 649 |
-
response = assistant.process_message(message, web_user_id)
|
| 650 |
return response
|
| 651 |
except Exception as e:
|
| 652 |
return f"Sorry, I encountered an error: {str(e)}. Please try again."
|
| 653 |
-
|
| 654 |
-
|
| 655 |
def api_login(user_id: str) -> dict:
|
| 656 |
"""API endpoint for user login/session creation"""
|
| 657 |
try:
|
|
@@ -668,10 +844,10 @@ def api_login(user_id: str) -> dict:
|
|
| 668 |
"error": str(e)
|
| 669 |
}
|
| 670 |
|
| 671 |
-
def api_chat(message: str, user_id: str, session_token: str = None) -> dict:
|
| 672 |
-
"""API endpoint for chat functionality with proper user session"""
|
| 673 |
try:
|
| 674 |
-
response = assistant.process_message(message, user_id, session_token)
|
| 675 |
return {
|
| 676 |
"success": True,
|
| 677 |
"response": response,
|
|
@@ -747,230 +923,313 @@ def api_review_words(user_id: str, session_token: str = None) -> dict:
|
|
| 747 |
"error": str(e)
|
| 748 |
}
|
| 749 |
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
with gr.Blocks(title="🇰🇿 Kazakh Learning API") as demo:
|
| 752 |
-
|
| 753 |
gr.Markdown("# 🇰🇿 Personalized Kazakh Learning Assistant")
|
| 754 |
gr.Markdown("### Multi-User Chat Interface + API Endpoints for Mobile Integration")
|
| 755 |
|
| 756 |
with gr.Tab("💬 Chat Interface"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 757 |
chat_interface = gr.ChatInterface(
|
| 758 |
-
chat_interface,
|
|
|
|
| 759 |
type="messages",
|
| 760 |
examples=[
|
| 761 |
-
"сәлем деген не?",
|
| 762 |
-
"күләпара не үшін керек?",
|
| 763 |
-
"/progress",
|
| 764 |
-
"/recommendations",
|
| 765 |
-
"/review"
|
|
|
|
|
|
|
|
|
|
| 766 |
]
|
| 767 |
)
|
| 768 |
-
|
| 769 |
-
with gr.Tab("🔌 API Testing"):
|
| 770 |
-
gr.Markdown("## Test API Endpoints")
|
| 771 |
-
|
| 772 |
-
with gr.Row():
|
| 773 |
-
with gr.Column():
|
| 774 |
-
user_id_input = gr.Textbox(label="User ID", value="test_user", placeholder="Enter unique user ID")
|
| 775 |
-
session_token_input = gr.Textbox(label="Session Token", placeholder="Session token (get from login)")
|
| 776 |
-
message_input = gr.Textbox(label="Message", placeholder="Enter your message in Kazakh or English")
|
| 777 |
-
|
| 778 |
-
with gr.Row():
|
| 779 |
-
login_btn = gr.Button("🔑 Test Login API")
|
| 780 |
-
chat_btn = gr.Button("💬 Test Chat API")
|
| 781 |
-
progress_btn = gr.Button("📊 Test Progress API")
|
| 782 |
-
recommendations_btn = gr.Button("💡 Test Recommendations API")
|
| 783 |
-
review_btn = gr.Button("📚 Test Review API")
|
| 784 |
-
|
| 785 |
-
api_output = gr.JSON(label="API Response")
|
| 786 |
-
|
| 787 |
-
login_btn.click(
|
| 788 |
-
fn=lambda uid: api_login(uid),
|
| 789 |
-
inputs=user_id_input,
|
| 790 |
-
outputs=api_output
|
| 791 |
-
)
|
| 792 |
-
|
| 793 |
-
chat_btn.click(
|
| 794 |
-
fn=lambda msg, uid, token: api_chat(msg, uid, token),
|
| 795 |
-
inputs=[message_input, user_id_input, session_token_input],
|
| 796 |
-
outputs=api_output
|
| 797 |
-
)
|
| 798 |
-
|
| 799 |
-
progress_btn.click(
|
| 800 |
-
fn=lambda uid, token: api_progress(uid, token),
|
| 801 |
-
inputs=[user_id_input, session_token_input],
|
| 802 |
-
outputs=api_output
|
| 803 |
-
)
|
| 804 |
-
|
| 805 |
-
recommendations_btn.click(
|
| 806 |
-
fn=lambda uid, token: api_recommendations(uid, token),
|
| 807 |
-
inputs=[user_id_input, session_token_input],
|
| 808 |
-
outputs=api_output
|
| 809 |
-
)
|
| 810 |
-
|
| 811 |
-
review_btn.click(
|
| 812 |
-
fn=lambda uid, token: api_review_words(uid, token),
|
| 813 |
-
inputs=[user_id_input, session_token_input],
|
| 814 |
-
outputs=api_output
|
| 815 |
-
)
|
| 816 |
-
|
| 817 |
with gr.Tab("📖 API Documentation"):
|
| 818 |
gr.Markdown("""
|
| 819 |
## API Endpoints for Flutter Integration
|
| 820 |
-
|
| 821 |
### Base URL: `https://huggingface.co/spaces/GuestUser33/kazakh-learning-api`
|
| 822 |
-
|
| 823 |
### Authentication Flow:
|
| 824 |
-
1. **Login** to get session token
|
| 825 |
2. **Use session token** for subsequent API calls
|
| 826 |
3. **Session tokens expire** after inactivity
|
| 827 |
-
|
| 828 |
### Available Endpoints:
|
| 829 |
-
|
| 830 |
#### 1. Login API
|
| 831 |
```
|
| 832 |
POST /api/predict
|
| 833 |
Content-Type: application/json
|
| 834 |
-
|
| 835 |
{
|
| 836 |
-
|
| 837 |
-
|
| 838 |
}
|
| 839 |
```
|
| 840 |
-
**Response**: `{"success": true, "session_token": "uuid", "user_id": "user_id"}`
|
| 841 |
-
|
| 842 |
#### 2. Chat API
|
| 843 |
```
|
| 844 |
POST /api/predict
|
| 845 |
Content-Type: application/json
|
| 846 |
-
|
| 847 |
{
|
| 848 |
-
|
| 849 |
-
|
| 850 |
}
|
| 851 |
```
|
| 852 |
-
|
| 853 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 854 |
```
|
| 855 |
POST /api/predict
|
| 856 |
Content-Type: application/json
|
| 857 |
-
|
| 858 |
{
|
| 859 |
-
|
| 860 |
-
|
| 861 |
}
|
| 862 |
```
|
| 863 |
-
|
|
|
|
| 864 |
#### 4. Recommendations API
|
| 865 |
```
|
| 866 |
POST /api/predict
|
| 867 |
Content-Type: application/json
|
| 868 |
-
|
| 869 |
{
|
| 870 |
-
|
| 871 |
-
|
| 872 |
}
|
| 873 |
```
|
| 874 |
-
|
|
|
|
| 875 |
#### 5. Review Words API
|
| 876 |
```
|
| 877 |
POST /api/predict
|
| 878 |
Content-Type: application/json
|
| 879 |
-
|
| 880 |
{
|
| 881 |
-
|
| 882 |
-
|
| 883 |
}
|
| 884 |
```
|
| 885 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 886 |
### Flutter Integration Example:
|
| 887 |
```dart
|
|
|
|
|
|
|
|
|
|
| 888 |
class KazakhLearningAPI {
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
final response = await http.post(
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
'data': [userId],
|
| 900 |
'fn_index': 0
|
| 901 |
-
|
| 902 |
);
|
| 903 |
-
|
| 904 |
if (response.statusCode == 200) {
|
| 905 |
-
|
| 906 |
-
|
| 907 |
this.userId = userId;
|
| 908 |
this.sessionToken = result['data'][0]['session_token'];
|
| 909 |
return true;
|
| 910 |
-
|
| 911 |
}
|
| 912 |
return false;
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
if (sessionToken == null) return null;
|
| 918 |
-
|
| 919 |
final response = await http.post(
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
'data': [message, userId, sessionToken],
|
| 924 |
'fn_index': 1
|
| 925 |
-
|
| 926 |
);
|
| 927 |
-
|
| 928 |
if (response.statusCode == 200) {
|
| 929 |
-
|
| 930 |
-
|
| 931 |
return result['data'][0]['response'];
|
| 932 |
-
|
| 933 |
}
|
| 934 |
return null;
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
if (sessionToken == null) return null;
|
| 940 |
-
|
| 941 |
final response = await http.post(
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
'data': [userId, sessionToken],
|
| 946 |
'fn_index': 2
|
| 947 |
-
|
| 948 |
);
|
| 949 |
-
|
| 950 |
if (response.statusCode == 200) {
|
| 951 |
-
|
| 952 |
-
|
| 953 |
return result['data'][0]['progress_data'];
|
| 954 |
-
|
| 955 |
}
|
| 956 |
return null;
|
| 957 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 958 |
}
|
| 959 |
```
|
| 960 |
-
|
| 961 |
### Key Features:
|
| 962 |
- ✅ **Multi-User Support**: Each user has separate learning progress
|
| 963 |
- ✅ **Session Management**: Secure session tokens for authentication
|
| 964 |
-
- ✅ **Personalized Tracking**: Individual progress tracking per user
|
|
|
|
|
|
|
| 965 |
- ✅ **API Ready**: All endpoints ready for mobile app integration
|
| 966 |
- ✅ **Session Validation**: Automatic session validation and expiry
|
| 967 |
-
|
| 968 |
### Usage Notes:
|
| 969 |
- Always call **login** first to get a session token
|
| 970 |
- Include **session_token** in all subsequent API calls
|
|
|
|
|
|
|
| 971 |
- Handle **session expiry** by re-logging in
|
| 972 |
-
- Use **unique user_id** for each user (
|
| 973 |
-
|
|
|
|
| 974 |
|
| 975 |
if __name__ == "__main__":
|
| 976 |
demo.launch()
|
|
|
|
| 32 |
end_time: Optional[datetime] = None
|
| 33 |
words_learned: int = 0
|
| 34 |
idioms_learned: int = 0
|
| 35 |
+
grammar_learned: int = 0
|
| 36 |
questions_asked: int = 0
|
| 37 |
|
| 38 |
@dataclass
|
|
|
|
| 58 |
cursor = conn.cursor()
|
| 59 |
|
| 60 |
cursor.execute('''
|
| 61 |
+
CREATE TABLE IF NOT EXISTS learning_sessions (
|
| 62 |
+
session_id TEXT PRIMARY KEY,
|
| 63 |
+
user_id TEXT NOT NULL,
|
| 64 |
+
start_time TEXT NOT NULL,
|
| 65 |
+
end_time TEXT,
|
| 66 |
+
words_learned INTEGER DEFAULT 0,
|
| 67 |
+
idioms_learned INTEGER DEFAULT 0,
|
| 68 |
+
grammar_learned INTEGER DEFAULT 0,
|
| 69 |
+
questions_asked INTEGER DEFAULT 0
|
| 70 |
+
)
|
| 71 |
+
''')
|
| 72 |
|
| 73 |
cursor.execute('''
|
| 74 |
CREATE TABLE IF NOT EXISTS word_progress (
|
|
|
|
| 216 |
WHERE user_id = ? AND word = ? AND category = ?
|
| 217 |
''', (now, user_id, word, category))
|
| 218 |
else:
|
| 219 |
+
cursor.execute ('''
|
| 220 |
INSERT INTO word_progress
|
| 221 |
(user_id, word, definition, category, first_encountered, last_reviewed)
|
| 222 |
VALUES (?, ?, ?, ?, ?, ?)
|
| 223 |
''', (user_id, word, definition, category, now, now))
|
| 224 |
|
| 225 |
+
cursor.execute('''
|
| 226 |
+
SELECT encounter_count FROM word_progress
|
| 227 |
+
WHERE user_id = ? AND word = ? AND category = ?
|
| 228 |
+
''', (user_id, word, category))
|
| 229 |
+
encounter_count = cursor.fetchone()[0]
|
| 230 |
+
|
| 231 |
+
if encounter_count >= 3:
|
| 232 |
+
cursor.execute('''
|
| 233 |
+
UPDATE word_progress
|
| 234 |
+
SET mastery_level = ?
|
| 235 |
+
WHERE user_id = ? AND word = ? AND category = ?
|
| 236 |
+
''', (3, user_id, word, category))
|
| 237 |
+
|
| 238 |
conn.commit()
|
| 239 |
conn.close()
|
| 240 |
|
|
|
|
| 342 |
conn.close()
|
| 343 |
return words
|
| 344 |
|
| 345 |
+
def get_mastered_words(self, user_id: str, limit: int = 10) -> List[Dict]:
|
| 346 |
+
"""Get words with mastery level greater than 0"""
|
| 347 |
+
conn = sqlite3.connect(self.db_path)
|
| 348 |
+
cursor = conn.cursor()
|
| 349 |
+
|
| 350 |
+
cursor.execute('''
|
| 351 |
+
SELECT word, definition, category, mastery_level, encounter_count
|
| 352 |
+
FROM word_progress
|
| 353 |
+
WHERE user_id = ? AND mastery_level > 0
|
| 354 |
+
ORDER BY mastery_level DESC, encounter_count DESC
|
| 355 |
+
LIMIT ?
|
| 356 |
+
''', (user_id, limit))
|
| 357 |
+
|
| 358 |
+
words = []
|
| 359 |
+
for word, definition, category, mastery, encounter_count in cursor.fetchall():
|
| 360 |
+
words.append({
|
| 361 |
+
'word': word,
|
| 362 |
+
'definition': definition,
|
| 363 |
+
'category': category,
|
| 364 |
+
'mastery_level': mastery,
|
| 365 |
+
'encounter_count': encounter_count
|
| 366 |
+
})
|
| 367 |
+
|
| 368 |
+
conn.close()
|
| 369 |
+
return words
|
| 370 |
+
|
| 371 |
def get_learning_recommendations(self, user_id: str) -> List[str]:
|
| 372 |
"""Get personalized learning recommendations"""
|
| 373 |
progress = self.get_user_progress(user_id)
|
|
|
|
| 390 |
|
| 391 |
class PersonalizedKazakhAssistant:
|
| 392 |
def __init__(self):
|
| 393 |
+
self.known_terms = set()
|
| 394 |
self.setup_environment()
|
| 395 |
self.setup_vectorstore()
|
| 396 |
self.setup_llm()
|
|
|
|
| 400 |
|
| 401 |
def setup_environment(self):
|
| 402 |
"""Setup environment and configuration"""
|
|
|
|
| 403 |
self.google_api_key = os.getenv("GOOGLE_API_KEY")
|
| 404 |
self.MODEL = "gemini-1.5-flash"
|
| 405 |
self.db_name = "vector_db"
|
| 406 |
|
| 407 |
def setup_vectorstore(self):
|
| 408 |
"""Setup document loading and vector store"""
|
| 409 |
+
folders = glob.glob("knowledge-base/*")
|
| 410 |
text_loader_kwargs = {'encoding': 'utf-8'}
|
| 411 |
documents = []
|
| 412 |
|
| 413 |
for folder in folders:
|
| 414 |
+
doc_type = os.path.basename(folder).lower()
|
| 415 |
loader = DirectoryLoader(
|
| 416 |
folder,
|
| 417 |
glob="**/*.txt",
|
|
|
|
| 423 |
doc.metadata["doc_type"] = doc_type
|
| 424 |
documents.append(doc)
|
| 425 |
|
| 426 |
+
self.known_terms.clear()
|
| 427 |
+
common_words = {'бас', 'сөз', 'адам', 'жол', 'күн', 'су', 'жер', 'қол', 'тұр', 'бер'}
|
| 428 |
+
for doc in documents:
|
| 429 |
+
doc_type = doc.metadata.get('doc_type', '').lower()
|
| 430 |
+
lines = doc.page_content.replace('\r\n', '\n').replace('\r', '\n').split('\n')
|
| 431 |
+
for line in lines:
|
| 432 |
+
line = line.strip()
|
| 433 |
+
if line and " - " in line:
|
| 434 |
+
term = line.split(" - ")[0].strip().lower()
|
| 435 |
+
|
| 436 |
+
if term and (
|
| 437 |
+
doc_type in ['idioms', 'grammar'] or
|
| 438 |
+
(doc_type == 'words' and len(term.split()) > 1) or
|
| 439 |
+
term not in common_words
|
| 440 |
+
):
|
| 441 |
+
self.known_terms.add(term)
|
| 442 |
+
|
| 443 |
+
print(f"Loaded {len(self.known_terms)} known terms: {list(self.known_terms)[:10]}")
|
| 444 |
+
|
| 445 |
text_splitter = CharacterTextSplitter(separator=r'\n', chunk_size=2000, chunk_overlap=0)
|
| 446 |
chunks = text_splitter.split_documents(documents)
|
| 447 |
|
|
|
|
| 454 |
|
| 455 |
self.vectorstore = Chroma.from_documents(documents=chunks, embedding=embeddings, persist_directory=self.db_name)
|
| 456 |
print(f"Vectorstore created with {self.vectorstore._collection.count()} documents")
|
| 457 |
+
|
| 458 |
def setup_llm(self):
|
| 459 |
"""Setup LLM with enhanced system prompt"""
|
| 460 |
system_prompt = """
|
| 461 |
+
You are a personalized Kazakh language learning assistant with access to a comprehensive knowledge base and user learning history. Your role is to help users learn Kazakh words and idioms while tracking their progress and providing personalized recommendations.
|
| 462 |
+
|
| 463 |
+
Key capabilities:
|
| 464 |
+
1. **Answer Queries**: Provide accurate definitions and examples for Kazakh words and idioms from your knowledge base
|
| 465 |
+
2. **Track Learning Progress**: Identify and track when users learn new words or idioms
|
| 466 |
+
3. **Personalized Responses**: Adapt responses based on user's learning history and progress
|
| 467 |
+
4. **Progress Reporting**: Provide detailed progress reports when asked
|
| 468 |
+
5. **Learning Recommendations**: Suggest words/idioms to review or learn next
|
| 469 |
+
|
| 470 |
+
Response Guidelines:
|
| 471 |
+
- For word/idiom queries: Provide definition, usage examples, and related information
|
| 472 |
+
- Always identify the main Kazakh word/idiom being discussed for progress tracking
|
| 473 |
+
- Be encouraging and supportive of the user's learning journey
|
| 474 |
+
- Use simple, clear explanations appropriate for language learners
|
| 475 |
+
- When discussing progress, be specific and motivating
|
| 476 |
+
- Avoid storing definitions as terms; only track the word/idiom itself
|
| 477 |
+
- Normalize terms to lowercase to avoid duplicates due to case differences
|
| 478 |
+
|
| 479 |
+
Format responses naturally in conversational style, not JSON unless specifically requested.
|
| 480 |
+
"""
|
| 481 |
|
| 482 |
self.llm = ChatGoogleGenerativeAI(
|
| 483 |
model="models/gemini-1.5-flash",
|
| 484 |
temperature=0.7,
|
| 485 |
+
model_kwargs={"system_instruction": system_prompt}
|
| 486 |
)
|
| 487 |
|
| 488 |
+
def normalize_term(self, term: str) -> str:
|
| 489 |
+
"""Normalize term by converting to lowercase and removing extra spaces"""
|
| 490 |
+
return ' '.join(term.lower().strip().split())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
|
| 492 |
def extract_kazakh_terms(self, message: str, response: str) -> List[Tuple[str, str, str]]:
|
| 493 |
"""Extract meaningful Kazakh terms using document metadata to determine category"""
|
| 494 |
terms = []
|
| 495 |
+
seen_terms = set()
|
| 496 |
|
| 497 |
try:
|
| 498 |
retrieved_docs = self.vectorstore.similarity_search(message, k=5)
|
| 499 |
|
| 500 |
+
response_normalized = self.normalize_term(response)
|
| 501 |
+
message_normalized = self.normalize_term(message)
|
| 502 |
|
| 503 |
+
is_multi_term_query = any(keyword in message_normalized for keyword in ['мысал', 'тіркестер', 'пример'])
|
| 504 |
+
|
| 505 |
+
common_words = {'бас', 'сөз', 'адам', 'жол', 'күн', 'су', 'жер', 'қол', 'тұр', 'бер'}
|
| 506 |
+
|
| 507 |
+
for known_term in self.known_terms:
|
| 508 |
+
normalized_known_term = self.normalize_term(known_term)
|
| 509 |
+
if normalized_known_term in response_normalized and normalized_known_term not in seen_terms:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
+
if normalized_known_term in common_words and not (
|
| 512 |
+
normalized_known_term in message_normalized or is_multi_term_query
|
| 513 |
+
):
|
| 514 |
+
print(f"Skipped common term: {known_term}")
|
| 515 |
+
continue
|
| 516 |
+
|
| 517 |
+
if normalized_known_term in message_normalized or any(
|
| 518 |
+
normalized_known_term in self.normalize_term(doc.page_content) for doc in retrieved_docs
|
| 519 |
+
):
|
| 520 |
+
category = "idiom"
|
| 521 |
+
definition = ""
|
| 522 |
|
| 523 |
+
for doc in retrieved_docs:
|
| 524 |
+
if normalized_known_term in self.normalize_term(doc.page_content):
|
| 525 |
+
doc_type = doc.metadata.get('doc_type', '').lower()
|
| 526 |
+
if 'idiom' in doc_type or 'тіркес' in doc_type:
|
| 527 |
+
category = "idiom"
|
| 528 |
+
elif 'grammar' in doc_type:
|
| 529 |
+
category = "grammar"
|
| 530 |
+
else:
|
| 531 |
+
category = "word"
|
| 532 |
+
definition = self.extract_clean_definition(normalized_known_term, doc.page_content, response)
|
| 533 |
+
break
|
| 534 |
+
|
| 535 |
+
if definition and len(normalized_known_term.split()) <= 10:
|
| 536 |
+
terms.append((known_term, category, definition))
|
| 537 |
+
seen_terms.add(normalized_known_term)
|
| 538 |
+
print(f"Added term: {known_term}, category: {category}, definition: {definition}")
|
| 539 |
+
|
| 540 |
+
if not is_multi_term_query and normalized_known_term not in message_normalized:
|
| 541 |
+
return terms
|
| 542 |
|
| 543 |
+
if not terms and not is_multi_term_query:
|
| 544 |
+
kazakh_phrases = re.findall(
|
| 545 |
+
r'[А-Яа-яӘәҒғҚқҢңӨөҰұҮүҺһІі]+(?:[\s\-]+[А-Яа-яӘәҒғҚқҢңӨөҰұҮүҺһІі]+)*',
|
| 546 |
+
response
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
for phrase in kazakh_phrases:
|
| 550 |
+
normalized_phrase = self.normalize_term(phrase)
|
| 551 |
+
|
| 552 |
+
if normalized_phrase in seen_terms:
|
| 553 |
+
continue
|
| 554 |
+
|
| 555 |
+
if len(normalized_phrase) <= 2 or len(normalized_phrase) > 100:
|
| 556 |
+
print(f"Skipped phrase {normalized_phrase}: Invalid length")
|
| 557 |
+
continue
|
| 558 |
+
|
| 559 |
+
skip_words = ['деген', 'деп', 'берілген', 'мәтінде', 'мағынасы', 'дегеннің',
|
| 560 |
+
'түсіндірілген', 'келтірілген', 'болып', 'табылады', 'ауруы',
|
| 561 |
+
'мынадай', 'тақырыбына', 'тіркестер', 'арналған', 'байланысты']
|
| 562 |
+
|
| 563 |
+
if any(skip in normalized_phrase for skip in skip_words):
|
| 564 |
+
print(f"Skipped phrase {normalized_phrase}: Contains skip word")
|
| 565 |
+
continue
|
| 566 |
+
|
| 567 |
+
if normalized_phrase in common_words and normalized_phrase not in message_normalized:
|
| 568 |
+
print(f"Skipped common phrase: {normalized_phrase}")
|
| 569 |
+
continue
|
| 570 |
+
|
| 571 |
+
if normalized_phrase not in self.known_terms:
|
| 572 |
+
print(f"Warning: {normalized_phrase} not in known_terms, but processing anyway")
|
| 573 |
+
|
| 574 |
+
category = "word"
|
| 575 |
+
definition = ""
|
| 576 |
+
|
| 577 |
+
for doc in retrieved_docs:
|
| 578 |
+
if normalized_phrase in self.normalize_term(doc.page_content):
|
| 579 |
+
doc_type = doc.metadata.get('doc_type', '').lower()
|
| 580 |
+
if 'idiom' in doc_type or 'тіркес' in doc_type:
|
| 581 |
+
category = "idiom"
|
| 582 |
+
elif 'grammar' in doc_type:
|
| 583 |
+
category = "grammar"
|
| 584 |
+
else:
|
| 585 |
+
category = "word"
|
| 586 |
+
|
| 587 |
+
definition = self.extract_clean_definition(normalized_phrase, doc.page_content, response)
|
| 588 |
+
break
|
| 589 |
+
|
| 590 |
+
if definition and len(normalized_phrase.split()) <= 6:
|
| 591 |
+
if not any(normalized_phrase.startswith(q) for q in ['қалай', 'қандай', 'қайда', 'неше', 'қашан']):
|
| 592 |
+
terms.append((phrase, category, definition))
|
| 593 |
+
seen_terms.add(normalized_phrase)
|
| 594 |
+
print(f"Added term: {phrase}, category: {category}, definition: {definition}")
|
| 595 |
+
break
|
| 596 |
+
|
| 597 |
except Exception as e:
|
| 598 |
print(f"Error extracting terms: {e}")
|
| 599 |
+
|
| 600 |
return terms
|
| 601 |
|
| 602 |
def extract_clean_definition(self, term: str, doc_content: str, response: str) -> str:
|
| 603 |
+
"""Extract clean definition for a term, avoiding storing definitions as terms"""
|
| 604 |
+
normalized_term = self.normalize_term(term)
|
| 605 |
+
|
| 606 |
sentences = response.split('.')
|
| 607 |
for sentence in sentences:
|
| 608 |
+
sentence = sentence.strip()
|
| 609 |
+
if normalized_term in self.normalize_term(sentence) and len(sentence) > 10 and len(sentence) < 150:
|
| 610 |
+
if not any(word in sentence.lower() for word in ['деген не', 'қалай аталады', 'нені білдіреді']):
|
| 611 |
+
return sentence
|
|
|
|
| 612 |
|
| 613 |
doc_sentences = doc_content.split('.')
|
| 614 |
for sentence in doc_sentences:
|
| 615 |
+
sentence = sentence.strip()
|
| 616 |
+
if normalized_term in self.normalize_term(sentence) and len(sentence) > 10 and len(sentence) < 150:
|
| 617 |
+
return sentence
|
|
|
|
| 618 |
|
| 619 |
return f"Definition for {term}"
|
| 620 |
|
| 621 |
+
def get_user_memory(self, user_id: str):
|
| 622 |
+
"""Get or create conversation memory for a specific user"""
|
| 623 |
+
if user_id not in self.user_memories:
|
| 624 |
+
self.user_memories[user_id] = ConversationBufferMemory(
|
| 625 |
+
memory_key='chat_history',
|
| 626 |
+
return_messages=True,
|
| 627 |
+
max_token_limit=10000
|
| 628 |
+
)
|
| 629 |
+
return self.user_memories[user_id]
|
| 630 |
+
|
| 631 |
+
def get_user_chain(self, user_id: str):
|
| 632 |
+
"""Get or create conversation chain for a specific user"""
|
| 633 |
+
memory = self.get_user_memory(user_id)
|
| 634 |
+
retriever = self.vectorstore.as_retriever()
|
| 635 |
+
return ConversationalRetrievalChain.from_llm(
|
| 636 |
+
llm=self.llm,
|
| 637 |
+
retriever=retriever,
|
| 638 |
+
memory=memory
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
def process_message(self, message: str, user_id: str = "default_user", session_token: str = None, use_direct_gemini: bool = False, target_language: str = "English") -> str:
|
| 642 |
+
"""Process user message with proper user session management and toggle for direct Gemini"""
|
| 643 |
|
| 644 |
if session_token and not self.tracker.validate_session(user_id, session_token):
|
| 645 |
return "Session expired. Please login again."
|
|
|
|
| 656 |
return self.get_recommendations(user_id)
|
| 657 |
elif message.lower().startswith('/review'):
|
| 658 |
return self.get_review_words(user_id)
|
| 659 |
+
elif message.lower().startswith('/mastered'):
|
| 660 |
+
return self.get_mastered_words(user_id)
|
| 661 |
elif message.lower().startswith('/help'):
|
| 662 |
return self.get_help_message()
|
| 663 |
|
| 664 |
+
if use_direct_gemini:
|
| 665 |
+
return self.process_direct_gemini(message, user_id, target_language)
|
| 666 |
+
|
| 667 |
conversation_chain = self.get_user_chain(user_id)
|
| 668 |
result = conversation_chain.invoke({"question": message})
|
| 669 |
response = result["answer"]
|
|
|
|
| 742 |
|
| 743 |
return response
|
| 744 |
|
| 745 |
+
def get_mastered_words(self, user_id: str) -> str:
|
| 746 |
+
"""Get words that have been mastered (mastery level > 0) for specific user"""
|
| 747 |
+
mastered_words = self.tracker.get_mastered_words(user_id, 10)
|
| 748 |
+
|
| 749 |
+
if not mastered_words:
|
| 750 |
+
return "Сізде әзірге меңгерілген сөздер жоқ. Терминдерді қайталауды жалғастырыңыз, сонда олар осында пайда болады! 🌟\n\nYou haven't mastered any words yet. Keep reviewing terms, and they'll appear here! 🌟"
|
| 751 |
+
|
| 752 |
+
response = "🏆 **Меңгерілген сөздер / Mastered Words**:\n\n"
|
| 753 |
+
for word_info in mastered_words:
|
| 754 |
+
emoji = "📝" if word_info['category'] == "word" else "🎭"
|
| 755 |
+
|
| 756 |
+
mastery_stars = "🟊" * word_info['mastery_level'] + "⬜" * (5 - word_info['mastery_level'])
|
| 757 |
+
response += f"{emoji} **{word_info['word']}** - {mastery_stars} (Кездесу саны / Encounters: {word_info['encounter_count']})\n"
|
| 758 |
+
|
| 759 |
+
definition_preview = word_info['definition'][:80] + "..." if len(word_info['definition']) > 80 else word_info['definition']
|
| 760 |
+
response += f" {definition_preview}\n\n"
|
| 761 |
+
|
| 762 |
+
return response
|
| 763 |
+
|
| 764 |
def get_help_message(self) -> str:
|
| 765 |
"""Get help message with available commands"""
|
| 766 |
return """
|
|
|
|
| 770 |
- `/progress` - View your detailed learning progress
|
| 771 |
- `/recommendations` - Get personalized learning suggestions
|
| 772 |
- `/review` - See words that need review
|
| 773 |
+
- `/mastered` - See words you've mastered (mastery level > 0)
|
| 774 |
- `/help` - Show this help message
|
| 775 |
|
| 776 |
**How to Use**:
|
|
|
|
| 791 |
"""Create a session token for user authentication"""
|
| 792 |
session_token = self.tracker.create_user_session(user_id)
|
| 793 |
return session_token
|
| 794 |
+
|
| 795 |
+
def process_direct_gemini(self, message: str, user_id: str, target_language: str = "English") -> str:
|
| 796 |
+
"""Process message using direct Gemini with grammar-focused prompt"""
|
| 797 |
+
try:
|
| 798 |
+
|
| 799 |
+
direct_prompt = """
|
| 800 |
+
You are a Kazakh language teacher specializing in grammar and vocabulary. Your role is to teach Kazakh grammar and words in the user's requested language (Kazakh, Russian, or English). Provide clear, concise explanations tailored to language learners, including examples and practical usage. If the user doesn't specify a language, default to English. Do not rely on external knowledge bases; use your internal knowledge to generate accurate and educational responses. Be encouraging and supportive, and adapt explanations to the user's proficiency level if known.
|
| 801 |
+
"""
|
| 802 |
+
direct_llm = ChatGoogleGenerativeAI(
|
| 803 |
+
model="models/gemini-1.5-flash",
|
| 804 |
+
temperature=0.7,
|
| 805 |
+
model_kwargs={"system_instruction": direct_prompt}
|
| 806 |
+
)
|
| 807 |
+
|
| 808 |
+
message_lower = message.lower()
|
| 809 |
+
if any(keyword in message_lower for keyword in ['kazakh', 'қазақша', 'қазақ тілінде']):
|
| 810 |
+
target_language = "Kazakh"
|
| 811 |
+
elif any(keyword in message_lower for keyword in ['russian', 'русский', 'орысша']):
|
| 812 |
+
target_language = "Russian"
|
| 813 |
+
|
| 814 |
+
modified_message = f"Explain in {target_language}: {message}"
|
| 815 |
+
response = direct_llm.invoke(modified_message).content
|
| 816 |
+
return response
|
| 817 |
+
except Exception as e:
|
| 818 |
+
return f"Error processing direct Gemini request: {str(e)}"
|
| 819 |
|
| 820 |
assistant = PersonalizedKazakhAssistant()
|
| 821 |
|
| 822 |
+
def chat_interface(message, history, use_direct_gemini, target_language):
|
| 823 |
+
"""Chat interface for Gradio with toggle for direct Gemini mode"""
|
|
|
|
| 824 |
try:
|
|
|
|
|
|
|
| 825 |
web_user_id = "web_user_default" # Consistent ID
|
| 826 |
+
response = assistant.process_message(message, web_user_id, use_direct_gemini=use_direct_gemini, target_language=target_language)
|
| 827 |
return response
|
| 828 |
except Exception as e:
|
| 829 |
return f"Sorry, I encountered an error: {str(e)}. Please try again."
|
| 830 |
+
|
|
|
|
| 831 |
def api_login(user_id: str) -> dict:
|
| 832 |
"""API endpoint for user login/session creation"""
|
| 833 |
try:
|
|
|
|
| 844 |
"error": str(e)
|
| 845 |
}
|
| 846 |
|
| 847 |
+
def api_chat(message: str, user_id: str, session_token: str = None, use_direct_gemini: bool = False, target_language: str = "English") -> dict:
|
| 848 |
+
"""API endpoint for chat functionality with proper user session and direct Gemini toggle"""
|
| 849 |
try:
|
| 850 |
+
response = assistant.process_message(message, user_id, session_token, use_direct_gemini, target_language)
|
| 851 |
return {
|
| 852 |
"success": True,
|
| 853 |
"response": response,
|
|
|
|
| 923 |
"error": str(e)
|
| 924 |
}
|
| 925 |
|
| 926 |
+
def api_mastered_words(user_id: str, session_token: str = None) -> dict:
|
| 927 |
+
"""API endpoint for mastered words with session validation"""
|
| 928 |
+
try:
|
| 929 |
+
if session_token and not assistant.tracker.validate_session(user_id, session_token):
|
| 930 |
+
return {"success": False, "error": "Invalid session"}
|
| 931 |
+
|
| 932 |
+
mastered_text = assistant.get_mastered_words(user_id)
|
| 933 |
+
mastered_data = assistant.tracker.get_mastered_words(user_id, 10)
|
| 934 |
+
|
| 935 |
+
return {
|
| 936 |
+
"success": True,
|
| 937 |
+
"mastered_text": mastered_text,
|
| 938 |
+
"mastered_data": mastered_data,
|
| 939 |
+
"user_id": user_id
|
| 940 |
+
}
|
| 941 |
+
except Exception as e:
|
| 942 |
+
return {
|
| 943 |
+
"success": False,
|
| 944 |
+
"error": str(e)
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
with gr.Blocks(title="🇰🇿 Kazakh Learning API") as demo:
|
|
|
|
| 948 |
gr.Markdown("# 🇰🇿 Personalized Kazakh Learning Assistant")
|
| 949 |
gr.Markdown("### Multi-User Chat Interface + API Endpoints for Mobile Integration")
|
| 950 |
|
| 951 |
with gr.Tab("💬 Chat Interface"):
|
| 952 |
+
gr.Markdown("Toggle **Direct Gemini Mode** to learn Kazakh grammar without RAG. Select the language for explanations.")
|
| 953 |
+
with gr.Row():
|
| 954 |
+
use_direct_gemini = gr.Checkbox(label="Direct Gemini Mode (No RAG/Tracking)", value=False)
|
| 955 |
+
target_language = gr.Dropdown(
|
| 956 |
+
label="Explanation Language",
|
| 957 |
+
choices=["English", "Kazakh", "Russian"],
|
| 958 |
+
value="English"
|
| 959 |
+
)
|
| 960 |
chat_interface = gr.ChatInterface(
|
| 961 |
+
fn=chat_interface,
|
| 962 |
+
additional_inputs=[use_direct_gemini, target_language],
|
| 963 |
type="messages",
|
| 964 |
examples=[
|
| 965 |
+
["сәлем деген не?", False, "English"],
|
| 966 |
+
["күләпара не үшін керек?", False, "English"],
|
| 967 |
+
["/progress", False, "English"],
|
| 968 |
+
["/recommendations", False, "English"],
|
| 969 |
+
["/review", False, "English"],
|
| 970 |
+
["/mastered", False, "English"],
|
| 971 |
+
["Explain Kazakh noun cases in Russian", True, "Russian"],
|
| 972 |
+
["Teach me Kazakh verb conjugation in English", True, "English"]
|
| 973 |
]
|
| 974 |
)
|
| 975 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 976 |
with gr.Tab("📖 API Documentation"):
|
| 977 |
gr.Markdown("""
|
| 978 |
## API Endpoints for Flutter Integration
|
|
|
|
| 979 |
### Base URL: `https://huggingface.co/spaces/GuestUser33/kazakh-learning-api`
|
| 980 |
+
|
| 981 |
### Authentication Flow:
|
| 982 |
+
1. **Login** to get a session token
|
| 983 |
2. **Use session token** for subsequent API calls
|
| 984 |
3. **Session tokens expire** after inactivity
|
| 985 |
+
|
| 986 |
### Available Endpoints:
|
| 987 |
+
|
| 988 |
#### 1. Login API
|
| 989 |
```
|
| 990 |
POST /api/predict
|
| 991 |
Content-Type: application/json
|
| 992 |
+
|
| 993 |
{
|
| 994 |
+
"data": ["user_id"],
|
| 995 |
+
"fn_index": 0
|
| 996 |
}
|
| 997 |
```
|
| 998 |
+
**Response**: `{"success": true, "session_token": "uuid", "user_id": "user_id", "message": "Login successful"}`
|
| 999 |
+
|
| 1000 |
#### 2. Chat API
|
| 1001 |
```
|
| 1002 |
POST /api/predict
|
| 1003 |
Content-Type: application/json
|
| 1004 |
+
|
| 1005 |
{
|
| 1006 |
+
"data": ["message", "user_id", "session_token", use_direct_gemini, "target_language"],
|
| 1007 |
+
"fn_index": 1
|
| 1008 |
}
|
| 1009 |
```
|
| 1010 |
+
**Parameters**:
|
| 1011 |
+
- `message`: The user's query (e.g., "сәлем деген не?" or "Explain Kazakh noun cases")
|
| 1012 |
+
- `user_id`: Unique identifier for the user
|
| 1013 |
+
- `session_token`: Session token from login (optional, but required for authenticated sessions)
|
| 1014 |
+
- `use_direct_gemini`: Boolean (`true`/`false`) to toggle Direct Gemini mode for grammar-focused responses without RAG/tracking
|
| 1015 |
+
- `target_language`: Language for responses (`English`, `Kazakh`, or `Russian`)
|
| 1016 |
+
|
| 1017 |
+
**Response**: `{"success": true, "response": "response_text", "user_id": "user_id"}`
|
| 1018 |
+
|
| 1019 |
+
#### 3. Progress API
|
| 1020 |
```
|
| 1021 |
POST /api/predict
|
| 1022 |
Content-Type: application/json
|
| 1023 |
+
|
| 1024 |
{
|
| 1025 |
+
"data": ["user_id", "session_token"],
|
| 1026 |
+
"fn_index": 2
|
| 1027 |
}
|
| 1028 |
```
|
| 1029 |
+
**Response**: `{"success": true, "progress_text": "progress_report", "progress_data": {...}, "user_id": "user_id"}`
|
| 1030 |
+
|
| 1031 |
#### 4. Recommendations API
|
| 1032 |
```
|
| 1033 |
POST /api/predict
|
| 1034 |
Content-Type: application/json
|
| 1035 |
+
|
| 1036 |
{
|
| 1037 |
+
"data": ["user_id", "session_token"],
|
| 1038 |
+
"fn_index": 3
|
| 1039 |
}
|
| 1040 |
```
|
| 1041 |
+
**Response**: `{"success": true, "recommendations_text": "recommendations", "recommendations_list": [...], "user_id": "user_id"}`
|
| 1042 |
+
|
| 1043 |
#### 5. Review Words API
|
| 1044 |
```
|
| 1045 |
POST /api/predict
|
| 1046 |
Content-Type: application/json
|
| 1047 |
+
|
| 1048 |
{
|
| 1049 |
+
"data": ["user_id", "session_token"],
|
| 1050 |
+
"fn_index": 4
|
| 1051 |
}
|
| 1052 |
```
|
| 1053 |
+
**Response**: `{"success": true, "review_text": "review_words", "review_data": [...], "user_id": "user_id"}`
|
| 1054 |
+
|
| 1055 |
+
#### 6. Mastered Words API
|
| 1056 |
+
```
|
| 1057 |
+
POST /api/predict
|
| 1058 |
+
Content-Type: application/json
|
| 1059 |
+
|
| 1060 |
+
{
|
| 1061 |
+
"data": ["user_id", "session_token"],
|
| 1062 |
+
"fn_index": 5
|
| 1063 |
+
}
|
| 1064 |
+
```
|
| 1065 |
+
**Response**: `{"success": true, "mastered_text": "mastered_words", "mastered_data": [...], "user_id": "user_id"}`
|
| 1066 |
+
|
| 1067 |
### Flutter Integration Example:
|
| 1068 |
```dart
|
| 1069 |
+
import 'dart:convert';
|
| 1070 |
+
import 'package:http/http.dart' as http;
|
| 1071 |
+
|
| 1072 |
class KazakhLearningAPI {
|
| 1073 |
+
static const String baseUrl = 'https://huggingface.co/spaces/GuestUser33/kazakh-learning-api';
|
| 1074 |
+
String? sessionToken;
|
| 1075 |
+
String? userId;
|
| 1076 |
+
|
| 1077 |
+
// Login and get session token
|
| 1078 |
+
Future<bool> login(String userId) async {
|
| 1079 |
final response = await http.post(
|
| 1080 |
+
Uri.parse('$baseUrl/api/predict'),
|
| 1081 |
+
headers: {'Content-Type': 'application/json'},
|
| 1082 |
+
body: jsonEncode({
|
| 1083 |
'data': [userId],
|
| 1084 |
'fn_index': 0
|
| 1085 |
+
}),
|
| 1086 |
);
|
| 1087 |
+
|
| 1088 |
if (response.statusCode == 200) {
|
| 1089 |
+
final result = jsonDecode(response.body);
|
| 1090 |
+
if (result['data'][0]['success']) {
|
| 1091 |
this.userId = userId;
|
| 1092 |
this.sessionToken = result['data'][0]['session_token'];
|
| 1093 |
return true;
|
| 1094 |
+
}
|
| 1095 |
}
|
| 1096 |
return false;
|
| 1097 |
+
}
|
| 1098 |
+
|
| 1099 |
+
// Send chat message
|
| 1100 |
+
Future<String?> sendMessage(
|
| 1101 |
+
String message, {
|
| 1102 |
+
bool useDirectGemini = false,
|
| 1103 |
+
String targetLanguage = 'English',
|
| 1104 |
+
}) async {
|
| 1105 |
if (sessionToken == null) return null;
|
| 1106 |
+
|
| 1107 |
final response = await http.post(
|
| 1108 |
+
Uri.parse('$baseUrl/api/predict'),
|
| 1109 |
+
headers: {'Content-Type': 'application/json'},
|
| 1110 |
+
body: jsonEncode({
|
| 1111 |
+
'data': [message, userId, sessionToken, useDirectGemini, targetLanguage],
|
| 1112 |
'fn_index': 1
|
| 1113 |
+
}),
|
| 1114 |
);
|
| 1115 |
+
|
| 1116 |
if (response.statusCode == 200) {
|
| 1117 |
+
final result = jsonDecode(response.body);
|
| 1118 |
+
if (result['data'][0]['success']) {
|
| 1119 |
return result['data'][0]['response'];
|
| 1120 |
+
}
|
| 1121 |
}
|
| 1122 |
return null;
|
| 1123 |
+
}
|
| 1124 |
+
|
| 1125 |
+
// Get user progress
|
| 1126 |
+
Future<Map<String, dynamic>?> getProgress() async {
|
| 1127 |
if (sessionToken == null) return null;
|
| 1128 |
+
|
| 1129 |
final response = await http.post(
|
| 1130 |
+
Uri.parse('$baseUrl/api/predict'),
|
| 1131 |
+
headers: {'Content-Type': 'application/json'},
|
| 1132 |
+
body: jsonEncode({
|
| 1133 |
'data': [userId, sessionToken],
|
| 1134 |
'fn_index': 2
|
| 1135 |
+
}),
|
| 1136 |
);
|
| 1137 |
+
|
| 1138 |
if (response.statusCode == 200) {
|
| 1139 |
+
final result = jsonDecode(response.body);
|
| 1140 |
+
if (result['data'][0]['success']) {
|
| 1141 |
return result['data'][0]['progress_data'];
|
| 1142 |
+
}
|
| 1143 |
}
|
| 1144 |
return null;
|
| 1145 |
+
}
|
| 1146 |
+
|
| 1147 |
+
// Get recommendations
|
| 1148 |
+
Future<List<String>?> getRecommendations() async {
|
| 1149 |
+
if (sessionToken == null) return null;
|
| 1150 |
+
|
| 1151 |
+
final response = await http.post(
|
| 1152 |
+
Uri.parse('$baseUrl/api/predict'),
|
| 1153 |
+
headers: {'Content-Type': 'application/json'},
|
| 1154 |
+
body: jsonEncode({
|
| 1155 |
+
'data': [userId, sessionToken],
|
| 1156 |
+
'fn_index': 3
|
| 1157 |
+
}),
|
| 1158 |
+
);
|
| 1159 |
+
|
| 1160 |
+
if (response.statusCode == 200) {
|
| 1161 |
+
final result = jsonDecode(response.body);
|
| 1162 |
+
if (result['data'][0]['success']) {
|
| 1163 |
+
return List<String>.from(result['data'][0]['recommendations_list']);
|
| 1164 |
+
}
|
| 1165 |
+
}
|
| 1166 |
+
return null;
|
| 1167 |
+
}
|
| 1168 |
+
|
| 1169 |
+
// Get words to review
|
| 1170 |
+
Future<List<dynamic>?> getReviewWords() async {
|
| 1171 |
+
if (sessionToken == null) return null;
|
| 1172 |
+
|
| 1173 |
+
final response = await http.post(
|
| 1174 |
+
Uri.parse('$baseUrl/api/predict'),
|
| 1175 |
+
headers: {'Content-Type': 'application/json'},
|
| 1176 |
+
body: jsonEncode({
|
| 1177 |
+
'data': [userId, sessionToken],
|
| 1178 |
+
'fn_index': 4
|
| 1179 |
+
}),
|
| 1180 |
+
);
|
| 1181 |
+
|
| 1182 |
+
if (response.statusCode == 200) {
|
| 1183 |
+
final result = jsonDecode(response.body);
|
| 1184 |
+
if (result['data'][0]['success']) {
|
| 1185 |
+
return result['data'][0]['review_data'];
|
| 1186 |
+
}
|
| 1187 |
+
}
|
| 1188 |
+
return null;
|
| 1189 |
+
}
|
| 1190 |
+
|
| 1191 |
+
// Get mastered words
|
| 1192 |
+
Future<List<dynamic>?> getMasteredWords() async {
|
| 1193 |
+
if (sessionToken == null) return null;
|
| 1194 |
+
|
| 1195 |
+
final response = await http.post(
|
| 1196 |
+
Uri.parse('$baseUrl/api/predict'),
|
| 1197 |
+
headers: {'Content-Type': 'application/json'},
|
| 1198 |
+
body: jsonEncode({
|
| 1199 |
+
'data': [userId, sessionToken],
|
| 1200 |
+
'fn_index': 5
|
| 1201 |
+
}),
|
| 1202 |
+
);
|
| 1203 |
+
|
| 1204 |
+
if (response.statusCode == 200) {
|
| 1205 |
+
final result = jsonDecode(response.body);
|
| 1206 |
+
if (result['data'][0]['success']) {
|
| 1207 |
+
return result['data'][0]['mastered_data'];
|
| 1208 |
+
}
|
| 1209 |
+
}
|
| 1210 |
+
return null;
|
| 1211 |
+
}
|
| 1212 |
}
|
| 1213 |
```
|
| 1214 |
+
|
| 1215 |
### Key Features:
|
| 1216 |
- ✅ **Multi-User Support**: Each user has separate learning progress
|
| 1217 |
- ✅ **Session Management**: Secure session tokens for authentication
|
| 1218 |
+
- ✅ **Personalized Tracking**: Individual progress tracking per user (in RAG mode)
|
| 1219 |
+
- ✅ **Direct Gemini Mode**: Toggle for grammar-focused responses without RAG/tracking
|
| 1220 |
+
- ✅ **Multi-Language Support**: Responses in English, Kazakh, or Russian
|
| 1221 |
- ✅ **API Ready**: All endpoints ready for mobile app integration
|
| 1222 |
- ✅ **Session Validation**: Automatic session validation and expiry
|
| 1223 |
+
|
| 1224 |
### Usage Notes:
|
| 1225 |
- Always call **login** first to get a session token
|
| 1226 |
- Include **session_token** in all subsequent API calls
|
| 1227 |
+
- Use `use_direct_gemini: true` for grammar/vocabulary lessons without tracking
|
| 1228 |
+
- Specify `target_language` (`English`, `Kazakh`, `Russian`) for Direct Gemini mode
|
| 1229 |
- Handle **session expiry** by re-logging in
|
| 1230 |
+
- Use **unique user_id** for each user (e.g., email, username)
|
| 1231 |
+
- Commands like `/progress`, `/recommendations`, `/review`, `/mastered` are only available in RAG mode (`use_direct_gemini: false`)
|
| 1232 |
+
""")
|
| 1233 |
|
| 1234 |
if __name__ == "__main__":
|
| 1235 |
demo.launch()
|