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:
+9
-6
@@ -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)
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user