refactor: review fixes and code improvements

- Add shared proxy module (bot/utils/proxy.py) to eliminate duplicate SOCKS5 parsing
- Fix AI client: escape HTML before Markdown→HTML conversion, unify timeouts,
  make health check optional and disabled by default, handle 429 retries
- Fix ai.py: correct forwarded message handling (aiogram 3.x forward_origin),
  pass relevant summaries via extra_system_content
- Fix dialogue.py: only respond to Astra's messages, use system context instead
  of prompt injection, answer on the limit message before phase transition
- Fix voice.py: load Whisper model in thread pool, safe WAV path generation
- Improve database.py: composite indexes, Boolean is_active, upsert file_id cache,
  add context cleanup helper
- Update weather.py and yadisk_download.py to use shared proxy connector
- Update yadisk.py: validate URL before cache clear, add download size limit,
  wrap sync file ops in to_thread
- Reuse S3 client via lru_cache
- Update setup_commands with /aiclear and /aiuser
- Update README, Dockerfile (Python 3.11), docker-compose (mount models)
- Pin dependency versions, remove unused httpx[socks]
- Add basic pytest tests for layout converter and voice normalization
This commit is contained in:
Галингер Р.С.
2026-07-07 18:42:10 +07:00
parent 76e5701eba
commit 0f674f8832
23 changed files with 894 additions and 578 deletions
+42 -19
View File
@@ -3,8 +3,11 @@ import logging
import math
import re
from bot.utils.ai_client import _get_connector
import aiohttp
from bot.utils.proxy import get_proxy_connector
from bot.utils.database import get_summaries, save_conversation_summary
from bot.utils.ai_client import get_client_timeout, ask_ai_simple
from config import ROUTERAI_API_KEY, ROUTERAI_BASE_URL
logger = logging.getLogger(__name__)
@@ -29,26 +32,36 @@ async def create_embedding(text: str) -> list[float] | None:
"encoding_format": "float",
}
import aiohttp
from bot.utils.ai_client import get_client_timeout
connector = _get_connector()
connector = get_proxy_connector()
try:
async with aiohttp.ClientSession(timeout=get_client_timeout(30), connector=connector) as session:
async with aiohttp.ClientSession(
timeout=get_client_timeout(30), connector=connector
) as session:
async with session.post(EMBEDDING_URL, json=payload, headers=headers) as response:
if response.status != 200:
error_body = await response.text()
logger.warning("Embedding error | status=%s error=%s", response.status, error_body[:200])
logger.warning(
"Embedding error | status=%s error=%s",
response.status,
error_body[:200],
)
return None
data = await response.json()
embedding = data["data"][0]["embedding"]
logger.info("Embedding created | dim=%d input_len=%d", len(embedding), min(len(text), 8000))
logger.info(
"Embedding created | dim=%d input_len=%d",
len(embedding),
min(len(text), 8000),
)
return embedding
except Exception as e:
logger.error("Embedding request error: %s", e)
except Exception as exc:
logger.error("Embedding request error: %s", exc)
return None
finally:
if connector:
await connector.close()
def cosine_similarity(a: list[float], b: list[float]) -> float:
@@ -60,14 +73,18 @@ def cosine_similarity(a: list[float], b: list[float]) -> float:
return dot / (norm_a * norm_b)
async def find_relevant_summaries(user_id: int, chat_id: int, query: str, top_k: int = 3) -> list[str]:
async def find_relevant_summaries(
user_id: int, chat_id: int, query: str, top_k: int = 3
) -> list[str]:
query_emb = await create_embedding(query)
if not query_emb:
return []
summaries = await get_summaries(user_id, chat_id, limit=20)
if not summaries:
logger.info("Relevant summaries | user=%d chat=%d no summaries found", user_id, chat_id)
logger.info(
"Relevant summaries | user=%d chat=%d no summaries found", user_id, chat_id
)
return []
scored = []
@@ -87,24 +104,30 @@ async def find_relevant_summaries(user_id: int, chat_id: int, query: str, top_k:
logger.info(
"Relevant summaries | user=%d chat=%d found=%d top_score=%.3f",
user_id, chat_id, len(top), top_score,
user_id,
chat_id,
len(top),
top_score,
)
return top
async def save_summary_with_embedding(user_id: int, chat_id: int, summary_text: str) -> None:
async def save_summary_with_embedding(
user_id: int, chat_id: int, summary_text: str
) -> None:
emb = await create_embedding(summary_text)
embedding_json = json.dumps(emb) if emb else None
await save_conversation_summary(user_id, chat_id, summary_text, embedding_json)
logger.info(
"Summary saved | user=%d chat=%d summary_len=%d emb=%s",
user_id, chat_id, len(summary_text), "yes" if emb else "no",
user_id,
chat_id,
len(summary_text),
"yes" if emb else "no",
)
async def generate_summary(messages: list[dict]) -> str | None:
from bot.utils.ai_client import ask_ai_simple
messages_text = "\n".join(
f"{'Пользователь' if m['role'] == 'user' else 'Астра'}: {m['content'][:300]}"
for m in messages[-50:]
@@ -127,7 +150,7 @@ async def generate_summary(messages: list[dict]) -> str | None:
logger.warning("Summary too short | len=%d", len(summary))
else:
logger.warning("Summary generation returned None")
except Exception as e:
logger.error("Summary generation error: %s", e)
except Exception as exc:
logger.error("Summary generation error: %s", exc)
return None