fixed injection watcher

This commit is contained in:
2026-06-11 08:18:30 +03:00
parent 481b93e84a
commit b5a1831b8e
5 changed files with 56 additions and 18 deletions
+8 -1
View File
@@ -48,8 +48,15 @@ async def send_message(
if not service.get_session(session_id): if not service.get_session(session_id):
raise HTTPException(status_code=404, detail="Session not found") raise HTTPException(status_code=404, detail="Session not found")
# Сохраняем user до стрима: иначе при обрыве SSE сообщение не попадает в БД.
service.save_user_message(session_id, payload.content)
async def event_stream(): async def event_stream():
async for chunk in service.stream_response(session_id, payload.content): async for chunk in service.stream_response(
session_id,
payload.content,
user_message_saved=True,
):
yield chunk yield chunk
return StreamingResponse( return StreamingResponse(
+19 -4
View File
@@ -172,13 +172,23 @@ class ChatService:
self.db.refresh(message) self.db.refresh(message)
return message return message
async def stream_response(self, session_id: int, user_text: str) -> AsyncIterator[str]: def save_user_message(self, session_id: int, user_text: str) -> None:
self._save_message(session_id, "user", user_text)
async def stream_response(
self,
session_id: int,
user_text: str,
*,
user_message_saved: bool = False,
) -> AsyncIterator[str]:
session = self.get_session(session_id) session = self.get_session(session_id)
if not session: if not session:
yield self._sse("error", {"message": "Session not found"}) yield self._sse("error", {"message": "Session not found"})
return return
self._save_message(session_id, "user", user_text) if not user_message_saved:
self._save_message(session_id, "user", user_text)
yield self._sse("status", {"phase": "preparing"}) yield self._sse("status", {"phase": "preparing"})
t0 = time.monotonic() t0 = time.monotonic()
messages = await asyncio.to_thread(_build_messages_for_session, session_id) messages = await asyncio.to_thread(_build_messages_for_session, session_id)
@@ -299,10 +309,15 @@ class ChatService:
if not final_content and reasoning: if not final_content and reasoning:
final_content = reasoning.strip() final_content = reasoning.strip()
if not final_content and all_tool_notices: if not final_content and all_tool_notices:
# Notices уже ушли в SSE event: notice; здесь только финальный текст в БД.
final_content = "\n\n".join(all_tool_notices) final_content = "\n\n".join(all_tool_notices)
yield self._sse("token", {"content": final_content})
if not final_content and tools_executed: if not final_content and tools_executed:
retry = await self.llm.complete(messages, tools=None, temperature=0.4) retry = await self.llm.complete(
messages,
tools=None,
temperature=0.4,
visible_reply=True,
)
final_content = (retry.get("content") or "").strip() final_content = (retry.get("content") or "").strip()
if final_content: if final_content:
yield self._sse("token", {"content": final_content}) yield self._sse("token", {"content": final_content})
+2 -1
View File
@@ -171,6 +171,7 @@ class LLMClient:
temperature: float = 0.7, temperature: float = 0.7,
model: str | None = None, model: str | None = None,
for_extraction: bool = False, for_extraction: bool = False,
visible_reply: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
use_tools = bool(tools) and self.tools_enabled and not for_extraction use_tools = bool(tools) and self.tools_enabled and not for_extraction
kwargs: dict[str, Any] = { kwargs: dict[str, Any] = {
@@ -198,7 +199,7 @@ class LLMClient:
reasoning = str(value) reasoning = str(value)
break break
if not content and reasoning: if not content and reasoning and not visible_reply:
content = reasoning content = reasoning
result: dict[str, Any] = { result: dict[str, Any] = {
+22 -9
View File
@@ -228,22 +228,17 @@ export const api = {
}); });
if (!response.ok || !response.body) { if (!response.ok || !response.body) {
throw new Error("Failed to send message"); const detail = await response.text().catch(() => "");
throw new Error(detail || `Ошибка отправки (${response.status})`);
} }
const reader = response.body.getReader(); const reader = response.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ""; let buffer = "";
while (true) { const flushParts = function* (parts: string[]) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop() ?? "";
for (const part of parts) { for (const part of parts) {
if (!part.trim()) continue;
const lines = part.split("\n"); const lines = part.split("\n");
let event = "message"; let event = "message";
let data = ""; let data = "";
@@ -257,6 +252,24 @@ export const api = {
yield { event, data: JSON.parse(data) }; yield { event, data: JSON.parse(data) };
} }
} }
};
while (true) {
const { done, value } = await reader.read();
if (value) {
buffer += decoder.decode(value, { stream: !done });
}
const parts = buffer.split("\n\n");
buffer = parts.pop() ?? "";
yield* flushParts(parts);
if (done) {
if (buffer.trim()) {
yield* flushParts([buffer]);
}
break;
}
} }
}, },
+5 -3
View File
@@ -190,7 +190,6 @@ export default function Chat() {
setStreaming(""); setStreaming("");
setLiveNotices([]); setLiveNotices([]);
setChatError(null); setChatError(null);
setLoading(false);
if (assistantText.trim()) { if (assistantText.trim()) {
setMessages((prev) => [ setMessages((prev) => [
...prev, ...prev,
@@ -202,8 +201,8 @@ export default function Chat() {
}, },
]); ]);
} }
void loadMessages(activeId); await loadMessages(activeId);
void loadSessions(); await loadSessions();
} }
if (chunk.event === "error") { if (chunk.event === "error") {
throw new Error(chunk.data.message); throw new Error(chunk.data.message);
@@ -214,6 +213,9 @@ export default function Chat() {
const message = err instanceof Error ? err.message : "Ошибка чата"; const message = err instanceof Error ? err.message : "Ошибка чата";
setChatError(message); setChatError(message);
setStreaming(""); setStreaming("");
if (activeId) {
await loadMessages(activeId);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }