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
5148341d
Commit
5148341d
authored
Sep 14, 2025
by
SalmaMohammedHamedMustafa
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
id from frontend
parent
7c00ed5b
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
117 additions
and
26 deletions
+117
-26
Dockerfile
self_hosted_env/voice_agent/Dockerfile
+2
-2
main.py
self_hosted_env/voice_agent/main.py
+19
-14
agent_service.py
self_hosted_env/voice_agent/services/agent_service.py
+39
-5
audio-recorder.html
self_hosted_env/voice_agent/static/audio-recorder.html
+57
-5
No files found.
self_hosted_env/voice_agent/Dockerfile
View file @
5148341d
...
@@ -8,7 +8,7 @@ RUN pip install --no-cache-dir -r requirements.txt
...
@@ -8,7 +8,7 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY
. .
COPY
. .
#just keep the container running without doing anything
#just keep the container running without doing anything
CMD
["sh", "-c", "while :; do sleep 10; done"]
#
CMD ["sh", "-c", "while :; do sleep 10; done"]
#run the app automatically when the container starts
#run the app automatically when the container starts
#
CMD ["python", "main.py"]
CMD
["python", "main.py"]
self_hosted_env/voice_agent/main.py
View file @
5148341d
...
@@ -64,13 +64,17 @@ def create_app() -> FastAPI:
...
@@ -64,13 +64,17 @@ def create_app() -> FastAPI:
@
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
):
):
"""
"""
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.
"""
"""
return
container
.
chat_service
.
process_message
(
student_id
=
"student_002"
,
file
=
file
,
text
=
text
)
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
)
@
app
.
get
(
"/get-audio-response"
)
@
app
.
get
(
"/get-audio-response"
)
async
def
get_audio_response
():
async
def
get_audio_response
():
...
@@ -90,14 +94,14 @@ def create_app() -> FastAPI:
...
@@ -90,14 +94,14 @@ def create_app() -> FastAPI:
# Agent management endpoints
# Agent management endpoints
@
app
.
get
(
"/conversation/stats"
)
@
app
.
get
(
"/conversation/stats"
)
async
def
get_conversation_stats
(
conversation_id
:
str
=
"default
"
):
async
def
get_conversation_stats
(
student_id
:
str
=
"student_001
"
):
"""Get conversation statistics"""
"""Get conversation statistics"""
return
container
.
chat_service
.
get_agent_stats
(
conversation
_id
)
return
container
.
chat_service
.
get_agent_stats
(
student
_id
)
@
app
.
post
(
"/conversation/clear"
)
@
app
.
post
(
"/conversation/clear"
)
async
def
clear_conversation
(
conversation_id
:
str
=
"default"
):
async
def
clear_conversation
(
student_id
:
str
=
Form
(
"student_001"
)
):
"""Clear conversation history"""
"""Clear conversation history"""
return
container
.
chat_service
.
clear_conversation
(
conversation
_id
)
return
container
.
chat_service
.
clear_conversation
(
student
_id
)
@
app
.
post
(
"/agent/system-prompt"
)
@
app
.
post
(
"/agent/system-prompt"
)
async
def
set_system_prompt
(
request
:
dict
):
async
def
set_system_prompt
(
request
:
dict
):
...
@@ -116,11 +120,11 @@ def create_app() -> FastAPI:
...
@@ -116,11 +120,11 @@ def create_app() -> FastAPI:
}
}
@
app
.
get
(
"/conversation/export"
)
@
app
.
get
(
"/conversation/export"
)
async
def
export_conversation
(
conversation_id
:
str
=
"default
"
):
async
def
export_conversation
(
student_id
:
str
=
"student_001
"
):
"""Export conversation history"""
"""Export conversation history"""
history
=
container
.
agent_service
.
export_conversation
(
conversation
_id
)
history
=
container
.
agent_service
.
export_conversation
(
student
_id
)
return
{
return
{
"
conversation_id"
:
conversation
_id
,
"
student_id"
:
student
_id
,
"messages"
:
history
,
"messages"
:
history
,
"total_messages"
:
len
(
history
)
"total_messages"
:
len
(
history
)
}
}
...
@@ -128,16 +132,16 @@ def create_app() -> FastAPI:
...
@@ -128,16 +132,16 @@ def create_app() -> FastAPI:
@
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"""
conversation_id
=
request
.
get
(
"conversation_id"
,
"default
"
)
student_id
=
request
.
get
(
"student_id"
,
"student_001
"
)
messages
=
request
.
get
(
"messages"
,
[])
messages
=
request
.
get
(
"messages"
,
[])
if
not
messages
:
if
not
messages
:
raise
HTTPException
(
status_code
=
400
,
detail
=
"Messages list cannot be empty"
)
raise
HTTPException
(
status_code
=
400
,
detail
=
"Messages list cannot be empty"
)
container
.
agent_service
.
import_conversation
(
messages
,
conversation
_id
)
container
.
agent_service
.
import_conversation
(
messages
,
student
_id
)
return
{
return
{
"status"
:
"success"
,
"status"
:
"success"
,
"message"
:
f
"Imported {len(messages)} messages to conversation {
conversation
_id}"
"message"
:
f
"Imported {len(messages)} messages to conversation {
student
_id}"
}
}
@
app
.
get
(
"/"
)
@
app
.
get
(
"/"
)
...
@@ -151,10 +155,11 @@ def create_app() -> FastAPI:
...
@@ -151,10 +155,11 @@ def create_app() -> FastAPI:
"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"
],
],
"endpoints"
:
{
"endpoints"
:
{
"chat"
:
"/chat (accepts audio or text, generates local agent response)"
,
"chat"
:
"/chat (accepts audio or text
with student_id
, generates local agent response)"
,
"get_audio_response"
:
"/get-audio-response (fetches agent's audio and text response)"
,
"get_audio_response"
:
"/get-audio-response (fetches agent's audio and text response)"
,
"conversation_stats"
:
"/conversation/stats (get conversation statistics)"
,
"conversation_stats"
:
"/conversation/stats (get conversation statistics)"
,
"clear_conversation"
:
"/conversation/clear (clear conversation history)"
,
"clear_conversation"
:
"/conversation/clear (clear conversation history)"
,
...
...
self_hosted_env/voice_agent/services/agent_service.py
View file @
5148341d
...
@@ -94,16 +94,48 @@ class AgentService:
...
@@ -94,16 +94,48 @@ class AgentService:
if
not
nationality_str
:
if
not
nationality_str
:
raise
HTTPException
(
status_code
=
404
,
detail
=
f
"Student with ID {student_id} not found"
)
raise
HTTPException
(
status_code
=
404
,
detail
=
f
"Student with ID {student_id} not found"
)
# Convert database nationality to enum (handle case-insensitive conversion)
logger
.
info
(
f
"Retrieved nationality from DB: '{nationality_str}' for student: {student_id}"
)
# Debug the enum
print
(
f
"DEBUG - StudentNationality enum:"
)
for
enum_item
in
StudentNationality
:
print
(
f
" {enum_item.name} = '{enum_item.value}'"
)
# Debug the conversion
nationality_lower
=
nationality_str
.
lower
()
.
strip
()
nationality_lower
=
nationality_str
.
lower
()
.
strip
()
print
(
f
"DEBUG - DB value: '{nationality_str}' -> lowercase: '{nationality_lower}'"
)
print
(
f
"DEBUG - Is 'saudi' in enum values? {'saudi' in [e.value for e in StudentNationality]}"
)
print
(
f
"DEBUG - Direct enum creation test:"
)
# Test direct enum creation
try
:
test_saudi
=
StudentNationality
(
'saudi'
)
print
(
f
" StudentNationality('saudi') = {test_saudi}"
)
except
Exception
as
e
:
print
(
f
" StudentNationality('saudi') failed: {e}"
)
try
:
test_egyptian
=
StudentNationality
(
'egyptian'
)
print
(
f
" StudentNationality('egyptian') = {test_egyptian}"
)
except
Exception
as
e
:
print
(
f
" StudentNationality('egyptian') failed: {e}"
)
# Convert string to StudentNationality enum
nationality_lower
=
nationality_str
.
lower
()
.
strip
()
print
(
f
"DEBUG - Looking for nationality: '{nationality_lower}'"
)
# Try explicit mapping first
nationality_mapping
=
{
nationality_mapping
=
{
'egyptian'
:
StudentNationality
.
EGYPTIAN
,
'egyptian'
:
StudentNationality
.
EGYPTIAN
,
'saudi'
:
StudentNationality
.
SAUDI
'saudi'
:
StudentNationality
.
SAUDI
}
}
nationality
=
nationality_mapping
.
get
(
nationality_lower
,
StudentNationality
.
EGYPTIAN
)
if
nationality_lower
in
nationality_mapping
:
if
nationality_lower
not
in
nationality_mapping
:
nationality
=
nationality_mapping
[
nationality_lower
]
logger
.
warning
(
f
"Unknown nationality '{nationality_str}' for student {student_id}, defaulting to EGYPTIAN"
)
logger
.
info
(
f
"Successfully mapped '{nationality_str}' to {nationality}"
)
else
:
logger
.
warning
(
f
"Unknown nationality '{nationality_str}' ('{nationality_lower}') for student {student_id}, defaulting to EGYPTIAN"
)
nationality
=
StudentNationality
.
EGYPTIAN
# Add user message to database
# Add user message to database
self
.
add_message_to_history
(
student_id
,
user_message
,
"user"
)
self
.
add_message_to_history
(
student_id
,
user_message
,
"user"
)
...
@@ -113,6 +145,8 @@ class AgentService:
...
@@ -113,6 +145,8 @@ class AgentService:
# Pick system prompt using the enum value
# Pick system prompt using the enum value
system_prompt
=
SYSTEM_PROMPTS
.
get
(
nationality
,
SYSTEM_PROMPTS
[
StudentNationality
.
EGYPTIAN
])
system_prompt
=
SYSTEM_PROMPTS
.
get
(
nationality
,
SYSTEM_PROMPTS
[
StudentNationality
.
EGYPTIAN
])
logger
.
info
(
f
"Using nationality: {nationality} for student: {student_id}"
)
print
(
f
"DEBUG - Selected system_prompt: {system_prompt}"
)
# Debug print
# Prepare messages
# Prepare messages
messages
=
[]
messages
=
[]
...
@@ -178,7 +212,7 @@ class AgentService:
...
@@ -178,7 +212,7 @@ class AgentService:
if
__name__
==
"__main__"
:
if
__name__
==
"__main__"
:
logging
.
basicConfig
(
level
=
logging
.
INFO
)
logging
.
basicConfig
(
level
=
logging
.
INFO
)
agent
=
AgentService
(
use_pgvector
=
Tru
e
)
agent
=
AgentService
(
use_pgvector
=
Fals
e
)
if
agent
.
is_available
():
if
agent
.
is_available
():
try
:
try
:
...
...
self_hosted_env/voice_agent/static/audio-recorder.html
View file @
5148341d
...
@@ -37,6 +37,20 @@
...
@@ -37,6 +37,20 @@
gap
:
15px
;
gap
:
15px
;
margin-bottom
:
30px
;
margin-bottom
:
30px
;
}
}
.student-id-controls
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
padding
:
10px
;
background
:
#f8f9fa
;
border-radius
:
5px
;
border
:
1px
solid
#dee2e6
;
}
.student-id-controls
label
{
font-weight
:
bold
;
color
:
#495057
;
min-width
:
80px
;
}
.voice-controls
,
.text-controls
{
.voice-controls
,
.text-controls
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
...
@@ -49,6 +63,13 @@
...
@@ -49,6 +63,13 @@
border-radius
:
5px
;
border-radius
:
5px
;
font-size
:
16px
;
font-size
:
16px
;
}
}
#studentIdInput
{
flex-grow
:
1
;
padding
:
8px
;
border
:
1px
solid
#ced4da
;
border-radius
:
4px
;
font-size
:
14px
;
}
button
{
button
{
background
:
#007bff
;
background
:
#007bff
;
color
:
white
;
color
:
white
;
...
@@ -143,6 +164,10 @@
...
@@ -143,6 +164,10 @@
<h1>
الدردشة
</h1>
<h1>
الدردشة
</h1>
<div
class=
"controls"
>
<div
class=
"controls"
>
<div
class=
"student-id-controls"
>
<label
for=
"studentIdInput"
>
رقم الطالب:
</label>
<input
type=
"text"
id=
"studentIdInput"
value=
"student_001"
placeholder=
"أدخل رقم الطالب..."
/>
</div>
<div
class=
"text-controls"
>
<div
class=
"text-controls"
>
<input
type=
"text"
id=
"textInput"
placeholder=
"اكتب رسالتك هنا..."
/>
<input
type=
"text"
id=
"textInput"
placeholder=
"اكتب رسالتك هنا..."
/>
<button
id=
"sendTextBtn"
>
إرسال نص
</button>
<button
id=
"sendTextBtn"
>
إرسال نص
</button>
...
@@ -355,6 +380,7 @@
...
@@ -355,6 +380,7 @@
}
}
initializeElements
()
{
initializeElements
()
{
this
.
studentIdInput
=
document
.
getElementById
(
'studentIdInput'
);
this
.
textInput
=
document
.
getElementById
(
'textInput'
);
this
.
textInput
=
document
.
getElementById
(
'textInput'
);
this
.
sendTextBtn
=
document
.
getElementById
(
'sendTextBtn'
);
this
.
sendTextBtn
=
document
.
getElementById
(
'sendTextBtn'
);
this
.
startBtn
=
document
.
getElementById
(
'startBtn'
);
this
.
startBtn
=
document
.
getElementById
(
'startBtn'
);
...
@@ -396,6 +422,7 @@
...
@@ -396,6 +422,7 @@
this
.
stopBtn
.
disabled
=
true
;
this
.
stopBtn
.
disabled
=
true
;
this
.
textInput
.
disabled
=
false
;
this
.
textInput
.
disabled
=
false
;
this
.
sendTextBtn
.
disabled
=
false
;
this
.
sendTextBtn
.
disabled
=
false
;
this
.
studentIdInput
.
disabled
=
false
;
break
;
break
;
case
RecordingState
.
RECORDING
:
case
RecordingState
.
RECORDING
:
...
@@ -405,6 +432,7 @@
...
@@ -405,6 +432,7 @@
this
.
stopBtn
.
disabled
=
false
;
this
.
stopBtn
.
disabled
=
false
;
this
.
textInput
.
disabled
=
true
;
this
.
textInput
.
disabled
=
true
;
this
.
sendTextBtn
.
disabled
=
true
;
this
.
sendTextBtn
.
disabled
=
true
;
this
.
studentIdInput
.
disabled
=
true
;
break
;
break
;
case
RecordingState
.
PROCESSING
:
case
RecordingState
.
PROCESSING
:
...
@@ -415,6 +443,7 @@
...
@@ -415,6 +443,7 @@
this
.
stopBtn
.
disabled
=
true
;
this
.
stopBtn
.
disabled
=
true
;
this
.
textInput
.
disabled
=
true
;
this
.
textInput
.
disabled
=
true
;
this
.
sendTextBtn
.
disabled
=
true
;
this
.
sendTextBtn
.
disabled
=
true
;
this
.
studentIdInput
.
disabled
=
true
;
break
;
break
;
}
}
}
}
...
@@ -426,6 +455,10 @@
...
@@ -426,6 +455,10 @@
getTextInput
()
{
getTextInput
()
{
return
this
.
textInput
.
value
.
trim
();
return
this
.
textInput
.
value
.
trim
();
}
}
getStudentId
()
{
return
this
.
studentIdInput
.
value
.
trim
()
||
'student_001'
;
}
}
}
// Message Manager
// Message Manager
...
@@ -461,18 +494,24 @@
...
@@ -461,18 +494,24 @@
this
.
uiManager
=
uiManager
;
this
.
uiManager
=
uiManager
;
}
}
async
sendTextMessage
(
text
)
{
async
sendTextMessage
(
text
,
studentId
)
{
if
(
!
text
)
{
if
(
!
text
)
{
this
.
uiManager
.
showStatus
(
'الرجاء إدخال رسالة.'
,
StatusType
.
ERROR
);
this
.
uiManager
.
showStatus
(
'الرجاء إدخال رسالة.'
,
StatusType
.
ERROR
);
return
false
;
return
false
;
}
}
if
(
!
studentId
)
{
this
.
uiManager
.
showStatus
(
'الرجاء إدخال رقم الطالب.'
,
StatusType
.
ERROR
);
return
false
;
}
this
.
uiManager
.
showStatus
(
'يتم إرسال النص...'
,
StatusType
.
PROCESSING
);
this
.
uiManager
.
showStatus
(
'يتم إرسال النص...'
,
StatusType
.
PROCESSING
);
this
.
messageManager
.
addUserMessage
(
text
);
this
.
messageManager
.
addUserMessage
(
text
);
try
{
try
{
const
formData
=
new
FormData
();
const
formData
=
new
FormData
();
formData
.
append
(
'text'
,
text
);
formData
.
append
(
'text'
,
text
);
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'
)
{
...
@@ -489,10 +528,16 @@
...
@@ -489,10 +528,16 @@
}
}
}
}
async
sendAudioMessage
(
audioBlob
)
{
async
sendAudioMessage
(
audioBlob
,
studentId
)
{
if
(
!
studentId
)
{
this
.
uiManager
.
showStatus
(
'الرجاء إدخال رقم الطالب.'
,
StatusType
.
ERROR
);
return
false
;
}
try
{
try
{
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
);
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'
)
{
...
@@ -563,14 +608,20 @@
...
@@ -563,14 +608,20 @@
async
handleSendText
()
{
async
handleSendText
()
{
const
text
=
this
.
uiManager
.
getTextInput
();
const
text
=
this
.
uiManager
.
getTextInput
();
const
studentId
=
this
.
uiManager
.
getStudentId
();
if
(
!
text
)
return
;
if
(
!
text
)
return
;
this
.
uiManager
.
clearTextInput
();
this
.
uiManager
.
clearTextInput
();
const
success
=
await
this
.
chatService
.
sendTextMessage
(
text
);
const
success
=
await
this
.
chatService
.
sendTextMessage
(
text
,
studentId
);
// UI state is handled within the chat service
// UI state is handled within the chat service
}
}
async
handleStartRecording
()
{
async
handleStartRecording
()
{
const
studentId
=
this
.
uiManager
.
getStudentId
();
if
(
!
studentId
)
{
this
.
uiManager
.
showStatus
(
'الرجاء إدخال رقم الطالب أولاً.'
,
StatusType
.
ERROR
);
return
;
}
await
this
.
audioRecorder
.
startRecording
();
await
this
.
audioRecorder
.
startRecording
();
}
}
...
@@ -579,8 +630,9 @@
...
@@ -579,8 +630,9 @@
}
}
async
handleAudioRecorded
(
audioBlob
)
{
async
handleAudioRecorded
(
audioBlob
)
{
const
studentId
=
this
.
uiManager
.
getStudentId
();
this
.
messageManager
.
addUserMessage
(
"تم إرسال الرسالة الصوتية."
);
this
.
messageManager
.
addUserMessage
(
"تم إرسال الرسالة الصوتية."
);
const
success
=
await
this
.
chatService
.
sendAudioMessage
(
audioBlob
);
const
success
=
await
this
.
chatService
.
sendAudioMessage
(
audioBlob
,
studentId
);
this
.
stateMachine
.
setState
(
RecordingState
.
IDLE
);
this
.
stateMachine
.
setState
(
RecordingState
.
IDLE
);
}
}
}
}
...
@@ -599,7 +651,7 @@
...
@@ -599,7 +651,7 @@
// Initialize application when DOM is ready
// Initialize application when DOM is ready
document
.
addEventListener
(
'DOMContentLoaded'
,
()
=>
{
document
.
addEventListener
(
'DOMContentLoaded'
,
()
=>
{
UnifiedChatApp
.
initialize
();
UnifiedChatApp
.
initialize
();
console
.
log
(
'
Simplified chat application
initialized successfully!'
);
console
.
log
(
'
Chat application with Student ID support
initialized successfully!'
);
});
});
</script>
</script>
</body>
</body>
...
...
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