Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83e70eadf6 | ||
|
|
60c0699c57 |
No files matched your search
+18
-12
@@ -73,7 +73,8 @@ def login(login_data: LoginRequest, response: Response, db: Session = Depends(ge
|
|||||||
user = db.query(User).filter(User.username == login_data.username).first()
|
user = db.query(User).filter(User.username == login_data.username).first()
|
||||||
if not user or not verify_password(login_data.password, user.hashed_password): raise HTTPException(status_code=401, detail="Identifiants incorrects.")
|
if not user or not verify_password(login_data.password, user.hashed_password): raise HTTPException(status_code=401, detail="Identifiants incorrects.")
|
||||||
if not verify_totp(user.totp_secret, login_data.totp_code): raise HTTPException(status_code=401, detail="2FA invalide.")
|
if not verify_totp(user.totp_secret, login_data.totp_code): raise HTTPException(status_code=401, detail="2FA invalide.")
|
||||||
response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=3600, samesite="lax")
|
# Correction : Extension de la durée de session à 7 jours (604800 secondes) pour éviter les erreurs 401 intempestives
|
||||||
|
response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=604800, samesite="lax")
|
||||||
return {"message": "Connexion réussie"}
|
return {"message": "Connexion réussie"}
|
||||||
|
|
||||||
@app.get("/dashboard")
|
@app.get("/dashboard")
|
||||||
@@ -143,10 +144,12 @@ def get_messages(project_id: int, db: Session = Depends(get_db), current_user: U
|
|||||||
|
|
||||||
@app.post("/api/projects/{project_id}/messages", response_model=List[MessageResponse])
|
@app.post("/api/projects/{project_id}/messages", response_model=List[MessageResponse])
|
||||||
async def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
async def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
final_content = message.content
|
extracted_files_data = []
|
||||||
|
files_names = []
|
||||||
|
|
||||||
if message.files:
|
if message.files:
|
||||||
files_text = ""
|
|
||||||
for f in message.files:
|
for f in message.files:
|
||||||
|
files_names.append(f.name)
|
||||||
content = f.content
|
content = f.content
|
||||||
if content.startswith("data:"):
|
if content.startswith("data:"):
|
||||||
try:
|
try:
|
||||||
@@ -175,7 +178,6 @@ async def create_message(project_id: int, message: MessageCreate, db: Session =
|
|||||||
if any(row_vals):
|
if any(row_vals):
|
||||||
extracted_text += "\t".join(row_vals) + "\n"
|
extracted_text += "\t".join(row_vals) + "\n"
|
||||||
|
|
||||||
# Extraction des Macros VBA
|
|
||||||
if ext in ['xlsm', 'xlsb', 'xls']:
|
if ext in ['xlsm', 'xlsb', 'xls']:
|
||||||
try:
|
try:
|
||||||
from oletools.olevba import VBA_Parser
|
from oletools.olevba import VBA_Parser
|
||||||
@@ -192,24 +194,28 @@ async def create_message(project_id: int, message: MessageCreate, db: Session =
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
extracted_text = "[Erreur: L'administrateur doit exécuter 'pip install openpyxl' sur le serveur pour lire Excel.]"
|
extracted_text = "[Erreur: L'administrateur doit exécuter 'pip install openpyxl' sur le serveur pour lire Excel.]"
|
||||||
else:
|
else:
|
||||||
extracted_text = f"[Fichier binaire non supporté textuellement : {f.name}]"
|
extracted_text = file_bytes.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
files_text += f"\n\n[Fichier attaché : {f.name}]\n```text\n{extracted_text}\n```"
|
extracted_files_data.append({"name": f.name, "content": extracted_text})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
files_text += f"\n\n[Fichier attaché : {f.name} - ERREUR DE DECODAGE: {str(e)}]"
|
extracted_files_data.append({"name": f.name, "content": f"[ERREUR DE DECODAGE: {str(e)}]"})
|
||||||
else:
|
else:
|
||||||
files_text += f"\n\n[Fichier attaché : {f.name}]\n```\n{content}\n```"
|
extracted_files_data.append({"name": f.name, "content": content})
|
||||||
|
|
||||||
final_content = message.content + files_text
|
|
||||||
|
|
||||||
db.add(Message(role=message.role, content=final_content, project_id=project_id))
|
# On ne stocke plus le contenu brut des fichiers en DB pour éviter d'exploser le contexte des requêtes suivantes
|
||||||
|
db_content = message.content
|
||||||
|
if files_names:
|
||||||
|
db_content += f"\n\n[Fichiers joints pour analyse : {', '.join(files_names)}]"
|
||||||
|
|
||||||
|
db.add(Message(role=message.role, content=db_content, project_id=project_id))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
|
history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
|
||||||
settings = db.query(SystemSettings).first()
|
settings = db.query(SystemSettings).first()
|
||||||
conf = message.config.dict() if message.config else {"workers": ["gemini-3.5-flash-lite"]}
|
conf = message.config.dict() if message.config else {"workers": ["gemini-3.5-flash-lite"]}
|
||||||
|
|
||||||
ai_resp = await run_orchestrator(db, history, settings, conf)
|
# Transmission des données de fichiers en mémoire vive à l'orchestrateur (Map-Reduce)
|
||||||
|
ai_resp = await run_orchestrator(db, history, settings, conf, extracted_files_data)
|
||||||
|
|
||||||
db.add(Message(role="assistant", content=ai_resp, project_id=project_id))
|
db.add(Message(role="assistant", content=ai_resp, project_id=project_id))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
+50
-4
@@ -194,22 +194,68 @@ async def ask_agent(client, model_id, messages, provider="openrouter"):
|
|||||||
kwargs = {"model": model_id, "messages": messages}
|
kwargs = {"model": model_id, "messages": messages}
|
||||||
if provider == "openrouter":
|
if provider == "openrouter":
|
||||||
kwargs["extra_headers"] = {"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "AETHAS38 Orchestrator"}
|
kwargs["extra_headers"] = {"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "AETHAS38 Orchestrator"}
|
||||||
|
# Activation du plugin natif de compression d'OpenRouter pour éviter le dépassement de contexte
|
||||||
|
kwargs["extra_body"] = {"plugins": [{"id": "context-compression"}]}
|
||||||
|
|
||||||
resp = await client.chat.completions.create(**kwargs)
|
resp = await client.chat.completions.create(**kwargs)
|
||||||
return resp.choices[0].message.content
|
return resp.choices[0].message.content
|
||||||
|
|
||||||
async def run_orchestrator(db: Session, history: list, settings: SystemSettings, config: dict) -> str:
|
async def run_orchestrator(db: Session, history: list, settings: SystemSettings, config: dict, extracted_files: list = None) -> str:
|
||||||
workers = config.get("workers", ["gemini-3.5-flash-lite"])
|
workers = config.get("workers", ["gemini-3.5-flash-lite"])
|
||||||
user_prompt = history[-1].content
|
user_prompt = history[-1].content
|
||||||
|
original_user_text = user_prompt.split("\n\n[Fichiers joints")[0] if "[Fichiers joints" in user_prompt else user_prompt
|
||||||
formatted_history = [{"role": msg.role, "content": msg.content} for msg in history[:-1]]
|
formatted_history = [{"role": msg.role, "content": msg.content} for msg in history[:-1]]
|
||||||
|
|
||||||
final_response = ""
|
final_response = ""
|
||||||
try:
|
try:
|
||||||
# Fallback sécurisé en cas de champ vide
|
|
||||||
p_mod = config.get("prompter")
|
p_mod = config.get("prompter")
|
||||||
if not p_mod: p_mod = "gemini-3.5-flash-lite"
|
if not p_mod: p_mod = "gemini-3.5-flash-lite"
|
||||||
|
|
||||||
p_client, p_prov = get_client_for_model(db, p_mod, settings)
|
p_client, p_prov = get_client_for_model(db, p_mod, settings)
|
||||||
prompt_system = "You are an expert prompt engineer. Translate and optimize the user request into clear, precise English tailored for AI execution."
|
|
||||||
|
# --- WORKFLOW MAP-REDUCE : PRÉ-TRAITEMENT SÉQUENTIEL & CHUNKING ---
|
||||||
|
files_context = ""
|
||||||
|
if extracted_files:
|
||||||
|
async def process_single_file(f):
|
||||||
|
file_sys = "You are an expert data analyst and senior developer. Extract the most important technical information from the file without losing critical code syntax."
|
||||||
|
content = f['content']
|
||||||
|
chunk_size = 150000 # Environ 35k à 40k tokens par morceau pour rester très large par rapport aux limites
|
||||||
|
|
||||||
|
# CHUNKING : Découpage intelligent si le fichier est massif
|
||||||
|
if len(content) > chunk_size:
|
||||||
|
chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
|
||||||
|
chunk_analyses = []
|
||||||
|
for idx, chunk in enumerate(chunks):
|
||||||
|
file_prompt = f"Demande de l'utilisateur : '{original_user_text}'.\n\nPartie {idx+1}/{len(chunks)} du fichier '{f['name']}'. Analysez, extrayez et résumez le code, VBA, SQL ou les données pertinentes.\n\nContenu :\n```\n{chunk}\n```"
|
||||||
|
try:
|
||||||
|
analysis = await ask_agent(p_client, p_mod, [{"role": "system", "content": file_sys}, {"role": "user", "content": file_prompt}], p_prov)
|
||||||
|
chunk_analyses.append(analysis)
|
||||||
|
except Exception as e:
|
||||||
|
chunk_analyses.append(f"[Erreur sur la partie {idx+1}: {str(e)}]")
|
||||||
|
|
||||||
|
await asyncio.sleep(1.5) # Pause anti-spam (429) entre les morceaux
|
||||||
|
|
||||||
|
return f"\n\n--- Extraction du fichier {f['name']} (en {len(chunks)} parties) ---\n" + "\n".join(chunk_analyses)
|
||||||
|
else:
|
||||||
|
file_prompt = f"Demande de l'utilisateur : '{original_user_text}'.\n\nAnalysez le fichier ci-dessous. Extrayez, résumez et conservez méticuleusement tout le code, les macros VBA, les requêtes SQL, ou les données métier pertinentes pour répondre à la demande.\n\nFichier : {f['name']}\nContenu :\n```\n{content}\n```"
|
||||||
|
try:
|
||||||
|
analysis = await ask_agent(p_client, p_mod, [{"role": "system", "content": file_sys}, {"role": "user", "content": file_prompt}], p_prov)
|
||||||
|
return f"\n\n--- Extraction du fichier {f['name']} ---\n{analysis}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"\n\n--- Erreur sur {f['name']} ---\n{str(e)}"
|
||||||
|
|
||||||
|
file_analyses = []
|
||||||
|
for f in extracted_files:
|
||||||
|
analysis = await process_single_file(f)
|
||||||
|
file_analyses.append(analysis)
|
||||||
|
# SÉQUENÇAGE : Pause de 1.5 seconde entre les fichiers pour éviter l'erreur 429
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
files_context = "".join(file_analyses)
|
||||||
|
user_prompt = f"{original_user_text}\n\nVoici les données pré-traitées des fichiers joints :\n{files_context}"
|
||||||
|
|
||||||
|
# --- OPTIMISATION & TRADUCTION ---
|
||||||
|
prompt_system = "You are an expert prompt engineer. Translate and optimize the user request and any file context into clear, precise English tailored for AI execution. Keep all code blocks intact."
|
||||||
optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov)
|
optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov)
|
||||||
|
|
||||||
if len(workers) == 1:
|
if len(workers) == 1:
|
||||||
@@ -237,7 +283,7 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings,
|
|||||||
"You may translate code comments into French if appropriate, but leave code syntax strictly intact."
|
"You may translate code comments into French if appropriate, but leave code syntax strictly intact."
|
||||||
)
|
)
|
||||||
|
|
||||||
synth = f"User Request: {user_prompt}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{str(r)}" for i, r in enumerate(responses)])
|
synth = f"User Request: {original_user_text}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{str(r)}" for i, r in enumerate(responses)])
|
||||||
final_response = await ask_agent(c_client, c_mod, [{"role": "system", "content": concat_system}, {"role": "user", "content": synth}], c_prov)
|
final_response = await ask_agent(c_client, c_mod, [{"role": "system", "content": concat_system}, {"role": "user", "content": synth}], c_prov)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -631,7 +631,7 @@
|
|||||||
attachedFiles.value.push({ name: file.name, content: e.target.result });
|
attachedFiles.value.push({ name: file.name, content: e.target.result });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Les fichiers binaires (PDF, Excel, etc.) sont lus en Base64 pour que Python s'en charge.
|
// Les fichiers binaires sont lus en Base64.
|
||||||
if (isText) {
|
if (isText) {
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
} else {
|
} else {
|
||||||
@@ -687,6 +687,10 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (Array.isArray(data)) messages.value = data;
|
if (Array.isArray(data)) messages.value = data;
|
||||||
await fetchFinances();
|
await fetchFinances();
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
alert("Votre session a expiré. Veuillez vous reconnecter.");
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
const err = await response.json(); alert("Erreur API IA : " + err.detail);
|
const err = await response.json(); alert("Erreur API IA : " + err.detail);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user