diff --git a/backend/app/enhance.py b/backend/app/enhance.py index 6bd9737..4290f54 100644 --- a/backend/app/enhance.py +++ b/backend/app/enhance.py @@ -22,6 +22,20 @@ _PLAN_PROMPT = ( "UNIQUEMENT les étapes, en français." ) +_PLAN_CODE_PROMPT = ( + "Tu es architecte logiciel. Établis un plan d'IMPLÉMENTATION concret pour " + "cette application, en 3 à 5 étapes courtes (une par ligne, numérotées " + "« 1. », « 2. »…).\n" + "- Étape 1 = ARCHITECTURE : quels fichiers créer et leur rôle (ex. un seul " + "index.html avec HTML+CSS+JS, ou séparer index.html / style.css / app.js).\n" + "- Étapes suivantes = construire UNE fonctionnalité concrète et testable à " + "la fois (structure/affichage, puis interactions, puis logique).\n" + "Reste RÉALISTE et réalisable en une passe : pas de dépendance externe, pas " + "de bibliothèque à installer, pas de « moteur IA » complexe si ce n'est pas " + "explicitement demandé — une logique simple en JavaScript suffit. " + "Pas d'introduction ni de conclusion, UNIQUEMENT les étapes, en français." +) + _CRITIQUE_PROMPT = ( "Tu es un relecteur exigeant. Voici une demande et la réponse d'un " "assistant. Si la réponse est correcte et complète, réponds exactement " @@ -90,13 +104,19 @@ async def make_plan( model: str, message: str, *, + code: bool = False, options: dict | None = None, keep_alive: str | None = None, ) -> list[str]: - """Renvoie la liste des étapes (vide si échec — jamais bloquant).""" + """Renvoie la liste des étapes (vide si échec — jamais bloquant). + + ``code=True`` bascule sur un plan d'ARCHITECTURE/implémentation concret + (fichiers, structure, fonctionnalités) plutôt qu'une liste d'objectifs. + """ try: raw = await _ask( - model, _PLAN_PROMPT, message[:1200], num_predict=220, + model, _PLAN_CODE_PROMPT if code else _PLAN_PROMPT, + message[:1200], num_predict=260, options=options, keep_alive=keep_alive, ) except (httpx.HTTPError, OSError) as exc: diff --git a/backend/app/routes/chat.py b/backend/app/routes/chat.py index 9d0ef73..24fe0e4 100644 --- a/backend/app/routes/chat.py +++ b/backend/app/routes/chat.py @@ -349,7 +349,8 @@ async def chat(req: ChatRequest) -> StreamingResponse: prep = asyncio.ensure_future(asyncio.gather( rag.recall(req.session_id, req.content, embed_model=cfg.get("embed_model")) if want_rag else asyncio.sleep(0, result=[]), - enhance.make_plan(model, req.content, options=run_opts, keep_alive=keep) + enhance.make_plan(model, req.content, code=use_code, + options=run_opts, keep_alive=keep) if want_plan else asyncio.sleep(0, result=[]), coder.pick_code_model(model, cfg.get("code_model")) if use_code else asyncio.sleep(0, result=model), diff --git a/backend/app/tools.py b/backend/app/tools.py index 0e16721..f68ec26 100644 --- a/backend/app/tools.py +++ b/backend/app/tools.py @@ -512,11 +512,17 @@ def _guard_shell(command: str) -> None: sortent du workspace. Combiné à la validation utilisateur, ça empêche le modèle d'écrire ailleurs que dans son workspace. """ - root = _workspace_root() + # Racine autorisée = base du workspace (pas le sous-dossier projet) : ainsi + # `ls /workspace` reste permis même quand la session cible un projet. + root = os.path.abspath(settings.workspace_dir) for token in _PATH_TOKEN.findall(command): token = token.strip() if not token: continue + # Jeton commençant par `//` = autorité d'URL (http://, ws://…) ou chemin + # réseau, jamais une évasion du système de fichiers : on l'ignore. + if token.startswith("//"): + continue if token.startswith("~"): raise ToolError( f"chemin hors du workspace refusé : {token}. "