mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 22:48:11 +02:00
Fix Ollama chat reliability
This commit is contained in:
1 parent
d47f7476ed
commit
e9c83e3a0d
5 files changed
+297
-78
No files matched your search
+71
-19
@@ -24,6 +24,12 @@ from .tools import TOOL_DEFINITIONS, ToolError, run_tool
|
|||||||
MAX_ITERATIONS = 6
|
MAX_ITERATIONS = 6
|
||||||
|
|
||||||
|
|
||||||
|
def _tools_not_supported(exc: OllamaError) -> bool:
|
||||||
|
"""Détecte l'erreur Ollama renvoyée par un modèle sans function calling."""
|
||||||
|
message = str(exc).lower()
|
||||||
|
return "does not support tools" in message or "does not support tool" in message
|
||||||
|
|
||||||
|
|
||||||
def _parse_args(raw) -> dict:
|
def _parse_args(raw) -> dict:
|
||||||
if isinstance(raw, dict):
|
if isinstance(raw, dict):
|
||||||
return raw
|
return raw
|
||||||
@@ -54,26 +60,62 @@ async def run_agent(
|
|||||||
tools = None
|
tools = None
|
||||||
collected: list[dict] = []
|
collected: list[dict] = []
|
||||||
text_parts: list[str] = []
|
text_parts: list[str] = []
|
||||||
|
active_tools = tools
|
||||||
|
tool_fallback_used = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for _ in range(MAX_ITERATIONS):
|
for _ in range(MAX_ITERATIONS):
|
||||||
content_buf = ""
|
content_buf = ""
|
||||||
|
thinking_buf = ""
|
||||||
|
thinking_status_sent = False
|
||||||
tool_calls: list[dict] = []
|
tool_calls: list[dict] = []
|
||||||
|
|
||||||
async for chunk in ollama.chat(
|
# Un modèle peut savoir discuter sans supporter les outils. Ollama
|
||||||
model, convo, tools=tools, options=options, stream=True
|
# refuse alors la requête entière : on retente une fois en chat simple.
|
||||||
):
|
while True:
|
||||||
msg = chunk.get("message", {})
|
try:
|
||||||
token = msg.get("content", "")
|
async for chunk in ollama.chat(
|
||||||
if token:
|
model, convo, tools=active_tools, options=options, stream=True
|
||||||
content_buf += token
|
):
|
||||||
yield {"type": "token", "content": token}
|
msg = chunk.get("message", {})
|
||||||
if msg.get("tool_calls"):
|
token = msg.get("content", "")
|
||||||
tool_calls.extend(msg["tool_calls"])
|
thinking = msg.get("thinking", "")
|
||||||
if chunk.get("done"):
|
if thinking:
|
||||||
|
thinking_buf += thinking
|
||||||
|
if not thinking_status_sent:
|
||||||
|
thinking_status_sent = True
|
||||||
|
yield {"type": "status", "message": "Réflexion…"}
|
||||||
|
if token:
|
||||||
|
content_buf += token
|
||||||
|
yield {"type": "token", "content": token}
|
||||||
|
if msg.get("tool_calls"):
|
||||||
|
tool_calls.extend(msg["tool_calls"])
|
||||||
|
if chunk.get("done"):
|
||||||
|
break
|
||||||
break
|
break
|
||||||
|
except OllamaError as exc:
|
||||||
|
if (
|
||||||
|
active_tools
|
||||||
|
and not tool_fallback_used
|
||||||
|
and not content_buf
|
||||||
|
and not tool_calls
|
||||||
|
and _tools_not_supported(exc)
|
||||||
|
):
|
||||||
|
active_tools = None
|
||||||
|
tool_fallback_used = True
|
||||||
|
yield {
|
||||||
|
"type": "notice",
|
||||||
|
"message": (
|
||||||
|
"Ce modèle ne supporte pas les outils ; "
|
||||||
|
"réponse en mode conversation simple."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
assistant_turn: dict = {"role": "assistant", "content": content_buf}
|
assistant_turn: dict = {"role": "assistant", "content": content_buf}
|
||||||
|
if thinking_buf:
|
||||||
|
assistant_turn["thinking"] = thinking_buf
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
assistant_turn["tool_calls"] = tool_calls
|
assistant_turn["tool_calls"] = tool_calls
|
||||||
convo.append(assistant_turn)
|
convo.append(assistant_turn)
|
||||||
@@ -106,7 +148,7 @@ async def run_agent(
|
|||||||
convo.append(
|
convo.append(
|
||||||
{
|
{
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"name": name,
|
"tool_name": name,
|
||||||
"content": json.dumps(
|
"content": json.dumps(
|
||||||
{
|
{
|
||||||
"ok": False,
|
"ok": False,
|
||||||
@@ -137,7 +179,7 @@ async def run_agent(
|
|||||||
convo.append(
|
convo.append(
|
||||||
{
|
{
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"name": name,
|
"tool_name": name,
|
||||||
"content": json.dumps(result, ensure_ascii=False),
|
"content": json.dumps(result, ensure_ascii=False),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -162,11 +204,21 @@ async def run_agent(
|
|||||||
yield {"type": "error", "message": f"Ollama : {exc}"}
|
yield {"type": "error", "message": f"Ollama : {exc}"}
|
||||||
return
|
return
|
||||||
except (httpx.HTTPError, OSError) as exc:
|
except (httpx.HTTPError, OSError) as exc:
|
||||||
yield {"type": "error", "message": str(exc)}
|
yield {
|
||||||
|
"type": "error",
|
||||||
|
"message": f"Impossible de joindre Ollama ({ollama.host}) : {exc}",
|
||||||
|
}
|
||||||
return
|
return
|
||||||
|
|
||||||
yield {
|
final_content = "\n\n".join(text_parts).strip()
|
||||||
"type": "final",
|
if not final_content and not collected:
|
||||||
"content": "\n\n".join(text_parts).strip(),
|
yield {
|
||||||
"tools": collected,
|
"type": "error",
|
||||||
}
|
"message": (
|
||||||
|
"Le modèle a terminé sans renvoyer de texte. Essayez un modèle "
|
||||||
|
"de chat récent ou désactivez son mode de réflexion avancée."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
yield {"type": "final", "content": final_content, "tools": collected}
|
||||||
+49
-15
@@ -9,6 +9,7 @@ Flux :
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
@@ -19,6 +20,7 @@ from ..agent import run_agent
|
|||||||
from ..config import settings
|
from ..config import settings
|
||||||
|
|
||||||
router = APIRouter(prefix="/api", tags=["chat"])
|
router = APIRouter(prefix="/api", tags=["chat"])
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChatRequest(BaseModel):
|
class ChatRequest(BaseModel):
|
||||||
@@ -54,20 +56,38 @@ async def chat(req: ChatRequest) -> StreamingResponse:
|
|||||||
yield _sse("start", {"model": model})
|
yield _sse("start", {"model": model})
|
||||||
final_content = ""
|
final_content = ""
|
||||||
tools_meta: list[dict] = []
|
tools_meta: list[dict] = []
|
||||||
|
error_message = ""
|
||||||
|
|
||||||
async for ev in run_agent(
|
try:
|
||||||
model,
|
async for ev in run_agent(
|
||||||
convo,
|
model,
|
||||||
options=agent_config.ollama_options(cfg),
|
convo,
|
||||||
enabled_tools=agent_config.enabled_tool_names(cfg),
|
options=agent_config.ollama_options(cfg),
|
||||||
confirm_shell=cfg.get("confirm_shell", True),
|
enabled_tools=agent_config.enabled_tool_names(cfg),
|
||||||
):
|
confirm_shell=cfg.get("confirm_shell", True),
|
||||||
etype = ev.pop("type")
|
):
|
||||||
if etype in ("token", "tool_call", "tool_result", "tool_confirm", "error"):
|
etype = ev.pop("type")
|
||||||
yield _sse(etype, ev)
|
if etype in (
|
||||||
elif etype == "final":
|
"token",
|
||||||
final_content = ev["content"]
|
"status",
|
||||||
tools_meta = ev["tools"]
|
"notice",
|
||||||
|
"tool_call",
|
||||||
|
"tool_result",
|
||||||
|
"tool_confirm",
|
||||||
|
):
|
||||||
|
yield _sse(etype, ev)
|
||||||
|
elif etype == "error":
|
||||||
|
error_message = ev.get("message", "Erreur Ollama inconnue")
|
||||||
|
yield _sse("error", {"message": error_message})
|
||||||
|
elif etype == "final":
|
||||||
|
final_content = ev["content"]
|
||||||
|
tools_meta = ev["tools"]
|
||||||
|
except Exception as exc:
|
||||||
|
# Après l'envoi des en-têtes SSE, une exception non gérée coupe le
|
||||||
|
# socket sans explication et laisse l'interface bloquée.
|
||||||
|
logger.exception("Échec inattendu du flux de chat")
|
||||||
|
error_message = f"Erreur interne du chat : {exc}"
|
||||||
|
yield _sse("error", {"message": error_message})
|
||||||
|
|
||||||
if final_content or tools_meta:
|
if final_content or tools_meta:
|
||||||
db.add_message(
|
db.add_message(
|
||||||
@@ -77,6 +97,20 @@ async def chat(req: ChatRequest) -> StreamingResponse:
|
|||||||
model,
|
model,
|
||||||
meta={"tools": tools_meta} if tools_meta else None,
|
meta={"tools": tools_meta} if tools_meta else None,
|
||||||
)
|
)
|
||||||
yield _sse("done", {"content": final_content, "tools": tools_meta})
|
yield _sse(
|
||||||
|
"done",
|
||||||
|
{
|
||||||
|
"content": final_content,
|
||||||
|
"tools": tools_meta,
|
||||||
|
"error": error_message or None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
return StreamingResponse(
|
||||||
|
event_stream(),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "no-cache, no-transform",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
},
|
||||||
|
)
|
||||||
+87
-30
@@ -176,15 +176,36 @@ export async function streamChat(
|
|||||||
onToolCall: (call: ToolCall) => void;
|
onToolCall: (call: ToolCall) => void;
|
||||||
onToolResult: (call: ToolCall) => void;
|
onToolResult: (call: ToolCall) => void;
|
||||||
onToolConfirm: (command: string) => void;
|
onToolConfirm: (command: string) => void;
|
||||||
|
onStatus: (msg: string) => void;
|
||||||
|
onNotice: (msg: string) => void;
|
||||||
onDone: (full: string) => void;
|
onDone: (full: string) => void;
|
||||||
onError: (msg: string) => void;
|
onError: (msg: string) => void;
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const res = await fetch("/api/chat", {
|
let res: Response;
|
||||||
method: "POST",
|
try {
|
||||||
headers: { "Content-Type": "application/json" },
|
res = await fetch("/api/chat", {
|
||||||
body: JSON.stringify(body),
|
method: "POST",
|
||||||
});
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
handlers.onError(
|
||||||
|
err instanceof Error ? err.message : "serveur Loki injoignable"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
let message = `requête refusée (${res.status})`;
|
||||||
|
try {
|
||||||
|
const payload = await res.json();
|
||||||
|
message = payload.detail ?? payload.error ?? message;
|
||||||
|
} catch {
|
||||||
|
/* réponse non JSON */
|
||||||
|
}
|
||||||
|
handlers.onError(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!res.body) {
|
if (!res.body) {
|
||||||
handlers.onError("pas de flux de réponse");
|
handlers.onError("pas de flux de réponse");
|
||||||
return;
|
return;
|
||||||
@@ -193,35 +214,71 @@ export async function streamChat(
|
|||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
|
let terminal = false;
|
||||||
|
let failed = false;
|
||||||
|
|
||||||
while (true) {
|
const dispatch = (raw: string) => {
|
||||||
const { done, value } = await reader.read();
|
const block = raw.replace(/\r\n/g, "\n");
|
||||||
if (done) break;
|
let event = "message";
|
||||||
buffer += decoder.decode(value, { stream: true });
|
const dataLines: string[] = [];
|
||||||
const events = buffer.split("\n\n");
|
for (const line of block.split("\n")) {
|
||||||
buffer = events.pop() ?? "";
|
if (line.startsWith("event:")) event = line.slice(6).trim();
|
||||||
|
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
||||||
for (const block of events) {
|
}
|
||||||
let event = "message";
|
if (dataLines.length === 0) return;
|
||||||
let data = "";
|
try {
|
||||||
for (const line of block.split("\n")) {
|
const payload = JSON.parse(dataLines.join("\n"));
|
||||||
if (line.startsWith("event: ")) event = line.slice(7).trim();
|
if (event === "token") handlers.onToken(payload.content);
|
||||||
else if (line.startsWith("data: ")) data += line.slice(6);
|
else if (event === "status") handlers.onStatus(payload.message);
|
||||||
|
else if (event === "notice") handlers.onNotice(payload.message);
|
||||||
|
else if (event === "tool_call")
|
||||||
|
handlers.onToolCall({ ...payload, status: "running" });
|
||||||
|
else if (event === "tool_result") handlers.onToolResult(payload);
|
||||||
|
else if (event === "tool_confirm") handlers.onToolConfirm(payload.command);
|
||||||
|
else if (event === "error") {
|
||||||
|
failed = true;
|
||||||
|
handlers.onError(payload.message);
|
||||||
|
} else if (event === "done") {
|
||||||
|
terminal = true;
|
||||||
|
if (payload.error) {
|
||||||
|
if (!failed) handlers.onError(payload.error);
|
||||||
|
failed = true;
|
||||||
|
} else if (!failed) {
|
||||||
|
handlers.onDone(payload.content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!data) continue;
|
} catch {
|
||||||
try {
|
if (!failed) {
|
||||||
const payload = JSON.parse(data);
|
failed = true;
|
||||||
if (event === "token") handlers.onToken(payload.content);
|
handlers.onError("réponse illisible reçue du serveur");
|
||||||
else if (event === "tool_call")
|
|
||||||
handlers.onToolCall({ ...payload, status: "running" });
|
|
||||||
else if (event === "tool_result") handlers.onToolResult(payload);
|
|
||||||
else if (event === "tool_confirm") handlers.onToolConfirm(payload.command);
|
|
||||||
else if (event === "done") handlers.onDone(payload.content);
|
|
||||||
else if (event === "error") handlers.onError(payload.message);
|
|
||||||
} catch {
|
|
||||||
/* bloc partiel */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
|
||||||
|
const events = buffer.split("\n\n");
|
||||||
|
buffer = events.pop() ?? "";
|
||||||
|
for (const block of events) {
|
||||||
|
if (block.trim()) dispatch(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer += decoder.decode();
|
||||||
|
if (buffer.trim()) dispatch(buffer);
|
||||||
|
} catch (err) {
|
||||||
|
if (!failed) {
|
||||||
|
failed = true;
|
||||||
|
handlers.onError(
|
||||||
|
err instanceof Error ? err.message : "connexion au chat interrompue"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!terminal && !failed) {
|
||||||
|
handlers.onError("le serveur a fermé la réponse avant sa fin");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export function ChatPanel() {
|
|||||||
messages,
|
messages,
|
||||||
streaming,
|
streaming,
|
||||||
streamContent,
|
streamContent,
|
||||||
|
streamStatus,
|
||||||
|
streamNotice,
|
||||||
streamTools,
|
streamTools,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
@@ -101,6 +103,8 @@ export function ChatPanel() {
|
|||||||
created_at: Date.now() / 1000,
|
created_at: Date.now() / 1000,
|
||||||
}}
|
}}
|
||||||
pending
|
pending
|
||||||
|
pendingStatus={streamStatus}
|
||||||
|
notice={streamNotice}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{pendingShell && (
|
{pendingShell && (
|
||||||
@@ -154,7 +158,17 @@ export function ChatPanel() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Bubble({ msg, pending }: { msg: Message; pending?: boolean }) {
|
function Bubble({
|
||||||
|
msg,
|
||||||
|
pending,
|
||||||
|
pendingStatus,
|
||||||
|
notice,
|
||||||
|
}: {
|
||||||
|
msg: Message;
|
||||||
|
pending?: boolean;
|
||||||
|
pendingStatus?: string;
|
||||||
|
notice?: string | null;
|
||||||
|
}) {
|
||||||
const time = new Date(msg.created_at * 1000).toLocaleTimeString("fr-FR", {
|
const time = new Date(msg.created_at * 1000).toLocaleTimeString("fr-FR", {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@@ -198,9 +212,16 @@ function Bubble({ msg, pending }: { msg: Message; pending?: boolean }) {
|
|||||||
{(msg.meta?.tools ?? []).map((t: ToolCall, i: number) => (
|
{(msg.meta?.tools ?? []).map((t: ToolCall, i: number) => (
|
||||||
<ToolCard key={i} call={t} />
|
<ToolCard key={i} call={t} />
|
||||||
))}
|
))}
|
||||||
|
{notice && (
|
||||||
|
<div className="mb-2 rounded-lg border border-[#4a3a2a] bg-card-deep px-3 py-2 text-xs text-warn">
|
||||||
|
{notice}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{(msg.content || pending) && (
|
{(msg.content || pending) && (
|
||||||
<div className="text-sm leading-[1.65] text-ink-2 whitespace-pre-wrap">
|
<div className="text-sm leading-[1.65] text-ink-2 whitespace-pre-wrap">
|
||||||
{msg.content}
|
{msg.content || (
|
||||||
|
<span className="text-muted-2">{pendingStatus || "Génération…"}</span>
|
||||||
|
)}
|
||||||
{pending && (
|
{pending && (
|
||||||
<span className="ml-0.5 inline-block h-3.5 w-[7px] animate-pulse bg-accent align-middle" />
|
<span className="ml-0.5 inline-block h-3.5 w-[7px] animate-pulse bg-accent align-middle" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ interface LokiState {
|
|||||||
messages: Message[];
|
messages: Message[];
|
||||||
streaming: boolean;
|
streaming: boolean;
|
||||||
streamContent: string; // réponse de l'agent en cours de frappe
|
streamContent: string; // réponse de l'agent en cours de frappe
|
||||||
|
streamStatus: string;
|
||||||
|
streamNotice: string | null;
|
||||||
streamTools: ToolCall[]; // appels d'outils de la réponse en cours
|
streamTools: ToolCall[]; // appels d'outils de la réponse en cours
|
||||||
|
|
||||||
fileTree: FileNode[];
|
fileTree: FileNode[];
|
||||||
@@ -77,6 +79,8 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
messages: [],
|
messages: [],
|
||||||
streaming: false,
|
streaming: false,
|
||||||
streamContent: "",
|
streamContent: "",
|
||||||
|
streamStatus: "",
|
||||||
|
streamNotice: null,
|
||||||
streamTools: [],
|
streamTools: [],
|
||||||
fileTree: [],
|
fileTree: [],
|
||||||
previewPath: null,
|
previewPath: null,
|
||||||
@@ -154,9 +158,6 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
try {
|
try {
|
||||||
const status = await getStatus();
|
const status = await getStatus();
|
||||||
set({ status });
|
set({ status });
|
||||||
if (!get().selectedModel && status.default_model) {
|
|
||||||
set({ selectedModel: status.default_model });
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
set({ status: { connected: false, host: "", default_model: "" } });
|
set({ status: { connected: false, host: "", default_model: "" } });
|
||||||
}
|
}
|
||||||
@@ -166,8 +167,14 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
set({ loadingModels: true });
|
set({ loadingModels: true });
|
||||||
try {
|
try {
|
||||||
const { models, default: def } = await getModels();
|
const { models, default: def } = await getModels();
|
||||||
set({ models });
|
const installed = new Set(models.map((model) => model.name));
|
||||||
if (!get().selectedModel && def) set({ selectedModel: def });
|
const current = get().selectedModel;
|
||||||
|
const selectedModel = installed.has(current)
|
||||||
|
? current
|
||||||
|
: installed.has(def)
|
||||||
|
? def
|
||||||
|
: models[0]?.name ?? "";
|
||||||
|
set({ models, selectedModel });
|
||||||
} finally {
|
} finally {
|
||||||
set({ loadingModels: false });
|
set({ loadingModels: false });
|
||||||
}
|
}
|
||||||
@@ -180,13 +187,25 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
|
|
||||||
newSession: async () => {
|
newSession: async () => {
|
||||||
const s = await createSession(get().selectedModel || undefined);
|
const s = await createSession(get().selectedModel || undefined);
|
||||||
set({ currentSessionId: s.id, messages: [], streamContent: "" });
|
set({
|
||||||
|
currentSessionId: s.id,
|
||||||
|
messages: [],
|
||||||
|
streamContent: "",
|
||||||
|
streamStatus: "",
|
||||||
|
streamNotice: null,
|
||||||
|
});
|
||||||
await get().refreshSessions();
|
await get().refreshSessions();
|
||||||
},
|
},
|
||||||
|
|
||||||
openSession: async (id) => {
|
openSession: async (id) => {
|
||||||
const { messages } = await getSession(id);
|
const { messages } = await getSession(id);
|
||||||
set({ currentSessionId: id, messages, streamContent: "" });
|
set({
|
||||||
|
currentSessionId: id,
|
||||||
|
messages,
|
||||||
|
streamContent: "",
|
||||||
|
streamStatus: "",
|
||||||
|
streamNotice: null,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
removeSession: async (id) => {
|
removeSession: async (id) => {
|
||||||
@@ -201,13 +220,37 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
if (get().streaming) return;
|
if (get().streaming) return;
|
||||||
content = content.trim();
|
content = content.trim();
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
|
if (!get().selectedModel) {
|
||||||
|
const errMsg: Message = {
|
||||||
|
id: `err-${Date.now()}`,
|
||||||
|
session_id: get().currentSessionId ?? "",
|
||||||
|
role: "assistant",
|
||||||
|
content: "⚠️ Aucun modèle Ollama installé ou sélectionné.",
|
||||||
|
created_at: Date.now() / 1000,
|
||||||
|
};
|
||||||
|
set({ messages: [...get().messages, errMsg] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Crée une session à la volée si aucune n'est ouverte.
|
// Crée une session à la volée si aucune n'est ouverte.
|
||||||
let sid = get().currentSessionId;
|
let sid = get().currentSessionId;
|
||||||
if (!sid) {
|
try {
|
||||||
const s = await createSession(get().selectedModel || undefined);
|
if (!sid) {
|
||||||
sid = s.id;
|
const s = await createSession(get().selectedModel || undefined);
|
||||||
set({ currentSessionId: s.id });
|
sid = s.id;
|
||||||
|
set({ currentSessionId: s.id });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const detail = err instanceof Error ? err.message : "backend injoignable";
|
||||||
|
const errMsg: Message = {
|
||||||
|
id: `err-${Date.now()}`,
|
||||||
|
session_id: "",
|
||||||
|
role: "assistant",
|
||||||
|
content: `⚠️ Impossible de créer la session : ${detail}`,
|
||||||
|
created_at: Date.now() / 1000,
|
||||||
|
};
|
||||||
|
set({ messages: [...get().messages, errMsg] });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Affichage optimiste du message utilisateur.
|
// Affichage optimiste du message utilisateur.
|
||||||
@@ -222,6 +265,8 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
messages: [...get().messages, userMsg],
|
messages: [...get().messages, userMsg],
|
||||||
streaming: true,
|
streaming: true,
|
||||||
streamContent: "",
|
streamContent: "",
|
||||||
|
streamStatus: "Connexion à Ollama…",
|
||||||
|
streamNotice: null,
|
||||||
streamTools: [],
|
streamTools: [],
|
||||||
pendingShell: null,
|
pendingShell: null,
|
||||||
});
|
});
|
||||||
@@ -230,6 +275,8 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
{ session_id: sid, content, model: get().selectedModel || undefined },
|
{ session_id: sid, content, model: get().selectedModel || undefined },
|
||||||
{
|
{
|
||||||
onToken: (t) => set({ streamContent: get().streamContent + t }),
|
onToken: (t) => set({ streamContent: get().streamContent + t }),
|
||||||
|
onStatus: (message) => set({ streamStatus: message }),
|
||||||
|
onNotice: (message) => set({ streamNotice: message }),
|
||||||
onToolCall: (call) =>
|
onToolCall: (call) =>
|
||||||
set({ streamTools: [...get().streamTools, call] }),
|
set({ streamTools: [...get().streamTools, call] }),
|
||||||
onToolResult: (call) => {
|
onToolResult: (call) => {
|
||||||
@@ -254,7 +301,13 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
typeof t.args?.path === "string" &&
|
typeof t.args?.path === "string" &&
|
||||||
/\.html?$/.test(t.args.path as string)
|
/\.html?$/.test(t.args.path as string)
|
||||||
);
|
);
|
||||||
set({ streaming: false, streamContent: "", streamTools: [] });
|
set({
|
||||||
|
streaming: false,
|
||||||
|
streamContent: "",
|
||||||
|
streamStatus: "",
|
||||||
|
streamNotice: null,
|
||||||
|
streamTools: [],
|
||||||
|
});
|
||||||
// Recharge depuis la base + l'arborescence (fichiers créés par l'agent).
|
// Recharge depuis la base + l'arborescence (fichiers créés par l'agent).
|
||||||
if (get().currentSessionId === sid) await get().openSession(sid!);
|
if (get().currentSessionId === sid) await get().openSession(sid!);
|
||||||
await get().refreshSessions();
|
await get().refreshSessions();
|
||||||
@@ -272,6 +325,8 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
set({
|
set({
|
||||||
streaming: false,
|
streaming: false,
|
||||||
streamContent: "",
|
streamContent: "",
|
||||||
|
streamStatus: "",
|
||||||
|
streamNotice: null,
|
||||||
streamTools: [],
|
streamTools: [],
|
||||||
messages: [...get().messages, errMsg],
|
messages: [...get().messages, errMsg],
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user