fix(agent): v4.13.1 - actions contenu document (resume/traduire/ameliorer/meeting) sans outils, apercu + appliquer/rejeter, mock ignore contexte
This commit is contained in:
@@ -1,5 +1,31 @@
|
||||
# Changelog — FlowDeck
|
||||
|
||||
## v4.13.1 (2026-09-06) — Corrections actions contenu Agent & aperçu à approuver
|
||||
|
||||
> Corrige les actions de contenu sur document (« Résumer », « Traduire cette page », « Proposer
|
||||
> des améliorations », « Créer une note de réunion ») qui partaient en recherche (`search_workspace`)
|
||||
> sans retour utile, et ajoute un flux **aperçu des changements → Appliquer / Rejeter**.
|
||||
|
||||
### Fix
|
||||
- **Le contenu du document n'est plus interprété comme des intentions d'outil** : le planificateur
|
||||
(mock) n'analyse que l'objectif de l'utilisateur, jamais le texte injecté dans `# Contexte`. Un
|
||||
document contenant « recherche », « collection », etc. ne déclenche plus `search_workspace` /
|
||||
`create_collection` intempestifs.
|
||||
- **Ask AI** n'emprunte plus le canal outil : il utilise la génération de texte directe.
|
||||
|
||||
### Added
|
||||
- **Endpoint `POST /api/agent/generate`** : génération de texte **sans outils** (l'IA répond à
|
||||
partir du contexte du document fourni, sans chercher/créer dans le workspace).
|
||||
- **Aperçu & approbation** : les actions de contenu sur document affichent la **proposition** de
|
||||
l'agent dans le panneau avec boutons **« ✓ Appliquer au document » / « ✕ Rejeter »**. Appliquer
|
||||
insère la proposition (mode `insert`) ou **remplace** le contenu de la page (mode `replace` pour
|
||||
« Traduire cette page ») via `window.fdApplyDocument()` ; rejeter la conserve sans modification.
|
||||
En mode hors-ligne, l'action explique que la modification nécessite un modèle connecté (pas
|
||||
d'application accidentelle).
|
||||
|
||||
### Infra
|
||||
- `VERSION` → 4.13.1 ; `app/main.py` version → 4.13.1.
|
||||
|
||||
## v4.13.0 (2026-09-06) — Agent : panneau « Notion AI », ouverture fiable (Ctrl+J) & contexte universel
|
||||
|
||||
> Refonte complète du panneau Agent (style Notion AI) : ouverture/fermeture **fiable** (API
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ async def lifespan(_app: FastAPI):
|
||||
|
||||
app = FastAPI(
|
||||
title="FlowDeck",
|
||||
version="4.13.0",
|
||||
version="4.13.1",
|
||||
docs_url="/docs" if settings.log_level == "DEBUG" else None,
|
||||
redoc_url=None,
|
||||
lifespan=lifespan,
|
||||
|
||||
@@ -310,6 +310,62 @@ async def run_conversation(request: Request, conversation_id: int):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/generate")
|
||||
async def agent_generate(request: Request):
|
||||
"""Headless text generation (NO tools) for content actions on documents.
|
||||
|
||||
Used by the Agent panel's contextual actions ("Résumer ce document",
|
||||
"Traduire cette page", "Proposer des améliorations", "AI meeting note").
|
||||
Because no tool schema is offered, the model answers with plain text based on
|
||||
the provided document context instead of issuing search_workspace / tools.
|
||||
"""
|
||||
user_id = await _current_user_id(request)
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
prompt = (body.get("prompt") or "").strip()
|
||||
if not prompt:
|
||||
raise HTTPException(status_code=400, detail="prompt est requis")
|
||||
provider = (body.get("provider") or "").strip().lower() or None
|
||||
model = (body.get("model") or "").strip() or None
|
||||
context = (body.get("context") or "") or ""
|
||||
|
||||
llm = LLMClient()
|
||||
if provider:
|
||||
if provider not in PROVIDERS:
|
||||
raise HTTPException(status_code=400, detail=f"Provider inconnu: {provider}")
|
||||
user_key = get_user_llm_key(user_id, provider)
|
||||
if user_key and user_key.get("api_key"):
|
||||
llm = LLMClient(provider=provider, api_key=user_key["api_key"],
|
||||
api_base=(user_key.get("api_base") or "").strip() or None)
|
||||
else:
|
||||
llm = LLMClient(provider=provider)
|
||||
offline = llm.provider == "offline" or not llm._has_credentials()
|
||||
|
||||
system = (
|
||||
"Tu es l'assistant d'édition de FlowDeck (contenu). "
|
||||
"Réponds UNIQUEMENT avec le texte demandé, en Markdown léger "
|
||||
"(paragraphes, listes à puces, titres si utile). "
|
||||
"N'ajoute aucun préambule, aucun commentaire, aucun bloc de code autour du texte. "
|
||||
"Fonde ta réponse sur le contexte du document fourni."
|
||||
)
|
||||
user_content = prompt
|
||||
if context and context.strip():
|
||||
user_content += "\n\n# Contexte du document\n" + context.strip()[:20000]
|
||||
messages = [
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": user_content},
|
||||
]
|
||||
try:
|
||||
resp = await llm.complete(messages, model=model or None, tools=None, stream=False)
|
||||
except Exception as exc: # noqa: BLE001 — surface connectivity errors
|
||||
return {"ok": False, "error": str(exc), "offline": offline, "model": model or ""}
|
||||
return {
|
||||
"ok": True,
|
||||
"text": (resp.text or "").strip(),
|
||||
"model": resp.model or model or "",
|
||||
"offline": offline,
|
||||
}
|
||||
|
||||
|
||||
# ── Audit & rollback ──
|
||||
|
||||
|
||||
|
||||
@@ -190,6 +190,11 @@ class LLMClient:
|
||||
async def _mock_complete(self, messages, model, tools) -> LLMResponse:
|
||||
user_content = self._last_user_content(messages)
|
||||
sys_content = self._system_content(messages)
|
||||
# Only the user's objective drives the planner. The engine appends the
|
||||
# workspace/document snapshot under "# Contexte"; that text must never
|
||||
# trigger keyword heuristics (a doc mentioning "recherche"/"collection"
|
||||
# used to misroute content requests into tool calls).
|
||||
objective = user_content.split("\n# Contexte")[0]
|
||||
|
||||
# Once tool results are already in the conversation, we have acted:
|
||||
# stop issuing new tool calls and conclude.
|
||||
@@ -200,7 +205,7 @@ class LLMClient:
|
||||
)
|
||||
|
||||
# Skill-driven: if the objective names a known skill, mirror its template.
|
||||
skill_hint = self._extract_skill_hint(user_content)
|
||||
skill_hint = self._extract_skill_hint(objective)
|
||||
|
||||
if skill_hint == "sprint":
|
||||
return LLMResponse(
|
||||
@@ -218,13 +223,13 @@ class LLMClient:
|
||||
# before the tool-intent heuristics and scoped to the user objective
|
||||
# only, so an injected "# Contexte" that happens to mention "collection"
|
||||
# can't misroute a writing request into a create-collection action.
|
||||
draft = self._draft_reply(user_content.split("\n# Contexte")[0])
|
||||
draft = self._draft_reply(objective)
|
||||
if draft:
|
||||
return LLMResponse(text=draft, model=model)
|
||||
|
||||
# Exact keyword → tool intent resolution.
|
||||
# Exact keyword → tool intent resolution (objective only).
|
||||
for regex, builder in _CREATE_PATTERNS:
|
||||
m = regex.search(user_content)
|
||||
m = regex.search(objective)
|
||||
if m:
|
||||
return LLMResponse(
|
||||
tool_calls=[dict(name=name, arguments=self._bind_placeholders(args, sys_content)) for name, args in [builder(m)]],
|
||||
@@ -233,7 +238,7 @@ class LLMClient:
|
||||
)
|
||||
|
||||
for regex, builder in _SEARCH_PATTERNS:
|
||||
m = regex.search(user_content)
|
||||
m = regex.search(objective)
|
||||
if m:
|
||||
return LLMResponse(
|
||||
tool_calls=[dict(name=name, arguments=args) for name, args in [builder(m)]],
|
||||
@@ -242,9 +247,9 @@ class LLMClient:
|
||||
)
|
||||
|
||||
# Loose "collection <X>" detection → treat as a create intent.
|
||||
low = user_content.lower()
|
||||
low = objective.lower()
|
||||
if "collection" in low:
|
||||
m = _COLLECTION_LINE.search(user_content)
|
||||
m = _COLLECTION_LINE.search(objective)
|
||||
if m:
|
||||
name = m.group(1).strip()
|
||||
return LLMResponse(
|
||||
@@ -256,7 +261,7 @@ class LLMClient:
|
||||
|
||||
# Plain conversational objective → final answer (no tool).
|
||||
return LLMResponse(
|
||||
text=self._summarize(user_content),
|
||||
text=self._summarize(objective),
|
||||
model=model,
|
||||
)
|
||||
|
||||
@@ -335,6 +340,24 @@ class LLMClient:
|
||||
"• Planifier le suivi et archiver ce compte-rendu.\n"
|
||||
)
|
||||
|
||||
# ── Traduction / analyse du document (hors-ligne) ──
|
||||
if re.search(r"traduis|traduit|translate", low):
|
||||
return (
|
||||
"⚠️ **Traduction non disponible en mode hors-ligne** (aucun modèle d'IA "
|
||||
"connecté).\n\n"
|
||||
"Connectez un fournisseur dans **Paramètres → Agent & IA**, puis relancez "
|
||||
"« Traduire cette page » : le document traduit apparaîtra ici, avec un aperçu "
|
||||
"à approuver ou à rejeter avant application."
|
||||
)
|
||||
if re.search(r"r[ée]sum|am[ée]lior|sugg[èe]re des|propose des", low):
|
||||
return (
|
||||
"⚠️ **Cette action nécessite un modèle d'IA connecté** pour analyser le "
|
||||
"document.\n\n"
|
||||
"Configurez une clé API dans **Paramètres → Agent & IA**, puis relancez "
|
||||
"l'action : l'agent générera la proposition ici, avec un aperçu à approuver "
|
||||
"ou à rejeter avant application."
|
||||
)
|
||||
|
||||
# ── Page / document draft (contextual "Ask AI") ──
|
||||
if title:
|
||||
t = title[:80]
|
||||
|
||||
@@ -629,8 +629,23 @@
|
||||
// (Notion-style inline generation). The reply is streamed in the agent
|
||||
// panel and inserted into this page via window.__fdInsertAI.
|
||||
askAI(){
|
||||
const self=this;
|
||||
const title=(this.pageTitle||'Sans titre').trim();
|
||||
this.aiRun('Rédige le contenu de la page FlowDeck intitulée « '+title+' ». Fournis un texte structuré et directement utilisable : paragraphes et, si utile, des listes à puces. Sépare chaque paragraphe par une ligne vide. Ne précède pas le texte de commentaires.');
|
||||
if(!(window.fdAgent && window.fdAgent.generate)){ self.showToast('Agent IA indisponible — configurez-le dans Paramètres → Agent & IA'); return; }
|
||||
if(self.aiBusy) return;
|
||||
self.aiBusy=true;
|
||||
self.showToast('FlowDeck Agent : génération en cours…');
|
||||
let ctx='';
|
||||
try{ if(window.fdGetPageContext) ctx=String(window.fdGetPageContext()||''); }catch(e){}
|
||||
const prompt='Rédige le contenu de la page FlowDeck intitulée « '+title+' ». Fournis un texte structuré et directement utilisable : paragraphes et, si utile, des listes à puces. Sépare chaque paragraphe par une ligne vide. Ne précède pas le texte de commentaires.';
|
||||
window.fdAgent.generate(prompt, ctx).then(function(text){
|
||||
self.aiBusy=false;
|
||||
if(text && text.trim()){ self.applyAIBlocks(text); }
|
||||
else { self.showToast('Aucun retour généré par l\u2019agent'); }
|
||||
}).catch(function(err){
|
||||
self.aiBusy=false;
|
||||
self.showToast((err && err.message) ? err.message : 'Erreur de génération');
|
||||
});
|
||||
},
|
||||
meetingNote(){
|
||||
// v4.12.0: crée une vraie « boîte de réunion » (bloc 'meeting') au lieu
|
||||
|
||||
@@ -55,6 +55,13 @@
|
||||
.fd-agent-msg-meta{font-size:10px;color:var(--text-dim,#888);margin-bottom:2px;text-transform:uppercase;letter-spacing:.5px}
|
||||
.fd-agent-msg-content{font-size:13.5px;color:var(--text,#ececec);white-space:pre-wrap;word-break:break-word;line-height:1.55}
|
||||
.fd-agent-msg-content a{color:var(--accent)}
|
||||
.fd-agent-msg-proposal .fd-agent-msg-content{background:rgba(35,131,226,.07);border:1px solid rgba(35,131,226,.22);border-radius:10px;padding:10px 12px}
|
||||
.fd-proposal-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:8px}
|
||||
.fd-proposal-btn{background:var(--bg-tertiary,#242424);border:1px solid var(--border,rgba(255,255,255,.12));color:var(--text,#e6e6e6);border-radius:8px;padding:6px 12px;font-size:12.5px;cursor:pointer}
|
||||
.fd-proposal-btn:hover{background:var(--bg-hover,#2e2e2e)}
|
||||
.fd-proposal-btn.primary{background:var(--accent,#2383E2);border-color:transparent;color:#fff;font-weight:600}
|
||||
.fd-proposal-btn:disabled{opacity:.5;cursor:not-allowed}
|
||||
.fd-proposal-note{font-size:11px;color:var(--text-dim,#999)}
|
||||
.fd-agent-thinking{font-size:12.5px;color:var(--text-dim,#999);padding:10px 12px;border-radius:9px;background:rgba(255,255,255,.04);margin:4px 0 8px;font-style:italic}
|
||||
.fd-ap-err{color:#ef8a8a;font-size:12px;padding:4px 2px}
|
||||
|
||||
@@ -175,11 +182,20 @@
|
||||
</div>
|
||||
|
||||
<template x-for="m in messages" :key="m.id">
|
||||
<div class="fd-agent-msg" :class="'fd-agent-msg-'+(m.role||'user')">
|
||||
<div class="fd-agent-msg" :class="'fd-agent-msg-'+(m.role||'user') + (m.proposal ? ' fd-agent-msg-proposal' : '')">
|
||||
<div class="fd-agent-msg-avatar" x-text="m.role==='assistant'?'🤖':'👤'"></div>
|
||||
<div class="fd-agent-msg-body">
|
||||
<div class="fd-agent-msg-meta" x-text="(m.role==='assistant'?'Agent':'Vous')+(m.model?' · '+m.model:'')"></div>
|
||||
<div class="fd-agent-msg-content" x-text="m.content"></div>
|
||||
<div class="fd-agent-msg-content" x-text="m.generating ? 'Génération…' : m.content"></div>
|
||||
<template x-if="m.proposal">
|
||||
<div class="fd-proposal-bar">
|
||||
<button class="fd-proposal-btn primary" type="button" @click="approveProposal(m)" :disabled="m.applied || m.offline">
|
||||
<span x-text="m.applied ? (m.rejected ? 'Rejetée' : '✓ Appliquée') : '✓ Appliquer au document'"></span>
|
||||
</button>
|
||||
<button class="fd-proposal-btn" type="button" @click="rejectProposal(m)" :disabled="m.applied">✕ Rejeter</button>
|
||||
<span class="fd-proposal-note" x-show="m.offline" x-cloak>Hors-ligne — configurez une clé API pour appliquer la modification.</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -376,10 +392,10 @@
|
||||
var c = this.activeContext || {};
|
||||
var out = [];
|
||||
if(c.kind === 'page'){
|
||||
out.push({icon:'📄', label:'Résumer ce document', prompt:'Résume ce document de façon structurée et concise.'});
|
||||
out.push({icon:'🌐', label:'Traduire cette page', prompt:'Traduis le contenu de ce document en anglais.', new:false});
|
||||
out.push({icon:'✍️', label:'Proposer des améliorations', prompt:'Analyse ce document et propose des améliorations concrètes de structure et de contenu.'});
|
||||
out.push({icon:'📝', label:'Créer une note de réunion', prompt:'Crée un squelette d\u2019AI meeting note structuré à partir de ce document.'});
|
||||
out.push({icon:'📄', label:'Résumer ce document', prompt:'Résume ce document de façon structurée et concise.', docRun:true, mode:'insert'});
|
||||
out.push({icon:'🌐', label:'Traduire cette page', prompt:'Traduis le contenu de ce document en anglais, en conservant sa structure (titres, listes, paragraphes).', docRun:true, mode:'replace'});
|
||||
out.push({icon:'✍️', label:'Proposer des améliorations', prompt:'Analyse ce document et propose une version améliorée (structure, clarté, complétude) sans en perdre le sens.', docRun:true, mode:'insert'});
|
||||
out.push({icon:'📝', label:'Créer une note de réunion', prompt:'Rédige une « AI Meeting Note » structurée à partir de ce document : en-tête avec date, sections Résumé, Décisions, Action items et Prochaines étapes.', docRun:true, mode:'insert'});
|
||||
} else if(c.kind === 'db'){
|
||||
out.push({icon:'🗄️', label:'Analyser cette base', prompt:'Analyse cette base de données et résume sa structure, ses propriétés et ses données.'});
|
||||
out.push({icon:'📊', label:'Créer une vue', prompt:'Propose une vue adaptée (board, tableau, calendrier…) pour cette base et explique pourquoi.'});
|
||||
@@ -399,6 +415,7 @@
|
||||
},
|
||||
runQuick(q){
|
||||
if(!q || !q.prompt) return;
|
||||
if(q.docRun){ this.runDocAction(q); return; }
|
||||
var self = this;
|
||||
this.ensureConversation().then(function(){
|
||||
self.input = q.prompt;
|
||||
@@ -406,6 +423,63 @@
|
||||
}).catch(function(){});
|
||||
},
|
||||
|
||||
// ── Document actions (no tools) → proposition à approuver / rejeter ──
|
||||
runDocAction(q){
|
||||
var self = this;
|
||||
this.ensureConversation().then(function(){
|
||||
self.errorMsg = '';
|
||||
self.messages.push({id:'udoc-'+Date.now(), role:'user', content: q.label});
|
||||
self.refreshContext();
|
||||
var ctxText = (self.activeContext && self.activeContext.kind === 'page' && self.activeContext.text) ? self.activeContext.text : '';
|
||||
var thinkId = 'g-'+Date.now()+Math.random();
|
||||
self.messages.push({id: thinkId, role:'assistant', generating: true});
|
||||
self.scrollBottom();
|
||||
|
||||
var payload = {prompt: q.prompt};
|
||||
if(ctxText) payload.context = ctxText;
|
||||
if(self.llmProvider) payload.provider = self.llmProvider;
|
||||
if(self.llmModel) payload.model = self.llmModel;
|
||||
|
||||
fetch('/api/agent/generate', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
self.messages = self.messages.filter(function(m){ return m.id !== thinkId; });
|
||||
if(d && d.ok && d.text){
|
||||
self.messages.push({
|
||||
id:'prop-'+Date.now()+Math.random(), role:'assistant',
|
||||
content: d.text, proposal: true, mode: q.mode || 'insert',
|
||||
offline: !!d.offline, model: d.model || '', applied: false, rejected: false
|
||||
});
|
||||
} else {
|
||||
self.messages.push({id:'err-'+Date.now()+Math.random(), role:'assistant',
|
||||
content: '⚠️ ' + ((d && d.error) || 'Échec de la génération'), model:''});
|
||||
}
|
||||
self.scrollBottom();
|
||||
}).catch(function(err){
|
||||
self.messages = self.messages.filter(function(m){ return m.id !== thinkId; });
|
||||
self.messages.push({id:'err-'+Date.now()+Math.random(), role:'assistant',
|
||||
content: '⚠️ ' + ((err && err.message) || 'Erreur réseau'), model:''});
|
||||
self.scrollBottom();
|
||||
});
|
||||
}).catch(function(){});
|
||||
},
|
||||
approveProposal(m){
|
||||
if(!m || m.applied || m.offline) return;
|
||||
var ok = window.fdApplyDocument ? window.fdApplyDocument(m.content, m.mode) : false;
|
||||
m.applied = true;
|
||||
m.rejected = false;
|
||||
if(!ok){
|
||||
m.content = '⚠️ Aucun document éditable n\u2019est ouvert pour appliquer cette proposition.';
|
||||
}
|
||||
this.$nextTick(function(){ this.scrollBottom(); });
|
||||
},
|
||||
rejectProposal(m){
|
||||
if(!m || m.applied) return;
|
||||
m.applied = true;
|
||||
m.rejected = true;
|
||||
this.$nextTick(function(){ this.scrollBottom(); });
|
||||
},
|
||||
|
||||
// ── Providers / models ──
|
||||
fetchProviders(){
|
||||
var self = this;
|
||||
@@ -832,6 +906,36 @@
|
||||
})();
|
||||
window.FlowDeckCtx = FlowDeckCtx;
|
||||
|
||||
// Apply an AI-produced document change to the open page editor.
|
||||
// mode 'replace' → remplace tout le contenu de la page ;
|
||||
// mode 'insert' → insère la proposition en fin de page (comme Ask AI).
|
||||
window.fdApplyDocument = function(text, mode){
|
||||
var E = window.E;
|
||||
if(!E || !E.blocks || typeof E.mkB !== 'function') return false;
|
||||
text = (text || '').trim();
|
||||
if(!text) return false;
|
||||
try{
|
||||
E.sync();
|
||||
if(mode === 'replace'){
|
||||
var nb = (typeof E.md2b === 'function') ? E.md2b(text) : null;
|
||||
if(!nb || !nb.length){ E.blocks = [E.mkB('paragraph', text)]; }
|
||||
else { E.blocks = nb; }
|
||||
} else {
|
||||
if(typeof E.applyAIBlocks === 'function'){ E.applyAIBlocks(text); }
|
||||
else {
|
||||
var lines = text.split(/\r?\n/).map(function(s){ return s.trim(); }).filter(Boolean);
|
||||
var startEmpty = E.blocks.length === 1 && E.blocks[0].type === 'paragraph' && !(E.blocks[0].content || '').trim();
|
||||
var nb2 = lines.map(function(l){ return E.mkB('paragraph', l); });
|
||||
E.blocks = startEmpty ? nb2 : E.blocks.concat(nb2);
|
||||
}
|
||||
}
|
||||
E.dirty = true;
|
||||
if(E.autoSave) E.autoSave();
|
||||
if(E.render) E.render();
|
||||
return true;
|
||||
}catch(e){ return false; }
|
||||
};
|
||||
|
||||
// Backward-compatible detection hooks (banner + Ask AI)
|
||||
window.fdPageContextProvider = function(){
|
||||
var c = FlowDeckCtx.current();
|
||||
|
||||
+50
-1
@@ -661,4 +661,53 @@ def test_router_user_key_test_failure(client, monkeypatch):
|
||||
|
||||
prov = client.get("/api/agent/providers").json()
|
||||
p = [x for x in prov["providers"] if x["id"] == "openai"][0]
|
||||
assert p["functional"] is False
|
||||
assert p["functional"] is False
|
||||
|
||||
|
||||
def test_generate_endpoint_returns_text(client, monkeypatch):
|
||||
"""/api/agent/generate performs headless text generation (no tools)."""
|
||||
from app.routers import agent as agent_router
|
||||
from app.services.llm_client import LLMResponse
|
||||
|
||||
class FakeLLM(agent_router.LLMClient):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def complete(self, messages, *, model=None, tools=None, stream=False):
|
||||
# The tool schema must never be offered for content generation.
|
||||
assert tools is None
|
||||
return LLMResponse(text="Voici le document traduit en anglais.", model="gpt-4o-mini")
|
||||
|
||||
monkeypatch.setattr(agent_router, "LLMClient", FakeLLM)
|
||||
|
||||
r = client.post("/api/agent/generate", json={
|
||||
"prompt": "Traduis ce document en anglais.",
|
||||
"context": "Titre du document : Doc\nContenu en français.",
|
||||
"model": "gpt-4o-mini",
|
||||
})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["ok"] is True
|
||||
assert "traduit en anglais" in body["text"]
|
||||
|
||||
|
||||
def test_generate_endpoint_requires_prompt(client):
|
||||
r = client.post("/api/agent/generate", json={"prompt": " "})
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_mock_planner_ignores_context_keywords(client):
|
||||
"""Document context words (ex: 'cherche', 'collection') must not trigger
|
||||
tool intents — the planner only inspects the user objective."""
|
||||
import asyncio
|
||||
from app.services.llm_client import LLMClient
|
||||
|
||||
llm = LLMClient(provider="offline")
|
||||
resp = asyncio.run(llm.complete(
|
||||
[{"role": "system", "content": "x"},
|
||||
{"role": "user", "content": "Résume ce document.\n\n# Contexte\n"
|
||||
"Le rapport mentionne une recherche sur la collection Z."}],
|
||||
model="gpt-4o", tools=[{"type": "function"}],
|
||||
))
|
||||
assert resp.tool_calls == []
|
||||
assert resp.text
|
||||
Reference in New Issue
Block a user