mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-11 13:24:42 +02:00
feat(stt): Asa agentique — boucle d'outils (function calling, Phase 1)
Asa ne répondait qu'à partir d'un contexte figé (RAG doc ou blocs live pré-câblés, à présélectionner) → les questions à état live (« gpu-01 tourne bien ? ») tombaient sur « la doc ne le précise pas ». Bascule vers une boucle de function-calling : le modèle décide quels outils appeler puis répond à partir de leurs résultats. - Nouveau contexte `asa` (STT_DEFAULT_CONTEXT=asa, défaut prod) + tools.py (registre schémas OpenAI + exécuteurs) + brain.ask_with_tools (boucle bornée STT_TOOL_MAX_ITERS=4, réponse forcée au-delà ; _post factorisé + retry). - Outils Phase 1, LECTURE SEULE : search_docs (RAG funk-docs), host_health (gpu-01|storage-01 : up/charge/RAM + llama-server), cluster_status, prometheus_query (PromQL arbitraire). Réutilisent sources.py / knowledge.py. - Trace des outils renvoyée dans `context` → visualiseur HUD (un bloc par appel). - 100 % local (Qwen3-8B) : tool-calling natif llama.cpp validé, survit au /no_think. LiteLLM transmet bien les tools. PromQL validés contre le vrai Prometheus in-cluster. 3 tests unitaires de la boucle (hors-ligne). Web search (SearXNG in-cluster) = Phase 2 ; actions admin pilotées par le LLM (hermes-exec comme outil) = Phase 3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
de1cd6c407
commit
19773a837a
11 changed files with 566 additions and 42 deletions
|
|
@ -20,6 +20,7 @@ from stt_server import brain
|
|||
from stt_server.brain import ask as brain_ask
|
||||
from stt_server.config import settings
|
||||
from stt_server import agent
|
||||
from stt_server import tools
|
||||
from stt_server.contexts import CONTEXTS, assemble, get_context
|
||||
from stt_server.knowledge import Knowledge
|
||||
from stt_server.longterm import LongTermMemory
|
||||
|
|
@ -170,6 +171,67 @@ async def _handle_agent(req: AskRequest, text: str) -> AskReply:
|
|||
context=_agent_context_debug({"pending": text}))
|
||||
|
||||
|
||||
async def _handle_agentic(
|
||||
req: "AskRequest", text: str, ctx, model: str, background: BackgroundTasks
|
||||
) -> "AskReply":
|
||||
"""Contexte à outils : recall mémoire (perso) + boucle de function-calling (état live).
|
||||
|
||||
La doc n'est plus injectée d'office (elle est devenue l'outil `search_docs`) ; on garde
|
||||
le recall long-terme pour la personnalisation. Le trace des outils alimente le visualiseur.
|
||||
"""
|
||||
t0 = time.perf_counter()
|
||||
history = sessions.history(req.session_id) if req.session_id else None
|
||||
memories, qvec = await longterm.recall(text) if longterm else ([], None)
|
||||
system = ctx.system_prompt
|
||||
if memories:
|
||||
souvenirs = "\n".join(f"- {m}" for m in memories)
|
||||
system += (
|
||||
"\n\nÉléments de mémoire long-terme (peuvent aider, ignore si hors-sujet) :\n"
|
||||
+ souvenirs
|
||||
)
|
||||
schemas = tools.schemas_for(ctx.tools)
|
||||
t_recall = time.perf_counter()
|
||||
async with httpx.AsyncClient() as client:
|
||||
deps = tools.ToolDeps(client=client, knowledge=knowledge, qvec=qvec)
|
||||
|
||||
async def _dispatch(name: str, args: dict) -> str:
|
||||
return await tools.run(name, args, deps)
|
||||
|
||||
try:
|
||||
reply, tool_trace = await brain.ask_with_tools(
|
||||
text, system, schemas=schemas, dispatch=_dispatch,
|
||||
model=model, history=history,
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
raise HTTPException(status_code=502, detail=f"upstream LiteLLM : {e}") from e
|
||||
t_gen = time.perf_counter()
|
||||
if req.session_id:
|
||||
sessions.add(req.session_id, "user", text)
|
||||
sessions.add(req.session_id, "assistant", reply)
|
||||
if longterm:
|
||||
background.add_task(longterm.store, req.session_id or "anon", text, qvec)
|
||||
log.info(
|
||||
"ask ctx=%s model=%s recall=%.0fms loop=%.0fms total=%.0fms mem=%d tools=%d",
|
||||
ctx.id, model, (t_recall - t0) * 1000, (t_gen - t_recall) * 1000,
|
||||
(t_gen - t0) * 1000, len(memories), len(tool_trace),
|
||||
)
|
||||
# Visualiseur HUD : on présente chaque appel d'outil comme un bloc (réutilise l'UI existante).
|
||||
ctx_debug = {
|
||||
"id": ctx.id, "label": ctx.label, "icon": ctx.icon,
|
||||
"system_prompt": ctx.system_prompt,
|
||||
"blocks": [
|
||||
{
|
||||
"source": f"tool:{t['name']}",
|
||||
"title": f"🔧 {t['name']}({', '.join(f'{k}={v}' for k, v in t['args'].items())})",
|
||||
"text": t["result"],
|
||||
}
|
||||
for t in tool_trace
|
||||
],
|
||||
"docs": [], "memories": memories,
|
||||
}
|
||||
return AskReply(reply=reply, model=model, context_id=ctx.id, context=ctx_debug)
|
||||
|
||||
|
||||
@app.post("/v1/ask", response_model=AskReply)
|
||||
async def v1_ask(req: AskRequest, background: BackgroundTasks) -> AskReply:
|
||||
text = req.text.strip()
|
||||
|
|
@ -185,6 +247,9 @@ async def v1_ask(req: AskRequest, background: BackgroundTasks) -> AskReply:
|
|||
# Contexte « agent » : flux dédié (confirmation 2 temps → hermes-exec), sans LLM.
|
||||
if ctx.id == "agent":
|
||||
return await _handle_agent(req, text)
|
||||
# Contexte à outils (« asa ») : boucle de function-calling (le modèle sonde l'état live).
|
||||
if ctx.tools:
|
||||
return await _handle_agentic(req, text, ctx, model, background)
|
||||
t0 = time.perf_counter()
|
||||
history = sessions.history(req.session_id) if req.session_id else None
|
||||
# recall : timeout serré, dégrade vite ; renvoie aussi le vecteur (réutilisé ci-dessous)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue