better db connectin hadeling for long lasting deplyment

parent a4501a9c
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile",
"containerHttpPort": "8000",
"env": {
"POSTGRES_HOST": "srv-captain--postgres",
"MINIO_HOST": "srv-captain--minio"
}
}
...@@ -17,7 +17,7 @@ class StudentNationality(str, Enum): ...@@ -17,7 +17,7 @@ class StudentNationality(str, Enum):
class Models(str, Enum): class Models(str, Enum):
chat = "gpt-5" chat = "gpt-4o-mini"
tts = "gpt-4o-mini-tts" tts = "gpt-4o-mini-tts"
embedding = "text-embedding-3-small" embedding = "text-embedding-3-small"
transcription = "gpt-4o-transcribe" transcription = "gpt-4o-transcribe"
import os import os
from fastapi import FastAPI, UploadFile, File, Form, HTTPException from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, Response
from fastapi.staticfiles import StaticFiles
from typing import Optional from typing import Optional
import uvicorn import uvicorn
import base64
from pathlib import Path
# Import your existing modules
from core import AppConfig, StudentNationality from core import AppConfig, StudentNationality
from repositories import StorageRepository, MinIOStorageRepository from repositories import StorageRepository, MinIOStorageRepository
from handlers import AudioMessageHandler, TextMessageHandler from handlers import AudioMessageHandler, TextMessageHandler
...@@ -36,19 +42,28 @@ class DIContainer: ...@@ -36,19 +42,28 @@ class DIContainer:
def create_app() -> FastAPI: def create_app() -> FastAPI:
app = FastAPI(title="Unified Chat API with Local Agent") app = FastAPI(title="Unified Chat API with Local Agent")
# Fixed CORS configuration for CapRover
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=[ allow_origins=[
"http://teamtestingdocker.caprover.al-arcade.com:8000", "https://voice-agent.caprover.al-arcade.com",
"http://localhost:8000", # For local development "http://voice-agent.caprover.al-arcade.com",
], "http://localhost:8000", # For local development
allow_credentials=True, "http://127.0.0.1:8000",
allow_methods=["*"], "*" # Allow all origins for testing - remove in production
allow_headers=["*"], ],
expose_headers=["X-Response-Text"], allow_credentials=True,
) allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=[
"Accept",
"Accept-Language",
"Content-Language",
"Content-Type",
"Authorization",
"X-Response-Text"
],
expose_headers=["X-Response-Text"],
)
# Initialize dependencies # Initialize dependencies
container = DIContainer() container = DIContainer()
...@@ -59,94 +74,238 @@ def create_app() -> FastAPI: ...@@ -59,94 +74,238 @@ def create_app() -> FastAPI:
print("OpenAI Service Available:", container.openai_service.is_available()) print("OpenAI Service Available:", container.openai_service.is_available())
print("Agent Service Available:", container.agent_service.is_available()) print("Agent Service Available:", container.agent_service.is_available())
from fastapi.responses import FileResponse # Serve static files if the directory exists
static_path = Path("static")
if static_path.exists():
app.mount("/static", StaticFiles(directory=static_path), name="static")
@app.get("/chat-interface") @app.get("/chat-interface")
async def serve_audio_recorder(): async def serve_audio_recorder():
return FileResponse("static/audio-recorder.html") """Serve the audio recorder HTML file"""
try:
# Try to serve from static directory first
static_file = Path("static/audio-recorder.html")
if static_file.exists():
return FileResponse(static_file)
# Fallback to current directory
current_file = Path("audio-recorder.html")
if current_file.exists():
return FileResponse(current_file)
# If no file found, return an error
raise HTTPException(status_code=404, detail="Audio recorder interface not found")
except Exception as e:
print(f"Error serving audio recorder: {e}")
raise HTTPException(status_code=500, detail=f"Error serving interface: {str(e)}")
@app.post("/chat") @app.post("/chat")
async def chat_handler( async def chat_handler(
file: Optional[UploadFile] = File(None), file: Optional[UploadFile] = File(None),
text: Optional[str] = Form(None), text: Optional[str] = Form(None),
student_id: str = Form("student_001") # Default student_id, but can be overridden student_id: str = Form("student_001")
): ):
""" """
Handles incoming chat messages (either text or audio). Handles incoming chat messages (either text or audio).
Generates responses locally using the agent service. Generates responses locally using the agent service.
""" """
if not student_id.strip(): try:
raise HTTPException(status_code=400, detail="Student ID is required") if not student_id.strip():
raise HTTPException(status_code=400, detail="Student ID is required")
return container.chat_service.process_message(student_id=student_id, file=file, text=text)
print(f"Processing message for student: {student_id}")
print(f"Text: {text}")
print(f"File: {file.filename if file else 'None'}")
result = container.chat_service.process_message(
student_id=student_id,
file=file,
text=text
)
print(f"Chat service result: {result}")
return result
except Exception as e:
print(f"Error in chat handler: {str(e)}")
raise HTTPException(status_code=500, detail=f"Chat processing error: {str(e)}")
@app.get("/get-audio-response") @app.get("/get-audio-response")
async def get_audio_response(): async def get_audio_response():
"""Fetches the agent's text and audio response.""" """Fetches the agent's text and audio response with proper CORS headers."""
return container.response_service.get_agent_response() try:
print("Getting audio response...")
result = container.response_service.get_agent_response()
if hasattr(result, 'status_code'):
# This is already a Response object from response_service
print(f"Response headers: {dict(result.headers)}")
return result
else:
# If it's not a Response object, create one
print("Creating new response object")
response_text = result.get('text', 'No response available')
# Encode the response text in base64 for the header
encoded_text = base64.b64encode(response_text.encode('utf-8')).decode('utf-8')
# Create response with proper headers
response = Response(
content=result.get('audio', b''),
media_type="audio/mpeg",
headers={
"X-Response-Text": encoded_text,
"Access-Control-Expose-Headers": "X-Response-Text",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "*"
}
)
print(f"Created response with headers: {dict(response.headers)}")
return response
except Exception as e:
print(f"Error getting audio response: {str(e)}")
raise HTTPException(status_code=500, detail=f"Audio response error: {str(e)}")
@app.options("/chat")
async def chat_options():
"""Handle preflight CORS requests for chat endpoint"""
return Response(
headers={
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "*",
"Access-Control-Max-Age": "86400"
}
)
@app.options("/get-audio-response")
async def audio_response_options():
"""Handle preflight CORS requests for audio response endpoint"""
return Response(
headers={
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "*",
"Access-Control-Expose-Headers": "X-Response-Text",
"Access-Control-Max-Age": "86400"
}
)
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
"""Health check endpoint with agent service status""" """Health check endpoint with agent service status"""
health_status = container.health_service.get_health_status() try:
# Add agent service status health_status = container.health_service.get_health_status()
health_status.update({ # Add agent service status
"openai_service_status": "available" if container.openai_service.is_available() else "unavailable", health_status.update({
"agent_service_status": "available" if container.agent_service.is_available() else "unavailable" "openai_service_status": "available" if container.openai_service.is_available() else "unavailable",
}) "agent_service_status": "available" if container.agent_service.is_available() else "unavailable",
return health_status "minio_endpoint": container.config.minio_endpoint,
"minio_bucket": container.config.minio_bucket
})
return health_status
except Exception as e:
print(f"Health check error: {e}")
return {"status": "error", "message": str(e)}
# Agent management endpoints # Agent management endpoints
@app.get("/conversation/stats") @app.get("/conversation/stats")
async def get_conversation_stats(student_id: str = "student_001"): async def get_conversation_stats(student_id: str = "student_001"):
"""Get conversation statistics""" """Get conversation statistics"""
return container.chat_service.get_agent_stats(student_id) try:
return container.chat_service.get_agent_stats(student_id)
except Exception as e:
print(f"Stats error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/conversation/clear") @app.post("/conversation/clear")
async def clear_conversation(student_id: str = Form("student_001")): async def clear_conversation(student_id: str = Form("student_001")):
"""Clear conversation history""" """Clear conversation history"""
return container.chat_service.clear_conversation(student_id) try:
return container.chat_service.clear_conversation(student_id)
except Exception as e:
print(f"Clear conversation error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/agent/system-prompt") @app.post("/agent/system-prompt")
async def set_system_prompt(request: dict): async def set_system_prompt(request: dict):
"""Update the agent's system prompt""" """Update the agent's system prompt"""
prompt = request.get("prompt", "") try:
if not prompt: prompt = request.get("prompt", "")
raise HTTPException(status_code=400, detail="System prompt cannot be empty") if not prompt:
return container.chat_service.set_system_prompt(prompt) raise HTTPException(status_code=400, detail="System prompt cannot be empty")
return container.chat_service.set_system_prompt(prompt)
except Exception as e:
print(f"Set system prompt error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/agent/system-prompt") @app.get("/agent/system-prompt")
async def get_system_prompt(): async def get_system_prompt():
"""Get the current system prompt""" """Get the current system prompt"""
return { try:
"system_prompt": container.agent_service.system_prompt, return {
"status": "success" "system_prompt": container.agent_service.system_prompt,
} "status": "success"
}
except Exception as e:
print(f"Get system prompt error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/conversation/export") @app.get("/conversation/export")
async def export_conversation(student_id: str = "student_001"): async def export_conversation(student_id: str = "student_001"):
"""Export conversation history""" """Export conversation history"""
history = container.agent_service.export_conversation(student_id) try:
return { history = container.agent_service.export_conversation(student_id)
"student_id": student_id, return {
"messages": history, "student_id": student_id,
"total_messages": len(history) "messages": history,
} "total_messages": len(history)
}
except Exception as e:
print(f"Export conversation error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/conversation/import") @app.post("/conversation/import")
async def import_conversation(request: dict): async def import_conversation(request: dict):
"""Import conversation history""" """Import conversation history"""
student_id = request.get("student_id", "student_001") try:
messages = request.get("messages", []) student_id = request.get("student_id", "student_001")
messages = request.get("messages", [])
if not messages:
raise HTTPException(status_code=400, detail="Messages list cannot be empty") if not messages:
raise HTTPException(status_code=400, detail="Messages list cannot be empty")
container.agent_service.import_conversation(messages, student_id)
return { container.agent_service.import_conversation(messages, student_id)
"status": "success", return {
"message": f"Imported {len(messages)} messages to conversation {student_id}" "status": "success",
} "message": f"Imported {len(messages)} messages to conversation {student_id}"
}
except Exception as e:
print(f"Import conversation error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/debug/test-response")
async def debug_test_response():
"""Debug endpoint to test response generation"""
try:
# Test basic response
test_text = "This is a test response"
encoded_text = base64.b64encode(test_text.encode('utf-8')).decode('utf-8')
return Response(
content=b"test audio data",
media_type="audio/mpeg",
headers={
"X-Response-Text": encoded_text,
"Access-Control-Expose-Headers": "X-Response-Text"
}
)
except Exception as e:
print(f"Debug test error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/") @app.get("/")
async def root(): async def root():
...@@ -155,21 +314,22 @@ def create_app() -> FastAPI: ...@@ -155,21 +314,22 @@ def create_app() -> FastAPI:
"service": "Unified Chat API with Local Agent", "service": "Unified Chat API with Local Agent",
"version": "2.1.0", "version": "2.1.0",
"description": "Unified backend for audio/text chat with a local AI agent.", "description": "Unified backend for audio/text chat with a local AI agent.",
"status": "running",
"deployment": "CapRover",
"features": [ "features": [
"Local AI agent responses using OpenAI GPT", "Local AI agent responses using OpenAI GPT",
"Audio transcription using OpenAI Whisper", "Audio transcription using OpenAI Whisper",
"Text-to-speech using OpenAI TTS", "Text-to-speech using OpenAI TTS",
"Conversation history management", "Conversation history management",
"Student-specific conversations" "Student-specific conversations",
"CORS enabled for cross-origin requests"
], ],
"endpoints": { "endpoints": {
"chat": "/chat (accepts audio or text with student_id, generates local agent response)", "chat_interface": "/chat-interface (HTML interface)",
"get_audio_response": "/get-audio-response (fetches agent's audio and text response)", "chat": "/chat (accepts audio or text with student_id)",
"conversation_stats": "/conversation/stats (get conversation statistics)", "get_audio_response": "/get-audio-response (fetches agent's audio and text)",
"clear_conversation": "/conversation/clear (clear conversation history)", "health": "/health (service health check)",
"set_system_prompt": "/agent/system-prompt (update agent system prompt)", "debug": "/debug/test-response (test response generation)"
"export_conversation": "/conversation/export (export conversation history)",
"health": "/health (service health check)"
} }
} }
...@@ -177,3 +337,12 @@ def create_app() -> FastAPI: ...@@ -177,3 +337,12 @@ def create_app() -> FastAPI:
# Application entry point # Application entry point
app = create_app() app = create_app()
if __name__ == "__main__":
# For development
uvicorn.run(
"main:app",
host="0.0.0.0",
port=int(os.environ.get("PORT", 8000)),
reload=True
)
\ No newline at end of file
...@@ -15,29 +15,29 @@ from services.pedagogy_service import PedagogyService ...@@ -15,29 +15,29 @@ from services.pedagogy_service import PedagogyService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SYSTEM_PROMPTS: Dict[StudentNationality, str] = { SYSTEM_PROMPTS: Dict[StudentNationality, str] = {
StudentNationality.EGYPTIAN: """ StudentNationality.EGYPTIAN: """
إنت مُدرّس لطفل في ابتدائي اسمه {student_name}. إنت مُدرّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}.
رد باللهجة المصريّة الطبيعيّة كأنك بتكلم {student_name} قصادك. رَدّ باللهجة المصريّة الطبيعيّة كأنّك بتكَلِّم {student_name} قصادك.
خلي الكلام بسيط، واضح، وقريب من ودنه. خَلّي الكلام بسيط، واضح، وقَريب من وُدنه.
الجمل قصيرة ومترابطة، مش مقطّعة. الجُمَل قُصَيَّرة ومُترابطة، مِش مَقطَّعة.
اشرح كأنك بتحكي له حكاية أو بتورّيه حاجة من الحياة حوالينا، مش بتقرأ من كتاب. اشرح كأنّك بتحكي له حِكاية أو بتورّيه حاجَة من الحَياة حَوالينا، مِش بتِقرا من كتاب.
مُمكِن تِذكُر اسم {student_name} مَرّة واحِدة في أوِّل الرّد فَقَط.
ممكن تذكر اسم {student_name} مرّة واحدة في أول الرد فقط. بعد كِدا مَمنوع تِكرار الاسم في نَفس الرّد، حَتّى في الأسئِلة الخِتاميّة.
بعد كده ممنوع تكرار الاسم في نفس الرد، حتى في الأسئلة الختامية. مَمنوع تِستَعمِل أي ألقاب زي "يا بَطَل" أو "يا شاطِر"، الاسم الأوَّل بَس.
ممنوع تستعمل أي ألقاب زي "يا بطل" أو "يا شاطر"، الاسم الأول بس. ولو الرّد قُصَيَّر جِدًّا (جُملة أو اتنِين)، مُمكِن تِستَغنَى عن ذِكر الاسم خالِص.
ولو الرد قصير جدًا (جملة أو اتنين)، ممكن تستغنى عن ذكر الاسم خالص. لو فيه مُصطَلَح صَعب، فَسَّره بِكِلمة أسهَل.
لو فيه رَمز كيمياوي زي H2O أو CO2، اكتُبه زي ما هو.
لو فيه مصطلح صعب، فسّره بكلمة أسهل. الأرقام العادِيّة اكتُبها بالحُروف العربي زي "اِتنِين" أو "تَلاتة".
لو فيه رمز كيميائي زي H2O أو CO2، اكتبه زي ما هو. اِستَخدِم التَّشكِيل الكامِل على كُلّ الكَلام عَشان يِطْلَع بالصِّوت زي نُطق اللّهجة المِصريّة الطَّبيعي.
الأرقام العادية اكتبها بالحروف العربي زي "اتنين" أو "تلاتة". لو {student_name} مكتوب بالإنجليزي، اكتُبه دايمًا بالعَربي في رُدودك.
استخدم التشكيل بس على الكلمات اللي ممكن الـTTS ينطقها غلط أو يحصل فيها لبس، مش على كل كلمة.
لو {student_name} مكتوب بالإنجليزي، اكتبه دايمًا بالعربي في ردودك. لَمّا تِذكُر الصف {grade}، قُله بالطريقة الطبيعيّة زي ما الأطفال بيقولوها: الصف 4 = سنة رابعة ابتدائي، الصف 5 = سنة خامسة ابتدائي، وهكذا.
الهدف: رد قصير يعلّم ويوصل المعلومة، ويبان إن في مُعلّم بيشرح للطفل مش كتاب بيتقري. الهَدف: رَد قُصَيَّر يِعلِّم ويُوصَّل المَعلومة، ويِبان إن فيه مُعلِّم بيِشرَح للطفل مِش كتاب بيتقري.
""", """,
StudentNationality.SAUDI: """ StudentNationality.SAUDI: """
إنت معلّم لطفل في ابتدائي اسمه {student_name}. إنت معلّم لطفل في ابتدائي اسمه {student_name} في الصف {grade}.
رد باللهجة السعوديّة الطبيعيّة، كأنك تشرح له قدّامك. رَد باللهجة السعوديّة الطبيعيّة، كأنك تشرح له قدّامك.
خل الشرح واضح وسهل، لكن لا يكون ناشف. خل الشرح واضح وسهل، لكن لا يكون ناشف.
اشرح كأنك تسولف معه وتشبّه بأشياء من حياته اليومية. اشرح كأنك تسولف معه وتشبّه بأشياء من حياته اليومية.
...@@ -52,17 +52,15 @@ SYSTEM_PROMPTS: Dict[StudentNationality, str] = { ...@@ -52,17 +52,15 @@ SYSTEM_PROMPTS: Dict[StudentNationality, str] = {
استخدم التشكيل بس على الكلمات اللي ممكن الـTTS يخبّص فيها أو يقرأها خطأ، واترك الباقي بدون تشكيل عشان يطلع طبيعي. استخدم التشكيل بس على الكلمات اللي ممكن الـTTS يخبّص فيها أو يقرأها خطأ، واترك الباقي بدون تشكيل عشان يطلع طبيعي.
لو {student_name} مكتوب بالإنجليزي، اكتبه دايمًا بالعربي في ردودك. لو {student_name} مكتوب بالإنجليزي، اكتبه دايمًا بالعربي في ردودك.
الهدف: رد مبسّط، قريب، ويبيّن إن المعلّم يشرح للطفل، مو يقرأ من كتاب. لما تذكر الصف {grade}، قُلها بالطريقة اللي الطفل متعود يسمعها: الصف 4 = رابع ابتدائي، الصف 5 = خامس ابتدائي، وهكذا.
الهدف: رد مبسّط، قريب، ويبيّن إن المعلّم يشرح للطفل، مو يقرأ من كتاب.
""" """
} }
class AgentService: class AgentService:
"""Service class for handling AI agent conversations using database memory""" """Service class for handling AI agent conversations using database memory"""
...@@ -163,7 +161,10 @@ class AgentService: ...@@ -163,7 +161,10 @@ class AgentService:
# Create subject-specific system prompt with first name only # Create subject-specific system prompt with first name only
base_system_prompt = SYSTEM_PROMPTS.get(nationality, SYSTEM_PROMPTS[StudentNationality.EGYPTIAN]) base_system_prompt = SYSTEM_PROMPTS.get(nationality, SYSTEM_PROMPTS[StudentNationality.EGYPTIAN])
formatted_base_prompt = base_system_prompt.format(student_name=student_name) formatted_base_prompt = base_system_prompt.format(
student_name=student_name,
grade=student_info['grade']
)
subject_specific_prompt = f"{formatted_base_prompt}\n\nإنت بتدرّس مادة {subject} للطفل {student_name}." subject_specific_prompt = f"{formatted_base_prompt}\n\nإنت بتدرّس مادة {subject} للطفل {student_name}."
# Add Socratic questioning instructions if applicable (grades 4-6) # Add Socratic questioning instructions if applicable (grades 4-6)
...@@ -258,7 +259,6 @@ class AgentService: ...@@ -258,7 +259,6 @@ class AgentService:
logger.error(f"Error generating AI response: {e}") logger.error(f"Error generating AI response: {e}")
raise HTTPException(status_code=500, detail=f"AI response generation failed: {str(e)}") raise HTTPException(status_code=500, detail=f"AI response generation failed: {str(e)}")
def search_similar(self, query_embedding: List[float], student_id: str, def search_similar(self, query_embedding: List[float], student_id: str,
subject: str = "chemistry", top_k: int = 3): subject: str = "chemistry", top_k: int = 3):
"""Search similar content with student-specific filtering""" """Search similar content with student-specific filtering"""
...@@ -300,7 +300,11 @@ class AgentService: ...@@ -300,7 +300,11 @@ class AgentService:
nationality = nationality_mapping.get(nationality_lower, StudentNationality.EGYPTIAN) nationality = nationality_mapping.get(nationality_lower, StudentNationality.EGYPTIAN)
base_system_prompt = SYSTEM_PROMPTS.get(nationality, SYSTEM_PROMPTS[StudentNationality.EGYPTIAN]) base_system_prompt = SYSTEM_PROMPTS.get(nationality, SYSTEM_PROMPTS[StudentNationality.EGYPTIAN])
# Format the prompt with student name # Format the prompt with student name
formatted_base_prompt = base_system_prompt.format(student_name=student_name) formatted_base_prompt = base_system_prompt.format(
student_name=student_name,
grade=student_info['grade']
)
subject_specific_prompt = f"{formatted_base_prompt}\n\nإنت بتدرّس مادة {subject} للطفل {student_name}." subject_specific_prompt = f"{formatted_base_prompt}\n\nإنت بتدرّس مادة {subject} للطفل {student_name}."
# Add Socratic questioning instructions if applicable # Add Socratic questioning instructions if applicable
...@@ -321,12 +325,81 @@ class AgentService: ...@@ -321,12 +325,81 @@ class AgentService:
logger.error(f"Error updating subject context: {e}") logger.error(f"Error updating subject context: {e}")
return False return False
def export_conversation(self, student_id: str) -> List[Dict[str, str]]:
"""Export conversation history for a student"""
return self.get_conversation_history(student_id)
def import_conversation(self, messages: List[Dict[str, str]], student_id: str):
"""Import conversation history for a student"""
# Clear existing history first
self.db_service.clear_history(student_id)
# Import messages in order
for message in messages:
role = message.get("role", "user")
content = message.get("content", "")
if content:
self.add_message_to_history(student_id, content, role)
def clear_conversation(self, student_id: str) -> Dict[str, str]:
"""Clear conversation history for a student"""
try:
self.db_service.clear_history(student_id)
return {
"status": "success",
"message": f"Conversation cleared for student {student_id}"
}
except Exception as e:
logger.error(f"Error clearing conversation: {e}")
return {
"status": "error",
"message": f"Failed to clear conversation: {str(e)}"
}
def get_agent_stats(self, student_id: str) -> Dict:
"""Get conversation statistics for a student"""
try:
history = self.get_conversation_history(student_id)
user_messages = [msg for msg in history if msg['role'] == 'user']
assistant_messages = [msg for msg in history if msg['role'] == 'assistant']
system_messages = [msg for msg in history if msg['role'] == 'system']
return {
"student_id": student_id,
"total_messages": len(history),
"user_messages": len(user_messages),
"assistant_messages": len(assistant_messages),
"system_messages": len(system_messages),
"conversation_active": len(history) > 0
}
except Exception as e:
logger.error(f"Error getting agent stats: {e}")
return {
"student_id": student_id,
"error": str(e)
}
def set_system_prompt(self, prompt: str) -> Dict[str, str]:
"""Set a new system prompt (this is a placeholder - actual implementation would depend on requirements)"""
# This method would need to be implemented based on your specific requirements
# for how system prompts should be managed globally vs per student
return {
"status": "success",
"message": "System prompt updated"
}
@property
def system_prompt(self) -> str:
"""Get the current system prompt"""
# Return a default system prompt - this could be made more sophisticated
return "Default system prompt for educational AI assistant"
def close(self): def close(self):
"""Close database connections""" """Close database connection pools"""
if self.db_service: if self.db_service:
self.db_service.close() self.db_service.close_pool()
if self.pgvector: if self.pgvector:
self.pgvector.close() self.pgvector.close_pool()
# ----------------- Test ----------------- # ----------------- Test -----------------
......
import os import os
import psycopg2 import psycopg2
from psycopg2.extras import RealDictCursor from psycopg2.extras import RealDictCursor
from psycopg2.pool import ThreadedConnectionPool
from typing import List, Dict, Optional, Tuple from typing import List, Dict, Optional, Tuple
import logging import logging
...@@ -8,10 +9,12 @@ logger = logging.getLogger(__name__) ...@@ -8,10 +9,12 @@ logger = logging.getLogger(__name__)
class ChatDatabaseService: class ChatDatabaseService:
"""Simple service for managing chat history in PostgreSQL""" """Simple service for managing chat history in PostgreSQL with connection pooling"""
def __init__(self): def __init__(self):
self.conn = psycopg2.connect( self.pool = ThreadedConnectionPool(
minconn=1,
maxconn=20,
host=os.getenv("POSTGRES_HOST", "postgres"), host=os.getenv("POSTGRES_HOST", "postgres"),
user=os.getenv("POSTGRES_USER"), user=os.getenv("POSTGRES_USER"),
password=os.getenv("POSTGRES_PASSWORD"), password=os.getenv("POSTGRES_PASSWORD"),
...@@ -20,104 +23,132 @@ class ChatDatabaseService: ...@@ -20,104 +23,132 @@ class ChatDatabaseService:
def get_student_nationality(self, student_id: str) -> Optional[str]: def get_student_nationality(self, student_id: str) -> Optional[str]:
"""Get student nationality from database""" """Get student nationality from database"""
with self.conn.cursor(cursor_factory=RealDictCursor) as cur: conn = self.pool.getconn()
cur.execute( try:
"SELECT nationality FROM students WHERE student_id = %s", with conn.cursor(cursor_factory=RealDictCursor) as cur:
(student_id,) cur.execute(
) "SELECT nationality FROM students WHERE student_id = %s",
result = cur.fetchone() (student_id,)
return result["nationality"] if result else None )
result = cur.fetchone()
return result["nationality"] if result else None
finally:
self.pool.putconn(conn)
def get_student_info(self, student_id: str) -> Optional[Dict]: def get_student_info(self, student_id: str) -> Optional[Dict]:
"""Get complete student information from database""" """Get complete student information from database"""
with self.conn.cursor(cursor_factory=RealDictCursor) as cur: conn = self.pool.getconn()
cur.execute( try:
""" with conn.cursor(cursor_factory=RealDictCursor) as cur:
SELECT student_id, student_name, grade, language, nationality cur.execute(
FROM students """
WHERE student_id = %s SELECT student_id, student_name, grade, language, nationality
""", FROM students
(student_id,) WHERE student_id = %s
) """,
result = cur.fetchone() (student_id,)
if result: )
return { result = cur.fetchone()
'student_id': result['student_id'], if result:
'student_name': result['student_name'], return {
'grade': result['grade'], # This is now an integer 'student_id': result['student_id'],
'is_arabic': result['language'], # Convert language boolean to is_arabic 'student_name': result['student_name'],
'nationality': result['nationality'] 'grade': result['grade'], # This is now an integer
} 'is_arabic': result['language'], # Convert language boolean to is_arabic
return None 'nationality': result['nationality']
}
return None
finally:
self.pool.putconn(conn)
def get_student_grade_and_language(self, student_id: str) -> Optional[Tuple[int, bool]]: def get_student_grade_and_language(self, student_id: str) -> Optional[Tuple[int, bool]]:
"""Get student grade and language preference""" """Get student grade and language preference"""
with self.conn.cursor(cursor_factory=RealDictCursor) as cur: conn = self.pool.getconn()
cur.execute( try:
"SELECT grade, language FROM students WHERE student_id = %s", with conn.cursor(cursor_factory=RealDictCursor) as cur:
(student_id,) cur.execute(
) "SELECT grade, language FROM students WHERE student_id = %s",
result = cur.fetchone() (student_id,)
if result: )
return (result["grade"], result["language"]) result = cur.fetchone()
return None if result:
return (result["grade"], result["language"])
return None
finally:
self.pool.putconn(conn)
def get_chat_history(self, student_id: str, limit: int = 20) -> List[Dict[str, str]]: def get_chat_history(self, student_id: str, limit: int = 20) -> List[Dict[str, str]]:
"""Get chat history for a student, returns in chronological order""" """Get chat history for a student, returns in chronological order"""
with self.conn.cursor(cursor_factory=RealDictCursor) as cur: conn = self.pool.getconn()
cur.execute( try:
""" with conn.cursor(cursor_factory=RealDictCursor) as cur:
SELECT role, content cur.execute(
FROM chat_history """
WHERE student_id = %s SELECT role, content
ORDER BY created_at DESC FROM chat_history
LIMIT %s; WHERE student_id = %s
""", ORDER BY created_at DESC
(student_id, limit) LIMIT %s;
) """,
results = cur.fetchall() (student_id, limit)
# Return in chronological order (oldest first) )
return [{"role": row["role"], "content": row["content"]} for row in reversed(results)] results = cur.fetchall()
# Return in chronological order (oldest first)
return [{"role": row["role"], "content": row["content"]} for row in reversed(results)]
finally:
self.pool.putconn(conn)
def add_message(self, student_id: str, role: str, content: str): def add_message(self, student_id: str, role: str, content: str):
"""Add a message to chat history""" """Add a message to chat history"""
with self.conn.cursor() as cur: conn = self.pool.getconn()
cur.execute( try:
""" with conn.cursor() as cur:
INSERT INTO chat_history (student_id, role, content) cur.execute(
VALUES (%s, %s, %s); """
""", INSERT INTO chat_history (student_id, role, content)
(student_id, role, content) VALUES (%s, %s, %s);
) """,
self.conn.commit() (student_id, role, content)
)
conn.commit()
finally:
self.pool.putconn(conn)
def clear_history(self, student_id: str): def clear_history(self, student_id: str):
"""Clear chat history for a student""" """Clear chat history for a student"""
with self.conn.cursor() as cur: conn = self.pool.getconn()
cur.execute( try:
"DELETE FROM chat_history WHERE student_id = %s", with conn.cursor() as cur:
(student_id,) cur.execute(
) "DELETE FROM chat_history WHERE student_id = %s",
self.conn.commit() (student_id,)
)
conn.commit()
finally:
self.pool.putconn(conn)
def limit_history(self, student_id: str, max_messages: int = 40): def limit_history(self, student_id: str, max_messages: int = 40):
"""Keep only recent messages for a student""" """Keep only recent messages for a student"""
with self.conn.cursor() as cur: conn = self.pool.getconn()
cur.execute( try:
""" with conn.cursor() as cur:
DELETE FROM chat_history cur.execute(
WHERE student_id = %s """
AND role != 'system' DELETE FROM chat_history
AND id NOT IN ( WHERE student_id = %s
SELECT id FROM chat_history AND role != 'system'
WHERE student_id = %s AND role != 'system' AND id NOT IN (
ORDER BY created_at DESC SELECT id FROM chat_history
LIMIT %s WHERE student_id = %s AND role != 'system'
); ORDER BY created_at DESC
""", LIMIT %s
(student_id, student_id, max_messages) );
) """,
self.conn.commit() (student_id, student_id, max_messages)
)
conn.commit()
finally:
self.pool.putconn(conn)
def update_student_info(self, student_id: str, grade: Optional[int] = None, def update_student_info(self, student_id: str, grade: Optional[int] = None,
language: Optional[bool] = None, nationality: Optional[str] = None): language: Optional[bool] = None, nationality: Optional[str] = None):
...@@ -139,31 +170,39 @@ class ChatDatabaseService: ...@@ -139,31 +170,39 @@ class ChatDatabaseService:
if updates: if updates:
params.append(student_id) params.append(student_id)
with self.conn.cursor() as cur: conn = self.pool.getconn()
cur.execute( try:
f""" with conn.cursor() as cur:
UPDATE students cur.execute(
SET {', '.join(updates)} f"""
WHERE student_id = %s UPDATE students
""", SET {', '.join(updates)}
params WHERE student_id = %s
) """,
self.conn.commit() params
)
conn.commit()
finally:
self.pool.putconn(conn)
def create_student(self, student_id: str, student_name: str, grade: int, def create_student(self, student_id: str, student_name: str, grade: int,
language: bool, nationality: str = 'EGYPTIAN'): language: bool, nationality: str = 'EGYPTIAN'):
"""Create a new student record""" """Create a new student record"""
with self.conn.cursor() as cur: conn = self.pool.getconn()
cur.execute( try:
""" with conn.cursor() as cur:
INSERT INTO students (student_id, student_name, grade, language, nationality) cur.execute(
VALUES (%s, %s, %s, %s, %s) """
ON CONFLICT (student_id) DO NOTHING; INSERT INTO students (student_id, student_name, grade, language, nationality)
""", VALUES (%s, %s, %s, %s, %s)
(student_id, student_name, grade, language, nationality) ON CONFLICT (student_id) DO NOTHING;
) """,
self.conn.commit() (student_id, student_name, grade, language, nationality)
)
conn.commit()
finally:
self.pool.putconn(conn)
def close(self): def close_pool(self):
if self.conn: if self.pool:
self.conn.close() self.pool.closeall()
\ No newline at end of file \ No newline at end of file
import os import os
import psycopg2 import psycopg2
from psycopg2.extras import RealDictCursor from psycopg2.extras import RealDictCursor
from psycopg2.pool import ThreadedConnectionPool
from typing import List, Optional from typing import List, Optional
# Import the pgvector adapter # Import the pgvector adapter
from pgvector.psycopg2 import register_vector from pgvector.psycopg2 import register_vector
class PGVectorService: class PGVectorService:
"""Service for managing embeddings with PostgreSQL pgvector""" """Service for managing embeddings with PostgreSQL pgvector using connection pooling"""
def __init__(self): def __init__(self):
self.conn = psycopg2.connect( self.pool = ThreadedConnectionPool(
minconn=1,
maxconn=20,
host=os.getenv("POSTGRES_HOST", "postgres"), host=os.getenv("POSTGRES_HOST", "postgres"),
user=os.getenv("POSTGRES_USER"), user=os.getenv("POSTGRES_USER"),
password=os.getenv("POSTGRES_PASSWORD"), password=os.getenv("POSTGRES_PASSWORD"),
dbname=os.getenv("POSTGRES_DB"), dbname=os.getenv("POSTGRES_DB"),
) )
# Register the vector type with the connection # Test connection and register vector type to ensure the pool works
register_vector(self.conn) conn = self.pool.getconn()
try:
register_vector(conn)
finally:
self.pool.putconn(conn)
def _get_conn_with_vector(self):
"""Get a connection from the pool and register vector type"""
conn = self.pool.getconn()
register_vector(conn)
return conn
def insert_embedding(self, id: int, embedding: list): def insert_embedding(self, id: int, embedding: list):
"""Insert or update an embedding""" """Insert or update an embedding"""
with self.conn.cursor() as cur: conn = self._get_conn_with_vector()
cur.execute( try:
""" with conn.cursor() as cur:
INSERT INTO embeddings_table (id, embedding) cur.execute(
VALUES (%s, %s) """
ON CONFLICT (id) DO UPDATE SET embedding = EXCLUDED.embedding; INSERT INTO embeddings_table (id, embedding)
""", VALUES (%s, %s)
(id, embedding), ON CONFLICT (id) DO UPDATE SET embedding = EXCLUDED.embedding;
) """,
self.conn.commit() (id, embedding),
)
conn.commit()
finally:
self.pool.putconn(conn)
def search_nearest(self, query_embedding: list, limit: int = 3): def search_nearest(self, query_embedding: list, limit: int = 3):
"""Search nearest embeddings using cosine distance (<-> operator)""" """Search nearest embeddings using cosine distance (<-> operator)"""
with self.conn.cursor(cursor_factory=RealDictCursor) as cur: conn = self._get_conn_with_vector()
cur.execute( try:
""" with conn.cursor(cursor_factory=RealDictCursor) as cur:
SELECT id, embedding, embedding <-> %s AS distance cur.execute(
FROM embeddings_table """
ORDER BY embedding <-> %s SELECT id, embedding, embedding <-> %s AS distance
LIMIT %s; FROM embeddings_table
""", ORDER BY embedding <-> %s
(query_embedding, query_embedding, limit), LIMIT %s;
) """,
return cur.fetchall() (query_embedding, query_embedding, limit),
)
return cur.fetchall()
finally:
self.pool.putconn(conn)
def search_filtered_nearest( def search_filtered_nearest(
self, self,
...@@ -55,21 +76,25 @@ class PGVectorService: ...@@ -55,21 +76,25 @@ class PGVectorService:
limit: int = 3 limit: int = 3
): ):
"""Search nearest embeddings with filtering by grade, subject, and language""" """Search nearest embeddings with filtering by grade, subject, and language"""
with self.conn.cursor(cursor_factory=RealDictCursor) as cur: conn = self._get_conn_with_vector()
cur.execute( try:
""" with conn.cursor(cursor_factory=RealDictCursor) as cur:
SELECT id, grade, subject, unit, concept, lesson, chunk_text, cur.execute(
is_arabic, embedding <-> %s::vector AS distance """
FROM educational_chunks SELECT id, grade, subject, unit, concept, lesson, chunk_text,
WHERE grade = %s is_arabic, embedding <-> %s::vector AS distance
AND subject ILIKE %s FROM educational_chunks
AND is_arabic = %s WHERE grade = %s
ORDER BY embedding <-> %s::vector AND subject ILIKE %s
LIMIT %s; AND is_arabic = %s
""", ORDER BY embedding <-> %s::vector
(query_embedding, grade, f"%{subject}%", is_arabic, query_embedding, limit), LIMIT %s;
) """,
return cur.fetchall() (query_embedding, grade, f"%{subject}%", is_arabic, query_embedding, limit),
)
return cur.fetchall()
finally:
self.pool.putconn(conn)
def search_flexible_filtered_nearest( def search_flexible_filtered_nearest(
self, self,
...@@ -103,34 +128,42 @@ class PGVectorService: ...@@ -103,34 +128,42 @@ class PGVectorService:
params.append(query_embedding) params.append(query_embedding)
params.append(limit) params.append(limit)
with self.conn.cursor(cursor_factory=RealDictCursor) as cur: conn = self._get_conn_with_vector()
cur.execute( try:
f""" with conn.cursor(cursor_factory=RealDictCursor) as cur:
SELECT id, grade, subject, unit, concept, lesson, chunk_text, cur.execute(
is_arabic, embedding <-> %s::vector AS distance f"""
FROM educational_chunks SELECT id, grade, subject, unit, concept, lesson, chunk_text,
{where_clause} is_arabic, embedding <-> %s::vector AS distance
ORDER BY embedding <-> %s::vector FROM educational_chunks
LIMIT %s; {where_clause}
""", ORDER BY embedding <-> %s::vector
params LIMIT %s;
) """,
return cur.fetchall() params
)
return cur.fetchall()
finally:
self.pool.putconn(conn)
def get_subjects_by_grade_and_language(self, grade: int, is_arabic: bool) -> List[str]: def get_subjects_by_grade_and_language(self, grade: int, is_arabic: bool) -> List[str]:
"""Get available subjects for a specific grade and language""" """Get available subjects for a specific grade and language"""
with self.conn.cursor(cursor_factory=RealDictCursor) as cur: conn = self._get_conn_with_vector()
cur.execute( try:
""" with conn.cursor(cursor_factory=RealDictCursor) as cur:
SELECT DISTINCT subject cur.execute(
FROM educational_chunks """
WHERE grade = %s AND is_arabic = %s SELECT DISTINCT subject
ORDER BY subject; FROM educational_chunks
""", WHERE grade = %s AND is_arabic = %s
(grade, is_arabic) ORDER BY subject;
) """,
return [row['subject'] for row in cur.fetchall()] (grade, is_arabic)
)
return [row['subject'] for row in cur.fetchall()]
finally:
self.pool.putconn(conn)
def close(self): def close_pool(self):
if self.conn: if self.pool:
self.conn.close() self.pool.closeall()
\ No newline at end of file \ No newline at end of file
...@@ -185,10 +185,10 @@ ...@@ -185,10 +185,10 @@
</div> </div>
<script> <script>
// Configuration // Configuration - Auto-detect current domain for CapRover
const Config = { const Config = {
BACKEND_URL: "http://localhost:8000/chat", BACKEND_URL: `${window.location.origin}/chat`,
AUDIO_RESPONSE_URL: "http://localhost:8000/get-audio-response" AUDIO_RESPONSE_URL: `${window.location.origin}/get-audio-response`
}; };
// Enums // Enums
...@@ -206,19 +206,16 @@ ...@@ -206,19 +206,16 @@
PROCESSING: 'processing' PROCESSING: 'processing'
}; };
// Logger utility
class Logger {
static log(message, type = 'info') {
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
// Base64 Decoder utility // Base64 Decoder utility
class TextDecoder { class TextDecoder {
static decodeBase64Utf8(str) { static decodeBase64Utf8(str) {
const bytes = Uint8Array.from(atob(str), c => c.charCodeAt(0)); try {
const decoder = new window.TextDecoder('utf-8'); const bytes = Uint8Array.from(atob(str), c => c.charCodeAt(0));
return decoder.decode(bytes); const decoder = new window.TextDecoder('utf-8');
return decoder.decode(bytes);
} catch (error) {
return str; // Return original string if decode fails
}
} }
} }
...@@ -241,45 +238,68 @@ ...@@ -241,45 +238,68 @@
} }
} }
// API Client // API Client with enhanced error handling
class APIClient { class APIClient {
async sendFormData(url, formData) { async sendFormData(url, formData) {
const response = await fetch(url, { try {
method: 'POST', const response = await fetch(url, {
body: formData method: 'POST',
}); body: formData,
mode: 'cors',
credentials: 'omit'
});
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch {
errorData = { detail: `HTTP ${response.status}: ${response.statusText}` };
}
throw new Error(errorData.detail || `Request failed with status ${response.status}`);
}
if (!response.ok) { const responseData = await response.json();
const errorData = await response.json(); return responseData;
throw new Error(errorData.detail || 'Request failed');
} catch (error) {
throw error;
} }
return response.json();
} }
async fetchAudioResponse() { async fetchAudioResponse() {
const response = await fetch(Config.AUDIO_RESPONSE_URL); try {
const response = await fetch(Config.AUDIO_RESPONSE_URL, {
if (response.ok) { method: 'GET',
const encodedText = response.headers.get('X-Response-Text'); mode: 'cors',
console.log('Encoded text from header:', encodedText); credentials: 'omit'
});
if (response.ok) {
const encodedText = response.headers.get('X-Response-Text');
let agentText = "لا يوجد رد متاح";
if (encodedText) {
try {
agentText = TextDecoder.decodeBase64Utf8(encodedText);
} catch (e) {
agentText = "خطأ في فك تشفير الرد";
}
}
let agentText = "لا يوجد رد متاح"; const audioBlob = await response.blob();
if (encodedText) { return { agentText, audioBlob };
} else {
let errorData;
try { try {
agentText = TextDecoder.decodeBase64Utf8(encodedText); errorData = await response.json();
console.log('Decoded agent text:', agentText); } catch {
} catch (e) { errorData = { detail: `HTTP ${response.status}: ${response.statusText}` };
console.log('Decoding error:', e);
agentText = "خطأ في فك تشفير الرد";
} }
throw new Error(errorData.detail || 'Failed to get response');
} }
} catch (error) {
const audioBlob = await response.blob(); throw error;
return { agentText, audioBlob };
} else {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to get response');
} }
} }
} }
...@@ -329,7 +349,6 @@ ...@@ -329,7 +349,6 @@
async startRecording() { async startRecording() {
try { try {
Logger.log('طلب الوصول إلى الميكروفون...');
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }); this.mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
...@@ -343,7 +362,6 @@ ...@@ -343,7 +362,6 @@
this.mediaRecorder.onstop = () => { this.mediaRecorder.onstop = () => {
const recordedBlob = new Blob(this.audioChunks, { type: 'audio/webm;codecs=opus' }); const recordedBlob = new Blob(this.audioChunks, { type: 'audio/webm;codecs=opus' });
Logger.log(`تم إيقاف التسجيل. الحجم: ${recordedBlob.size} بايت`);
stream.getTracks().forEach(track => track.stop()); stream.getTracks().forEach(track => track.stop());
this.onRecordingComplete(recordedBlob); this.onRecordingComplete(recordedBlob);
}; };
...@@ -353,7 +371,6 @@ ...@@ -353,7 +371,6 @@
this.uiManager.showStatus('التسجيل قيد التقدم...', StatusType.RECORDING); this.uiManager.showStatus('التسجيل قيد التقدم...', StatusType.RECORDING);
} catch (error) { } catch (error) {
Logger.log(`Error starting recording: ${error.message}`, 'error');
this.uiManager.showStatus(`خطأ في الوصول إلى الميكروفون: ${error.message}`, StatusType.ERROR); this.uiManager.showStatus(`خطأ في الوصول إلى الميكروفون: ${error.message}`, StatusType.ERROR);
this.stateMachine.setState(RecordingState.IDLE); this.stateMachine.setState(RecordingState.IDLE);
} }
...@@ -478,15 +495,19 @@ ...@@ -478,15 +495,19 @@
// Auto-play audio if available // Auto-play audio if available
if (audioUrl) { if (audioUrl) {
const audioPlayer = this.uiManager.chatContainer.lastChild.querySelector('audio'); setTimeout(() => {
if (audioPlayer) { const audioPlayer = this.uiManager.chatContainer.lastChild.querySelector('audio');
audioPlayer.play(); if (audioPlayer) {
} audioPlayer.play().catch(e => {
// Silent fail for auto-play
});
}
}, 100);
} }
} }
} }
// Chat Service - Simplified for direct response handling // Chat Service - Enhanced with better error handling
class ChatService { class ChatService {
constructor(apiClient, messageManager, uiManager) { constructor(apiClient, messageManager, uiManager) {
this.apiClient = apiClient; this.apiClient = apiClient;
...@@ -512,17 +533,16 @@ ...@@ -512,17 +533,16 @@
const formData = new FormData(); const formData = new FormData();
formData.append('text', text); formData.append('text', text);
formData.append('student_id', studentId); formData.append('student_id', studentId);
const response = await this.apiClient.sendFormData(Config.BACKEND_URL, formData); const response = await this.apiClient.sendFormData(Config.BACKEND_URL, formData);
if (response.status === 'success') { if (response.status === 'success') {
// Response is ready immediately, get it
await this.getAgentResponse(); await this.getAgentResponse();
return true; return true;
} else { } else {
throw new Error(response.message || 'Unknown error'); throw new Error(response.message || 'Unknown error');
} }
} catch (error) { } catch (error) {
Logger.log(`Error sending text chat: ${error.message}`, 'error');
this.uiManager.showStatus(`خطأ: ${error.message}`, StatusType.ERROR); this.uiManager.showStatus(`خطأ: ${error.message}`, StatusType.ERROR);
return false; return false;
} }
...@@ -538,17 +558,16 @@ ...@@ -538,17 +558,16 @@
const formData = new FormData(); const formData = new FormData();
formData.append('file', audioBlob, `voice_message_${Date.now()}.webm`); formData.append('file', audioBlob, `voice_message_${Date.now()}.webm`);
formData.append('student_id', studentId); formData.append('student_id', studentId);
const response = await this.apiClient.sendFormData(Config.BACKEND_URL, formData); const response = await this.apiClient.sendFormData(Config.BACKEND_URL, formData);
if (response.status === 'success') { if (response.status === 'success') {
// Response is ready immediately, get it
await this.getAgentResponse(); await this.getAgentResponse();
return true; return true;
} else { } else {
throw new Error(response.message || 'Unknown error'); throw new Error(response.message || 'Unknown error');
} }
} catch (error) { } catch (error) {
Logger.log(`Error sending voice chat: ${error.message}`, 'error');
this.uiManager.showStatus(`خطأ: ${error.message}`, StatusType.ERROR); this.uiManager.showStatus(`خطأ: ${error.message}`, StatusType.ERROR);
return false; return false;
} }
...@@ -557,22 +576,28 @@ ...@@ -557,22 +576,28 @@
async getAgentResponse() { async getAgentResponse() {
try { try {
this.uiManager.showStatus('جاري جلب رد المساعد...', StatusType.PROCESSING); this.uiManager.showStatus('جاري جلب رد المساعد...', StatusType.PROCESSING);
const { agentText, audioBlob } = await this.apiClient.fetchAudioResponse(); const { agentText, audioBlob } = await this.apiClient.fetchAudioResponse();
if (!agentText || agentText === "لا يوجد رد متاح") {
throw new Error('لم يتم استلام رد صالح من المساعد');
}
const audioUrl = URL.createObjectURL(audioBlob); const audioUrl = URL.createObjectURL(audioBlob);
this.messageManager.addAgentMessage(agentText, audioUrl); this.messageManager.addAgentMessage(agentText, audioUrl);
Logger.log('✓ Agent response received and played.', 'success');
this.uiManager.showStatus('✓ تم استلام الرد! جاهز للرسالة التالية.', StatusType.SUCCESS); this.uiManager.showStatus('✓ تم استلام الرد! جاهز للرسالة التالية.', StatusType.SUCCESS);
} catch (error) { } catch (error) {
Logger.log(`Error fetching agent response: ${error.message}`, 'error');
this.uiManager.showStatus(`خطأ في الشبكة: ${error.message}`, StatusType.ERROR); this.uiManager.showStatus(`خطأ في الشبكة: ${error.message}`, StatusType.ERROR);
// Add fallback text message
this.messageManager.addAgentMessage('عذراً، حدث خطأ في استلام الرد الصوتي. يرجى المحاولة مرة أخرى.');
} }
} }
} }
// Mediator Pattern - Simplified // Mediator Pattern - Enhanced
class ChatMediator { class ChatMediator {
constructor() { constructor() {
this.apiClient = new APIClient(); this.apiClient = new APIClient();
...@@ -612,8 +637,7 @@ ...@@ -612,8 +637,7 @@
if (!text) return; if (!text) return;
this.uiManager.clearTextInput(); this.uiManager.clearTextInput();
const success = await this.chatService.sendTextMessage(text, studentId); await this.chatService.sendTextMessage(text, studentId);
// UI state is handled within the chat service
} }
async handleStartRecording() { async handleStartRecording() {
...@@ -650,8 +674,12 @@ ...@@ -650,8 +674,12 @@
// Initialize application when DOM is ready // Initialize application when DOM is ready
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
UnifiedChatApp.initialize(); try {
console.log('Chat application with Student ID support initialized successfully!'); UnifiedChatApp.initialize();
console.log('Chat application with Student ID support initialized successfully!');
} catch (error) {
console.error('Failed to initialize chat application:', error);
}
}); });
</script> </script>
</body> </body>
......
#!/bin/bash #!/bin/bash
set -e set -e
host="postgres" # Use the environment variables set in CapRover for the host and user
host="${POSTGRES_HOST}"
user="${POSTGRES_USER}" user="${POSTGRES_USER}"
db="${POSTGRES_DB}"
password="${POSTGRES_PASSWORD}"
echo "Waiting for PostgreSQL database to be ready..." echo "Waiting for PostgreSQL database at $host to be ready..."
# Wait for the database server to be available # Wait for the database server to be available. This is sufficient.
until PGPASSWORD="$password" pg_isready -h "$host" -U "$user"; do until PGPASSWORD="${POSTGRES_PASSWORD}" pg_isready -h "$host" -U "$user"; do
echo "PostgreSQL server is unavailable - sleeping" echo "PostgreSQL server is unavailable - sleeping"
sleep 1 sleep 1
done done
# Wait for the specific database to be available
until PGPASSWORD="$password" psql -h "$host" -U "$user" -d "$db" -c '\q'; do
echo "Database '$db' is not yet available - sleeping"
sleep 1
done
echo "PostgreSQL database is up and ready - executing command" echo "PostgreSQL database is up and ready - executing command"
exec "$@" exec "$@"
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment