Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
A
AI Tutor
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Salma Mohammed Hamed
AI Tutor
Commits
12111380
Commit
12111380
authored
Sep 19, 2025
by
SalmaMohammedHamedMustafa
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
better db connectin hadeling for long lasting deplyment
parent
a4501a9c
Changes
10
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
685 additions
and
341 deletions
+685
-341
captain-definition
self_hosted_env/voice_agent/captain-definition
+9
-0
enums.py
self_hosted_env/voice_agent/core/enums.py
+1
-1
main.py
self_hosted_env/voice_agent/main.py
+233
-64
postgres_app.tar
self_hosted_env/voice_agent/postgres_app.tar
+0
-0
agent_service.py
self_hosted_env/voice_agent/services/agent_service.py
+104
-31
chat_database_service.py
..._hosted_env/voice_agent/services/chat_database_service.py
+144
-105
pgvector_service.py
self_hosted_env/voice_agent/services/pgvector_service.py
+100
-67
audio-recorder.html
self_hosted_env/voice_agent/static/audio-recorder.html
+88
-60
voice_agent.tar
self_hosted_env/voice_agent/voice_agent.tar
+0
-0
wait-for-postgres.sh
self_hosted_env/voice_agent/wait-for-postgres.sh
+6
-13
No files found.
self_hosted_env/voice_agent/captain-definition
0 → 100644
View file @
12111380
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile",
"containerHttpPort": "8000",
"env": {
"POSTGRES_HOST": "srv-captain--postgres",
"MINIO_HOST": "srv-captain--minio"
}
}
self_hosted_env/voice_agent/core/enums.py
View file @
12111380
...
@@ -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"
self_hosted_env/voice_agent/main.py
View file @
12111380
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://voice-agent.caprover.al-arcade.com"
,
"http://localhost:8000"
,
# For local development
"http://localhost:8000"
,
# For local development
"http://127.0.0.1:8000"
,
"*"
# Allow all origins for testing - remove in production
],
],
allow_credentials
=
True
,
allow_credentials
=
True
,
allow_methods
=
[
"*"
],
allow_methods
=
[
"GET"
,
"POST"
,
"PUT"
,
"DELETE"
,
"OPTIONS"
],
allow_headers
=
[
"*"
],
allow_headers
=
[
"Accept"
,
"Accept-Language"
,
"Content-Language"
,
"Content-Type"
,
"Authorization"
,
"X-Response-Text"
],
expose_headers
=
[
"X-Response-Text"
],
expose_headers
=
[
"X-Response-Text"
],
)
)
# Initialize dependencies
# Initialize dependencies
container
=
DIContainer
()
container
=
DIContainer
()
...
@@ -59,83 +74,204 @@ def create_app() -> FastAPI:
...
@@ -59,83 +74,204 @@ 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.
"""
"""
try
:
if
not
student_id
.
strip
():
if
not
student_id
.
strip
():
raise
HTTPException
(
status_code
=
400
,
detail
=
"Student ID is required"
)
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"""
try
:
health_status
=
container
.
health_service
.
get_health_status
()
health_status
=
container
.
health_service
.
get_health_status
()
# Add agent service status
# Add agent service status
health_status
.
update
({
health_status
.
update
({
"openai_service_status"
:
"available"
if
container
.
openai_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"
"agent_service_status"
:
"available"
if
container
.
agent_service
.
is_available
()
else
"unavailable"
,
"minio_endpoint"
:
container
.
config
.
minio_endpoint
,
"minio_bucket"
:
container
.
config
.
minio_bucket
})
})
return
health_status
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"""
try
:
return
container
.
chat_service
.
get_agent_stats
(
student_id
)
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"""
try
:
return
container
.
chat_service
.
clear_conversation
(
student_id
)
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"""
try
:
prompt
=
request
.
get
(
"prompt"
,
""
)
prompt
=
request
.
get
(
"prompt"
,
""
)
if
not
prompt
:
if
not
prompt
:
raise
HTTPException
(
status_code
=
400
,
detail
=
"System prompt cannot be empty"
)
raise
HTTPException
(
status_code
=
400
,
detail
=
"System prompt cannot be empty"
)
return
container
.
chat_service
.
set_system_prompt
(
prompt
)
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"""
try
:
return
{
return
{
"system_prompt"
:
container
.
agent_service
.
system_prompt
,
"system_prompt"
:
container
.
agent_service
.
system_prompt
,
"status"
:
"success"
"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"""
try
:
history
=
container
.
agent_service
.
export_conversation
(
student_id
)
history
=
container
.
agent_service
.
export_conversation
(
student_id
)
return
{
return
{
"student_id"
:
student_id
,
"student_id"
:
student_id
,
"messages"
:
history
,
"messages"
:
history
,
"total_messages"
:
len
(
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"""
try
:
student_id
=
request
.
get
(
"student_id"
,
"student_001"
)
student_id
=
request
.
get
(
"student_id"
,
"student_001"
)
messages
=
request
.
get
(
"messages"
,
[])
messages
=
request
.
get
(
"messages"
,
[])
...
@@ -147,6 +283,29 @@ def create_app() -> FastAPI:
...
@@ -147,6 +283,29 @@ def create_app() -> FastAPI:
"status"
:
"success"
,
"status"
:
"success"
,
"message"
:
f
"Imported {len(messages)} messages to conversation {student_id}"
"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
self_hosted_env/voice_agent/postgres_app.tar
0 → 100644
View file @
12111380
File added
self_hosted_env/voice_agent/services/agent_service.py
View file @
12111380
...
@@ -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,6 +52,8 @@ SYSTEM_PROMPTS: Dict[StudentNationality, str] = {
...
@@ -52,6 +52,8 @@ SYSTEM_PROMPTS: Dict[StudentNationality, str] = {
استخدم التشكيل بس على الكلمات اللي ممكن الـTTS يخبّص فيها أو يقرأها خطأ، واترك الباقي بدون تشكيل عشان يطلع طبيعي.
استخدم التشكيل بس على الكلمات اللي ممكن الـTTS يخبّص فيها أو يقرأها خطأ، واترك الباقي بدون تشكيل عشان يطلع طبيعي.
لو {student_name} مكتوب بالإنجليزي، اكتبه دايمًا بالعربي في ردودك.
لو {student_name} مكتوب بالإنجليزي، اكتبه دايمًا بالعربي في ردودك.
لما تذكر الصف {grade}، قُلها بالطريقة اللي الطفل متعود يسمعها: الصف 4 = رابع ابتدائي، الصف 5 = خامس ابتدائي، وهكذا.
الهدف: رد مبسّط، قريب، ويبيّن إن المعلّم يشرح للطفل، مو يقرأ من كتاب.
الهدف: رد مبسّط، قريب، ويبيّن إن المعلّم يشرح للطفل، مو يقرأ من كتاب.
"""
"""
}
}
...
@@ -59,10 +61,6 @@ SYSTEM_PROMPTS: Dict[StudentNationality, str] = {
...
@@ -59,10 +61,6 @@ SYSTEM_PROMPTS: Dict[StudentNationality, str] = {
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
pool
s"""
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 -----------------
...
...
self_hosted_env/voice_agent/services/chat_database_service.py
View file @
12111380
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,17 +23,23 @@ class ChatDatabaseService:
...
@@ -20,17 +23,23 @@ 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
()
try
:
with
conn
.
cursor
(
cursor_factory
=
RealDictCursor
)
as
cur
:
cur
.
execute
(
cur
.
execute
(
"SELECT nationality FROM students WHERE student_id =
%
s"
,
"SELECT nationality FROM students WHERE student_id =
%
s"
,
(
student_id
,)
(
student_id
,)
)
)
result
=
cur
.
fetchone
()
result
=
cur
.
fetchone
()
return
result
[
"nationality"
]
if
result
else
None
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
()
try
:
with
conn
.
cursor
(
cursor_factory
=
RealDictCursor
)
as
cur
:
cur
.
execute
(
cur
.
execute
(
"""
"""
SELECT student_id, student_name, grade, language, nationality
SELECT student_id, student_name, grade, language, nationality
...
@@ -49,10 +58,14 @@ class ChatDatabaseService:
...
@@ -49,10 +58,14 @@ class ChatDatabaseService:
'nationality'
:
result
[
'nationality'
]
'nationality'
:
result
[
'nationality'
]
}
}
return
None
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
()
try
:
with
conn
.
cursor
(
cursor_factory
=
RealDictCursor
)
as
cur
:
cur
.
execute
(
cur
.
execute
(
"SELECT grade, language FROM students WHERE student_id =
%
s"
,
"SELECT grade, language FROM students WHERE student_id =
%
s"
,
(
student_id
,)
(
student_id
,)
...
@@ -61,10 +74,14 @@ class ChatDatabaseService:
...
@@ -61,10 +74,14 @@ class ChatDatabaseService:
if
result
:
if
result
:
return
(
result
[
"grade"
],
result
[
"language"
])
return
(
result
[
"grade"
],
result
[
"language"
])
return
None
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
()
try
:
with
conn
.
cursor
(
cursor_factory
=
RealDictCursor
)
as
cur
:
cur
.
execute
(
cur
.
execute
(
"""
"""
SELECT role, content
SELECT role, content
...
@@ -78,10 +95,14 @@ class ChatDatabaseService:
...
@@ -78,10 +95,14 @@ class ChatDatabaseService:
results
=
cur
.
fetchall
()
results
=
cur
.
fetchall
()
# Return in chronological order (oldest first)
# Return in chronological order (oldest first)
return
[{
"role"
:
row
[
"role"
],
"content"
:
row
[
"content"
]}
for
row
in
reversed
(
results
)]
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
()
try
:
with
conn
.
cursor
()
as
cur
:
cur
.
execute
(
cur
.
execute
(
"""
"""
INSERT INTO chat_history (student_id, role, content)
INSERT INTO chat_history (student_id, role, content)
...
@@ -89,20 +110,28 @@ class ChatDatabaseService:
...
@@ -89,20 +110,28 @@ class ChatDatabaseService:
"""
,
"""
,
(
student_id
,
role
,
content
)
(
student_id
,
role
,
content
)
)
)
self
.
conn
.
commit
()
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
()
try
:
with
conn
.
cursor
()
as
cur
:
cur
.
execute
(
cur
.
execute
(
"DELETE FROM chat_history WHERE student_id =
%
s"
,
"DELETE FROM chat_history WHERE student_id =
%
s"
,
(
student_id
,)
(
student_id
,)
)
)
self
.
conn
.
commit
()
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
()
try
:
with
conn
.
cursor
()
as
cur
:
cur
.
execute
(
cur
.
execute
(
"""
"""
DELETE FROM chat_history
DELETE FROM chat_history
...
@@ -117,7 +146,9 @@ class ChatDatabaseService:
...
@@ -117,7 +146,9 @@ class ChatDatabaseService:
"""
,
"""
,
(
student_id
,
student_id
,
max_messages
)
(
student_id
,
student_id
,
max_messages
)
)
)
self
.
conn
.
commit
()
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,7 +170,9 @@ class ChatDatabaseService:
...
@@ -139,7 +170,9 @@ class ChatDatabaseService:
if
updates
:
if
updates
:
params
.
append
(
student_id
)
params
.
append
(
student_id
)
with
self
.
conn
.
cursor
()
as
cur
:
conn
=
self
.
pool
.
getconn
()
try
:
with
conn
.
cursor
()
as
cur
:
cur
.
execute
(
cur
.
execute
(
f
"""
f
"""
UPDATE students
UPDATE students
...
@@ -148,12 +181,16 @@ class ChatDatabaseService:
...
@@ -148,12 +181,16 @@ class ChatDatabaseService:
"""
,
"""
,
params
params
)
)
self
.
conn
.
commit
()
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
()
try
:
with
conn
.
cursor
()
as
cur
:
cur
.
execute
(
cur
.
execute
(
"""
"""
INSERT INTO students (student_id, student_name, grade, language, nationality)
INSERT INTO students (student_id, student_name, grade, language, nationality)
...
@@ -162,8 +199,10 @@ class ChatDatabaseService:
...
@@ -162,8 +199,10 @@ class ChatDatabaseService:
"""
,
"""
,
(
student_id
,
student_name
,
grade
,
language
,
nationality
)
(
student_id
,
student_name
,
grade
,
language
,
nationality
)
)
)
self
.
conn
.
commit
()
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
self_hosted_env/voice_agent/services/pgvector_service.py
View file @
12111380
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
()
try
:
with
conn
.
cursor
()
as
cur
:
cur
.
execute
(
cur
.
execute
(
"""
"""
INSERT INTO embeddings_table (id, embedding)
INSERT INTO embeddings_table (id, embedding)
...
@@ -30,11 +45,15 @@ class PGVectorService:
...
@@ -30,11 +45,15 @@ class PGVectorService:
"""
,
"""
,
(
id
,
embedding
),
(
id
,
embedding
),
)
)
self
.
conn
.
commit
()
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
()
try
:
with
conn
.
cursor
(
cursor_factory
=
RealDictCursor
)
as
cur
:
cur
.
execute
(
cur
.
execute
(
"""
"""
SELECT id, embedding, embedding <->
%
s AS distance
SELECT id, embedding, embedding <->
%
s AS distance
...
@@ -45,6 +64,8 @@ class PGVectorService:
...
@@ -45,6 +64,8 @@ class PGVectorService:
(
query_embedding
,
query_embedding
,
limit
),
(
query_embedding
,
query_embedding
,
limit
),
)
)
return
cur
.
fetchall
()
return
cur
.
fetchall
()
finally
:
self
.
pool
.
putconn
(
conn
)
def
search_filtered_nearest
(
def
search_filtered_nearest
(
self
,
self
,
...
@@ -55,7 +76,9 @@ class PGVectorService:
...
@@ -55,7 +76,9 @@ 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
()
try
:
with
conn
.
cursor
(
cursor_factory
=
RealDictCursor
)
as
cur
:
cur
.
execute
(
cur
.
execute
(
"""
"""
SELECT id, grade, subject, unit, concept, lesson, chunk_text,
SELECT id, grade, subject, unit, concept, lesson, chunk_text,
...
@@ -70,6 +93,8 @@ class PGVectorService:
...
@@ -70,6 +93,8 @@ class PGVectorService:
(
query_embedding
,
grade
,
f
"
%
{subject}
%
"
,
is_arabic
,
query_embedding
,
limit
),
(
query_embedding
,
grade
,
f
"
%
{subject}
%
"
,
is_arabic
,
query_embedding
,
limit
),
)
)
return
cur
.
fetchall
()
return
cur
.
fetchall
()
finally
:
self
.
pool
.
putconn
(
conn
)
def
search_flexible_filtered_nearest
(
def
search_flexible_filtered_nearest
(
self
,
self
,
...
@@ -103,7 +128,9 @@ class PGVectorService:
...
@@ -103,7 +128,9 @@ 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
()
try
:
with
conn
.
cursor
(
cursor_factory
=
RealDictCursor
)
as
cur
:
cur
.
execute
(
cur
.
execute
(
f
"""
f
"""
SELECT id, grade, subject, unit, concept, lesson, chunk_text,
SELECT id, grade, subject, unit, concept, lesson, chunk_text,
...
@@ -116,10 +143,14 @@ class PGVectorService:
...
@@ -116,10 +143,14 @@ class PGVectorService:
params
params
)
)
return
cur
.
fetchall
()
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
()
try
:
with
conn
.
cursor
(
cursor_factory
=
RealDictCursor
)
as
cur
:
cur
.
execute
(
cur
.
execute
(
"""
"""
SELECT DISTINCT subject
SELECT DISTINCT subject
...
@@ -130,7 +161,9 @@ class PGVectorService:
...
@@ -130,7 +161,9 @@ class PGVectorService:
(
grade
,
is_arabic
)
(
grade
,
is_arabic
)
)
)
return
[
row
[
'subject'
]
for
row
in
cur
.
fetchall
()]
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
self_hosted_env/voice_agent/static/audio-recorder.html
View file @
12111380
...
@@ -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
)
{
try
{
const
bytes
=
Uint8Array
.
from
(
atob
(
str
),
c
=>
c
.
charCodeAt
(
0
));
const
bytes
=
Uint8Array
.
from
(
atob
(
str
),
c
=>
c
.
charCodeAt
(
0
));
const
decoder
=
new
window
.
TextDecoder
(
'utf-8'
);
const
decoder
=
new
window
.
TextDecoder
(
'utf-8'
);
return
decoder
.
decode
(
bytes
);
return
decoder
.
decode
(
bytes
);
}
catch
(
error
)
{
return
str
;
// Return original string if decode fails
}
}
}
}
}
...
@@ -241,36 +238,51 @@
...
@@ -241,36 +238,51 @@
}
}
}
}
// API Client
// API Client
with enhanced error handling
class
APIClient
{
class
APIClient
{
async
sendFormData
(
url
,
formData
)
{
async
sendFormData
(
url
,
formData
)
{
try
{
const
response
=
await
fetch
(
url
,
{
const
response
=
await
fetch
(
url
,
{
method
:
'POST'
,
method
:
'POST'
,
body
:
formData
body
:
formData
,
mode
:
'cors'
,
credentials
:
'omit'
});
});
if
(
!
response
.
ok
)
{
if
(
!
response
.
ok
)
{
const
errorData
=
await
response
.
json
();
let
errorData
;
throw
new
Error
(
errorData
.
detail
||
'Request failed'
);
try
{
errorData
=
await
response
.
json
();
}
catch
{
errorData
=
{
detail
:
`HTTP
${
response
.
status
}
:
${
response
.
statusText
}
`
};
}
throw
new
Error
(
errorData
.
detail
||
`Request failed with status
${
response
.
status
}
`
);
}
}
return
response
.
json
();
const
responseData
=
await
response
.
json
();
return
responseData
;
}
catch
(
error
)
{
throw
error
;
}
}
}
async
fetchAudioResponse
()
{
async
fetchAudioResponse
()
{
const
response
=
await
fetch
(
Config
.
AUDIO_RESPONSE_URL
);
try
{
const
response
=
await
fetch
(
Config
.
AUDIO_RESPONSE_URL
,
{
method
:
'GET'
,
mode
:
'cors'
,
credentials
:
'omit'
});
if
(
response
.
ok
)
{
if
(
response
.
ok
)
{
const
encodedText
=
response
.
headers
.
get
(
'X-Response-Text'
);
const
encodedText
=
response
.
headers
.
get
(
'X-Response-Text'
);
console
.
log
(
'Encoded text from header:'
,
encodedText
);
let
agentText
=
"لا يوجد رد متاح"
;
let
agentText
=
"لا يوجد رد متاح"
;
if
(
encodedText
)
{
if
(
encodedText
)
{
try
{
try
{
agentText
=
TextDecoder
.
decodeBase64Utf8
(
encodedText
);
agentText
=
TextDecoder
.
decodeBase64Utf8
(
encodedText
);
console
.
log
(
'Decoded agent text:'
,
agentText
);
}
catch
(
e
)
{
}
catch
(
e
)
{
console
.
log
(
'Decoding error:'
,
e
);
agentText
=
"خطأ في فك تشفير الرد"
;
agentText
=
"خطأ في فك تشفير الرد"
;
}
}
}
}
...
@@ -278,9 +290,17 @@
...
@@ -278,9 +290,17 @@
const
audioBlob
=
await
response
.
blob
();
const
audioBlob
=
await
response
.
blob
();
return
{
agentText
,
audioBlob
};
return
{
agentText
,
audioBlob
};
}
else
{
}
else
{
const
errorData
=
await
response
.
json
();
let
errorData
;
try
{
errorData
=
await
response
.
json
();
}
catch
{
errorData
=
{
detail
:
`HTTP
${
response
.
status
}
:
${
response
.
statusText
}
`
};
}
throw
new
Error
(
errorData
.
detail
||
'Failed to get response'
);
throw
new
Error
(
errorData
.
detail
||
'Failed to get response'
);
}
}
}
catch
(
error
)
{
throw
error
;
}
}
}
}
}
...
@@ -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
)
{
setTimeout
(()
=>
{
const
audioPlayer
=
this
.
uiManager
.
chatContainer
.
lastChild
.
querySelector
(
'audio'
);
const
audioPlayer
=
this
.
uiManager
.
chatContainer
.
lastChild
.
querySelector
(
'audio'
);
if
(
audioPlayer
)
{
if
(
audioPlayer
)
{
audioPlayer
.
play
();
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 -
Simplifi
ed
// Mediator Pattern -
Enhanc
ed
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'
,
()
=>
{
try
{
UnifiedChatApp
.
initialize
();
UnifiedChatApp
.
initialize
();
console
.
log
(
'Chat application with Student ID support initialized successfully!'
);
console
.
log
(
'Chat application with Student ID support initialized successfully!'
);
}
catch
(
error
)
{
console
.
error
(
'Failed to initialize chat application:'
,
error
);
}
});
});
</script>
</script>
</body>
</body>
...
...
self_hosted_env/voice_agent/voice_agent.tar
0 → 100644
View file @
12111380
File added
self_hosted_env/voice_agent/wait-for-postgres.sh
View file @
12111380
#!/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
"
$@
"
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment