Полный отказ от LLM: детерминированный rules-анализ
Решение Виктора: ИИ убран из проекта — карточки детей (ПД) не должны покиать контур. Анализ полностью локальный и детерминированный. - services/rules_analysis.py: правила вместо Claude (бывший analyze_with_fallback + rule_based_urgency из closed_cases): urgency по возрасту/профилям, зоны через geo+scoring, behavioral_prediction/immediate_actions/key_locations — справочники. - search_engine: анализатор инъекцией (CaseAnalyzer, Protocol), импорт claude_service удалён; рантайм — rules_analyze. - УДАЛЕНЫ: services/claude_service.py, backend/services/claude_service.py (устаревшая копия), test_claude_service.py; ключевые правила-кейсы перенесены в test_rules_analysis.py (+тест отсутствия LLM-упоминаний). - docker-compose/.env.example: ANTHROPIC_API_KEY удалён. - Фронт: бейдж 'ИИ-анализ' -> 'Локальный расчёт'. - Доки: DEPLOY/SERVICES_README/ANALYZE_API переписаны, FIXES_REPORT помечен историческим, vector_tasks B20: zones/probability — из движка. pytest: 187 passed (было 214; -27 удалённых LLM-тестов, +тест rules). Контракт /analyze не изменён (фронт совместим).
This commit is contained in:
+33
-19
@@ -2,20 +2,20 @@
|
||||
|
||||
Граница (vector_tasks.md B13-принцип 1, B14):
|
||||
- build_search_model(input) -> SearchModel — чистая функция.
|
||||
- Единственная внешняя зависимость — claude_analyze (async, httpx):
|
||||
она уже была «внутри» расчёта в старом analyze_case; изолируется
|
||||
параметром движка, чтобы тесты могли подменять её моком.
|
||||
- Анализатор (нарратив/зоны) инъекцией: рантайм — services.rules_analysis
|
||||
(детерминированные правила, без LLM — карточки детей не покидают контур);
|
||||
тесты подменяют моком. Анализатору достаточно вернуть объект с полями
|
||||
urgency/primary_zones/search_radius_km/... (structurally typed).
|
||||
- Ни одной привязки к полевым транспортам/протоколам (критерий B20:
|
||||
grep по этому файлу не находит соответствующих терминов).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Awaitable, Callable
|
||||
from typing import Any, Awaitable, Callable, Protocol
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from services.claude_service import AnalysisResult
|
||||
from services.distance_service import (
|
||||
calculate_max_distance,
|
||||
get_base_speed,
|
||||
@@ -32,8 +32,22 @@ from services.psychotype_service import (
|
||||
)
|
||||
from services.scoring_service import WeightedScorer
|
||||
|
||||
# Тип анализатора: async (case_data) -> AnalysisResult, как claude_analyze.
|
||||
ClaudeAnalyzer = Callable[[dict[str, Any]], Awaitable[AnalysisResult]]
|
||||
|
||||
class AnalysisOutcome(Protocol):
|
||||
"""Структурный контракт результата анализатора (duck-typed)."""
|
||||
|
||||
urgency: str
|
||||
primary_zones: list[Any]
|
||||
search_radius_km: float
|
||||
key_locations: list[str]
|
||||
behavioral_prediction: str
|
||||
immediate_actions: list[str]
|
||||
summary: str
|
||||
fallback_used: bool
|
||||
|
||||
|
||||
# Тип анализатора: async (case_data) -> объект с полями AnalysisOutcome.
|
||||
CaseAnalyzer = Callable[[dict[str, Any]], Awaitable[AnalysisOutcome]]
|
||||
|
||||
# Диагнозы, которые форма собирает, но поведенческая модель не покрывает
|
||||
# (vector_tasks.md B12: профили без аналога/данных — честно помечать «вне модели»).
|
||||
@@ -72,7 +86,7 @@ class SearchInput(BaseModel):
|
||||
time_of_day: str | None = None
|
||||
|
||||
def to_case_data(self) -> dict[str, Any]:
|
||||
"""Плоский dict для сервисов (claude/scoring) — тот же состав полей,
|
||||
"""Плоский dict для сервисов (анализатор/скоринг) — тот же состав полей,
|
||||
что раньше уходил из analyze_case."""
|
||||
data: dict[str, Any] = {
|
||||
'age': self.age,
|
||||
@@ -209,13 +223,13 @@ def derive_time_of_day(loss_time: Any) -> str:
|
||||
async def build_search_model(
|
||||
search_input: SearchInput,
|
||||
*,
|
||||
analyzer: ClaudeAnalyzer,
|
||||
analyzer: CaseAnalyzer,
|
||||
analyzed_at: datetime | None = None,
|
||||
) -> SearchModel:
|
||||
"""Чистая функция расчёта поисковой модели (B14).
|
||||
|
||||
Вся логика бывшего analyze_case: деривации полей, психотип,
|
||||
WeightedScorer, коэффициенты формулы Экстремум, claude_analyze.
|
||||
WeightedScorer, коэффициенты формулы Экстремум, analyzer (rules/мок).
|
||||
DB / auth / HTTP здесь не нужны; анализатор подменяется параметром.
|
||||
"""
|
||||
case_data = search_input.to_case_data()
|
||||
@@ -246,7 +260,7 @@ async def build_search_model(
|
||||
case_data['psychotype_modifiers'] = psychotype_modifiers
|
||||
|
||||
max_distance_km = calculate_max_distance(case_data)
|
||||
claude_result = await analyzer(case_data)
|
||||
analysis = await analyzer(case_data)
|
||||
|
||||
scorer = WeightedScorer()
|
||||
if case_data.get('age'):
|
||||
@@ -282,12 +296,12 @@ async def build_search_model(
|
||||
active_profiles=scorer.get_active_profiles_info(),
|
||||
critical_warnings=list(scorer.critical_warnings),
|
||||
unmodeled_profiles=unmodeled_profiles(case_data.get('diagnosis_type') or []),
|
||||
urgency=claude_result.urgency,
|
||||
primary_zones=[zone.model_dump() for zone in claude_result.primary_zones],
|
||||
search_radius_km=claude_result.search_radius_km,
|
||||
key_locations=claude_result.key_locations,
|
||||
behavioral_prediction=claude_result.behavioral_prediction,
|
||||
immediate_actions=claude_result.immediate_actions,
|
||||
summary=claude_result.summary,
|
||||
fallback_used=claude_result.fallback_used,
|
||||
urgency=analysis.urgency,
|
||||
primary_zones=[zone.model_dump() if hasattr(zone, "model_dump") else dict(zone) for zone in analysis.primary_zones],
|
||||
search_radius_km=analysis.search_radius_km,
|
||||
key_locations=analysis.key_locations,
|
||||
behavioral_prediction=analysis.behavioral_prediction,
|
||||
immediate_actions=analysis.immediate_actions,
|
||||
summary=analysis.summary,
|
||||
fallback_used=analysis.fallback_used,
|
||||
)
|
||||
Reference in New Issue
Block a user