Compare commits

..
2 Commits
Author SHA1 Message Date
xavier 9288e849af feat: intégration du moteur d'IA (ai.py) et génération de la première réponse
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-07 11:00:49 +02:00
xavier 00a92de1c4 fix: alignement de l'alias de routage FastAPI sur le dossier physique assets pour corriger l'affichage du logo
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-07 10:50:50 +02:00
6 changed files with 133 additions and 53 deletions

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
from openai import OpenAI
from fastapi import HTTPException
from .models import SystemSettings
def get_ai_response(messages: list, settings: SystemSettings) -> str:
"""
Route la conversation vers le premier fournisseur IA disponible configuré par l'admin.
Prend l'historique des messages et retourne le texte généré.
"""
if not settings:
raise HTTPException(status_code=500, detail="Configuration système introuvable.")
# Formatage de l'historique pour l'API (OpenAI compatible)
formatted_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
# 1. Test OpenRouter (Idéal car donne accès à tout)
if settings.openrouter_api_key:
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=settings.openrouter_api_key,
)
model = "google/gemini-pro" # Fallback par défaut via OpenRouter
# 2. Test DeepSeek
elif settings.deepseek_api_key:
client = OpenAI(
base_url="https://api.deepseek.com/v1",
api_key=settings.deepseek_api_key,
)
model = "deepseek-chat"
# 3. Test Groq
elif settings.groq_api_key:
client = OpenAI(
base_url="https://api.groq.com/openai/v1",
api_key=settings.groq_api_key,
)
model = "llama3-8b-8192"
else:
raise HTTPException(status_code=400, detail="Aucun moteur IA (OpenRouter, DeepSeek, Groq) n'est configuré avec une clé API.")
try:
response = client.chat.completions.create(
model=model,
messages=formatted_messages,
# Identifiant unique de l'application pour OpenRouter
extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "AETHAS38 Multi-IA"} if settings.openrouter_api_key else {}
)
return response.choices[0].message.content
except Exception as e:
print(f"Erreur API IA : {str(e)}")
raise HTTPException(status_code=502, detail="Erreur de communication avec le fournisseur IA.")
+35 -2
View File
@@ -11,6 +11,8 @@ from .models import User, Project, Message
from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token
from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse
from .models import User, Project, Message, SystemSettings from .models import User, Project, Message, SystemSettings
from .ai import get_ai_response
from .models import User, Project, Message, SystemSettings
# Création des tables dans la base de données # Création des tables dans la base de données
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
@@ -20,7 +22,9 @@ app = FastAPI(title="AETHAS38 - Orchestrateur Multi-IA")
# Configuration des fichiers statiques (images, css locaux, etc.) # Configuration des fichiers statiques (images, css locaux, etc.)
assets_path = os.path.join(os.getcwd(), "frontend", "assets") assets_path = os.path.join(os.getcwd(), "frontend", "assets")
os.makedirs(assets_path, exist_ok=True) # Crée le dossier s'il n'existe pas os.makedirs(assets_path, exist_ok=True) # Crée le dossier s'il n'existe pas
app.mount("/static", StaticFiles(directory=assets_path), name="static") app.mount("/assets", StaticFiles(directory=assets_path), name="assets")
# Définition des routes du logo dans login
def is_setup_required(db: Session) -> bool: def is_setup_required(db: Session) -> bool:
"""Vérifie si la base de données contient au moins un administrateur.""" """Vérifie si la base de données contient au moins un administrateur."""
@@ -243,4 +247,33 @@ def create_message(project_id: int, message: MessageCreate, db: Session = Depend
db.add(new_message) db.add(new_message)
db.commit() db.commit()
db.refresh(new_message) db.refresh(new_message)
return new_message return new_message
@app.post("/api/projects/{project_id}/messages", response_model=List[MessageResponse])
def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Ajoute un message, interroge l'IA et retourne l'historique mis à jour."""
project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not project:
raise HTTPException(status_code=404, detail="Projet introuvable")
# 1. Sauvegarde du message de l'utilisateur
user_message = Message(role=message.role, content=message.content, project_id=project_id)
db.add(user_message)
db.commit()
# 2. Récupération de l'historique complet pour le contexte
history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
# 3. Récupération des clés API depuis la base
settings = db.query(SystemSettings).first()
# 4. Interrogation de l'IA via notre routeur (ai.py)
ai_response_text = get_ai_response(history, settings)
# 5. Sauvegarde de la réponse de l'IA
ai_message = Message(role="assistant", content=ai_response_text, project_id=project_id)
db.add(ai_message)
db.commit()
# On retourne le nouvel historique contenant la question ET la réponse
return db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
+14 -11
View File
@@ -31,7 +31,7 @@
<aside :class="['absolute top-0 left-0 h-full w-72 bg-aethas-panel border-r border-gray-700 flex flex-col transition-transform duration-300', isSidebarOpen ? 'translate-x-0' : '-translate-x-full']"> <aside :class="['absolute top-0 left-0 h-full w-72 bg-aethas-panel border-r border-gray-700 flex flex-col transition-transform duration-300', isSidebarOpen ? 'translate-x-0' : '-translate-x-full']">
<div class="p-4 border-b border-gray-700 flex items-center justify-between"> <div class="p-4 border-b border-gray-700 flex items-center justify-between">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<img src="/static/logo.png" alt="Logo" class="w-10 h-10 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0"> <img src="/assets/logo.png" alt="Logo" class="w-10 h-10 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0">
<span class="font-bold text-lg tracking-wider neon-text">AETHAS38</span> <span class="font-bold text-lg tracking-wider neon-text">AETHAS38</span>
</div> </div>
<button @click="toggleSidebar" class="md:hidden text-gray-400 hover:text-white"><i data-lucide="x" class="w-5 h-5"></i></button> <button @click="toggleSidebar" class="md:hidden text-gray-400 hover:text-white"><i data-lucide="x" class="w-5 h-5"></i></button>
@@ -95,7 +95,7 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button @click="toggleSidebar" class="text-gray-400 hover:text-white transition-colors p-1.5 rounded hover:bg-gray-700"><i data-lucide="menu" class="w-6 h-6"></i></button> <button @click="toggleSidebar" class="text-gray-400 hover:text-white transition-colors p-1.5 rounded hover:bg-gray-700"><i data-lucide="menu" class="w-6 h-6"></i></button>
<div v-show="!isSidebarOpen" class="flex items-center gap-2 transition-opacity duration-300"> <div v-show="!isSidebarOpen" class="flex items-center gap-2 transition-opacity duration-300">
<img src="/static/logo.png" alt="Logo" class="w-8 h-8 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0"> <img src="/assets/logo.png" alt="Logo" class="w-8 h-8 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0">
<span class="font-bold text-lg neon-text">AETHAS38</span> <span class="font-bold text-lg neon-text">AETHAS38</span>
</div> </div>
</div> </div>
@@ -107,7 +107,7 @@
<!-- Affichage par défaut --> <!-- Affichage par défaut -->
<div v-if="!activeProject" class="flex-1 overflow-y-auto p-4 md:p-8 flex flex-col items-center justify-center"> <div v-if="!activeProject" class="flex-1 overflow-y-auto p-4 md:p-8 flex flex-col items-center justify-center">
<img src="/static/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mb-6 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800 flex-shrink-0"> <img src="/assets/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mb-6 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800 flex-shrink-0">
<h2 class="text-2xl font-bold text-gray-200 mb-2 text-center">Comment puis-je vous aider aujourd'hui ?</h2> <h2 class="text-2xl font-bold text-gray-200 mb-2 text-center">Comment puis-je vous aider aujourd'hui ?</h2>
<p class="text-gray-500 text-sm text-center">Sélectionnez ou créez un projet pour commencer.</p> <p class="text-gray-500 text-sm text-center">Sélectionnez ou créez un projet pour commencer.</p>
</div> </div>
@@ -264,22 +264,25 @@
nextTick(() => lucide.createIcons()); nextTick(() => lucide.createIcons());
try { try {
await fetch(`/api/projects/${activeProject.value.id}/messages`, { const response = await fetch(`/api/projects/${activeProject.value.id}/messages`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: 'user', content: userText }) body: JSON.stringify({ role: 'user', content: userText })
}); });
// SIMULATION TEMPORAIRE : Attente de l'IA (2 secondes) if (response.ok) {
setTimeout(() => { // L'API nous renvoie l'historique à jour avec la réponse de l'IA
isAiThinking.value = false; messages.value = await response.json();
scrollToBottom(); } else {
// La prochaine étape remplacera ce setTimeout par le véritable appel au moteur IA ! const err = await response.json();
}, 2000); alert("Erreur: " + err.detail);
}
} catch (error) { } catch (error) {
console.error("Erreur envoi message", error); console.error("Erreur envoi message", error);
} finally {
isAiThinking.value = false; isAiThinking.value = false;
scrollToBottom();
nextTick(() => lucide.createIcons());
} }
}; };
+1 -1
View File
@@ -22,7 +22,7 @@
<div id="app" class="w-full max-w-3xl transition-all duration-500 my-8"> <div id="app" class="w-full max-w-3xl transition-all duration-500 my-8">
<div class="text-center mb-8"> <div class="text-center mb-8">
<img src="/static/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mx-auto mb-4 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800"> <img src="/assets/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mx-auto mb-4 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800">
<h1 class="text-3xl font-bold tracking-wider neon-text">AETHAS38</h1> <h1 class="text-3xl font-bold tracking-wider neon-text">AETHAS38</h1>
<p class="text-gray-400 text-sm mt-2">Configuration Initiale (Super Admin)</p> <p class="text-gray-400 text-sm mt-2">Configuration Initiale (Super Admin)</p>
</div> </div>
+28 -38
View File
@@ -6,26 +6,12 @@
<title>AETHAS38 - Connexion</title> <title>AETHAS38 - Connexion</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
tailwind.config = { tailwind.config = { darkMode: 'class', theme: { extend: { colors: { aethas: { dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7' } } } } }
darkMode: 'class',
theme: {
extend: {
colors: {
aethas: {
dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7'
}
}
}
}
}
</script> </script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style> <style>
body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; } body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; }
.glass-panel { .glass-panel { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(10px); border: 1px solid rgba(6, 182, 212, 0.2); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); }
background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(10px);
border: 1px solid rgba(6, 182, 212, 0.2); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
}
.neon-text { background: linear-gradient(to right, #06b6d4, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .neon-text { background: linear-gradient(to right, #06b6d4, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
</style> </style>
</head> </head>
@@ -33,34 +19,32 @@
<div id="app" class="w-full max-w-md transition-all duration-500"> <div id="app" class="w-full max-w-md transition-all duration-500">
<div class="text-center mb-8"> <div class="text-center mb-8">
<img src="https://via.placeholder.com/150x150/0f172a/06b6d4?text=Logo+A38" alt="AETHAS38 Logo" class="w-24 h-24 mx-auto mb-4 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)]"> <img src="/assets/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mx-auto mb-4 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800">
<h1 class="text-3xl font-bold tracking-wider neon-text">AETHAS38</h1> <h1 class="text-3xl font-bold tracking-wider neon-text">AETHAS38</h1>
<p class="text-gray-400 text-sm mt-2">Authentification Sécurisée</p> <p class="text-gray-400 text-sm mt-2">Authentification Sécurisée</p>
</div> </div>
<div class="glass-panel rounded-2xl p-6 sm:p-8"> <div class="glass-panel rounded-2xl p-6 sm:p-8">
<form @submit.prevent="submitLogin" class="space-y-5"> <form @submit.prevent="submitLogin" class="space-y-5">
<div v-if="errorMessage" class="p-3 bg-red-900/50 border border-red-500 rounded text-red-200 text-sm text-center"> <div v-if="errorMessage" class="p-3 bg-red-900/50 border border-red-500 rounded text-red-200 text-sm text-center">{{ errorMessage }}</div>
{{ errorMessage }}
</div>
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-1">Nom d'utilisateur</label> <label class="block text-sm font-medium text-gray-300 mb-1">Nom d'utilisateur</label>
<input v-model="form.username" type="text" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="Admin"> <input v-model="form.username" type="text" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="Admin">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-1">Mot de passe</label> <label class="block text-sm font-medium text-gray-300 mb-1">Mot de passe</label>
<input v-model="form.password" type="password" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="••••••••"> <input v-model="form.password" type="password" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="••••••••">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-1">Code 2FA (6 chiffres)</label> <label class="block text-sm font-medium text-gray-300 mb-1">Code 2FA (6 chiffres)</label>
<input v-model="form.totp_code" type="text" inputmode="numeric" pattern="[0-9]{6}" required maxlength="6" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors text-center tracking-widest text-lg" placeholder="123456"> <input v-model="form.totp_code" type="text" required pattern="[0-9]{6}" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors text-center tracking-widest text-lg" placeholder="123456">
</div> </div>
<button type="submit" :disabled="isSubmitting" class="w-full bg-gradient-to-r from-aethas-cyan to-aethas-purple hover:opacity-90 text-white font-semibold py-3 px-4 rounded-lg transition-all shadow-[0_0_10px_rgba(168,85,247,0.4)] flex justify-center items-center mt-6"> <button type="submit" :disabled="isLoading" class="w-full bg-gradient-to-r from-aethas-cyan to-aethas-purple hover:opacity-90 text-white font-semibold py-3 px-4 rounded-lg transition-all shadow-[0_0_10px_rgba(168,85,247,0.4)] flex justify-center items-center mt-6">
<span v-if="isSubmitting">Vérification...</span> <span v-if="isLoading">Connexion...</span>
<span v-else>Se connecter</span> <span v-else>Se connecter</span>
</button> </button>
</form> </form>
@@ -68,37 +52,43 @@
</div> </div>
<script> <script>
const { createApp, reactive, ref } = Vue; const { createApp, ref, reactive } = Vue;
createApp({ createApp({
setup() { setup() {
const form = reactive({ username: '', password: '', totp_code: '' }); const form = reactive({ username: '', password: '', totp_code: '' });
const isSubmitting = ref(false); const isLoading = ref(false);
const errorMessage = ref(''); const errorMessage = ref('');
const submitLogin = async () => { const submitLogin = async () => {
isSubmitting.value = true; isLoading.value = true;
errorMessage.value = ''; errorMessage.value = '';
try { try {
const response = await fetch('/api/login', { const response = await fetch('/api/login', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form) body: JSON.stringify(form)
}); });
const data = await response.json();
if (!response.ok) throw new Error(data.detail || "Erreur de connexion");
// Redirection vers le tableau de bord une fois connecté const data = await response.json();
window.location.href = "/dashboard";
if (!response.ok) {
throw new Error(data.detail || "Erreur de connexion");
}
window.location.href = '/dashboard';
} catch (error) { } catch (error) {
errorMessage.value = error.message; errorMessage.value = error.message;
} finally { } finally {
isSubmitting.value = false; isLoading.value = false;
} }
}; };
return { form, isSubmitting, errorMessage, submitLogin };
return { form, isLoading, errorMessage, submitLogin };
} }
}).mount('#app'); }).mount('#app');
</script> </script>
</body> </body>
</html> </html>
+2 -1
View File
@@ -10,4 +10,5 @@ python-dotenv
python-multipart python-multipart
bcrypt bcrypt
email-validator email-validator
PyJWT PyJWT
openai