Plan step

This commit is contained in:
2026-09-23 22:42:21 +03:00
parent 1f2dc25e0b
commit 762ac2f14b
15 changed files with 823 additions and 283 deletions
+59 -1
View File
@@ -1,12 +1,66 @@
import hashlib
import json
import logging
from pathlib import Path
from typing import Optional, Type
from openai import OpenAI
from pydantic import BaseModel
class NoSchema(BaseModel): ...
logger = logging.getLogger("llm client")
class ResponseCache:
"""File-backed cache: prompt-hash -> validated-model JSON. Makes runs idempotent."""
def __init__(self, path: Optional[Path] = None) -> None:
self.path = path
self._mem: dict[str, str] = {}
if path and path.exists():
try:
self._mem = json.loads(path.read_text("utf-8"))
except Exception: # noqa: BLE001
logger.warning("Corrupt cache at %s; starting fresh", path)
@staticmethod
def key(system: str, user: str, schema: Type[BaseModel]) -> str:
h = hashlib.sha256()
h.update(schema.__name__.encode())
h.update(b"\x00")
h.update(system.encode())
h.update(b"\x00")
h.update(user.encode())
return h.hexdigest()
def get(self, k: str) -> Optional[str]:
return self._mem.get(k)
def put(self, k: str, v: str) -> None:
self._mem[k] = v
if self.path:
self.path.parent.mkdir(parents=True, exist_ok=True)
tmp = self.path.with_suffix(self.path.suffix + ".tmp")
tmp.write_text(json.dumps(self._mem, indent=2, sort_keys=True), "utf-8")
tmp.replace(self.path)
else:
logger.warning("No path specified for cache!")
class LLMClient:
def __init__(self, base_url, api_key: str | None = None, model: str = "gpt-4o-mini"):
def __init__(self, base_url, api_key: str | None = None, model: str = "gpt-4o-mini", cache: Optional[ResponseCache] = None):
self.model = model
self.client = OpenAI(base_url=base_url, api_key=api_key or "ollama")
self.cache = cache
def chat_with_schema(self, prompt: str, schema: dict, system: str | None = None) -> str | None:
# --- idempotency: serve from cache when prompt+system+schema match ---
cache_key = ResponseCache.key(system or "", prompt, NoSchema) if self.cache else None
if cache_key and self.cache and (hit := self.cache.get(cache_key)) is not None:
return json.loads(hit).get('response', "")
assert_strict_mode_clean(schema)
kwargs = {
"model": self.model,
@@ -19,6 +73,10 @@ class LLMClient:
"response_format": schema,
}
resp = self.client.chat.completions.create(**kwargs)
if cache_key and self.cache:
self.cache.put(cache_key, json.dumps({"response": resp.choices[0].message.content or ""}))
return resp.choices[0].message.content
def chat(self, system: str, user: str, json_mode: bool = False, images: list[str] | None = None) -> str | None: