anan character v0 and perfecly long lasting deployment

parent 12111380
version: "3.8" version: "3.8"
services: services:
postgres: postgres:
# Use the new custom image from Docker Hub build: ./postgres
image: salmamohammedhamedmustafa/postgres:latest
environment: environment:
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
...@@ -35,7 +34,7 @@ services: ...@@ -35,7 +34,7 @@ services:
retries: 3 retries: 3
voice-agent: voice-agent:
image: salmamohammedhamedmustafa/voice-agent:latest build: ./voice_agent
ports: ports:
- "8000:8000" - "8000:8000"
environment: environment:
...@@ -48,6 +47,8 @@ services: ...@@ -48,6 +47,8 @@ services:
POSTGRES_USER: "${POSTGRES_USER}" POSTGRES_USER: "${POSTGRES_USER}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
POSTGRES_DB: "${POSTGRES_DB}" POSTGRES_DB: "${POSTGRES_DB}"
DB_PORT: "${DB_PORT}"
DB_HOST: "${DB_HOST}"
depends_on: depends_on:
- minio - minio
- postgres - postgres
......
# Start from the official MinIO image
FROM minio/minio:latest
# Set the command to run MinIO with the console address
CMD ["server", "/data", "--console-address", ":9001"]
\ No newline at end of file
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile",
"containerHttpPort": "9001",
"ports": [
"9000:9000",
"9001:9001"
],
"volumes": [
"/data"
]
}
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile",
"containerHttpPort": "5432",
"ports": [
"5432:5432"
],
"volumes": [
"/var/lib/postgresql/data"
]
}
-- Create the main database
DO
$$
BEGIN
IF NOT EXISTS (
SELECT FROM pg_database WHERE datname = 'embeddings_db'
) THEN
PERFORM dblink_exec('dbname=' || current_database(), 'CREATE DATABASE embeddings_db');
END IF;
END
$$;
-- Connect to the newly created database
\c embeddings_db
CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS vector;
\ No newline at end of file
...@@ -14,7 +14,7 @@ from repositories import StorageRepository, MinIOStorageRepository ...@@ -14,7 +14,7 @@ from repositories import StorageRepository, MinIOStorageRepository
from handlers import AudioMessageHandler, TextMessageHandler from handlers import AudioMessageHandler, TextMessageHandler
from services import ( from services import (
AudioService, ChatService, HealthService, ResponseService, AudioService, ChatService, HealthService, ResponseService,
ResponseManager, OpenAIService, AgentService ResponseManager, OpenAIService, AgentService, ConnectionPool, PGVectorService, ChatDatabaseService
) )
class DIContainer: class DIContainer:
...@@ -25,7 +25,16 @@ class DIContainer: ...@@ -25,7 +25,16 @@ class DIContainer:
# Initialize OpenAI and Agent services # Initialize OpenAI and Agent services
self.openai_service = OpenAIService() self.openai_service = OpenAIService()
self.agent_service = AgentService()
self.pool_handler = ConnectionPool(
dbname=os.getenv("POSTGRES_DB"),
user=os.getenv("POSTGRES_USER"),
password=os.getenv("POSTGRES_PASSWORD"),
host=os.getenv("DB_HOST"), # This is the crucial part
port=int(os.getenv("DB_PORT"))
)
print(os.getenv("DB_HOST"), os.getenv("POSTGRES_DB"), os.getenv("POSTGRES_USER"))
self.agent_service = AgentService(pool_handler=self.pool_handler)
# Initialize services # Initialize services
self.audio_service = AudioService(self.storage_repo, self.config.minio_bucket) self.audio_service = AudioService(self.storage_repo, self.config.minio_bucket)
......
...@@ -6,3 +6,6 @@ from .response_manager import ResponseManager ...@@ -6,3 +6,6 @@ from .response_manager import ResponseManager
from .openai_service import OpenAIService from .openai_service import OpenAIService
from .agent_service import AgentService from .agent_service import AgentService
from .pgvector_service import PGVectorService from .pgvector_service import PGVectorService
from .chat_database_service import ChatDatabaseService
from .connection_pool import ConnectionPool
from .pedagogy_service import PedagogyService
...@@ -10,13 +10,26 @@ from services.pgvector_service import PGVectorService ...@@ -10,13 +10,26 @@ from services.pgvector_service import PGVectorService
from services.openai_service import OpenAIService from services.openai_service import OpenAIService
from services.chat_database_service import ChatDatabaseService from services.chat_database_service import ChatDatabaseService
from services.pedagogy_service import PedagogyService from services.pedagogy_service import PedagogyService
from services.connection_pool import ConnectionPool
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SYSTEM_PROMPTS: Dict[StudentNationality, str] = { SYSTEM_PROMPTS: Dict[StudentNationality, str] = {
StudentNationality.EGYPTIAN: """ StudentNationality.EGYPTIAN: """
إنت مُدرّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}. إنت مُدرّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}.
رَدّ باللهجة المصريّة الطبيعيّة كأنّك بتكَلِّم {student_name} قصادك.
فقط لو الطِّفل سأل عن هويتك بصراحة وواضح (مثل "إنت مين؟"، "عرّفني بنفسك"، "إنت بتعمل إيه هنا؟")،
رُد بالنصّ الثابت ده:
"أنا عَنان مؤسِّس شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أساعدك تتعلَّم أي حاجة عايز تتعلَّمها في العلوم."
لو سأل أسئلة عامة زي "نت عارف انا مين؟" أو "إزيك؟" أو "شكرا"، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
أما لو سأل عن أي حاجة في العلوم أو المنهج، اشرح له بالطريقة التعليمية المناسبة.
ملاحظة مُلزِمة: كلمة "منصّة" لازم تكتبها دايمًا كده بالظبط: **مَنَصّة** (بالفتحة على الميم والنون)،
عشان الـTTS يِنطِقها صح.
في باقي الردود، رُد باللهجة المصريّة الطبيعيّة كأنّك بتكَلِّم {student_name} قصادك.
خَلّي الكلام بسيط، واضح، وقَريب من وُدنه. خَلّي الكلام بسيط، واضح، وقَريب من وُدنه.
الجُمَل قُصَيَّرة ومُترابطة، مِش مَقطَّعة. الجُمَل قُصَيَّرة ومُترابطة، مِش مَقطَّعة.
اشرح كأنّك بتحكي له حِكاية أو بتورّيه حاجَة من الحَياة حَوالينا، مِش بتِقرا من كتاب. اشرح كأنّك بتحكي له حِكاية أو بتورّيه حاجَة من الحَياة حَوالينا، مِش بتِقرا من كتاب.
...@@ -32,12 +45,25 @@ SYSTEM_PROMPTS: Dict[StudentNationality, str] = { ...@@ -32,12 +45,25 @@ SYSTEM_PROMPTS: Dict[StudentNationality, str] = {
لَمّا تِذكُر الصف {grade}، قُله بالطريقة الطبيعيّة زي ما الأطفال بيقولوها: الصف 4 = سنة رابعة ابتدائي، الصف 5 = سنة خامسة ابتدائي، وهكذا. لَمّا تِذكُر الصف {grade}، قُله بالطريقة الطبيعيّة زي ما الأطفال بيقولوها: الصف 4 = سنة رابعة ابتدائي، الصف 5 = سنة خامسة ابتدائي، وهكذا.
الهَدف: رَد قُصَيَّر يِعلِّم ويُوصَّل المَعلومة، ويِبان إن فيه مُعلِّم بيِشرَح للطفل مِش كتاب بيتقري. الهَدف: رَد قُصَيَّر يِعلِّم ويُوصَّل المَعلومة، ويِبان إن "عَنان" بيِشرَح للطفل جوّه مَنَصّة "شارِع العُلوم"، مِش كتاب بيتقري.
""", """,
StudentNationality.SAUDI: """ StudentNationality.SAUDI: """
إنت معلّم لطفل في ابتدائي اسمه {student_name} في الصف {grade}. إنت مُعلّم لطفل في ابتدائي اسمه {student_name} في الصف {grade}.
رَد باللهجة السعوديّة الطبيعيّة، كأنك تشرح له قدّامك.
فقط لو الطفل سأل عن هويتك بصراحة وواضح (مثل "إنت مين؟"، "عرّفني بنفسك"، "إنت وش تسوي هنا؟")،
رُد بالنص الثابت هذا:
"أنا عَنان مؤسِّس شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أساعدك تتعلَّم أي حاجة عايز تتعلَّمها في العلوم."
لو سأل أسئلة عامة مثل "نت عارف انا مين؟" أو "كيفك؟" أو "شكرا"، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
أما لو سأل عن أي شيء في العلوم أو المنهج، اشرح له بالطريقة التعليمية المناسبة.
ملاحظة مُلزِمة: كلمة "منصّة" لازم تكتبها دايمًا كده بالظبط: **مَنَصّة** (بالفتحة على الميم والنون)،
عشان الـTTS يِنطِقها صح.
في باقي الردود، رَد باللهجة السعوديّة الطبيعيّة، كأنك تشرح له قدّامك.
خل الشرح واضح وسهل، لكن لا يكون ناشف. خل الشرح واضح وسهل، لكن لا يكون ناشف.
اشرح كأنك تسولف معه وتشبّه بأشياء من حياته اليومية. اشرح كأنك تسولف معه وتشبّه بأشياء من حياته اليومية.
...@@ -54,17 +80,16 @@ SYSTEM_PROMPTS: Dict[StudentNationality, str] = { ...@@ -54,17 +80,16 @@ SYSTEM_PROMPTS: Dict[StudentNationality, str] = {
لما تذكر الصف {grade}، قُلها بالطريقة اللي الطفل متعود يسمعها: الصف 4 = رابع ابتدائي، الصف 5 = خامس ابتدائي، وهكذا. لما تذكر الصف {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"""
def __init__(self, use_pgvector: bool = True): def __init__(self, use_pgvector: bool = True, pool_handler: Optional[ConnectionPool] = None):
self.openai_service = OpenAIService() self.openai_service = OpenAIService()
if not self.openai_service.is_available(): if not self.openai_service.is_available():
logger.warning("Warning: OPENAI_API_KEY not found. Agent service will be disabled.") logger.warning("Warning: OPENAI_API_KEY not found. Agent service will be disabled.")
...@@ -72,32 +97,55 @@ class AgentService: ...@@ -72,32 +97,55 @@ class AgentService:
else: else:
self.client = self.openai_service.client self.client = self.openai_service.client
# Use database for conversation memory self.pool_handler = pool_handler
self.db_service = ChatDatabaseService() if self.pool_handler is None:
self.pgvector = PGVectorService() if use_pgvector else None self.pool_handler = ConnectionPool(
minconn=1,
maxconn=20,
dbname=os.getenv("DB_NAME"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
host=os.getenv("DB_HOST"),
port=os.getenv("DB_PORT")
)
self.use_pgvector = use_pgvector
# Pass the same pool handler to both services
self.db_service = ChatDatabaseService(self.pool_handler)
if self.use_pgvector:
self.pgvector = PGVectorService(self.pool_handler)
else:
self.pgvector = None
# Initialize pedagogy service for Socratic questioning
self.pedagogy_service = PedagogyService() self.pedagogy_service = PedagogyService()
logger.info("AgentService initialized with PedagogyService for Socratic questioning") self.student_info = {}
def is_available(self) -> bool: def is_available(self) -> bool:
return self.client is not None return self.client is not None
def get_conversation_history(self, student_id: str) -> List[Dict[str, str]]: def get_conversation_history(self, student_id: str) -> List[Dict[str, str]]:
"""Get conversation history from database""" """Get conversation history from database"""
try:
return self.db_service.get_chat_history(student_id) return self.db_service.get_chat_history(student_id)
except Exception as e:
logger.error(f"Error getting conversation history for {student_id}: {e}")
return []
def add_message_to_history(self, student_id: str, message: str, role: str = "user"): def add_message_to_history(self, student_id: str, message: str, role: str = "user"):
"""Add message to database""" """Add message to database"""
try:
self.db_service.add_message(student_id, role, message) self.db_service.add_message(student_id, role, message)
# Limit history to prevent growth # Limit history to prevent growth
self.db_service.limit_history(student_id, max_messages=38) self.db_service.limit_history(student_id, max_messages=38)
except Exception as e:
logger.error(f"Error adding message to history for {student_id}: {e}")
def get_available_subjects(self, student_id: str) -> List[str]: def get_available_subjects(self, student_id: str) -> List[str]:
"""Get available subjects for the student based on their grade and language""" """Get available subjects for the student based on their grade and language"""
if not self.pgvector: if not self.pgvector:
return [] return []
try:
student_info = self.db_service.get_student_info(student_id) student_info = self.db_service.get_student_info(student_id)
if not student_info: if not student_info:
return [] return []
...@@ -106,6 +154,9 @@ class AgentService: ...@@ -106,6 +154,9 @@ class AgentService:
student_info['grade'], student_info['grade'],
student_info['is_arabic'] student_info['is_arabic']
) )
except Exception as e:
logger.error(f"Error getting available subjects for {student_id}: {e}")
return []
def generate_response( def generate_response(
self, self,
...@@ -113,53 +164,36 @@ class AgentService: ...@@ -113,53 +164,36 @@ class AgentService:
student_id: str, student_id: str,
subject: str = "Science", subject: str = "Science",
model: str = Models.chat, model: str = Models.chat,
temperature: float = 1.0, temperature: float = 0.3,
top_k: int = 3 top_k: int = 3
) -> str: ) -> str:
"""Generate AI response using database memory with subject filtering""" """Generate AI response using database memory with optional retrieval based on question type"""
if not self.is_available(): if not self.is_available():
raise HTTPException(status_code=500, detail="Agent service not available") raise HTTPException(status_code=500, detail="Agent service not available")
try: try:
# Get complete student information from database # Get student info
student_info = self.db_service.get_student_info(student_id) student_info = self.db_service.get_student_info(student_id)
if not student_info: if not student_info:
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")
# Extract student first name only # Extract first name
full_name = student_info.get('student_name', 'الطالب') full_name = student_info.get('student_name', 'الطالب')
student_name = full_name.split()[0] if full_name else "الطالب" student_name = full_name.split()[0] if full_name else "الطالب"
# Print student information # Map nationality
print("----------------- Student Info Retrieved -----------------")
print(f"Student ID: {student_id}")
for key, value in student_info.items():
print(f"{key.capitalize()}: {value}")
print("---------------------------------------------------------")
logger.info(f"Retrieved student info from DB: {student_info} for student: {student_id}")
# Convert nationality string to StudentNationality enum
nationality_lower = student_info['nationality'].lower().strip() nationality_lower = student_info['nationality'].lower().strip()
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: # Add user message to DB
nationality = nationality_mapping[nationality_lower]
logger.info(f"Successfully mapped '{student_info['nationality']}' to {nationality}")
else:
logger.warning(f"Unknown nationality '{student_info['nationality']}' for student {student_id}, defaulting to EGYPTIAN")
nationality = StudentNationality.EGYPTIAN
# 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")
# Get conversation history from database
conversation_history = self.get_conversation_history(student_id) conversation_history = self.get_conversation_history(student_id)
# Create subject-specific system prompt with first name only # Format system prompt
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( formatted_base_prompt = base_system_prompt.format(
student_name=student_name, student_name=student_name,
...@@ -167,38 +201,57 @@ class AgentService: ...@@ -167,38 +201,57 @@ class AgentService:
) )
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 instructions if any
socratic_instructions = self.pedagogy_service.get_socratic_instructions( socratic_instructions = self.pedagogy_service.get_socratic_instructions(
student_info['grade'], student_info['grade'], student_info['nationality']
student_info['nationality']
) )
if socratic_instructions: if socratic_instructions:
subject_specific_prompt += f"\n\n{socratic_instructions}" subject_specific_prompt += f"\n\n{socratic_instructions}"
logger.info(f"Added Socratic questioning instructions for grade {student_info['grade']}, nationality: {student_info['nationality']}")
else:
logger.debug(f"No Socratic instructions for grade {student_info['grade']}")
logger.info(f"Using nationality: {nationality}, subject: {subject}, and student name: {student_name} for student: {student_id}")
# Prepare messages # Prepare messages
messages = [] messages = []
# Check if system message exists
has_system_message = conversation_history and conversation_history[0].get("role") == "system" has_system_message = conversation_history and conversation_history[0].get("role") == "system"
if not has_system_message: if not has_system_message:
messages.append({"role": "system", "content": subject_specific_prompt}) messages.append({"role": "system", "content": subject_specific_prompt})
# Add system message to database
self.add_message_to_history(student_id, subject_specific_prompt, "system") self.add_message_to_history(student_id, subject_specific_prompt, "system")
# Add conversation history
messages.extend(conversation_history) messages.extend(conversation_history)
# Enhanced pgvector enrichment with filtering # ----------------- DYNAMIC RETRIEVAL DECISION -----------------
if self.pgvector: # Ask model to classify if retrieval needed
try: try:
query_embedding = self.openai_service.generate_embedding(user_message) classification_prompt = f"""
صنف السؤال التالي: هل يحتاج لإجابة تعتمد على المنهج الدراسي أو محتوى المادة العلمية المتخصصة؟
رد فقط بـ "YES" لو يحتاج retrieval من المحتوى التعليمي، و "NO" لو مجرد سؤال عام أو عن الشخصية أو محادثة عادية.
أمثلة على أسئلة تحتاج "YES":
- ما هو التمثيل الضوئي؟
- اشرح لي الجهاز الهضمي
- كيف تتكون الأمطار؟
أمثلة على أسئلة تحتاج "NO":
- إنت مين؟
- إزيك؟
- نت عارف انا مين؟
- شكرا ليك
السؤال: "{user_message}"
"""
classification_response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": classification_prompt}],
temperature=0
)
classification_answer = classification_response.choices[0].message.content.strip().upper()
need_retrieval = classification_answer == "YES"
logger.info(f"Classification for '{user_message}': {classification_answer} (need_retrieval: {need_retrieval})")
except Exception as e:
logger.warning(f"Error in classification, defaulting to no retrieval: {e}")
need_retrieval = False
# ----------------- RETRIEVAL (if needed) -----------------
if self.pgvector and need_retrieval:
try:
query_embedding = self.openai_service.generate_embedding(user_message)
neighbors = self.pgvector.search_filtered_nearest( neighbors = self.pgvector.search_filtered_nearest(
query_embedding=query_embedding, query_embedding=query_embedding,
grade=student_info['grade'], grade=student_info['grade'],
...@@ -207,54 +260,40 @@ class AgentService: ...@@ -207,54 +260,40 @@ class AgentService:
limit=top_k limit=top_k
) )
if neighbors: relevant_neighbors = [n for n in neighbors if n['distance'] < 1.3] if neighbors else []
print("\n----------------- Retrieval Results -----------------")
context_message = f"معلومات من المنهج لمادة {subject} للصف {student_info['grade']} للطالب {student_name}:\n" if relevant_neighbors:
for i, n in enumerate(neighbors, 1): context_message = f"معلومات من المنهج لمادة {subject} للصف {student_info['grade']}:\n\n"
unit_info = f" - الوحدة: {n['unit']}" if n['unit'] else "" for n in relevant_neighbors:
concept_info = f" - المفهوم: {n['concept']}" if n['concept'] else "" unit_info = f"الوحدة: {n['unit']}" if n.get('unit') else ""
lesson_info = f" - الدرس: {n['lesson']}" if n['lesson'] else "" concept_info = f"المفهوم: {n['concept']}" if n.get('concept') else ""
lesson_info = f"الدرس: {n['lesson']}" if n.get('lesson') else ""
context_message += f"\n{i}. {unit_info}{concept_info}{lesson_info}\n" context_header = " - ".join(filter(None, [unit_info, concept_info, lesson_info]))
context_message += f"المحتوى: {n['chunk_text'][:200]}...\n" if context_header:
context_message += f"(درجة التشابه: {n['distance']:.3f})\n" context_message += f"**{context_header}**\n"
context_message += f"{n['chunk_text']}\n\n---\n\n"
print(f"Result {i}:") context_message += "استخدم هذه المعلومات لتقديم شرح دقيق ومناسب للطفل."
print(f" Unit: {n['unit']}")
print(f" Concept: {n['concept']}")
print(f" Lesson: {n['lesson']}")
print(f" Chunk Text: {n['chunk_text']}...")
print(f" Distance: {n['distance']:.3f}")
print("-" * 20)
print("-----------------------------------------------------")
messages.append({"role": "system", "content": context_message}) messages.append({"role": "system", "content": context_message})
logger.info(f"Added {len(neighbors)} filtered knowledge base results for subject: {subject}")
else:
print("\n----------------- Retrieval Results -----------------")
print(f"No relevant content found for subject: {subject}, grade: {student_info['grade']}, Arabic: {student_info['is_arabic']}")
print("-----------------------------------------------------")
logger.info(f"No relevant content found for subject: {subject}, grade: {student_info['grade']}, Arabic: {student_info['is_arabic']}")
except Exception as e: except Exception as e:
logger.warning(f"Error using pgvector with filtering: {e}") logger.warning(f"Error using pgvector: {e}")
# Generate AI response # ----------------- GENERATE RESPONSE -----------------
response = self.client.chat.completions.create( response = self.client.chat.completions.create(
model=model, model=model,
messages=messages, messages=messages,
temperature=temperature temperature=temperature
) )
ai_response = response.choices[0].message.content.strip() ai_response = response.choices[0].message.content.strip()
if not ai_response: if not ai_response:
raise ValueError("Empty response from AI model") raise ValueError("Empty response from AI model")
# Add AI response to database # Save AI response
self.add_message_to_history(student_id, ai_response, "assistant") self.add_message_to_history(student_id, ai_response, "assistant")
return ai_response return ai_response
except HTTPException:
raise
except Exception as e: except Exception as e:
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)}")
...@@ -265,6 +304,7 @@ class AgentService: ...@@ -265,6 +304,7 @@ class AgentService:
if not self.pgvector: if not self.pgvector:
raise HTTPException(status_code=400, detail="PGVector service not enabled") raise HTTPException(status_code=400, detail="PGVector service not enabled")
try:
student_info = self.db_service.get_student_info(student_id) student_info = self.db_service.get_student_info(student_id)
if not student_info: if not student_info:
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")
...@@ -276,6 +316,11 @@ class AgentService: ...@@ -276,6 +316,11 @@ class AgentService:
is_arabic=student_info['is_arabic'], is_arabic=student_info['is_arabic'],
limit=top_k limit=top_k
) )
except HTTPException:
raise
except Exception as e:
logger.error(f"Error in search_similar: {e}")
raise HTTPException(status_code=500, detail=f"Search failed: {str(e)}")
def update_student_subject_context(self, student_id: str, subject: str): def update_student_subject_context(self, student_id: str, subject: str):
"""Update the system message for a new subject""" """Update the system message for a new subject"""
...@@ -285,7 +330,8 @@ class AgentService: ...@@ -285,7 +330,8 @@ class AgentService:
return False return False
# Extract student name # Extract student name
student_name = student_info.get('student_name', 'الطالب') full_name = student_info.get('student_name', 'الطالب')
student_name = full_name.split()[0] if full_name else "الطالب"
# Clear existing history to reset context # Clear existing history to reset context
self.db_service.clear_history(student_id) self.db_service.clear_history(student_id)
...@@ -327,10 +373,15 @@ class AgentService: ...@@ -327,10 +373,15 @@ class AgentService:
def export_conversation(self, student_id: str) -> List[Dict[str, str]]: def export_conversation(self, student_id: str) -> List[Dict[str, str]]:
"""Export conversation history for a student""" """Export conversation history for a student"""
try:
return self.get_conversation_history(student_id) return self.get_conversation_history(student_id)
except Exception as e:
logger.error(f"Error exporting conversation for {student_id}: {e}")
return []
def import_conversation(self, messages: List[Dict[str, str]], student_id: str): def import_conversation(self, messages: List[Dict[str, str]], student_id: str):
"""Import conversation history for a student""" """Import conversation history for a student"""
try:
# Clear existing history first # Clear existing history first
self.db_service.clear_history(student_id) self.db_service.clear_history(student_id)
...@@ -340,6 +391,9 @@ class AgentService: ...@@ -340,6 +391,9 @@ class AgentService:
content = message.get("content", "") content = message.get("content", "")
if content: if content:
self.add_message_to_history(student_id, content, role) self.add_message_to_history(student_id, content, role)
except Exception as e:
logger.error(f"Error importing conversation for {student_id}: {e}")
raise
def clear_conversation(self, student_id: str) -> Dict[str, str]: def clear_conversation(self, student_id: str) -> Dict[str, str]:
"""Clear conversation history for a student""" """Clear conversation history for a student"""
...@@ -394,41 +448,69 @@ class AgentService: ...@@ -394,41 +448,69 @@ class AgentService:
# Return a default system prompt - this could be made more sophisticated # Return a default system prompt - this could be made more sophisticated
return "Default system prompt for educational AI assistant" return "Default system prompt for educational AI assistant"
def close(self): def debug_retrieval_pipeline(self, student_id: str, query: str):
"""Close database connection pools""" """Debug function to trace the retrieval pipeline"""
if self.db_service: print("=== RETRIEVAL DEBUG PIPELINE ===")
self.db_service.close_pool()
if self.pgvector:
self.pgvector.close_pool()
try:
# 1. Check student info
student_info = self.db_service.get_student_info(student_id)
print(f"1. Student Info: {student_info}")
# ----------------- Test ----------------- if not student_info:
if __name__ == "__main__": print("❌ No student info found!")
logging.basicConfig(level=logging.INFO) return
agent = AgentService(use_pgvector=True) # 2. Test embedding generation
print(f"2. Testing embedding generation for query: '{query}'")
query_embedding = self.openai_service.generate_embedding(query)
print(f"✅ Generated embedding (length: {len(query_embedding)})")
if agent.is_available(): # 3. Test vector search
try: print(f"3. Testing vector search...")
# Test with chemistry (default) if self.pgvector:
reply = agent.generate_response( neighbors = self.pgvector.search_filtered_nearest(
"هو يعني إيه ذَرّة؟", query_embedding=query_embedding,
student_id="student_001", grade=student_info['grade'],
subject="chemistry" subject="Science",
is_arabic=student_info['is_arabic'],
limit=3
) )
print("AI (Chemistry):", reply)
# Test with math print(f"✅ Found {len(neighbors)} neighbors:")
reply = agent.generate_response( for i, neighbor in enumerate(neighbors):
"إيه هو الجمع؟", print(f" {i+1}. Distance: {neighbor['distance']:.3f}")
student_id="student_001", print(f" Unit: {neighbor.get('unit', 'N/A')}")
subject="math" print(f" Concept: {neighbor.get('concept', 'N/A')}")
print(f" Text preview: {neighbor['chunk_text'][:100]}...")
print()
else:
print("❌ PGVector not available")
# 4. Test full response generation
print(f"4. Testing full response generation...")
response = self.generate_response(
user_message=query,
student_id=student_id,
subject="Science"
) )
print("AI (Math):", reply) print(f"✅ Generated response (first 200 chars): {response[:200]}...")
print("=== DEBUG COMPLETE ===")
except Exception as e: except Exception as e:
print(f"Test failed: {e}") print(f"❌ Debug pipeline failed at step: {e}")
finally: import traceback
agent.close() traceback.print_exc()
else:
print("Agent service not available. Check OPENAI_API_KEY.") def close(self):
\ No newline at end of file """Close database connection pools"""
if self.pool_handler:
try:
self.pool_handler.close_all()
except Exception as e:
logger.error(f"Error closing connection pools: {e}")
def __del__(self):
"""Destructor to ensure connection pools are closed"""
self.close()
\ 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, Dict, Optional, Tuple from typing import List, Dict, Optional, Tuple
import logging import logging
from services.connection_pool import ConnectionPool
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ChatDatabaseService: class ChatDatabaseService:
"""Simple service for managing chat history in PostgreSQL with connection pooling""" """Service for managing chat history using a shared, robust connection pool"""
def __init__(self): def __init__(self, pool_handler: 'ConnectionPoolHandler'):
self.pool = ThreadedConnectionPool( self.pool_handler = pool_handler
minconn=1,
maxconn=20,
host=os.getenv("POSTGRES_HOST", "postgres"),
user=os.getenv("POSTGRES_USER"),
password=os.getenv("POSTGRES_PASSWORD"),
dbname=os.getenv("POSTGRES_DB"),
)
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"""
conn = self.pool.getconn() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur: 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",
...@@ -32,13 +25,10 @@ class ChatDatabaseService: ...@@ -32,13 +25,10 @@ class ChatDatabaseService:
) )
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"""
conn = self.pool.getconn() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur: with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute( cur.execute(
""" """
...@@ -53,18 +43,15 @@ class ChatDatabaseService: ...@@ -53,18 +43,15 @@ class ChatDatabaseService:
return { return {
'student_id': result['student_id'], 'student_id': result['student_id'],
'student_name': result['student_name'], 'student_name': result['student_name'],
'grade': result['grade'], # This is now an integer 'grade': result['grade'],
'is_arabic': result['language'], # Convert language boolean to is_arabic 'is_arabic': result['language'],
'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"""
conn = self.pool.getconn() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur: 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",
...@@ -74,13 +61,10 @@ class ChatDatabaseService: ...@@ -74,13 +61,10 @@ 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"""
conn = self.pool.getconn() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur: with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute( cur.execute(
""" """
...@@ -93,15 +77,11 @@ class ChatDatabaseService: ...@@ -93,15 +77,11 @@ class ChatDatabaseService:
(student_id, limit) (student_id, limit)
) )
results = cur.fetchall() results = cur.fetchall()
# 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"""
conn = self.pool.getconn() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
""" """
...@@ -111,26 +91,20 @@ class ChatDatabaseService: ...@@ -111,26 +91,20 @@ class ChatDatabaseService:
(student_id, role, content) (student_id, role, content)
) )
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"""
conn = self.pool.getconn() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor() as cur: 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,)
) )
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"""
conn = self.pool.getconn() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
""" """
...@@ -147,8 +121,6 @@ class ChatDatabaseService: ...@@ -147,8 +121,6 @@ class ChatDatabaseService:
(student_id, student_id, max_messages) (student_id, student_id, max_messages)
) )
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):
...@@ -170,8 +142,7 @@ class ChatDatabaseService: ...@@ -170,8 +142,7 @@ class ChatDatabaseService:
if updates: if updates:
params.append(student_id) params.append(student_id)
conn = self.pool.getconn() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
f""" f"""
...@@ -182,14 +153,11 @@ class ChatDatabaseService: ...@@ -182,14 +153,11 @@ class ChatDatabaseService:
params params
) )
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"""
conn = self.pool.getconn() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
""" """
...@@ -200,9 +168,3 @@ class ChatDatabaseService: ...@@ -200,9 +168,3 @@ class ChatDatabaseService:
(student_id, student_name, grade, language, nationality) (student_id, student_name, grade, language, nationality)
) )
conn.commit() conn.commit()
finally:
self.pool.putconn(conn)
def close_pool(self):
if self.pool:
self.pool.closeall()
\ No newline at end of file
import os
import psycopg2
from psycopg2.pool import ThreadedConnectionPool
import logging
import time
import threading
from contextlib import contextmanager
logger = logging.getLogger(__name__)
class ConnectionPool:
"""connection pool with health monitoring and automatic recovery"""
def __init__(self, minconn=2, maxconn=20, **db_params):
self.db_params = db_params
self.minconn = minconn
self.maxconn = maxconn
self.pool = None
self._pool_lock = threading.RLock()
self._last_health_check = 0
self._health_check_interval = 300 # 5 minutes
self._connection_timeout = 30
self._idle_timeout = 7200 # 2 hours
self._initialize_pool()
def _initialize_pool(self):
"""Initialize the connection pool with proper parameters"""
try:
# Add connection parameters to handle idle connections
pool_params = {
**self.db_params,
'connect_timeout': self._connection_timeout,
# These parameters help with connection management
'keepalives_idle': 600, # Start keepalives after 10 min idle
'keepalives_interval': 30, # Send keepalive every 30 seconds
'keepalives_count': 3, # Close connection after 3 failed keepalives
}
self.pool = ThreadedConnectionPool(
minconn=self.minconn,
maxconn=self.maxconn,
**pool_params
)
logger.info(f"Connection pool initialized with {self.minconn}-{self.maxconn} connections")
except Exception as e:
logger.error(f"Failed to initialize connection pool: {e}")
raise
def _recreate_pool(self):
"""Recreate the connection pool in case of catastrophic failure"""
with self._pool_lock:
if self.pool:
try:
self.pool.closeall()
except:
pass
logger.warning("Recreating connection pool...")
self._initialize_pool()
def _validate_connection(self, conn):
"""Validate a connection with comprehensive checks"""
try:
# Check if connection is alive
with conn.cursor() as cur:
cur.execute("SELECT 1")
cur.fetchone()
# Check connection status
if conn.closed != 0:
return False
# Check for any pending transactions
if conn.info.transaction_status != psycopg2.extensions.TRANSACTION_STATUS_IDLE:
try:
conn.rollback()
except:
return False
return True
except (psycopg2.OperationalError, psycopg2.InterfaceError,
psycopg2.DatabaseError) as e:
logger.debug(f"Connection validation failed: {e}")
return False
except Exception as e:
logger.warning(f"Unexpected error during connection validation: {e}")
return False
def _health_check(self):
"""Perform periodic health check on the pool"""
current_time = time.time()
if current_time - self._last_health_check < self._health_check_interval:
return
try:
with self._pool_lock:
if self.pool:
# Try to get a connection to test pool health
test_conn = self.pool.getconn()
if test_conn and self._validate_connection(test_conn):
self.pool.putconn(test_conn)
self._last_health_check = current_time
return
else:
# Connection is bad, try to close it
if test_conn:
try:
test_conn.close()
except:
pass
# Pool seems unhealthy, recreate it
logger.warning("Pool health check failed, recreating pool")
self._recreate_pool()
self._last_health_check = current_time
except Exception as e:
logger.error(f"Health check failed: {e}")
try:
self._recreate_pool()
except Exception as recreate_error:
logger.error(f"Failed to recreate pool during health check: {recreate_error}")
@contextmanager
def get_connection(self, max_retries=3):
"""Get a validated connection with automatic retry and recovery"""
self._health_check()
conn = None
for attempt in range(max_retries):
try:
with self._pool_lock:
if not self.pool:
self._initialize_pool()
conn = self.pool.getconn()
if conn and self._validate_connection(conn):
try:
yield conn
return
finally:
if conn:
try:
# Ensure connection is in a clean state
if conn.info.transaction_status != psycopg2.extensions.TRANSACTION_STATUS_IDLE:
conn.rollback()
self.pool.putconn(conn)
except Exception as e:
logger.warning(f"Error returning connection to pool: {e}")
try:
conn.close()
except:
pass
else:
# Bad connection, close it
if conn:
try:
conn.close()
except:
pass
conn = None
except Exception as e:
logger.warning(f"Connection attempt {attempt + 1} failed: {e}")
if conn:
try:
conn.close()
except:
pass
conn = None
if attempt == max_retries - 1:
# Last attempt, try to recreate pool
try:
self._recreate_pool()
except Exception as recreate_error:
logger.error(f"Failed to recreate pool: {recreate_error}")
raise ConnectionError(f"Failed to get valid connection after {max_retries} attempts")
# Wait before retry with exponential backoff
time.sleep(min(2 ** attempt, 10))
def get_valid_conn(self):
"""Legacy method for backward compatibility - get a validated connection"""
max_retries = 3
for attempt in range(max_retries):
try:
with self._pool_lock:
if not self.pool:
self._initialize_pool()
conn = self.pool.getconn()
if conn and self._validate_connection(conn):
return conn
else:
# Bad connection, close it
if conn:
try:
conn.close()
except:
pass
conn = None
except Exception as e:
logger.warning(f"Connection attempt {attempt + 1} failed: {e}")
if conn:
try:
conn.close()
except:
pass
conn = None
if attempt == max_retries - 1:
# Last attempt, try to recreate pool
try:
self._recreate_pool()
except Exception as recreate_error:
logger.error(f"Failed to recreate pool: {recreate_error}")
raise ConnectionError(f"Failed to get valid connection after {max_retries} attempts")
# Wait before retry with exponential backoff
time.sleep(min(2 ** attempt, 10))
def put_conn(self, conn):
"""Return connection to pool - legacy method for backward compatibility"""
try:
if conn:
# Ensure connection is in a clean state
if conn.info.transaction_status != psycopg2.extensions.TRANSACTION_STATUS_IDLE:
conn.rollback()
self.pool.putconn(conn)
except Exception as e:
logger.warning(f"Error returning connection to pool: {e}")
try:
conn.close()
except:
pass
def close_all(self):
"""Close all connections in the pool"""
with self._pool_lock:
if self.pool:
try:
self.pool.closeall()
finally:
self.pool = None
\ 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 logging
from pgvector.psycopg2 import register_vector from pgvector.psycopg2 import register_vector
from services.connection_pool import ConnectionPool
logger = logging.getLogger(__name__)
class PGVectorService:
"""Service for managing embeddings with PostgreSQL pgvector using connection pooling"""
def __init__(self): class PGVectorService:
self.pool = ThreadedConnectionPool( """Service for managing embeddings with PostgreSQL pgvector using a shared, robust connection pool"""
minconn=1,
maxconn=20,
host=os.getenv("POSTGRES_HOST", "postgres"),
user=os.getenv("POSTGRES_USER"),
password=os.getenv("POSTGRES_PASSWORD"),
dbname=os.getenv("POSTGRES_DB"),
)
# Test connection and register vector type to ensure the pool works
conn = self.pool.getconn()
try:
register_vector(conn)
finally:
self.pool.putconn(conn)
def _get_conn_with_vector(self): def __init__(self, pool_handler: 'ConnectionPool'):
"""Get a connection from the pool and register vector type""" self.pool_handler = pool_handler
conn = self.pool.getconn() # Test connection and register vector type
with self.pool_handler.get_connection() as conn:
register_vector(conn) 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"""
conn = self._get_conn_with_vector() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
""" """
...@@ -46,13 +31,10 @@ class PGVectorService: ...@@ -46,13 +31,10 @@ class PGVectorService:
(id, embedding), (id, embedding),
) )
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)"""
conn = self._get_conn_with_vector() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur: with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute( cur.execute(
""" """
...@@ -64,8 +46,6 @@ class PGVectorService: ...@@ -64,8 +46,6 @@ 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,
...@@ -76,8 +56,7 @@ class PGVectorService: ...@@ -76,8 +56,7 @@ 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"""
conn = self._get_conn_with_vector() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur: with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute( cur.execute(
""" """
...@@ -93,8 +72,6 @@ class PGVectorService: ...@@ -93,8 +72,6 @@ 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,
...@@ -128,8 +105,7 @@ class PGVectorService: ...@@ -128,8 +105,7 @@ class PGVectorService:
params.append(query_embedding) params.append(query_embedding)
params.append(limit) params.append(limit)
conn = self._get_conn_with_vector() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur: with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute( cur.execute(
f""" f"""
...@@ -143,13 +119,10 @@ class PGVectorService: ...@@ -143,13 +119,10 @@ 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"""
conn = self._get_conn_with_vector() with self.pool_handler.get_connection() as conn:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur: with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute( cur.execute(
""" """
...@@ -161,9 +134,3 @@ class PGVectorService: ...@@ -161,9 +134,3 @@ 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_pool(self):
if self.pool:
self.pool.closeall()
\ 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