finwise-ml / app.py
minstradamus's picture
Update app.py
a9649a7 verified
import os
import tempfile
from decimal import Decimal
from typing import List, Optional
import pandas as pd
from fastapi import FastAPI, HTTPException, UploadFile, File
from pydantic import BaseModel
from common import prepare_components_series, fit_and_forecast, current_month_snapshot
from advice import _load as advice_load, _gen as advice_gen, _to_bullets, clean_ru
from receipt_total_api import extract_info
app = FastAPI()
class Transaction(BaseModel):
date: str
amount: Decimal
type: str
category: Optional[str] = None
description: Optional[str] = None
class ForecastRequest(BaseModel):
granularity: str
steps: int
model: Optional[str] = None
transactions: List[Transaction]
class ForecastResponse(BaseModel):
period_end: List[str]
income_forecast: List[float]
expense_forecast: List[float]
class AdviceRequest(BaseModel):
question: Optional[str] = None
transactions: List[Transaction] = []
class AdviceResponse(BaseModel):
advice: str
class ReceiptResponse(BaseModel):
total: Optional[float]
advice_tokenizer = None
advice_model = None
@app.on_event("startup")
def load_models():
global advice_tokenizer, advice_model
advice_tokenizer, advice_model = advice_load()
from datetime import datetime
@app.post("/forecast", response_model=ForecastResponse)
def forecast(req: ForecastRequest):
if not req.transactions:
raise HTTPException(status_code=400, detail="transactions is empty")
df = pd.DataFrame([t.dict() for t in req.transactions])
gran = (req.granularity or "month").lower()
freq = "A-DEC" if gran.startswith("y") else "M"
steps = int(req.steps or 1)
method = (req.model or "auto").lower()
inc, exp, _ = prepare_components_series(df, freq=freq)
inc_fc = fit_and_forecast(inc, steps, freq, method=method)
exp_fc = fit_and_forecast(exp, steps, freq, method=method)
period_end = [
d.strftime("%Y-%m-%dT%H:%M:%SZ")
for d in inc_fc.index.to_pydatetime().tolist()
]
return ForecastResponse(
period_end=period_end,
income_forecast=[float(x) for x in inc_fc.values.tolist()],
expense_forecast=[float(x) for x in exp_fc.values.tolist()],
)
@app.post("/advice", response_model=AdviceResponse)
def advice(req: AdviceRequest):
tx = [t.dict() for t in req.transactions] if req.transactions else []
df = pd.DataFrame(tx) if tx else None
snap = current_month_snapshot(df) if df is not None and not df.empty else {}
if snap:
ctx = [
f"Месяц: {snap['month']}",
f"Доход: {snap['income_total']:.0f}",
f"Расход: {abs(snap['expense_total']):.0f}",
f"Нетто: {snap['net']:.0f}",
]
if snap.get("top_expense_categories"):
ctx.append("Топ статей расходов:")
for cat, val in snap["top_expense_categories"]:
ctx.append(f"- {cat}: {abs(val):.0f}")
context = "\n".join(ctx)
else:
context = "Данных за текущий месяц нет."
question = (req.question or "").strip()
system_msg = (
"Ты финансовый помощник. Отвечай по-русски. "
"Верни ТОЛЬКО список из 5–7 конкретных шагов экономии с цифрами (лимиты, проценты, частота). "
"Каждая строка должна начинаться с символов \"- \". Никаких вступлений."
)
messages = [
{"role": "system", "content": system_msg},
{
"role": "user",
"content": (
f"Мои данные за текущий месяц:\n{context}\n\nВопрос: {question}\n"
"Начни ответ сразу со строки, которая начинается с \"- \". Верни только список."
),
},
]
raw = advice_gen(messages, advice_tokenizer, advice_model, det=True)
text = _to_bullets(clean_ru(raw))
if text.count("\n") + 1 < 3:
raw2 = advice_gen(messages, advice_tokenizer, advice_model, det=False)
text2 = _to_bullets(clean_ru(raw2))
if text2:
text = text2
return AdviceResponse(advice=text)
@app.post("/receipt-total-file", response_model=ReceiptResponse)
async def receipt_total_file(file: UploadFile = File(...)):
suffix = os.path.splitext(file.filename or "")[1] or ".jpg"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
contents = await file.read()
tmp.write(contents)
tmp_path = tmp.name
try:
total = extract_total(tmp_path)
return ReceiptResponse(total=total)
finally:
try:
os.remove(tmp_path)
except OSError:
pass
if __name__ == "__main__":
import uvicorn
uvicorn.run("app:app", host="0.0.0.0", port=7860, workers=1)