Spaces:
Sleeping
Sleeping
| 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 | |
| def load_models(): | |
| global advice_tokenizer, advice_model | |
| advice_tokenizer, advice_model = advice_load() | |
| from datetime import datetime | |
| 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()], | |
| ) | |
| 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) | |
| 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) | |