A2-A6: logging instead of print, claude-sonnet-5 model id, timezone-aware datetime, FastAPI lifespan, .docx parse errors -> HTTP 400

This commit is contained in:
root
2026-07-25 11:57:03 +00:00
parent 0de5beeb64
commit cd65116a2a
4 changed files with 28 additions and 16 deletions
+9 -6
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@@ -24,7 +25,14 @@ allow_credentials = os.getenv('CORS_ALLOW_CREDENTIALS', 'true').strip().lower()
if '*' in cors_origins: if '*' in cors_origins:
allow_credentials = False 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( app.add_middleware(
CORSMiddleware, 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(health_router)
app.include_router(auth_router) app.include_router(auth_router)
app.include_router(analyze_router) app.include_router(analyze_router)
+10 -4
View File
@@ -26,9 +26,12 @@ def _parse_bool(value: str | None) -> bool | None:
def _extract_docx_text(content: bytes) -> str: def _extract_docx_text(content: bytes) -> str:
with zipfile.ZipFile(BytesIO(content)) as archive: try:
xml = archive.read('word/document.xml') with zipfile.ZipFile(BytesIO(content)) as archive:
root = ET.fromstring(xml) 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'} ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
paragraphs: list[str] = [] paragraphs: list[str] = []
for paragraph in root.findall('.//w:body/w:p', ns): 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) @router.post('/parse-doc', response_model=ParseDocResponse)
async def admin_parse_doc(file: UploadFile = File(...)) -> dict: async def admin_parse_doc(file: UploadFile = File(...)) -> dict:
content = await file.read() 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) preview = _coerce_preview(raw_text)
return {'filename': file.filename, 'parsed': True, 'preview': preview, 'raw_text': raw_text} return {'filename': file.filename, 'parsed': True, 'preview': preview, 'raw_text': raw_text}
+2 -2
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime, timezone
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
@@ -103,7 +103,7 @@ async def analyze_case(payload: AnalysisRequest) -> dict[str, Any]:
result = { result = {
'case_id': str(payload.case_id) if payload.case_id else None, '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, 'max_distance_km': max_distance_km,
'psychotype': psychotype, 'psychotype': psychotype,
'psychotype_modifiers': psychotype_modifiers, 'psychotype_modifiers': psychotype_modifiers,
+7 -4
View File
@@ -4,6 +4,7 @@ Integrates geo_service zones and scoring_service rankings.
""" """
import os import os
import json import json
import logging
from typing import Dict, List, Optional from typing import Dict, List, Optional
from pydantic import BaseModel from pydantic import BaseModel
import httpx import httpx
@@ -11,6 +12,8 @@ import httpx
from .geo_service import build_search_zones from .geo_service import build_search_zones
from .scoring_service import WeightedScorer from .scoring_service import WeightedScorer
logger = logging.getLogger(__name__)
class PrimaryZone(BaseModel): class PrimaryZone(BaseModel):
priority: int priority: int
@@ -63,7 +66,7 @@ async def analyze_case(case_data: dict) -> AnalysisResult:
return await analyze_with_claude(case_data, api_key) return await analyze_with_claude(case_data, api_key)
except Exception as e: except Exception as e:
# Логируем ошибку и переходим на fallback # Логируем ошибку и переходим на 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 # Fallback: используем только scoring_service
return await analyze_with_fallback(case_data) 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['forest_pct']}%, "
zones_data += f"дороги {zone['road_density']} км/км²\n" zones_data += f"дороги {zone['road_density']} км/км²\n"
except Exception as e: except Exception as e:
print(f"Geo/scoring service error: {e}") logger.warning(f"Geo/scoring service error: {e}")
# Формируем промпт на русском языке # Формируем промпт на русском языке
prompt = f"""Ты — эксперт по поисково-спасательным операциям (ПСО) МЧС Республики Беларусь. Проанализируй следующий случай пропажи человека и дай структурированные рекомендации. prompt = f"""Ты — эксперт по поисково-спасательным операциям (ПСО) МЧС Республики Беларусь. Проанализируй следующий случай пропажи человека и дай структурированные рекомендации.
@@ -154,7 +157,7 @@ async def analyze_with_claude(case_data: dict, api_key: str) -> AnalysisResult:
"content-type": "application/json" "content-type": "application/json"
}, },
json={ json={
"model": "claude-sonnet-4-20250514", "model": "claude-sonnet-5",
"max_tokens": 4096, "max_tokens": 4096,
"messages": [ "messages": [
{ {
@@ -244,7 +247,7 @@ async def analyze_with_fallback(case_data: dict) -> AnalysisResult:
search_radius_km = scorer.distance_multiplier * 2.0 search_radius_km = scorer.distance_multiplier * 2.0
except Exception as e: 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: if not primary_zones: