Chat: rendu Markdown, blocs de code copiables, metriques de tokens, apercu HTML

Frontend
- MessageContent: rendu Markdown complet (react-markdown + remark-gfm) avec
  blocs de code dans un cadre + bouton Copier, et styles thematises (titres,
  listes, gras, liens, tableaux, citations).
- ChatPanel: affiche sous chaque reponse de l'agent la vitesse (tokens/s) et le
  nombre de jetons generes / en entree.
- PreviewPanel: iframe en sandbox="allow-scripts" pour que le JS des pages
  generees s'execute (animations, canvas) tout en gardant une origine opaque.
- Icon: ajout CopyIcon / CheckIcon.

Backend
- agent: cumule les metriques Ollama (eval_count, eval_duration,
  prompt_eval_count) et calcule les tokens/seconde, emis dans l'evenement final.
- chat: persiste les stats dans meta et les renvoie au client.
- agent_config: l'invite systeme par defaut demande un formatage Markdown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-06-30 13:45:59 +02:00
1 parent aed5f8148c
commit e8e6a1446c
10 files changed
+1683 -13

No files matched your search

+25 -1
View File
@@ -75,6 +75,15 @@ async def run_agent(
tool_repair_attempts = 0
request_options = dict(options or {})
# Métriques cumulées sur tous les appels Ollama du tour agentique : Ollama
# les renvoie dans le chunk final (done=true) de chaque génération.
stats = {"eval_count": 0, "eval_duration": 0, "prompt_eval_count": 0}
def _accumulate(chunk: dict) -> None:
stats["eval_count"] += chunk.get("eval_count") or 0
stats["eval_duration"] += chunk.get("eval_duration") or 0
stats["prompt_eval_count"] += chunk.get("prompt_eval_count") or 0
try:
for _ in range(MAX_ITERATIONS):
content_buf = ""
@@ -107,6 +116,7 @@ async def run_agent(
if msg.get("tool_calls"):
tool_calls.extend(msg["tool_calls"])
if chunk.get("done"):
_accumulate(chunk)
break
break
except OllamaError as exc:
@@ -249,6 +259,7 @@ async def run_agent(
final_chunk += tok
yield {"type": "token", "content": tok}
if chunk.get("done"):
_accumulate(chunk)
break
if final_chunk.strip():
text_parts.append(final_chunk.strip())
@@ -276,4 +287,17 @@ async def run_agent(
}
return
yield {"type": "final", "content": final_content, "tools": collected}
eval_secs = stats["eval_duration"] / 1e9
final_stats = {
"eval_count": stats["eval_count"],
"prompt_eval_count": stats["prompt_eval_count"],
"tokens_per_sec": (
round(stats["eval_count"] / eval_secs, 1) if eval_secs > 0 else None
),
}
yield {
"type": "final",
"content": final_content,
"tools": collected,
"stats": final_stats,
}
+4 -1
View File
@@ -18,7 +18,10 @@ DEFAULT_SYSTEM_PROMPT = (
"Utilise-les pour accomplir les tâches concrètement, puis réponds de façon "
"concise en français. Pour un fichier long, appelle write_file en plusieurs "
"morceaux (overwrite puis append) afin de toujours produire un JSON valide. "
"Après avoir écrit un fichier, propose un aperçu."
"Après avoir écrit un fichier, propose un aperçu. "
"Formate TOUJOURS tes réponses en Markdown : titres, listes, gras pour les "
"points clés, tableaux si pertinent, et surtout des blocs de code avec le "
"langage indiqué (```python, ```html…) pour tout extrait de code ou commande."
)
# Outils disponibles. Les sensibles (web_search, run_shell) sont désactivés
+9 -1
View File
@@ -58,6 +58,7 @@ async def chat(req: ChatRequest) -> StreamingResponse:
yield _sse("start", {"model": model})
final_content = ""
tools_meta: list[dict] = []
stats_meta: dict | None = None
error_message = ""
queue: asyncio.Queue[dict | None] = asyncio.Queue()
@@ -113,6 +114,7 @@ async def chat(req: ChatRequest) -> StreamingResponse:
elif etype == "final":
final_content = ev["content"]
tools_meta = ev["tools"]
stats_meta = ev.get("stats")
finally:
if not producer.done():
producer.cancel()
@@ -120,18 +122,24 @@ async def chat(req: ChatRequest) -> StreamingResponse:
await producer
if final_content or tools_meta:
meta: dict = {}
if tools_meta:
meta["tools"] = tools_meta
if stats_meta:
meta["stats"] = stats_meta
db.add_message(
req.session_id,
"assistant",
final_content,
model,
meta={"tools": tools_meta} if tools_meta else None,
meta=meta or None,
)
yield _sse(
"done",
{
"content": final_content,
"tools": tools_meta,
"stats": stats_meta,
"error": error_message or None,
},
)