Spaces:
Sleeping
Sleeping
Upload 17 files
Browse files- app.py +53 -0
- static/index.html +3 -3
- static/js/app.js +44 -2
app.py
CHANGED
|
@@ -1492,6 +1492,9 @@ async def run_job(job_id: str, params: dict):
|
|
| 1492 |
from datetime import datetime, timezone
|
| 1493 |
now = datetime.now(timezone.utc)
|
| 1494 |
system += f"\n\nDate et heure actuelles (UTC) : {now.strftime('%Y-%m-%d %H:%M:%S')} ({now.strftime('%A %d %B %Y')})"
|
|
|
|
|
|
|
|
|
|
| 1495 |
# FILTRER les messages tool et assistant.avec_tool_calls de l'historique :
|
| 1496 |
# - Les messages tool n'ont pas de tool_call_id valide (le client ne peut pas le reconstruire)
|
| 1497 |
# - Les assistant.avec_tool_calls doivent être suivis de tool messages, sinon l'API rejette
|
|
@@ -1616,6 +1619,56 @@ async def run_job(job_id: str, params: dict):
|
|
| 1616 |
if job["tool_calls"] and provider in ("openai", "groq", "custom"):
|
| 1617 |
await _execute_tool_calls(job, params)
|
| 1618 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1619 |
job["status"] = "cancelled" if job["cancel_event"].is_set() else "done"
|
| 1620 |
|
| 1621 |
# 5) Log usage
|
|
|
|
| 1492 |
from datetime import datetime, timezone
|
| 1493 |
now = datetime.now(timezone.utc)
|
| 1494 |
system += f"\n\nDate et heure actuelles (UTC) : {now.strftime('%Y-%m-%d %H:%M:%S')} ({now.strftime('%A %d %B %Y')})"
|
| 1495 |
+
# Instruction de sentinelle <end> : le modèle DOIT terminer sa réponse par <end>
|
| 1496 |
+
# pour indiquer qu'il a fini. Si absent, on retry.
|
| 1497 |
+
system += "\n\nIMPORTANT : Tu DOIS terminer chaque réponse finale par la balise <end> (sans autre texte après). Cette balise indique que ta réponse est complète."
|
| 1498 |
# FILTRER les messages tool et assistant.avec_tool_calls de l'historique :
|
| 1499 |
# - Les messages tool n'ont pas de tool_call_id valide (le client ne peut pas le reconstruire)
|
| 1500 |
# - Les assistant.avec_tool_calls doivent être suivis de tool messages, sinon l'API rejette
|
|
|
|
| 1619 |
if job["tool_calls"] and provider in ("openai", "groq", "custom"):
|
| 1620 |
await _execute_tool_calls(job, params)
|
| 1621 |
|
| 1622 |
+
# 5) Vérifier que la réponse n'est pas vide et contient <end>.
|
| 1623 |
+
# Si elle est vide OU ne contient pas <end>, on retry jusqu'à 3 fois
|
| 1624 |
+
# en demandant au modèle de compléter sa réponse.
|
| 1625 |
+
max_retries = 3
|
| 1626 |
+
for retry in range(max_retries):
|
| 1627 |
+
# Si le job a été annulé, ne pas retry
|
| 1628 |
+
if job["cancel_event"].is_set():
|
| 1629 |
+
break
|
| 1630 |
+
current_buffer = job["buffer"]
|
| 1631 |
+
# Vérifier si on a une réponse non vide ET contenant <end>
|
| 1632 |
+
has_end = "<end>" in current_buffer
|
| 1633 |
+
has_content = any(seg.get("type") == "text" and seg.get("content", "").strip()
|
| 1634 |
+
for seg in job.get("segments", []))
|
| 1635 |
+
if has_end and has_content:
|
| 1636 |
+
break # Réponse complète, on sort
|
| 1637 |
+
if retry == max_retries - 1:
|
| 1638 |
+
break # Dernier retry, on accepte ce qu'on a
|
| 1639 |
+
# Retry : demander au modèle de compléter
|
| 1640 |
+
print(f"[INFO] Réponse incomplète (empty={not has_content}, end={has_end}), retry {retry+1}/{max_retries}")
|
| 1641 |
+
job["status_text"] = "Finalisation de la réponse..."
|
| 1642 |
+
retry_messages = list(params.get("messages", []))
|
| 1643 |
+
# Ajouter le dernier message assistant (même incomplet)
|
| 1644 |
+
retry_messages.append({"role": "assistant", "content": current_buffer or "(vide)"})
|
| 1645 |
+
retry_messages.append({"role": "user", "content": "Ta réponse précédente semble incomplète ou vide. Donne-moi maintenant une réponse complète et détaillée. N'oublie pas de terminer par la balise <end>."})
|
| 1646 |
+
buffer_before_retry = len(job["buffer"])
|
| 1647 |
+
job["tool_calls"] = [] # reset pour éviter de re-exécuter les tools
|
| 1648 |
+
try:
|
| 1649 |
+
if provider == "openai":
|
| 1650 |
+
await run_openai_like(job, "https://api.openai.com/v1/chat/completions", api_key, model, system, retry_messages, tools=None, reasoning_effort=reasoning_effort)
|
| 1651 |
+
elif provider == "groq":
|
| 1652 |
+
await run_openai_like(job, "https://api.groq.com/openai/v1/chat/completions", api_key, model, system, retry_messages, tools=None, reasoning_effort=reasoning_effort)
|
| 1653 |
+
elif provider == "custom":
|
| 1654 |
+
await run_openai_like(job, base_url.rstrip("/") + "/chat/completions", api_key, model, system, retry_messages, tools=None, reasoning_effort=reasoning_effort)
|
| 1655 |
+
# Ajouter le nouveau texte comme segment
|
| 1656 |
+
retry_text = job["buffer"][buffer_before_retry:]
|
| 1657 |
+
if retry_text:
|
| 1658 |
+
job["segments"].append({"type": "text", "content": retry_text})
|
| 1659 |
+
except Exception as retry_err:
|
| 1660 |
+
print(f"[WARN] Retry {retry+1} échoué: {retry_err}")
|
| 1661 |
+
break
|
| 1662 |
+
job["status_text"] = None
|
| 1663 |
+
|
| 1664 |
+
# 6) Strip la balise <end> du buffer et des segments
|
| 1665 |
+
if "<end>" in job["buffer"]:
|
| 1666 |
+
job["buffer"] = job["buffer"].replace("<end>", "").rstrip()
|
| 1667 |
+
if job.get("segments"):
|
| 1668 |
+
for seg in job["segments"]:
|
| 1669 |
+
if seg.get("type") == "text" and "<end>" in (seg.get("content") or ""):
|
| 1670 |
+
seg["content"] = seg["content"].replace("<end>", "").rstrip()
|
| 1671 |
+
|
| 1672 |
job["status"] = "cancelled" if job["cancel_event"].is_set() else "done"
|
| 1673 |
|
| 1674 |
# 5) Log usage
|
static/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
| 9 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 10 |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 11 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js"></script>
|
| 12 |
-
<link rel="stylesheet" href="/static/css/main.css?v=
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
|
|
@@ -335,7 +335,7 @@
|
|
| 335 |
</div>
|
| 336 |
</div>
|
| 337 |
|
| 338 |
-
<script src="/static/js/i18n.js?v=
|
| 339 |
-
<script src="/static/js/app.js?v=
|
| 340 |
</body>
|
| 341 |
</html>
|
|
|
|
| 9 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 10 |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 11 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js"></script>
|
| 12 |
+
<link rel="stylesheet" href="/static/css/main.css?v=20">
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
|
|
|
|
| 335 |
</div>
|
| 336 |
</div>
|
| 337 |
|
| 338 |
+
<script src="/static/js/i18n.js?v=20"></script>
|
| 339 |
+
<script src="/static/js/app.js?v=20"></script>
|
| 340 |
</body>
|
| 341 |
</html>
|
static/js/app.js
CHANGED
|
@@ -805,6 +805,48 @@ const App = {
|
|
| 805 |
}
|
| 806 |
},
|
| 807 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
async sendMessage() {
|
| 809 |
const input = document.getElementById('messageInput');
|
| 810 |
const text = input.value.trim();
|
|
@@ -856,7 +898,7 @@ const App = {
|
|
| 856 |
const apiKey = '';
|
| 857 |
const system = `Tu es un assistant IA utile, précis et honnête. Réponds dans la langue de l'utilisateur. Formatte tes réponses en Markdown quand pertinent.`;
|
| 858 |
|
| 859 |
-
const messages =
|
| 860 |
|
| 861 |
// Séparer images et fichiers texte pour l'envoi au serveur
|
| 862 |
const allAttachments = attachments.length > 0 ? attachments : (userMsg.attachments || []);
|
|
@@ -1074,7 +1116,7 @@ const App = {
|
|
| 1074 |
const model = chat.model;
|
| 1075 |
const customProvider = chat.customProviderName || '';
|
| 1076 |
const baseUrl = chat.baseUrl || '';
|
| 1077 |
-
const messages =
|
| 1078 |
// Récupérer les attachments du dernier message user (pour retry avec images/fichiers)
|
| 1079 |
const userAttachments = lastUser.attachments || [];
|
| 1080 |
const images = userAttachments.filter(a => a.base64).map(a => ({ base64: a.base64, mime: a.mime }));
|
|
|
|
| 805 |
}
|
| 806 |
},
|
| 807 |
|
| 808 |
+
// Reconstruit les messages envoyés à l'API en incluant l'historique des tool calls et leurs résultats.
|
| 809 |
+
// Pour chaque message assistant qui a utilisé des tools, on inclut:
|
| 810 |
+
// - Le texte généré
|
| 811 |
+
// - Un résumé des tools appelés + leurs résultats (en format texte dans le contenu)
|
| 812 |
+
// Le modèle reçoit donc le contexte complet de ses recherches précédentes.
|
| 813 |
+
buildApiMessages(chat) {
|
| 814 |
+
const msgs = [];
|
| 815 |
+
const recent = chat.messages.slice(-20); // Garder les 20 derniers messages
|
| 816 |
+
for (const m of recent) {
|
| 817 |
+
if (m.role === 'user') {
|
| 818 |
+
msgs.push({ role: 'user', content: m.content || '' });
|
| 819 |
+
} else if (m.role === 'assistant') {
|
| 820 |
+
let content = m.content || '';
|
| 821 |
+
// Si le message a des segments, reconstruire le contenu avec les tool calls
|
| 822 |
+
if (m.segments && m.segments.length > 0) {
|
| 823 |
+
content = '';
|
| 824 |
+
for (const seg of m.segments) {
|
| 825 |
+
if (seg.type === 'text') {
|
| 826 |
+
content += seg.content || '';
|
| 827 |
+
} else if (seg.type === 'tool_call') {
|
| 828 |
+
let argsStr = '';
|
| 829 |
+
try { argsStr = JSON.stringify(seg.args); } catch (e) { argsStr = String(seg.args || ''); }
|
| 830 |
+
content += `\n\n[Outil utilisé: ${seg.name}]\nArguments: ${argsStr}\nRésultat: ${(seg.result || '').substring(0, 2000)}\n`;
|
| 831 |
+
}
|
| 832 |
+
}
|
| 833 |
+
}
|
| 834 |
+
// Aussi inclure les toolCalls (ancien format) si segments absent
|
| 835 |
+
if (!m.segments && m.toolCalls && m.toolCalls.length > 0) {
|
| 836 |
+
for (const tc of m.toolCalls) {
|
| 837 |
+
let argsStr = '';
|
| 838 |
+
try { argsStr = JSON.stringify(tc.args); } catch (e) { argsStr = String(tc.args || ''); }
|
| 839 |
+
content += `\n\n[Outil utilisé: ${tc.tool || tc.name}]\nArguments: ${argsStr}\nRésultat: ${(tc.result_preview || '').substring(0, 2000)}\n`;
|
| 840 |
+
}
|
| 841 |
+
}
|
| 842 |
+
if (content.trim()) {
|
| 843 |
+
msgs.push({ role: 'assistant', content: content.trim() });
|
| 844 |
+
}
|
| 845 |
+
}
|
| 846 |
+
}
|
| 847 |
+
return msgs;
|
| 848 |
+
},
|
| 849 |
+
|
| 850 |
async sendMessage() {
|
| 851 |
const input = document.getElementById('messageInput');
|
| 852 |
const text = input.value.trim();
|
|
|
|
| 898 |
const apiKey = '';
|
| 899 |
const system = `Tu es un assistant IA utile, précis et honnête. Réponds dans la langue de l'utilisateur. Formatte tes réponses en Markdown quand pertinent.`;
|
| 900 |
|
| 901 |
+
const messages = this.buildApiMessages(chat);
|
| 902 |
|
| 903 |
// Séparer images et fichiers texte pour l'envoi au serveur
|
| 904 |
const allAttachments = attachments.length > 0 ? attachments : (userMsg.attachments || []);
|
|
|
|
| 1116 |
const model = chat.model;
|
| 1117 |
const customProvider = chat.customProviderName || '';
|
| 1118 |
const baseUrl = chat.baseUrl || '';
|
| 1119 |
+
const messages = this.buildApiMessages(chat);
|
| 1120 |
// Récupérer les attachments du dernier message user (pour retry avec images/fichiers)
|
| 1121 |
const userAttachments = lastUser.attachments || [];
|
| 1122 |
const images = userAttachments.filter(a => a.base64).map(a => ({ base64: a.base64, mime: a.mime }));
|