AnatoliiG commited on
Commit ·
6eebe14
1
Parent(s): 3b802b2
fix api bugs
Browse files- src/api/routes.py +49 -30
- src/core/config.py +1 -1
- src/core/engine.py +33 -23
- src/ui/callbacks.py +10 -4
src/api/routes.py
CHANGED
|
@@ -4,7 +4,7 @@ import logging
|
|
| 4 |
import threading
|
| 5 |
|
| 6 |
from fastapi import APIRouter, HTTPException, Request
|
| 7 |
-
from fastapi.responses import StreamingResponse
|
| 8 |
|
| 9 |
from src.core.config import settings
|
| 10 |
from src.core.engine import engine
|
|
@@ -26,14 +26,21 @@ async def chat_completions(request: Request):
|
|
| 26 |
except Exception:
|
| 27 |
raise HTTPException(status_code=400, detail="Invalid JSON")
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
stop = data.get("stop", [])
|
| 38 |
if isinstance(stop, str):
|
| 39 |
stop = [stop]
|
|
@@ -43,6 +50,30 @@ async def chat_completions(request: Request):
|
|
| 43 |
if s not in stop:
|
| 44 |
stop.append(s)
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
abort_event = threading.Event()
|
| 47 |
|
| 48 |
async def stream_generator():
|
|
@@ -51,18 +82,9 @@ async def chat_completions(request: Request):
|
|
| 51 |
|
| 52 |
def worker():
|
| 53 |
try:
|
| 54 |
-
gen_kwargs =
|
| 55 |
-
|
| 56 |
-
"temperature": float(temperature),
|
| 57 |
-
"top_p": float(top_p),
|
| 58 |
-
"stop": stop,
|
| 59 |
-
"abort_event": abort_event,
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
# Запускаем генерацию
|
| 63 |
-
for chunk in engine.generate_stream(messages, **gen_kwargs):
|
| 64 |
loop.call_soon_threadsafe(queue.put_nowait, chunk)
|
| 65 |
-
|
| 66 |
loop.call_soon_threadsafe(queue.put_nowait, None)
|
| 67 |
except Exception as e:
|
| 68 |
if not abort_event.is_set():
|
|
@@ -74,7 +96,6 @@ async def chat_completions(request: Request):
|
|
| 74 |
try:
|
| 75 |
while True:
|
| 76 |
if await request.is_disconnected():
|
| 77 |
-
logger.info("Client disconnected! Aborting generation...")
|
| 78 |
abort_event.set()
|
| 79 |
break
|
| 80 |
|
|
@@ -99,17 +120,15 @@ async def chat_completions(request: Request):
|
|
| 99 |
yield f"data: {json.dumps(chunk)}\n\n"
|
| 100 |
|
| 101 |
except asyncio.CancelledError:
|
| 102 |
-
logger.info("Task cancelled. Stopping worker.")
|
| 103 |
abort_event.set()
|
| 104 |
raise
|
| 105 |
|
| 106 |
-
# Возвращаем стрим
|
| 107 |
-
headers = {
|
| 108 |
-
"X-Accel-Buffering": "no",
|
| 109 |
-
"Cache-Control": "no-cache",
|
| 110 |
-
"Connection": "keep-alive",
|
| 111 |
-
"Content-Type": "text/event-stream",
|
| 112 |
-
}
|
| 113 |
return StreamingResponse(
|
| 114 |
-
stream_generator(),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
)
|
|
|
|
| 4 |
import threading
|
| 5 |
|
| 6 |
from fastapi import APIRouter, HTTPException, Request
|
| 7 |
+
from fastapi.responses import JSONResponse, StreamingResponse
|
| 8 |
|
| 9 |
from src.core.config import settings
|
| 10 |
from src.core.engine import engine
|
|
|
|
| 26 |
except Exception:
|
| 27 |
raise HTTPException(status_code=400, detail="Invalid JSON")
|
| 28 |
|
| 29 |
+
# Бережно собираем сообщения, сохраняя служебные поля для Tool Calling
|
| 30 |
+
messages = []
|
| 31 |
+
for m in data.get("messages", []):
|
| 32 |
+
msg = {
|
| 33 |
+
"role": m.get("role", "user"),
|
| 34 |
+
"content": get_clean_text(m.get("content")),
|
| 35 |
+
}
|
| 36 |
+
if "tool_calls" in m:
|
| 37 |
+
msg["tool_calls"] = m["tool_calls"]
|
| 38 |
+
if "tool_call_id" in m:
|
| 39 |
+
msg["tool_call_id"] = m["tool_call_id"]
|
| 40 |
+
messages.append(msg)
|
| 41 |
+
|
| 42 |
+
# Параметры от агента
|
| 43 |
+
is_stream = data.get("stream", False)
|
| 44 |
stop = data.get("stop", [])
|
| 45 |
if isinstance(stop, str):
|
| 46 |
stop = [stop]
|
|
|
|
| 50 |
if s not in stop:
|
| 51 |
stop.append(s)
|
| 52 |
|
| 53 |
+
gen_kwargs = {
|
| 54 |
+
"max_tokens": data.get("max_tokens", settings.DEFAULT_MAX_TOKENS),
|
| 55 |
+
"temperature": data.get("temperature", settings.DEFAULT_TEMP),
|
| 56 |
+
"top_p": data.get("top_p", 0.95),
|
| 57 |
+
"stop": stop,
|
| 58 |
+
"stream": is_stream,
|
| 59 |
+
"tools": data.get("tools", None),
|
| 60 |
+
"tool_choice": data.get("tool_choice", None),
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
# Если Агент просит ответ целиком (stream=False)
|
| 64 |
+
if not is_stream:
|
| 65 |
+
loop = asyncio.get_running_loop()
|
| 66 |
+
try:
|
| 67 |
+
# Выполняем синхронный код в пуле потоков, чтобы не заблокировать FastAPI
|
| 68 |
+
response = await loop.run_in_executor(
|
| 69 |
+
None, lambda: engine.generate(messages, **gen_kwargs)
|
| 70 |
+
)
|
| 71 |
+
return JSONResponse(content=response)
|
| 72 |
+
except Exception as e:
|
| 73 |
+
logger.error(f"Sync generation error: {e}")
|
| 74 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 75 |
+
|
| 76 |
+
# Если это Чат (stream=True)
|
| 77 |
abort_event = threading.Event()
|
| 78 |
|
| 79 |
async def stream_generator():
|
|
|
|
| 82 |
|
| 83 |
def worker():
|
| 84 |
try:
|
| 85 |
+
gen_kwargs["abort_event"] = abort_event
|
| 86 |
+
for chunk in engine.generate(messages, **gen_kwargs):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
loop.call_soon_threadsafe(queue.put_nowait, chunk)
|
|
|
|
| 88 |
loop.call_soon_threadsafe(queue.put_nowait, None)
|
| 89 |
except Exception as e:
|
| 90 |
if not abort_event.is_set():
|
|
|
|
| 96 |
try:
|
| 97 |
while True:
|
| 98 |
if await request.is_disconnected():
|
|
|
|
| 99 |
abort_event.set()
|
| 100 |
break
|
| 101 |
|
|
|
|
| 120 |
yield f"data: {json.dumps(chunk)}\n\n"
|
| 121 |
|
| 122 |
except asyncio.CancelledError:
|
|
|
|
| 123 |
abort_event.set()
|
| 124 |
raise
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
return StreamingResponse(
|
| 127 |
+
stream_generator(),
|
| 128 |
+
media_type="text/event-stream",
|
| 129 |
+
headers={
|
| 130 |
+
"X-Accel-Buffering": "no",
|
| 131 |
+
"Cache-Control": "no-cache",
|
| 132 |
+
"Connection": "keep-alive",
|
| 133 |
+
},
|
| 134 |
)
|
src/core/config.py
CHANGED
|
@@ -6,7 +6,7 @@ from pydantic_settings import BaseSettings
|
|
| 6 |
class Settings(BaseSettings):
|
| 7 |
# Меняем на нужную нам супер-модель
|
| 8 |
REPO_ID: str = "empero-ai/Qwythos-9B-Claude-Mythos-5-1M-GGUF"
|
| 9 |
-
FILENAME: str = "Qwythos-9B-Claude-Mythos-5-1M-
|
| 10 |
|
| 11 |
CONTEXT_SIZE: int = 32768
|
| 12 |
DEFAULT_MAX_TOKENS: int = 8192
|
|
|
|
| 6 |
class Settings(BaseSettings):
|
| 7 |
# Меняем на нужную нам супер-модель
|
| 8 |
REPO_ID: str = "empero-ai/Qwythos-9B-Claude-Mythos-5-1M-GGUF"
|
| 9 |
+
FILENAME: str = "Qwythos-9B-Claude-Mythos-5-1M-MTP-Q5_K_M.gguf"
|
| 10 |
|
| 11 |
CONTEXT_SIZE: int = 32768
|
| 12 |
DEFAULT_MAX_TOKENS: int = 8192
|
src/core/engine.py
CHANGED
|
@@ -32,46 +32,56 @@ class ModelEngine:
|
|
| 32 |
except Exception as e:
|
| 33 |
print(f"CRITICAL ERROR loading model: {e}")
|
| 34 |
|
| 35 |
-
def
|
| 36 |
self,
|
| 37 |
-
messages: List[Dict[str,
|
| 38 |
-
abort_event: Optional[threading.Event] = None,
|
| 39 |
**kwargs,
|
| 40 |
-
)
|
| 41 |
if not self.llm:
|
| 42 |
raise RuntimeError("Model not loaded")
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
acquired = False
|
| 49 |
while not acquired:
|
| 50 |
if abort_event and abort_event.is_set():
|
| 51 |
print("Request aborted while waiting in queue.")
|
| 52 |
return
|
| 53 |
-
|
| 54 |
acquired = self.lock.acquire(timeout=0.5)
|
| 55 |
|
| 56 |
try:
|
| 57 |
if abort_event and abort_event.is_set():
|
| 58 |
return
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
break
|
| 73 |
-
|
| 74 |
-
yield chunk
|
| 75 |
|
| 76 |
finally:
|
| 77 |
self.lock.release()
|
|
|
|
| 32 |
except Exception as e:
|
| 33 |
print(f"CRITICAL ERROR loading model: {e}")
|
| 34 |
|
| 35 |
+
def generate(
|
| 36 |
self,
|
| 37 |
+
messages: List[Dict[str, Any]],
|
| 38 |
+
abort_event: Optional[threading.Event] = None,
|
| 39 |
**kwargs,
|
| 40 |
+
):
|
| 41 |
if not self.llm:
|
| 42 |
raise RuntimeError("Model not loaded")
|
| 43 |
|
| 44 |
+
stream_mode = kwargs.get("stream", True)
|
| 45 |
+
|
| 46 |
+
# Подготавливаем аргументы для llama_cpp_python
|
| 47 |
+
llama_kwargs = {
|
| 48 |
+
"messages": messages,
|
| 49 |
+
"max_tokens": int(kwargs.get("max_tokens", settings.DEFAULT_MAX_TOKENS)),
|
| 50 |
+
"temperature": float(kwargs.get("temperature", settings.DEFAULT_TEMP)),
|
| 51 |
+
"top_p": float(kwargs.get("top_p", 0.95)),
|
| 52 |
+
"stop": kwargs.get("stop", []),
|
| 53 |
+
"stream": stream_mode,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
# Прокидываем инструменты (Tool Calling)
|
| 57 |
+
if kwargs.get("tools"):
|
| 58 |
+
llama_kwargs["tools"] = kwargs["tools"]
|
| 59 |
+
if kwargs.get("tool_choice"):
|
| 60 |
+
llama_kwargs["tool_choice"] = kwargs["tool_choice"]
|
| 61 |
|
| 62 |
acquired = False
|
| 63 |
while not acquired:
|
| 64 |
if abort_event and abort_event.is_set():
|
| 65 |
print("Request aborted while waiting in queue.")
|
| 66 |
return
|
|
|
|
| 67 |
acquired = self.lock.acquire(timeout=0.5)
|
| 68 |
|
| 69 |
try:
|
| 70 |
if abort_event and abort_event.is_set():
|
| 71 |
return
|
| 72 |
|
| 73 |
+
response = self.llm.create_chat_completion(**llama_kwargs)
|
| 74 |
+
|
| 75 |
+
# Если просят стрим - отдаем генератор
|
| 76 |
+
if stream_mode:
|
| 77 |
+
for chunk in response:
|
| 78 |
+
if abort_event and abort_event.is_set():
|
| 79 |
+
print("Request aborted during generation.")
|
| 80 |
+
break
|
| 81 |
+
yield chunk
|
| 82 |
+
# Если просят целый ответ (Агенты) - отдаем сразу словарь
|
| 83 |
+
else:
|
| 84 |
+
return response
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
finally:
|
| 87 |
self.lock.release()
|
src/ui/callbacks.py
CHANGED
|
@@ -30,6 +30,7 @@ def set_interactive(is_interactive):
|
|
| 30 |
)
|
| 31 |
|
| 32 |
|
|
|
|
| 33 |
def bot_response(history, system_prompt, temperature, max_tokens):
|
| 34 |
messages = [{"role": "system", "content": system_prompt}]
|
| 35 |
for msg in history[-7:]:
|
|
@@ -40,14 +41,19 @@ def bot_response(history, system_prompt, temperature, max_tokens):
|
|
| 40 |
history.append({"role": "assistant", "content": ""})
|
| 41 |
|
| 42 |
try:
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
)
|
| 46 |
|
| 47 |
partial_text = ""
|
| 48 |
for chunk in stream:
|
| 49 |
-
delta = chunk["choices"][0]
|
| 50 |
-
|
|
|
|
| 51 |
partial_text += delta["content"]
|
| 52 |
history[-1]["content"] = partial_text
|
| 53 |
yield history
|
|
|
|
| 30 |
)
|
| 31 |
|
| 32 |
|
| 33 |
+
# Замени bot_response на этот:
|
| 34 |
def bot_response(history, system_prompt, temperature, max_tokens):
|
| 35 |
messages = [{"role": "system", "content": system_prompt}]
|
| 36 |
for msg in history[-7:]:
|
|
|
|
| 41 |
history.append({"role": "assistant", "content": ""})
|
| 42 |
|
| 43 |
try:
|
| 44 |
+
# Указываем stream=True для UI
|
| 45 |
+
stream = engine.generate(
|
| 46 |
+
messages=messages,
|
| 47 |
+
max_tokens=max_tokens,
|
| 48 |
+
temperature=temperature,
|
| 49 |
+
stream=True,
|
| 50 |
)
|
| 51 |
|
| 52 |
partial_text = ""
|
| 53 |
for chunk in stream:
|
| 54 |
+
delta = chunk["choices"][0].get("delta", {})
|
| 55 |
+
# Безопасная проверка контента
|
| 56 |
+
if delta.get("content"):
|
| 57 |
partial_text += delta["content"]
|
| 58 |
history[-1]["content"] = partial_text
|
| 59 |
yield history
|