From cd65116a2a3ee25cf42b66186810463ee426e939 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 25 Jul 2026 11:57:03 +0000 Subject: [PATCH] A2-A6: logging instead of print, claude-sonnet-5 model id, timezone-aware datetime, FastAPI lifespan, .docx parse errors -> HTTP 400 --- backend/main.py | 15 +++++++++------ backend/routers/admin.py | 14 ++++++++++---- backend/routers/analyze.py | 4 ++-- backend/services/claude_service.py | 11 +++++++---- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/backend/main.py b/backend/main.py index adf2402..ba0588d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -24,7 +25,14 @@ allow_credentials = os.getenv('CORS_ALLOW_CREDENTIALS', 'true').strip().lower() if '*' in cors_origins: allow_credentials = False -app = FastAPI(title='Vector API', version='0.1.0') + +@asynccontextmanager +async def lifespan(app: FastAPI): + init_db() + yield + + +app = FastAPI(title='Vector API', version='0.1.0', lifespan=lifespan) app.add_middleware( CORSMiddleware, @@ -35,11 +43,6 @@ app.add_middleware( ) -@app.on_event('startup') -def startup() -> None: - init_db() - - app.include_router(health_router) app.include_router(auth_router) app.include_router(analyze_router) diff --git a/backend/routers/admin.py b/backend/routers/admin.py index 1310535..90abe1f 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -26,9 +26,12 @@ def _parse_bool(value: str | None) -> bool | None: def _extract_docx_text(content: bytes) -> str: - with zipfile.ZipFile(BytesIO(content)) as archive: - xml = archive.read('word/document.xml') - root = ET.fromstring(xml) + try: + with zipfile.ZipFile(BytesIO(content)) as archive: + xml = archive.read('word/document.xml') + root = ET.fromstring(xml) + except (zipfile.BadZipFile, KeyError, ET.ParseError) as exc: + raise ValueError('Не удалось прочитать .docx: файл повреждён или имеет неверный формат') from exc ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'} paragraphs: list[str] = [] for paragraph in root.findall('.//w:body/w:p', ns): @@ -134,6 +137,9 @@ def admin_dashboard() -> dict: @router.post('/parse-doc', response_model=ParseDocResponse) async def admin_parse_doc(file: UploadFile = File(...)) -> dict: content = await file.read() - raw_text = _extract_docx_text(content) + try: + raw_text = _extract_docx_text(content) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc preview = _coerce_preview(raw_text) return {'filename': file.filename, 'parsed': True, 'preview': preview, 'raw_text': raw_text} diff --git a/backend/routers/analyze.py b/backend/routers/analyze.py index 179eb6e..93c6f8f 100644 --- a/backend/routers/analyze.py +++ b/backend/routers/analyze.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from typing import Any from uuid import UUID @@ -103,7 +103,7 @@ async def analyze_case(payload: AnalysisRequest) -> dict[str, Any]: result = { 'case_id': str(payload.case_id) if payload.case_id else None, - 'analyzed_at': datetime.utcnow().isoformat(), + 'analyzed_at': datetime.now(timezone.utc).isoformat(), 'max_distance_km': max_distance_km, 'psychotype': psychotype, 'psychotype_modifiers': psychotype_modifiers, diff --git a/backend/services/claude_service.py b/backend/services/claude_service.py index 4b42ebd..299ae09 100644 --- a/backend/services/claude_service.py +++ b/backend/services/claude_service.py @@ -4,6 +4,7 @@ Integrates geo_service zones and scoring_service rankings. """ import os import json +import logging from typing import Dict, List, Optional from pydantic import BaseModel import httpx @@ -11,6 +12,8 @@ import httpx from .geo_service import build_search_zones from .scoring_service import WeightedScorer +logger = logging.getLogger(__name__) + class PrimaryZone(BaseModel): priority: int @@ -63,7 +66,7 @@ async def analyze_case(case_data: dict) -> AnalysisResult: return await analyze_with_claude(case_data, api_key) except Exception as e: # Логируем ошибку и переходим на fallback - print(f"Claude API unavailable: {e}. Using fallback scoring service.") + logger.warning(f"Claude API unavailable: {e}. Using fallback scoring service.") # Fallback: используем только scoring_service return await analyze_with_fallback(case_data) @@ -108,7 +111,7 @@ async def analyze_with_claude(case_data: dict, api_key: str) -> AnalysisResult: zones_data += f"лес {zone['forest_pct']}%, " zones_data += f"дороги {zone['road_density']} км/км²\n" except Exception as e: - print(f"Geo/scoring service error: {e}") + logger.warning(f"Geo/scoring service error: {e}") # Формируем промпт на русском языке prompt = f"""Ты — эксперт по поисково-спасательным операциям (ПСО) МЧС Республики Беларусь. Проанализируй следующий случай пропажи человека и дай структурированные рекомендации. @@ -154,7 +157,7 @@ async def analyze_with_claude(case_data: dict, api_key: str) -> AnalysisResult: "content-type": "application/json" }, json={ - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-5", "max_tokens": 4096, "messages": [ { @@ -244,7 +247,7 @@ async def analyze_with_fallback(case_data: dict) -> AnalysisResult: search_radius_km = scorer.distance_multiplier * 2.0 except Exception as e: - print(f"Geo/scoring service error in fallback: {e}") + logger.warning(f"Geo/scoring service error in fallback: {e}") # Если зоны не построены, используем базовые if not primary_zones: