fixed injection watcher
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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] = {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user