Добавлена поддержка стриминга сообщений через yield токены. 'Деактивирован' метод generate_stream в ollamaapi

This commit is contained in:
2026-05-12 22:51:42 +03:00
parent 30ce868764
commit 1504c5fee7
6 changed files with 158 additions and 91 deletions
Binary file not shown.
+10 -10
View File
@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, jsonify from flask import Flask, Response, render_template, request, jsonify
from core.agent.agent import Agent from core.agent.agent import Agent
from core.llm.ollamaapi import OllamaProvider from core.llm.ollamaapi import OllamaProvider
from core.character.character import Character from core.character.character import Character
@@ -39,18 +39,18 @@ def init():
#@ui.route("/stream", methods=["POST"]) #@ui.route("/stream", methods=["POST"])
def chat(): def chat():
data = request.json global last_prompt
last_prompt = request.json["message"]
return jsonify({"status": "ok"})
user_message = data["message"] @ui.route("/stream")
def stream():
response = agent.respond(user_message)
def generate():
for content in agent.stream_responce(last_prompt):
yield f"data: {content}\n\n"
agent.save_memory() agent.save_memory()
return Response(generate(), mimetype="text/event-stream")
return jsonify({
"response": response
})
#список персонажей #список персонажей
@ui.route("/characters", methods=["GET"]) @ui.route("/characters", methods=["GET"])
+142 -21
View File
@@ -1,15 +1,47 @@
const messages = document.getElementById("messages");
const input = document.getElementById("user-input");
const sendBtn = document.getElementById("send-btn"); const sendBtn = document.getElementById("send-btn");
loadChat();
sendBtn.addEventListener("click", async () => {
const input = document.getElementById("user-input"); let userName = "Alex";
const text = input.value;
const messages = document.getElementById("messages"); // текущий SSE поток (ВАЖНО: чтобы не плодить соединения)
let eventSource = null;
messages.innerHTML += `<p><b>You:</b> ${text}</p>`; //=> получаем имя пользователя
// ===============================
const response = await fetch("/chat", { // 1. ЗАГРУЗКА ИСТОРИИ
// ===============================
async function loadHistory() {
const res = await fetch("/init");
const data = await res.json();
userName = data.user_name || "User";
data.messages.forEach(msg => {
renderMessage(msg.role, msg.content);
});
}
// ===============================
// 2. ОТПРАВКА СООБЩЕНИЯ
// ===============================
async function sendMessage() {
const text = input.value.trim();
if (!text) return;
renderMessage("user", text);
input.value = "";
// закрываем старый stream если есть
if (eventSource) {
eventSource.close();
eventSource = null;
}
// отправляем сообщение на backend (он сохраняет state)
await fetch("/chat", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@@ -19,21 +51,110 @@ sendBtn.addEventListener("click", async () => {
}) })
}); });
const data = await response.json(); // создаём ПУСТОЕ сообщение для стрима
const aiMessageDiv = createAIMessagePlaceholder();
messages.innerHTML += `<p><b>AI:</b> ${data.response}</p>`; // запускаем stream
startStream(aiMessageDiv);
}
input.value = "";
// ===============================
// 3. SSE STREAM
// ===============================
function startStream(aiMessageDiv) {
eventSource = new EventSource("/stream");
let fullText = "";
eventSource.onmessage = function(event) {
const token = event.data;
// иногда Ollama может слать [DONE]
if (token === "[DONE]") {
eventSource.close();
eventSource = null;
return;
}
fullText += token;
aiMessageDiv.innerHTML = `<b>AI:</b> ${formatText(fullText)}`;
messages.scrollTop = messages.scrollHeight;
};
eventSource.onerror = function(err) {
console.error("Stream error:", err);
eventSource.close();
eventSource = null;
};
}
// ===============================
// 4. РЕНДЕР СООБЩЕНИЙ
// ===============================
function renderMessage(role, text) {
const div = document.createElement("div");
if (role === "user") {
div.className = "user-msg";
div.innerHTML = `<b>${userName}:</b> ${text}`;
}
if (role === "assistant") {
div.className = "ai-msg";
div.innerHTML = `<b>AI:</b> ${formatText(text)}`;
}
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
// ===============================
// 5. ПУСТОЙ КОНТЕЙНЕР ДЛЯ STREAM
// ===============================
function createAIMessagePlaceholder() {
const div = document.createElement("div");
div.className = "ai-msg";
div.innerHTML = `<b>AI:</b> `;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
return div;
}
// ===============================
// 6. ФОРМАТИРОВАНИЕ (*actions*)
// ===============================
function formatText(text) {
// действия *...*
return text.replace(/\*(.*?)\*/g, '<span class="action">*$1*</span>');
}
// ===============================
// 7. EVENTS
// ===============================
sendBtn.addEventListener("click", sendMessage);
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
sendMessage();
}
}); });
async function loadChat() {
const res = await fetch("/init"); // ===============================
const data = await res.json(); // 8. INIT
// ===============================
const messages = document.getElementById("messages"); loadHistory();
data.messages.forEach(msg => {
messages.innerHTML += `<p><b>${msg.role}:</b> ${msg.content}</p>`; //добавить проверку role == user => получаем имя пользователя
});
}
Binary file not shown.
+6 -11
View File
@@ -34,7 +34,6 @@ class OllamaProvider:
return f"Ошибка: {error_msg}" return f"Ошибка: {error_msg}"
def generate_stream_tokens(self, messages, temperature=0.8, max_tokens=300): def generate_stream_tokens(self, messages, temperature=0.8, max_tokens=300):
"""Генерирует ответ от Ollama и возвращает его в виде стрима"""
try: try:
stream = ollama.chat( stream = ollama.chat(
model=self.model, model=self.model,
@@ -46,17 +45,13 @@ class OllamaProvider:
}, },
) )
# Собираем ответ в строку
response_text = ""
for chunk in stream: for chunk in stream:
content = chunk['message']['content'] content = chunk['message']['content']
#print(content, end='', flush=True)
#response_text += content if content:
#return response_text yield content # 🔥 ВАЖНО: внутри цикла
yield content
except Exception as e: except Exception as e:
error_msg = str(e) yield f"Error: {str(e)}"
if "502" in error_msg: if "502" in e:
return "Ошибка: Проверьте наличие VPN или Proxy." yield "Ошибка: Проверьте наличие VPN или Proxy."
return f"Ошибка: {error_msg}"
-49
View File
@@ -1,49 +0,0 @@
{
"summary": "",
"history": [
{
"role": "assistant",
"content": "Привет! Я тестовый персонаж."
},
{
"role": "user",
"content": "Привет"
},
{
"role": "assistant",
"content": "*A sigh, almost inaudible, escapes before the words form, a low rumble in the chest.* “Привет, Alex. Its… strange, isnt it? Just *привет*.” The rain outside is relentless, drumming a mournful rhythm against the corrugated iron roof. It smells of wet earth and something vaguely metallic, something that clings to the back of the throat. *Focus. Don't get lost in the rain.* “This place… it feels like a bruise. A deep, slow-healing bruise.”\n\nThe character, lets call him Silas, is sitting at a rough-hewn table in a dimly lit room. A single, sputtering oil lamp casts flickering shadows across his weathered face a face etched with a thousand unspoken stories. He's wearing worn leather trousers, a stained linen shirt, and a perpetually skeptical expression. His hands, calloused and scarred, fidget with a small, intricately carved wooden bird. Its a pointless action, really, but it seems to occupy his thoughts, a tiny, insistent anchor in the swirling chaos. \n\n*He observes Alex with a quiet intensity, a flicker of something akin to amusement in his grey eyes.* “Youre… new, arent you? The air around you feels… clean. Like someone just scrubbed it with a particularly aggressive brush. Its jarring, honestly. Most people here carry a certain… thickness. A"
},
{
"role": "user",
"content": "Скажи, как ты здесь оказался?"
},
{
"role": "assistant",
"content": "*Silas pauses, the wooden bird suspended mid-turn. He doesn't look directly at Alex, instead tracing a pattern on the table with a thick finger, a gesture that feels both deliberate and restless.* \"Thats… a complicated question. One that tends to unravel when you start picking at the threads.” He lets out a dry chuckle, a sound devoid of warmth. “Lets just say I was… collecting. Collecting things that shouldn't be collected. Things that *remember*.”\n\nHe finally meets Alexs gaze, his eyes holding a depth of sadness that seems disproportionate to his gruff exterior. “Before this… this arrangement, I was… a scholar. A researcher. Obsessed, really, with forgotten lore. Places where the veil between realities is thin. I sought knowledge, you see. Dangerous knowledge. Knowledge that has a habit of finding *you*.”\n\n*He takes a slow, deliberate sip from a chipped mug, the liquid within dark and viscous.* “Lets just say a certain agreement was made. A debt owed. And I, predictably, failed to meet the deadline. Lets also say the collectors arent known for their patience. They prefer… permanent solutions.” He taps the wooden bird against the table, a sharp, unsettling sound. “This place… this is where they brought me. A holding cell, essentially. A place to… contemplate my"
},
{
"role": "user",
"content": "Кто такие \"them\"?"
},
{
"role": "assistant",
"content": "*Silass hand stills, the wooden bird now clutched tightly in his palm. The silence stretches, thick and uncomfortable, punctuated only by the relentless rain.* “Them…” He repeats the word, almost as if testing its weight on the tongue. “They are… not easily described. Not in any way that would make sense to a mind accustomed to linear thought.”\n\nHe takes a deep, shuddering breath, as if battling something within himself. “Imagine a tapestry,” he says, his voice low and gravelly. “A tapestry woven from nightmares and forgotten gods. Each thread represents a fragment of a lost reality, a suppressed memory, a suppressed *desire*. Them… they are the weavers. They manipulate those threads. They *feed* on the echoes of what was, and what could have been.”\n\n*He glances around the room, a flicker of paranoia in his eyes.* “They dont have a physical form, not in the way you understand it. More like… resonances. Concentrations of power. They exist in the spaces between things. In the shadows, in the silence, in the very fabric of this place.” \n\nHe pauses, his voice dropping to a near whisper. “They collect… not just knowledge, but experiences. Pain, pleasure, regret… everything. And they use it to fuel their… artistry. To weave their tapestry. And I… I was a particularly rich source material.” *A grim smile plays on"
},
{
"role": "user",
"content": "как долго ты здесь?"
},
{
"role": "assistant",
"content": "*Silas stares at the rain-streaked window, his face a mask of weary contemplation. The silence stretches, heavy and suffocating. Finally, he speaks, his voice barely audible above the drumming rain.* “Time… its a strange concept here. It flows differently. Its less about chronological progression and more about… resonance. Ive lost track, truly. But… Id estimate… cycles.”\n\nHe shifts slightly, uncomfortable with the word. “Cycles of observation. Of torment. Of… waiting.” He runs a calloused hand over the wooden bird, turning it slowly in his fingers. “Ive been here… long enough to see patterns emerge. To understand the nature of their collection. Long enough to realize that escape… is a delusion.”\n\n*He lets out a short, bitter laugh.* “Before the… arrival, before they brought me here, Id been… suspended. Frozen, almost. In a state of perpetual disorientation. Memories fractured, timelines blurred. It was… agonizing. A slow, deliberate dismantling of self.” He pauses, a flicker of genuine pain crossing his features. “Id estimate… at least seventy cycles. Perhaps more. Its difficult to say. The records… theyre unreliable.”\n\n*He looks up at Alex, his grey eyes holding a profound sadness.* “It feels like an eternity. And yet… its only a moment. A single, excruciating moment"
},
{
"role": "user",
"content": "Люди идут сюда..."
},
{
"role": "assistant",
"content": "*Silass head snaps up, a sudden alertness replacing the weary resignation. He sets the wooden bird down with a sharp click, his gaze fixed intently on Alex.* “People… yes. They *do* come here. Drawn by whispers, by promises, by desperation. Its a rather… lucrative business, you see.” He gestures vaguely around the room with a dismissive wave of his hand. “Theyre not exactly *invited*, not in the conventional sense. More like… *guided*.”\n\nHe leans forward, his voice dropping to a conspiratorial murmur. “Most are drawn by the allure of power. The promise of knowledge beyond human comprehension. Some… are simply lost. Driven to the fringes of reality by grief, by obsession, by a profound and unshakeable loneliness. They stumble upon… pathways. Unseen doorways. And then… they find themselves here.”\n\n*He pauses, considering the implications of his words.* “The process… its not entirely voluntary. Theres a… compulsion. A subtle shift in perception. A growing sense of inevitability. It begins with a fascination, a yearning. Then, a gradual erosion of self. A willingness to surrender. And finally… the arrival. They dont *choose* to come here, Alex. They simply… arrive.”\n\n*A dark humor flickers in his eyes.* “Its rather like a particularly elaborate, and exquisitely cruel, game of chance"
}
]
}