- All domain monetary fields now use Decimal: - Account.balance, Asset.value, Asset.growth_rate - Liability.balance, Liability.interest, Liability.payment - Transaction.amount, RecurringCashflow.amount - ExchangeRate.rate - ForecastScenario.income_multiplier, expense_multiplier, growth_multiplier - CurrencyConverter: all arithmetic in Decimal, quantize to 0.01 with ROUND_HALF_UP - ForecastService: Decimal arithmetic throughout (income, expenses, balance, growth, liability cost) - ScenarioService: Decimal multipliers, deepcopy safe with Decimal fields - assistant.py: _DecimalEncoder for json.dumps (Decimal -> str in JSON) Pydantic v2 + Decimal: - model_dump(mode='json') converts Decimal to str (JSON-safe) - model_validate() parses str back to Decimal - Round-trip preserves precision (100.50 stays 100.50) Tests: 63/63 pass.
80 lines
2.6 KiB
Python
80 lines
2.6 KiB
Python
import json
|
|
from decimal import Decimal
|
|
|
|
from infrastructure.ai import prompts
|
|
from domain import CurrencyConverter, FinancialModel
|
|
from application.forecast import ForecastService
|
|
|
|
|
|
class AssistantError(Exception):
|
|
pass
|
|
|
|
|
|
class _DecimalEncoder(json.JSONEncoder):
|
|
"""JSON-сериализатор: Decimal → str (для AI-промптов)."""
|
|
|
|
def default(self, o):
|
|
if isinstance(o, Decimal):
|
|
return str(o)
|
|
return super().default(o)
|
|
|
|
|
|
class AssistantService:
|
|
def __init__(
|
|
self,
|
|
model: FinancialModel,
|
|
converter: CurrencyConverter | None = None,
|
|
display_currency: str | None = None,
|
|
):
|
|
self.model = model
|
|
self.converter = converter or CurrencyConverter(model.exchange_rates)
|
|
self.display_currency = display_currency or model.base_currency
|
|
|
|
def analyze(self, months: int = 12) -> dict:
|
|
forecast_service = ForecastService(self.model)
|
|
forecast_result = forecast_service.forecast_cashflow(months)
|
|
summary = forecast_service.summary(months)
|
|
|
|
prompt = prompts.format_context(
|
|
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False, cls=_DecimalEncoder),
|
|
forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False, cls=_DecimalEncoder),
|
|
months=months,
|
|
base_currency=self.model.base_currency,
|
|
display_currency=self.display_currency,
|
|
)
|
|
|
|
return {
|
|
"prompt": prompt,
|
|
"summary": summary,
|
|
"forecast": forecast_result,
|
|
"ai_response": None,
|
|
}
|
|
|
|
def advice(self, question: str, months: int = 12) -> dict:
|
|
forecast_service = ForecastService(self.model)
|
|
forecast_result = forecast_service.forecast_cashflow(months)
|
|
|
|
prompt = prompts.ADVICE_PROMPT.format(
|
|
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False, cls=_DecimalEncoder),
|
|
forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False, cls=_DecimalEncoder),
|
|
question=question,
|
|
base_currency=self.model.base_currency,
|
|
display_currency=self.display_currency,
|
|
)
|
|
|
|
return {
|
|
"prompt": prompt,
|
|
"ai_response": None,
|
|
}
|
|
|
|
def compare_scenarios(self, scenarios_json: str) -> dict:
|
|
prompt = prompts.SCENARIO_COMPARISON_PROMPT.format(
|
|
scenarios_json=scenarios_json,
|
|
base_currency=self.model.base_currency,
|
|
display_currency=self.display_currency,
|
|
)
|
|
return {
|
|
"prompt": prompt,
|
|
"ai_response": None,
|
|
}
|