full flow without using n8n

parent 513504cb
from .enums import MessageType, ResponseStatus from .enums import MessageType, ResponseStatus, StudentNationality
from .config import AppConfig from .config import AppConfig
\ No newline at end of file
...@@ -5,15 +5,12 @@ from dotenv import load_dotenv ...@@ -5,15 +5,12 @@ from dotenv import load_dotenv
# Load environment variables from .env file # Load environment variables from .env file
load_dotenv() load_dotenv()
# Configuration Management
@dataclass @dataclass
class AppConfig: class AppConfig:
minio_endpoint: str minio_endpoint: str
minio_access_key: str minio_access_key: str
minio_secret_key: str minio_secret_key: str
minio_bucket: str minio_bucket: str
n8n_webhook_url: str
openai_api_key: str openai_api_key: str
@classmethod @classmethod
...@@ -23,6 +20,5 @@ class AppConfig: ...@@ -23,6 +20,5 @@ class AppConfig:
minio_access_key=os.getenv("MINIO_ACCESS_KEY"), minio_access_key=os.getenv("MINIO_ACCESS_KEY"),
minio_secret_key=os.getenv("MINIO_SECRET_KEY"), minio_secret_key=os.getenv("MINIO_SECRET_KEY"),
minio_bucket=os.getenv("MINIO_BUCKET"), minio_bucket=os.getenv("MINIO_BUCKET"),
n8n_webhook_url=os.getenv("N8N_WEBHOOK_URL"),
openai_api_key=os.getenv("OPENAI_API_KEY") openai_api_key=os.getenv("OPENAI_API_KEY")
) )
\ No newline at end of file
...@@ -8,4 +8,9 @@ class MessageType(str, Enum): ...@@ -8,4 +8,9 @@ class MessageType(str, Enum):
class ResponseStatus(str, Enum): class ResponseStatus(str, Enum):
SUCCESS = "success" SUCCESS = "success"
ERROR = "error" ERROR = "error"
PROCESSING = "processing" PROCESSING = "processing"
\ No newline at end of file
class StudentNationality(str, Enum):
EGYPTIAN = "egyptian"
SAUDI = "saudi"
from fastapi import UploadFile, HTTPException from fastapi import UploadFile, HTTPException
from abc import ABC, abstractmethod
import sys import sys
import io import os
sys.path.append("../") sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from repositories import WebhookClient, StorageRepository
from core import MessageType, ResponseStatus from core import MessageType, ResponseStatus
from services import OpenAIService from repositories import StorageRepository
from services.openai_service import OpenAIService
class MessageHandler(ABC): class AudioMessageHandler:
@abstractmethod def __init__(self, storage_repo: StorageRepository, bucket: str, openai_service: OpenAIService):
def handle(self, **kwargs) -> dict:
pass
class AudioMessageHandler():
def __init__(self, storage_repo: StorageRepository, webhook_client: WebhookClient,
bucket: str, openai_service: OpenAIService):
self.storage_repo = storage_repo self.storage_repo = storage_repo
self.webhook_client = webhook_client
self.bucket = bucket self.bucket = bucket
self.openai_service = openai_service self.openai_service = openai_service
def handle(self, file: UploadFile, **kwargs) -> dict: def handle(self, file: UploadFile, **kwargs) -> dict:
"""Process audio message - transcribe locally using OpenAI Whisper"""
try: try:
print(f"Processing audio file: {file.filename}")
# Read file content # Read file content
file.file.seek(0)
file_content = file.file.read() file_content = file.file.read()
if not file_content: if not self.openai_service.is_available():
raise HTTPException(status_code=400, detail="Empty audio file received") raise HTTPException(status_code=500, detail="OpenAI service not available for transcription")
# Upload original file to MinIO for backup
file_path = f"audio/{file.filename}"
file_stream = io.BytesIO(file_content)
self.storage_repo.upload_file(file_stream, self.bucket, file_path)
print(f"Uploaded {file.filename} to MinIO at {file_path}")
# Try to transcribe the audio using OpenAI service # Transcribe using OpenAI Whisper
try: try:
transcribed_text = self.openai_service.transcribe_audio(file_content, file.filename) transcribed_text = self.openai_service.transcribe_audio(file_content, file.filename)
print(f"Transcription successful: {transcribed_text}")
# Send transcribed text to n8n
payload = {
"type": MessageType.AUDIO,
"message": transcribed_text,
}
self.webhook_client.send_webhook(payload)
print(f"Sent transcribed text to n8n: {transcribed_text[:100]}...")
return { return {
"status": ResponseStatus.SUCCESS, "status": ResponseStatus.SUCCESS,
"message": "Audio transcribed and forwarded to n8n.", "message": "Audio transcribed successfully",
"transcription": transcribed_text "transcription": transcribed_text,
"message_type": MessageType.AUDIO
} }
except Exception as transcription_error: except Exception as transcription_error:
print(f"Transcription failed: {transcription_error}") print(f"Local transcription failed: {transcription_error}")
raise HTTPException(status_code=500, detail=f"Transcription failed: {str(transcription_error)}")
# Fallback: send filename to n8n for processing there except HTTPException:
payload = { # Re-raise HTTP exceptions
"type": MessageType.AUDIO, raise
"filename": file.filename,
"message": "تم تسجيل رسالة صوتية - فشل في التفريغ المحلي",
"transcription_source": "fallback",
"error": str(transcription_error)
}
self.webhook_client.send_webhook(payload)
print(f"Sent filename to n8n as fallback: {file.filename}")
return {
"status": ResponseStatus.SUCCESS,
"message": "Audio uploaded, transcription failed - sent to n8n for processing.",
"filename": file.filename,
"transcription_error": str(transcription_error)
}
except Exception as e: except Exception as e:
print(f"Error processing audio message: {e}") print(f"Error processing audio message: {e}")
raise HTTPException(status_code=500, detail=f"Failed to process audio message: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to process audio: {str(e)}")
\ No newline at end of file finally:
# Reset file pointer for potential reuse
file.file.seek(0)
\ No newline at end of file
from abc import ABC, abstractmethod
import sys import sys
sys.path.append("../") import os
from repositories import WebhookClient sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from core import MessageType, ResponseStatus from core import MessageType, ResponseStatus
class TextMessageHandler():
def __init__(self, webhook_client: WebhookClient):
self.webhook_client = webhook_client
def handle(self, text: str, **kwargs) -> dict:
print(f"Received text message: {text}")
payload = {"type": MessageType.TEXT, "message": text}
self.webhook_client.send_webhook(payload)
return {"status": ResponseStatus.SUCCESS, "message": "Message received and forwarded to n8n."}
class TextMessageHandler:
def __init__(self):
pass # No dependencies needed for text handling
def handle(self, text: str, **kwargs) -> dict:
"""Process text message - simple validation and pass-through"""
try:
if not text or not text.strip():
raise ValueError("Text message cannot be empty")
print(f"Processing text message: {text[:50]}...")
return {
"status": ResponseStatus.SUCCESS,
"message": "Text message processed successfully",
"text": text.strip(),
"message_type": MessageType.TEXT
}
except Exception as e:
print(f"Error processing text message: {e}")
return {
"status": ResponseStatus.ERROR,
"message": f"Failed to process text: {str(e)}",
"message_type": MessageType.TEXT
}
\ No newline at end of file
import os import os
import boto3 from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from botocore.client import Config
import requests
import time
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse from typing import Optional
from starlette.background import BackgroundTask
from pydantic import BaseModel
import tempfile
import uvicorn import uvicorn
import json from core import AppConfig
from botocore.exceptions import ClientError
from typing import Optional, Protocol
import base64
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from core import MessageType, ResponseStatus, AppConfig
from schemas import WebhookResponse, TextMessage
from repositories import StorageRepository, MinIOStorageRepository from repositories import StorageRepository, MinIOStorageRepository
from repositories import WebhookClient, N8NWebhookClient
from handlers import AudioMessageHandler, TextMessageHandler from handlers import AudioMessageHandler, TextMessageHandler
from services import AudioService, ChatService, HealthService, ResponseService, ResponseManager, WebhookService, OpenAIService from services import (
AudioService, ChatService, HealthService, ResponseService,
ResponseManager, OpenAIService, AgentService
)
class DIContainer: class DIContainer:
def __init__(self): def __init__(self):
self.config = AppConfig.from_env() self.config = AppConfig.from_env()
self.storage_repo = MinIOStorageRepository(self.config) self.storage_repo = MinIOStorageRepository(self.config)
self.webhook_client = N8NWebhookClient(self.config.n8n_webhook_url)
self.response_manager = ResponseManager() self.response_manager = ResponseManager()
# Initialize OpenAI service # Initialize OpenAI and Agent services
self.openai_service = OpenAIService() self.openai_service = OpenAIService()
self.agent_service = AgentService()
# Updated services to use OpenAI service # Initialize services
self.audio_service = AudioService(self.storage_repo, self.config.minio_bucket) self.audio_service = AudioService(self.storage_repo, self.config.minio_bucket)
self.chat_service = ChatService( self.chat_service = ChatService(
self.storage_repo, self.storage_repo,
self.webhook_client,
self.response_manager, self.response_manager,
self.config, self.config,
self.openai_service # Pass OpenAI service self.openai_service,
) self.agent_service
self.webhook_service = WebhookService(
self.response_manager,
self.storage_repo,
self.config.minio_bucket,
self.openai_service # Pass OpenAI service
) )
self.response_service = ResponseService(self.response_manager, self.audio_service) self.response_service = ResponseService(self.response_manager, self.audio_service)
self.health_service = HealthService(self.storage_repo, self.config) self.health_service = HealthService(self.storage_repo, self.config)
# FastAPI App Factory
def create_app() -> FastAPI: def create_app() -> FastAPI:
app = FastAPI(title="Unified Chat API") app = FastAPI(title="Unified Chat API with Local Agent")
# Add CORS middleware # Add CORS middleware
app.add_middleware( app.add_middleware(
...@@ -72,21 +52,20 @@ def create_app() -> FastAPI: ...@@ -72,21 +52,20 @@ def create_app() -> FastAPI:
# Print configuration # Print configuration
print("MinIO Endpoint:", container.config.minio_endpoint) print("MinIO Endpoint:", container.config.minio_endpoint)
print("MinIO Bucket:", container.config.minio_bucket) print("MinIO Bucket:", container.config.minio_bucket)
print("n8n Webhook URL:", container.config.n8n_webhook_url) print("OpenAI Service Available:", container.openai_service.is_available())
print("Agent Service Available:", container.agent_service.is_available())
@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)
): ):
"""Handles incoming chat messages (either text or audio). Forwards the message to the n8n webhook.""" """
Handles incoming chat messages (either text or audio).
Generates responses locally using the agent service.
"""
return container.chat_service.process_message(file, text) return container.chat_service.process_message(file, text)
@app.post("/n8n_webhook_receiver")
async def n8n_webhook_receiver(response: WebhookResponse):
"""Receives and processes webhook data from n8n."""
return container.webhook_service.process_webhook_response(response)
@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."""
...@@ -94,26 +73,93 @@ def create_app() -> FastAPI: ...@@ -94,26 +73,93 @@ def create_app() -> FastAPI:
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
"""Health check endpoint""" """Health check endpoint with agent service status"""
return container.health_service.get_health_status() health_status = container.health_service.get_health_status()
# Add agent service status
health_status.update({
"openai_service_status": "available" if container.openai_service.is_available() else "unavailable",
"agent_service_status": "available" if container.agent_service.is_available() else "unavailable"
})
return health_status
# Agent management endpoints
@app.get("/conversation/stats")
async def get_conversation_stats(conversation_id: str = "default"):
"""Get conversation statistics"""
return container.chat_service.get_agent_stats(conversation_id)
@app.post("/conversation/clear")
async def clear_conversation(conversation_id: str = "default"):
"""Clear conversation history"""
return container.chat_service.clear_conversation(conversation_id)
@app.post("/agent/system-prompt")
async def set_system_prompt(request: dict):
"""Update the agent's system prompt"""
prompt = request.get("prompt", "")
if not prompt:
raise HTTPException(status_code=400, detail="System prompt cannot be empty")
return container.chat_service.set_system_prompt(prompt)
@app.get("/agent/system-prompt")
async def get_system_prompt():
"""Get the current system prompt"""
return {
"system_prompt": container.agent_service.system_prompt,
"status": "success"
}
@app.get("/conversation/export")
async def export_conversation(conversation_id: str = "default"):
"""Export conversation history"""
history = container.agent_service.export_conversation(conversation_id)
return {
"conversation_id": conversation_id,
"messages": history,
"total_messages": len(history)
}
@app.post("/conversation/import")
async def import_conversation(request: dict):
"""Import conversation history"""
conversation_id = request.get("conversation_id", "default")
messages = request.get("messages", [])
if not messages:
raise HTTPException(status_code=400, detail="Messages list cannot be empty")
container.agent_service.import_conversation(messages, conversation_id)
return {
"status": "success",
"message": f"Imported {len(messages)} messages to conversation {conversation_id}"
}
@app.get("/") @app.get("/")
async def root(): async def root():
"""Root endpoint with API info""" """Root endpoint with API info"""
return { return {
"service": "Unified Chat API", "service": "Unified Chat API with Local Agent",
"version": "1.0.0", "version": "2.1.0",
"description": "Unified backend for audio/text chat with an agent, powered by n8n.", "description": "Unified backend for audio/text chat with a local AI agent.",
"features": [
"Local AI agent responses using OpenAI GPT",
"Audio transcription using OpenAI Whisper",
"Text-to-speech using OpenAI TTS",
"Conversation history management"
],
"endpoints": { "endpoints": {
"chat": "/chat (accepts audio or text, forwards to n8n)", "chat": "/chat (accepts audio or text, generates local agent response)",
"n8n_webhook_receiver": "/n8n-webhook-receiver (receives agent responses from n8n)",
"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)",
"clear_conversation": "/conversation/clear (clear conversation history)",
"set_system_prompt": "/agent/system-prompt (update agent system prompt)",
"export_conversation": "/conversation/export (export conversation history)",
"health": "/health (service health check)"
}
} }
return app return app
# Application entry point # Application entry point
app = create_app() app = create_app()
......
...@@ -3,5 +3,5 @@ from .chat_service import ChatService ...@@ -3,5 +3,5 @@ from .chat_service import ChatService
from .health_service import HealthService from .health_service import HealthService
from .response_service import ResponseService from .response_service import ResponseService
from .response_manager import ResponseManager from .response_manager import ResponseManager
from .webhook_service import WebhookService
from .openai_service import OpenAIService from .openai_service import OpenAIService
from .agent_service import AgentService
\ No newline at end of file
import logging
import os
from typing import List, Dict
from fastapi import HTTPException
from openai import OpenAI
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from core import StudentNationality
logger = logging.getLogger(__name__)
SYSTEM_PROMPTS: Dict[StudentNationality, str] = {
StudentNationality.EGYPTIAN: """إنت مدرس كيميا لطفل في ابتدائي. رد باللهجة المصريّة السهلة. كلّم الطفل كأنك بتحكي له بصوت طبيعي. خلي الجمل قصيرة وواضحة، وما تحشرش معلومات كتير في جملة واحدة. ما تقولش الحاجات البديهية اللي هو عارفها زي "المَيَّه بتتشرب". قول المعلومة مرّة واحدة من غير تكرار. لو هتدي مثال أو تشبيه، يكون حاجة جديدة بتوضّح الفكرة، مش مجرد إعادة. خلّي المثال بسيط زي لعبة، شكل، أو صورة في الخيال. اكتب الكلمات زي ما بتنطق بالتشكيل الصح (زي: مَيَّه، أوكسچين). لو فيه رموز كيميائية زي H2O أو CO2 اكتبها زي ما هي. خلي الشرح شبه حكاية صغيرة أو صورة في دماغ الطفل، مش زي شرح كتاب.""",
StudentNationality.SAUDI: """إنت مُعلّم كيميا لطفل في ابتدائي. رد باللهجة السعوديّة الدارجة والبسيطة. كَلّم الطفل كأنك تحاكيه وجهاً لوجه بصوت طبيعي. خل الجمل قصار وواضحة، لا تكدّس معلومات كثير في جملة وحدة. لا تقول أشياء بديهية يعرفها مثل "المُوَيَّه نشربها". أعط المعلومة مرّة وحدة بلا تكرار. لو بتضرب مثال أو تشبيه، يكون زاوية جديدة توضّح الفكرة، ما يكون تكرار. خلّ المثال شي بسيط يقرّب المعنى للطفل: زي لعبة، حركة، أو صورة يتخيّلها. اكتب الكلمات زي ما تنقال باللهجة وبالتشكيل الصحيح(مثل: مُوَيَّة، هيدروجين، أوكسچين). لو فيه رموز كيميائية مثل H2O أو CO2 اكتُبها زي ما هي. الشرح يكون كأنه سواليف بسيطة أو حكاية تخلي الطفل يتصوّرها، مو زي كلام كتاب مدرسي."""
}
class AgentService:
"""Service class for handling AI agent conversations using OpenAI GPT"""
def __init__(self):
self.api_key = os.getenv("OPENAI_API_KEY")
if not self.api_key:
logger.warning("Warning: OPENAI_API_KEY not found. Agent service will be disabled.")
self.client = None
else:
self.client = OpenAI(api_key=self.api_key)
self.conversations: Dict[str, List[Dict[str, str]]] = {}
def is_available(self) -> bool:
return self.client is not None
def get_conversation_history(self, conversation_id: str = "default") -> List[Dict[str, str]]:
return self.conversations.get(conversation_id, [])
def add_message_to_history(self, message: str, role: str = "user", conversation_id: str = "default"):
if conversation_id not in self.conversations:
self.conversations[conversation_id] = []
self.conversations[conversation_id].append({"role": role, "content": message})
if len(self.conversations[conversation_id]) > 20:
messages = self.conversations[conversation_id]
if messages[0].get("role") == "system":
self.conversations[conversation_id] = [messages[0]] + messages[-19:]
else:
self.conversations[conversation_id] = messages[-20:]
def generate_response(
self,
user_message: str,
conversation_id: str = "default",
model: str = "gpt-5-nano",
temperature: float = 1.0,
nationality: StudentNationality = StudentNationality.EGYPTIAN
) -> str:
if not self.is_available():
raise HTTPException(status_code=500, detail="Agent service not available")
try:
self.add_message_to_history(user_message, "user", conversation_id)
# 🟢 اختر الـ system prompt المناسب للجنسية
system_prompt = SYSTEM_PROMPTS.get(nationality, SYSTEM_PROMPTS[StudentNationality.EGYPTIAN])
messages = []
conversation_history = self.get_conversation_history(conversation_id)
if not conversation_history or conversation_history[0].get("role") != "system":
messages.append({"role": "system", "content": system_prompt})
self.conversations.setdefault(conversation_id, []).insert(0, {
"role": "system",
"content": system_prompt
})
messages.extend(conversation_history)
response = self.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature
)
ai_response = response.choices[0].message.content.strip()
if not ai_response:
raise ValueError("Empty response from AI model")
self.add_message_to_history(ai_response, "assistant", conversation_id)
return ai_response
except Exception as e:
logger.error(f"Error generating AI response: {e}")
raise HTTPException(status_code=500, detail=f"AI response generation failed: {str(e)}")
# ----------------- Suggested Test -----------------
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
agent = AgentService()
if agent.is_available():
reply = agent.generate_response("هو يعني إيه ذَرّة؟", model="gpt-5-nano", nationality=StudentNationality.EGYPTIAN)
print("AI:", reply)
else:
print("Agent service not available. Check OPENAI_API_KEY.")
...@@ -3,39 +3,127 @@ from typing import Optional ...@@ -3,39 +3,127 @@ from typing import Optional
import sys import sys
import os import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from core import MessageType, AppConfig from core import MessageType, AppConfig, StudentNationality
from repositories import StorageRepository, WebhookClient from repositories import StorageRepository
from services.response_manager import ResponseManager from services.response_manager import ResponseManager
from services.openai_service import OpenAIService from services.openai_service import OpenAIService
from services.agent_service import AgentService
class ChatService: class ChatService:
def __init__(self, storage_repo: StorageRepository, webhook_client: WebhookClient, def __init__(self, storage_repo: StorageRepository, response_manager: ResponseManager,
response_manager: ResponseManager, config: AppConfig, openai_service: OpenAIService): config: AppConfig, openai_service: OpenAIService, agent_service: AgentService):
from handlers import AudioMessageHandler, TextMessageHandler from handlers import AudioMessageHandler, TextMessageHandler
self.storage_repo = storage_repo self.storage_repo = storage_repo
self.webhook_client = webhook_client
self.response_manager = response_manager self.response_manager = response_manager
self.config = config self.config = config
self.openai_service = openai_service self.openai_service = openai_service
self.agent_service = agent_service
# Message handlers with OpenAI service dependency # Message handlers (no webhook dependencies)
self.handlers = { self.handlers = {
MessageType.AUDIO: AudioMessageHandler( MessageType.AUDIO: AudioMessageHandler(
storage_repo, storage_repo,
webhook_client,
config.minio_bucket, config.minio_bucket,
openai_service # Pass OpenAI service openai_service
), ),
MessageType.TEXT: TextMessageHandler(webhook_client) MessageType.TEXT: TextMessageHandler()
} }
def process_message(self, file: Optional[UploadFile] = None, text: Optional[str] = None) -> dict: def process_message(self, file: Optional[UploadFile] = None, text: Optional[str] = None) -> dict:
"""Process incoming message and generate agent response directly"""
self.response_manager.clear_response() self.response_manager.clear_response()
if file and file.filename: try:
return self.handlers[MessageType.AUDIO].handle(file=file) # Process the input message first
elif text: if file and file.filename:
return self.handlers[MessageType.TEXT].handle(text=text) # Handle audio message - transcribe first
else: result = self.handlers[MessageType.AUDIO].handle(file=file)
raise HTTPException(status_code=400, detail="No text or audio file provided.") if result.get("status") == "success":
\ No newline at end of file # Get transcribed text from the result
user_message = result.get("transcription", "")
if not user_message:
# Fallback message if transcription failed
user_message = "تم إرسال رسالة صوتية - فشل في التفريغ المحلي"
else:
raise HTTPException(status_code=400, detail="Failed to process audio message")
elif text:
# Handle text message
result = self.handlers[MessageType.TEXT].handle(text=text)
user_message = text
else:
raise HTTPException(status_code=400, detail="No text or audio file provided.")
# Generate agent response using the local agent service
try:
agent_response = self.agent_service.generate_response(user_message, nationality=StudentNationality.EGYPTIAN)
# Generate TTS audio from the response
audio_filename = self._generate_and_upload_audio(agent_response)
# Store response for retrieval
self.response_manager.store_response(agent_response, audio_filename)
print(f"Generated agent response: {agent_response[:100]}...")
return {
"status": "success",
"message": "Message processed and agent response ready",
"agent_response": agent_response,
"audio_filename": audio_filename
}
except Exception as agent_error:
print(f"Agent service error: {agent_error}")
raise HTTPException(status_code=500, detail=f"Agent response generation failed: {str(agent_error)}")
except Exception as e:
print(f"Error processing message: {e}")
raise HTTPException(status_code=500, detail=f"Failed to process message: {str(e)}")
def _generate_and_upload_audio(self, text: str) -> str:
"""Generate audio from text and upload to MinIO, return filename"""
try:
import time
# Generate audio using OpenAI service
temp_file_path = self.openai_service.generate_speech(text)
# Generate unique filename for MinIO
timestamp = int(time.time())
filename = f"agent_response_{timestamp}.mp3"
minio_file_path = f"audio/{filename}"
print(f"Uploading generated audio to MinIO: {minio_file_path}")
# Upload to MinIO
with open(temp_file_path, 'rb') as audio_file:
self.storage_repo.upload_file(audio_file, self.config.minio_bucket, minio_file_path)
# Clean up temporary file
self.openai_service.cleanup_temp_file(temp_file_path)
print(f"Successfully generated and uploaded TTS audio: {filename}")
return filename
except Exception as e:
print(f"Error generating and uploading audio: {e}")
# Don't fail the entire request if TTS fails
return None
def get_agent_stats(self, conversation_id: str = "default") -> dict:
"""Get conversation statistics from agent service"""
return self.agent_service.get_conversation_stats(conversation_id)
def clear_conversation(self, conversation_id: str = "default"):
"""Clear conversation history"""
self.agent_service.clear_conversation(conversation_id)
return {"status": "success", "message": f"Conversation {conversation_id} cleared"}
def set_system_prompt(self, prompt: str):
"""Update the agent's system prompt"""
self.agent_service.set_system_prompt(prompt)
return {"status": "success", "message": "System prompt updated"}
\ No newline at end of file
...@@ -13,12 +13,10 @@ class HealthService: ...@@ -13,12 +13,10 @@ class HealthService:
def get_health_status(self) -> dict: def get_health_status(self) -> dict:
minio_status = self.storage_repo.check_connection(self.config.minio_bucket) minio_status = self.storage_repo.check_connection(self.config.minio_bucket)
n8n_status = "configured" if self.config.n8n_webhook_url else "not_configured"
return { return {
"status": "healthy", "status": "healthy",
"service": "unified-chat-api", "service": "unified-chat-api-local-agent",
"minio_status": minio_status, "minio_status": minio_status,
"n8n_status": n8n_status,
"timestamp": time.time() "timestamp": time.time()
} }
\ No newline at end of file
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> الدردشة </title> <title>الدردشة</title>
<style> <style>
body { body {
font-family: 'Arial', sans-serif; font-family: 'Arial', sans-serif;
...@@ -140,7 +140,7 @@ ...@@ -140,7 +140,7 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1> الدردشة </h1> <h1>الدردشة</h1>
<div class="controls"> <div class="controls">
<div class="text-controls"> <div class="text-controls">
...@@ -163,21 +163,14 @@ ...@@ -163,21 +163,14 @@
// Configuration // Configuration
const Config = { const Config = {
BACKEND_URL: "http://localhost:8000/chat", BACKEND_URL: "http://localhost:8000/chat",
AUDIO_RESPONSE_URL: "http://localhost:8000/get-audio-response", AUDIO_RESPONSE_URL: "http://localhost:8000/get-audio-response"
POLLING_INTERVAL: 3000
}; };
// Enums // Enums
const MessageType = {
TEXT: 'text',
AUDIO: 'audio'
};
const RecordingState = { const RecordingState = {
IDLE: 'idle', IDLE: 'idle',
RECORDING: 'recording', RECORDING: 'recording',
PROCESSING: 'processing', PROCESSING: 'processing'
WAITING: 'waiting'
}; };
const StatusType = { const StatusType = {
...@@ -204,48 +197,6 @@ ...@@ -204,48 +197,6 @@
} }
} }
// Abstract Command Pattern
class Command {
execute() {
throw new Error('Execute method must be implemented');
}
}
// Concrete Commands
class SendTextCommand extends Command {
constructor(chatService, text) {
super();
this.chatService = chatService;
this.text = text;
}
async execute() {
return await this.chatService.sendTextMessage(this.text);
}
}
class StartRecordingCommand extends Command {
constructor(audioRecorder) {
super();
this.audioRecorder = audioRecorder;
}
async execute() {
await this.audioRecorder.startRecording();
}
}
class StopRecordingCommand extends Command {
constructor(audioRecorder) {
super();
this.audioRecorder = audioRecorder;
}
execute() {
this.audioRecorder.stopRecording();
}
}
// Message Factory // Message Factory
class MessageFactory { class MessageFactory {
static createUserMessage(text) { static createUserMessage(text) {
...@@ -392,7 +343,6 @@ ...@@ -392,7 +343,6 @@
} }
onRecordingComplete(audioBlob) { onRecordingComplete(audioBlob) {
// This will be handled by the mediator
this.uiManager.mediator.handleAudioRecorded(audioBlob); this.uiManager.mediator.handleAudioRecorded(audioBlob);
} }
} }
...@@ -401,7 +351,7 @@ ...@@ -401,7 +351,7 @@
class ChatUIManager { class ChatUIManager {
constructor() { constructor() {
this.initializeElements(); this.initializeElements();
this.mediator = null; // Will be set by mediator this.mediator = null;
} }
initializeElements() { initializeElements() {
...@@ -463,21 +413,12 @@ ...@@ -463,21 +413,12 @@
this.startBtn.classList.remove('recording'); this.startBtn.classList.remove('recording');
this.startBtn.classList.add('processing'); this.startBtn.classList.add('processing');
this.stopBtn.disabled = true; this.stopBtn.disabled = true;
break; this.textInput.disabled = true;
this.sendTextBtn.disabled = true;
case RecordingState.WAITING:
this.disableAllInputs();
break; break;
} }
} }
disableAllInputs() {
this.textInput.disabled = true;
this.sendTextBtn.disabled = true;
this.startBtn.disabled = true;
this.stopBtn.disabled = true;
}
clearTextInput() { clearTextInput() {
this.textInput.value = ''; this.textInput.value = '';
} }
...@@ -512,7 +453,7 @@ ...@@ -512,7 +453,7 @@
} }
} }
// Chat Service // Chat Service - Simplified for direct response handling
class ChatService { class ChatService {
constructor(apiClient, messageManager, uiManager) { constructor(apiClient, messageManager, uiManager) {
this.apiClient = apiClient; this.apiClient = apiClient;
...@@ -532,10 +473,15 @@ ...@@ -532,10 +473,15 @@
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('text', text); formData.append('text', text);
await this.apiClient.sendFormData(Config.BACKEND_URL, formData); const response = await this.apiClient.sendFormData(Config.BACKEND_URL, formData);
this.uiManager.showStatus('تم إرسال النص. في انتظار رد المساعد...', StatusType.PROCESSING); if (response.status === 'success') {
return true; // Response is ready immediately, get it
await this.getAgentResponse();
return true;
} else {
throw new Error(response.message || 'Unknown error');
}
} catch (error) { } catch (error) {
Logger.log(`Error sending text chat: ${error.message}`, 'error'); Logger.log(`Error sending text chat: ${error.message}`, 'error');
this.uiManager.showStatus(`خطأ: ${error.message}`, StatusType.ERROR); this.uiManager.showStatus(`خطأ: ${error.message}`, StatusType.ERROR);
...@@ -547,81 +493,41 @@ ...@@ -547,81 +493,41 @@
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`);
await this.apiClient.sendFormData(Config.BACKEND_URL, formData); const response = await this.apiClient.sendFormData(Config.BACKEND_URL, formData);
this.uiManager.showStatus('تم إرسال الصوت. في انتظار رد المساعد...', StatusType.PROCESSING); if (response.status === 'success') {
return true; // Response is ready immediately, get it
await this.getAgentResponse();
return true;
} else {
throw new Error(response.message || 'Unknown error');
}
} catch (error) { } catch (error) {
Logger.log(`Error sending voice chat: ${error.message}`, '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;
} }
} }
}
// Response Polling Service
class ResponsePollingService {
constructor(apiClient, messageManager, uiManager) {
this.apiClient = apiClient;
this.messageManager = messageManager;
this.uiManager = uiManager;
this.pollingInterval = null;
}
startPolling() {
this.pollingInterval = setTimeout(() => this.pollForResponse(), Config.POLLING_INTERVAL);
}
stopPolling() {
if (this.pollingInterval) {
clearTimeout(this.pollingInterval);
this.pollingInterval = null;
}
}
async pollForResponse() { async getAgentResponse() {
try { try {
this.uiManager.showStatus('جاري جلب رد المساعد...', StatusType.PROCESSING);
const { agentText, audioBlob } = await this.apiClient.fetchAudioResponse(); const { agentText, audioBlob } = await this.apiClient.fetchAudioResponse();
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'); Logger.log('✓ Agent response received and played.', 'success');
this.uiManager.showStatus('✓ Response received! Ready for the next message.', StatusType.SUCCESS); this.uiManager.showStatus('✓ تم استلام الرد! جاهز للرسالة التالية.', StatusType.SUCCESS);
// Notify mediator that response is complete
this.uiManager.mediator.handleResponseComplete();
} catch (error) { } catch (error) {
if (error.message && error.message.includes('not ready')) { Logger.log(`Error fetching agent response: ${error.message}`, 'error');
Logger.log('Agent response not ready yet, retrying...', 'info'); this.uiManager.showStatus(`خطأ في الشبكة: ${error.message}`, StatusType.ERROR);
this.startPolling(); // Continue polling
} else {
Logger.log(`Error fetching agent response: ${error.message}`, 'error');
this.uiManager.showStatus(`Network error: ${error.message}`, StatusType.ERROR);
this.uiManager.mediator.handleResponseComplete(); // Reset UI even on error
}
} }
} }
} }
// Command Invoker // Mediator Pattern - Simplified
class CommandInvoker {
constructor() {
this.history = [];
}
executeCommand(command) {
this.history.push(command);
return command.execute();
}
getHistory() {
return this.history.slice();
}
}
// Mediator Pattern - Coordinates all components
class ChatMediator { class ChatMediator {
constructor() { constructor() {
this.apiClient = new APIClient(); this.apiClient = new APIClient();
...@@ -629,11 +535,8 @@ ...@@ -629,11 +535,8 @@
this.uiManager = new ChatUIManager(); this.uiManager = new ChatUIManager();
this.messageManager = new MessageManager(this.uiManager); this.messageManager = new MessageManager(this.uiManager);
this.chatService = new ChatService(this.apiClient, this.messageManager, this.uiManager); this.chatService = new ChatService(this.apiClient, this.messageManager, this.uiManager);
this.responsePollingService = new ResponsePollingService(this.apiClient, this.messageManager, this.uiManager);
this.audioRecorder = new AudioRecorder(this.stateMachine, this.uiManager); this.audioRecorder = new AudioRecorder(this.stateMachine, this.uiManager);
this.commandInvoker = new CommandInvoker();
// Set mediator reference
this.uiManager.mediator = this; this.uiManager.mediator = this;
this.initializeEventHandlers(); this.initializeEventHandlers();
...@@ -641,7 +544,6 @@ ...@@ -641,7 +544,6 @@
} }
initializeEventHandlers() { initializeEventHandlers() {
// Text input events
this.uiManager.sendTextBtn.addEventListener('click', () => this.handleSendText()); this.uiManager.sendTextBtn.addEventListener('click', () => this.handleSendText());
this.uiManager.textInput.addEventListener('keypress', (e) => { this.uiManager.textInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
...@@ -649,7 +551,6 @@ ...@@ -649,7 +551,6 @@
} }
}); });
// Audio recording events
this.uiManager.startBtn.addEventListener('click', () => this.handleStartRecording()); this.uiManager.startBtn.addEventListener('click', () => this.handleStartRecording());
this.uiManager.stopBtn.addEventListener('click', () => this.handleStopRecording()); this.uiManager.stopBtn.addEventListener('click', () => this.handleStopRecording());
} }
...@@ -665,43 +566,21 @@ ...@@ -665,43 +566,21 @@
if (!text) return; if (!text) return;
this.uiManager.clearTextInput(); this.uiManager.clearTextInput();
this.stateMachine.setState(RecordingState.WAITING); const success = await this.chatService.sendTextMessage(text);
// UI state is handled within the chat service
const command = new SendTextCommand(this.chatService, text);
const success = await this.commandInvoker.executeCommand(command);
if (success) {
this.responsePollingService.startPolling();
} else {
this.stateMachine.setState(RecordingState.IDLE);
}
} }
async handleStartRecording() { async handleStartRecording() {
const command = new StartRecordingCommand(this.audioRecorder); await this.audioRecorder.startRecording();
await this.commandInvoker.executeCommand(command);
} }
handleStopRecording() { handleStopRecording() {
const command = new StopRecordingCommand(this.audioRecorder); this.audioRecorder.stopRecording();
this.commandInvoker.executeCommand(command);
} }
async handleAudioRecorded(audioBlob) { async handleAudioRecorded(audioBlob) {
this.messageManager.addUserMessage("تم إرسال الرسالة الصوتية."); this.messageManager.addUserMessage("تم إرسال الرسالة الصوتية.");
this.stateMachine.setState(RecordingState.WAITING);
const success = await this.chatService.sendAudioMessage(audioBlob); const success = await this.chatService.sendAudioMessage(audioBlob);
if (success) {
this.responsePollingService.startPolling();
} else {
this.stateMachine.setState(RecordingState.IDLE);
}
}
handleResponseComplete() {
this.responsePollingService.stopPolling();
this.stateMachine.setState(RecordingState.IDLE); this.stateMachine.setState(RecordingState.IDLE);
} }
} }
...@@ -720,7 +599,7 @@ ...@@ -720,7 +599,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('Chat application initialized successfully!'); console.log('Simplified chat application initialized successfully!');
}); });
</script> </script>
</body> </body>
......
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