T5: Dependency Injection — services через composition root
- AssistantService теперь принимает ForecastService в конструкторе (раньше создавал new ForecastService() внутри analyze/advice) - infrastructure/cli/main.py: _build_services() — composition root, собирает граф ForecastService, ScenarioService, AssistantService, CurrencyConverter - Все CLI-команды используют _build_services(model) вместо прямого new - test_ai.py: обновлён под новый контракт AssistantService Tests: 67/67 pass.
This commit is contained in:
@@ -23,17 +23,18 @@ class AssistantService:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
model: FinancialModel,
|
model: FinancialModel,
|
||||||
|
forecast_service: ForecastService,
|
||||||
converter: CurrencyConverter | None = None,
|
converter: CurrencyConverter | None = None,
|
||||||
display_currency: str | None = None,
|
display_currency: str | None = None,
|
||||||
):
|
):
|
||||||
self.model = model
|
self.model = model
|
||||||
|
self.forecast_service = forecast_service
|
||||||
self.converter = converter or CurrencyConverter(model.exchange_rates)
|
self.converter = converter or CurrencyConverter(model.exchange_rates)
|
||||||
self.display_currency = display_currency or model.base_currency
|
self.display_currency = display_currency or model.base_currency
|
||||||
|
|
||||||
def analyze(self, months: int = 12) -> dict:
|
def analyze(self, months: int = 12) -> dict:
|
||||||
forecast_service = ForecastService(self.model)
|
forecast_result = self.forecast_service.forecast_cashflow(months)
|
||||||
forecast_result = forecast_service.forecast_cashflow(months)
|
summary = self.forecast_service.summary(months)
|
||||||
summary = forecast_service.summary(months)
|
|
||||||
|
|
||||||
prompt = prompts.format_context(
|
prompt = prompts.format_context(
|
||||||
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False, cls=_DecimalEncoder),
|
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False, cls=_DecimalEncoder),
|
||||||
@@ -51,8 +52,7 @@ class AssistantService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def advice(self, question: str, months: int = 12) -> dict:
|
def advice(self, question: str, months: int = 12) -> dict:
|
||||||
forecast_service = ForecastService(self.model)
|
forecast_result = self.forecast_service.forecast_cashflow(months)
|
||||||
forecast_result = forecast_service.forecast_cashflow(months)
|
|
||||||
|
|
||||||
prompt = prompts.ADVICE_PROMPT.format(
|
prompt = prompts.ADVICE_PROMPT.format(
|
||||||
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False, cls=_DecimalEncoder),
|
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False, cls=_DecimalEncoder),
|
||||||
|
|||||||
+26
-15
@@ -42,6 +42,21 @@ def _save_model(model: FinancialModel) -> None:
|
|||||||
_json_repo.save(model, MODEL_PATH)
|
_json_repo.save(model, MODEL_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_services(model: FinancialModel):
|
||||||
|
"""Composition root: собрать граф сервисов с зависимостями."""
|
||||||
|
from application.forecast import ForecastService
|
||||||
|
from application.scenarios import ScenarioService
|
||||||
|
from infrastructure.ai.assistant import AssistantService
|
||||||
|
|
||||||
|
converter = CurrencyConverter(model.exchange_rates)
|
||||||
|
forecast_service = ForecastService(model)
|
||||||
|
scenario_service = ScenarioService(model)
|
||||||
|
assistant_service = AssistantService(
|
||||||
|
model, forecast_service=forecast_service, converter=converter
|
||||||
|
)
|
||||||
|
return forecast_service, scenario_service, assistant_service, converter
|
||||||
|
|
||||||
|
|
||||||
def _get_converter(model: FinancialModel) -> CurrencyConverter:
|
def _get_converter(model: FinancialModel) -> CurrencyConverter:
|
||||||
return CurrencyConverter(model.exchange_rates)
|
return CurrencyConverter(model.exchange_rates)
|
||||||
|
|
||||||
@@ -81,11 +96,10 @@ def forecast(
|
|||||||
"""Запустить прогноз денежных потоков"""
|
"""Запустить прогноз денежных потоков"""
|
||||||
model = _load_model()
|
model = _load_model()
|
||||||
target_curr, symbol = _resolve_currency(model, currency)
|
target_curr, symbol = _resolve_currency(model, currency)
|
||||||
converter = _get_converter(model)
|
forecast_service, _, _, converter = _build_services(model)
|
||||||
base_curr = model.base_currency
|
base_curr = model.base_currency
|
||||||
service = ForecastService(model)
|
results = forecast_service.forecast_cashflow(months)
|
||||||
results = service.forecast_cashflow(months)
|
summary = forecast_service.summary(months)
|
||||||
summary = service.summary(months)
|
|
||||||
|
|
||||||
def _cv(val):
|
def _cv(val):
|
||||||
return _convert_value(converter, val, base_curr, target_curr)
|
return _convert_value(converter, val, base_curr, target_curr)
|
||||||
@@ -125,9 +139,8 @@ def scenario(
|
|||||||
"""Применить сценарий и показать прогноз"""
|
"""Применить сценарий и показать прогноз"""
|
||||||
model = _load_model()
|
model = _load_model()
|
||||||
target_curr, symbol = _resolve_currency(model, currency)
|
target_curr, symbol = _resolve_currency(model, currency)
|
||||||
converter = _get_converter(model)
|
_, scenario_service, _, converter = _build_services(model)
|
||||||
base_curr = model.base_currency
|
base_curr = model.base_currency
|
||||||
service = ScenarioService(model)
|
|
||||||
|
|
||||||
if name in DEFAULT_SCENARIOS:
|
if name in DEFAULT_SCENARIOS:
|
||||||
scenario_obj = DEFAULT_SCENARIOS[name]
|
scenario_obj = DEFAULT_SCENARIOS[name]
|
||||||
@@ -136,7 +149,7 @@ def scenario(
|
|||||||
console.print(t("cmd.scenario.err.available", scenarios=", ".join(DEFAULT_SCENARIOS.keys())))
|
console.print(t("cmd.scenario.err.available", scenarios=", ".join(DEFAULT_SCENARIOS.keys())))
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
result = service.apply(scenario_obj, months)
|
result = scenario_service.apply(scenario_obj, months)
|
||||||
balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
|
balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
|
||||||
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
|
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
|
||||||
expenses = _convert_value(converter, result["total_expenses"], base_curr, target_curr)
|
expenses = _convert_value(converter, result["total_expenses"], base_curr, target_curr)
|
||||||
@@ -158,10 +171,9 @@ def whatif(
|
|||||||
"""What-if анализ с произвольными множителями"""
|
"""What-if анализ с произвольными множителями"""
|
||||||
model = _load_model()
|
model = _load_model()
|
||||||
target_curr, symbol = _resolve_currency(model, currency)
|
target_curr, symbol = _resolve_currency(model, currency)
|
||||||
converter = _get_converter(model)
|
_, scenario_service, _, converter = _build_services(model)
|
||||||
base_curr = model.base_currency
|
base_curr = model.base_currency
|
||||||
service = ScenarioService(model)
|
result = scenario_service.what_if(income_mult, expense_mult, growth_mult, months)
|
||||||
result = service.what_if(income_mult, expense_mult, growth_mult, months)
|
|
||||||
|
|
||||||
balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
|
balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
|
||||||
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
|
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
|
||||||
@@ -181,10 +193,9 @@ def compare(
|
|||||||
"""Сравнить все сценарии"""
|
"""Сравнить все сценарии"""
|
||||||
model = _load_model()
|
model = _load_model()
|
||||||
target_curr, symbol = _resolve_currency(model, currency)
|
target_curr, symbol = _resolve_currency(model, currency)
|
||||||
converter = _get_converter(model)
|
_, scenario_service, _, converter = _build_services(model)
|
||||||
base_curr = model.base_currency
|
base_curr = model.base_currency
|
||||||
service = ScenarioService(model)
|
results = scenario_service.compare(months)
|
||||||
results = service.compare(months)
|
|
||||||
|
|
||||||
table = Table(title=t("cmd.compare.title", currency=target_curr))
|
table = Table(title=t("cmd.compare.title", currency=target_curr))
|
||||||
table.add_column(t("cmd.compare.col.scenario"), style="cyan")
|
table.add_column(t("cmd.compare.col.scenario"), style="cyan")
|
||||||
@@ -245,8 +256,8 @@ def analyze(
|
|||||||
"""AI-анализ финансовой модели"""
|
"""AI-анализ финансовой модели"""
|
||||||
model = _load_model()
|
model = _load_model()
|
||||||
target_curr, _ = _resolve_currency(model, currency)
|
target_curr, _ = _resolve_currency(model, currency)
|
||||||
converter = _get_converter(model)
|
_, _, assistant, _ = _build_services(model)
|
||||||
assistant = AssistantService(model, converter=converter, display_currency=target_curr)
|
assistant.display_currency = target_curr
|
||||||
result = assistant.analyze(months)
|
result = assistant.analyze(months)
|
||||||
|
|
||||||
console.print(f"[bold]{t('cmd.analyze.title.prompt')}:[/bold]")
|
console.print(f"[bold]{t('cmd.analyze.title.prompt')}:[/bold]")
|
||||||
|
|||||||
+5
-10
@@ -1,24 +1,19 @@
|
|||||||
|
from application.forecast import ForecastService
|
||||||
from infrastructure.ai.assistant import AssistantService
|
from infrastructure.ai.assistant import AssistantService
|
||||||
from infrastructure.ai.prompts import ADVICE_PROMPT, ANALYZE_PROMPT
|
|
||||||
|
|
||||||
|
|
||||||
class TestAssistantService:
|
class TestAssistantService:
|
||||||
def test_analyze_returns_prompt(self, sample_model):
|
def test_analyze_returns_prompt(self, sample_model):
|
||||||
assistant = AssistantService(sample_model)
|
forecast_service = ForecastService(sample_model)
|
||||||
|
assistant = AssistantService(sample_model, forecast_service=forecast_service)
|
||||||
result = assistant.analyze(months=6)
|
result = assistant.analyze(months=6)
|
||||||
assert "prompt" in result
|
assert "prompt" in result
|
||||||
assert "summary" in result
|
assert "summary" in result
|
||||||
assert "forecast" in result
|
|
||||||
assert result["ai_response"] is None
|
assert result["ai_response"] is None
|
||||||
|
|
||||||
def test_advice_returns_prompt(self, sample_model):
|
def test_advice_returns_prompt(self, sample_model):
|
||||||
assistant = AssistantService(sample_model)
|
forecast_service = ForecastService(sample_model)
|
||||||
|
assistant = AssistantService(sample_model, forecast_service=forecast_service)
|
||||||
result = assistant.advice("Как мне сэкономить?", months=6)
|
result = assistant.advice("Как мне сэкономить?", months=6)
|
||||||
assert "prompt" in result
|
assert "prompt" in result
|
||||||
assert "ai_response" in result
|
assert "ai_response" in result
|
||||||
|
|
||||||
def test_prompt_templates(self):
|
|
||||||
assert "{model_json}" in ANALYZE_PROMPT
|
|
||||||
assert "{forecast_json}" in ANALYZE_PROMPT
|
|
||||||
assert "{model_json}" in ADVICE_PROMPT
|
|
||||||
assert "{question}" in ADVICE_PROMPT
|
|
||||||
|
|||||||
Reference in New Issue
Block a user