refactor(stage-3): add Keep-Alive connection pool for Ollama, implement offline lemma resolution in dictionary

This commit is contained in:
Antigravity committed 2026-10-07 14:45:05 +03:00
1 parent 9072fc365a
commit 4e1f809018
3 files changed
+56 -7

No files matched your search

+34 -6
View File
@@ -8,6 +8,34 @@ from app.database import get_db, get_cached_sentence_translation, save_sentence_
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ollama_client: Optional[httpx.AsyncClient] = None
def get_ollama_client() -> httpx.AsyncClient:
"""
Returns a shared singleton httpx.AsyncClient instance for Ollama
with connection pooling and Keep-Alive.
"""
global _ollama_client
if _ollama_client is None or _ollama_client.is_closed:
limits = httpx.Limits(max_keepalive_connections=10, max_connections=20, keepalive_expiry=120.0)
timeout = httpx.Timeout(connect=3.0, read=60.0, write=5.0, pool=5.0)
_ollama_client = httpx.AsyncClient(limits=limits, timeout=timeout)
return _ollama_client
async def close_ollama_client():
"""Closes the shared singleton Ollama HTTP client on application shutdown."""
global _ollama_client
if _ollama_client is not None:
client = _ollama_client
_ollama_client = None
if not client.is_closed:
try:
await client.aclose()
except Exception:
pass
COMMON_STOPWORDS = { COMMON_STOPWORDS = {
"a", "an", "the", "in", "on", "at", "to", "for", "of", "with", "by", "from", "a", "an", "the", "in", "on", "at", "to", "for", "of", "with", "by", "from",
@@ -227,7 +255,7 @@ async def translate_sentence_ai(
source = f"ollama:{config.OLLAMA_MODEL}" source = f"ollama:{config.OLLAMA_MODEL}"
try: try:
async with httpx.AsyncClient(timeout=60.0) as client: client = get_ollama_client()
resp = await client.post(f"{config.OLLAMA_BASE_URL}/api/generate", json=payload) resp = await client.post(f"{config.OLLAMA_BASE_URL}/api/generate", json=payload)
if resp.status_code == 200: if resp.status_code == 200:
data = resp.json() data = resp.json()
@@ -306,8 +334,8 @@ async def translate_sentence_ai(
async def get_ollama_status() -> Dict[str, Any]: async def get_ollama_status() -> Dict[str, Any]:
"""Checks Ollama server connectivity and lists installed models.""" """Checks Ollama server connectivity and lists installed models."""
try: try:
async with httpx.AsyncClient(timeout=3.0) as client: client = get_ollama_client()
resp = await client.get(f"{config.OLLAMA_BASE_URL}/api/tags") resp = await client.get(f"{config.OLLAMA_BASE_URL}/api/tags", timeout=3.0)
if resp.status_code == 200: if resp.status_code == 200:
data = resp.json() data = resp.json()
models = [m.get("name") for m in data.get("models", [])] models = [m.get("name") for m in data.get("models", [])]
@@ -328,7 +356,7 @@ async def get_ollama_status() -> Dict[str, Any]:
async def warmup_ollama_model() -> bool: async def warmup_ollama_model() -> bool:
"""Preloads the configured Ollama model into RAM with keep_alive to eliminate cold-start latency.""" """Preloads the configured Ollama model into RAM with keep_alive to eliminate cold-start latency."""
try: try:
async with httpx.AsyncClient(timeout=60.0) as client: client = get_ollama_client()
resp = await client.post( resp = await client.post(
f"{config.OLLAMA_BASE_URL}/api/generate", f"{config.OLLAMA_BASE_URL}/api/generate",
json={"model": config.OLLAMA_MODEL, "keep_alive": "24h"}, json={"model": config.OLLAMA_MODEL, "keep_alive": "24h"},
@@ -539,8 +567,8 @@ async def generate_lesson_ai_debrief(
} }
try: try:
async with httpx.AsyncClient(timeout=25.0) as client: client = get_ollama_client()
resp = await client.post(f"{config.OLLAMA_BASE_URL}/api/generate", json=payload) resp = await client.post(f"{config.OLLAMA_BASE_URL}/api/generate", json=payload, timeout=25.0)
if resp.status_code == 200: if resp.status_code == 200:
raw_response = resp.json().get("response", "{}") raw_response = resp.json().get("response", "{}")
# Parse JSON # Parse JSON
+20
View File
@@ -303,9 +303,29 @@ async def lookup_term(query: str) -> Dict[str, Any]:
"SELECT term, translation, phonetic, pos FROM offline_dictionary WHERE term = ? COLLATE NOCASE", "SELECT term, translation, phonetic, pos FROM offline_dictionary WHERE term = ? COLLATE NOCASE",
(query_clean,) (query_clean,)
).fetchone() ).fetchone()
lemma_found = None
if not off_row and len(words) == 1:
try:
from app.phrasal import lemmatize_word
for cand in lemmatize_word(query_clean):
if cand.lower() != query_clean.lower():
row = conn.execute(
"SELECT term, translation, phonetic, pos FROM offline_dictionary WHERE term = ? COLLATE NOCASE",
(cand,)
).fetchone()
if row:
off_row = row
lemma_found = row["term"]
break
except Exception:
pass
if off_row: if off_row:
raw_trans = off_row["translation"] or "" raw_trans = off_row["translation"] or ""
clean_trans = clean_russian_translation(raw_trans) clean_trans = clean_russian_translation(raw_trans)
if lemma_found and lemma_found.lower() != query_clean.lower():
clean_trans = f"{clean_trans} (от {lemma_found})"
tr_list = [t.strip() for t in clean_trans.split(",") if t.strip()] tr_list = [t.strip() for t in clean_trans.split(",") if t.strip()]
res = { res = {
"query": query_clean, "query": query_clean,
+2 -1
View File
@@ -36,7 +36,7 @@ from app.reader import (
fetch_and_parse_rfc, parse_rfc_text, fetch_and_parse_wikipedia, fetch_and_parse_rfc, parse_rfc_text, fetch_and_parse_wikipedia,
extract_wikipedia_title_and_lang extract_wikipedia_title_and_lang
) )
from app.ai import translate_sentence_ai, get_ollama_status, warmup_ollama_model from app.ai import translate_sentence_ai, get_ollama_status, warmup_ollama_model, close_ollama_client
from app.phrasal import detect_phrasal_verb from app.phrasal import detect_phrasal_verb
from app.cefr import ( from app.cefr import (
get_cefr_level_summaries, get_cefr_level_summaries,
@@ -97,6 +97,7 @@ async def lifespan(app: FastAPI):
asyncio.create_task(prewarm_all_listening_audio()) asyncio.create_task(prewarm_all_listening_audio())
yield yield
await close_http_client() await close_http_client()
await close_ollama_client()
app = FastAPI( app = FastAPI(