Этап 7: долговременная память
- Факты о пользователе в SQLite, в системном промпте с номерами - Инструменты remember / update_memory / forget, автоматическое запоминание отключается - Цикл вызова инструментов со стримингом (до 5 кругов), проверка и приведение аргументов - Откат без инструментов для моделей, которые их не поддерживают, с одним предупреждением - Действия в журнале, вкладка «Память» в настройках - Фейковый OpenAI-совместимый сервер для тестов, тесты полного цикла через Assistant Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
542389c0f9
commit
84d8aaa848
@@ -0,0 +1,90 @@
|
||||
"""Фейковый OpenAI-совместимый сервер: отдаёт заранее заданные ответы стримом."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class Reply:
|
||||
text: str = ""
|
||||
# (имя, аргументы JSON); аргументы отдаются несколькими кусками, как у настоящих серверов
|
||||
tool_calls: list[tuple[str, str]] = field(default_factory=list)
|
||||
status: int = 200
|
||||
error: str = ""
|
||||
|
||||
|
||||
class FakeLLMServer:
|
||||
def __init__(self) -> None:
|
||||
self.replies: list[Reply] = []
|
||||
self.requests: list[dict[str, Any]] = []
|
||||
server = self
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args: object) -> None:
|
||||
pass
|
||||
|
||||
def do_POST(self) -> None:
|
||||
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
||||
server.requests.append(body)
|
||||
reply = server.replies.pop(0) if server.replies else Reply(text="")
|
||||
if reply.status != 200:
|
||||
payload = json.dumps({"error": {"message": reply.error}}).encode()
|
||||
self.send_response(reply.status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
return
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.end_headers()
|
||||
for delta in _deltas(reply):
|
||||
chunk = {
|
||||
"id": "fake",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 0,
|
||||
"model": body["model"],
|
||||
"choices": [{"index": 0, "delta": delta, "finish_reason": None}],
|
||||
}
|
||||
self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode())
|
||||
self.wfile.write(b"data: [DONE]\n\n")
|
||||
|
||||
self._server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
threading.Thread(target=self._server.serve_forever, daemon=True).start()
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return f"http://127.0.0.1:{self._server.server_port}/v1"
|
||||
|
||||
def close(self) -> None:
|
||||
self._server.shutdown()
|
||||
self._server.server_close()
|
||||
|
||||
|
||||
def _deltas(reply: Reply) -> list[dict[str, Any]]:
|
||||
deltas: list[dict[str, Any]] = []
|
||||
for start in range(0, len(reply.text), 5):
|
||||
deltas.append({"content": reply.text[start : start + 5]})
|
||||
for index, (name, arguments) in enumerate(reply.tool_calls):
|
||||
middle = len(arguments) // 2
|
||||
deltas.append(
|
||||
{
|
||||
"tool_calls": [
|
||||
{
|
||||
"index": index,
|
||||
"id": f"call_{index}",
|
||||
"type": "function",
|
||||
"function": {"name": name, "arguments": arguments[:middle]},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
deltas.append(
|
||||
{"tool_calls": [{"index": index, "function": {"arguments": arguments[middle:]}}]}
|
||||
)
|
||||
return deltas
|
||||
Reference in New Issue
Block a user