Upload app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env phython3
|
| 2 |
+
#-*- coding: utf-8 -*-
|
| 3 |
+
import os
|
| 4 |
+
import io
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
import base64
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import List, Optional, Dict , Any
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
from openai import OpenAI
|
| 13 |
+
from pydantic import BaseModel, Field, ValidationError
|
| 14 |
+
import yaml
|
| 15 |
+
|
| 16 |
+
from schemas import FinancialEctract, ExtraxtedPeriod, MultipleSuggestion
|
| 17 |
+
from finanxe_core import(
|
| 18 |
+
copute_ratios,
|
| 19 |
+
credit_decision,
|
| 20 |
+
loan_decision,
|
| 21 |
+
invesement_decision,
|
| 22 |
+
build_report_dict,
|
| 23 |
+
)
|
| 24 |
+
from llm_extract import(
|
| 25 |
+
get_client,
|
| 26 |
+
upload_file_to_openai,
|
| 27 |
+
extract_financials_from_files,
|
| 28 |
+
suggest_multiples_with_llm,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
HF_SPACE = os.environ.get("HF_SPACE_NAME","hf-credit-loan-investment-app")
|
| 32 |
+
VISION_MODEL = os.environ.get("OPENAI_VISION_MODEL", "gpt-5")
|
| 33 |
+
TEXT_MODEL = os.environ.get("OPENAI_TEXT_MODEL", "gpt-5")
|
| 34 |
+
BASE_RATE =float(os.environ.get("BASE_RATE", "2.0")) #% per annum, cofigurable
|
| 35 |
+
|
| 36 |
+
def _load_policies() -> dict:
|
| 37 |
+
cfg_path= os.path.join(os.path.dirname(__file__),"config","risk_policies.yaml")
|
| 38 |
+
with open(cfg_path, "r",encoding="utf-8") as f:
|
| 39 |
+
return yaml.safe_load(f)
|
| 40 |
+
|
| 41 |
+
POLICIES = _load_policies()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def analyze(files: List[gr.File],company_name: str, industry_hint: str, currency: str,
|
| 45 |
+
base_rate:float, want_credit:bool, want_loan: bool,want_invest: bool, debug:bool):
|
| 46 |
+
"""Main pipline used by the Gradio UI."""
|
| 47 |
+
try:
|
| 48 |
+
client = get_client()
|
| 49 |
+
except Exception as e:
|
| 50 |
+
raise gr.Error(str(e))
|
| 51 |
+
|
| 52 |
+
if not files or len(files) == 0:
|
| 53 |
+
raise gr.Error("決算書ファイル(PDF/画像)を1つ以上アップロードしてください。")
|
| 54 |
+
|
| 55 |
+
# 1)Upload files to OpenAI and extract stuctured financials via vision +Structured Outputs
|
| 56 |
+
try:
|
| 57 |
+
file_ids = []
|
| 58 |
+
for f in files :
|
| 59 |
+
file_ids.append(upload_file_to_openai(client, f.name, f.read()))
|
| 60 |
+
except Exception as e:
|
| 61 |
+
raise gr.Error(f"ファイルのアップロードに失敗しました:{e}")
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
extract = extract_financials_from_files(
|
| 65 |
+
client=client,
|
| 66 |
+
file_ids=file_ids,
|
| 67 |
+
company_hint=company_name or None,
|
| 68 |
+
currency_hint=currency_hint or None,
|
| 69 |
+
model=VISION_MODEL,
|
| 70 |
+
debug=debug,
|
| 71 |
+
)
|
| 72 |
+
except Exception as e:
|
| 73 |
+
raise gr.Error(f"LLM抽出に失敗しました:{e}")
|
| 74 |
+
|
| 75 |
+
#allow override company name / indutry if provided
|
| 76 |
+
if company_name:
|
| 77 |
+
extract.company_name =company_name
|
| 78 |
+
if industry_hint:
|
| 79 |
+
extract.industry = industry_hint
|
| 80 |
+
|
| 81 |
+
# 2) Compute derived rasios and risk score
|
| 82 |
+
ratios =copute_ratios(extract)
|
| 83 |
+
|
| 84 |
+
# 3) Decisions
|
| 85 |
+
decisions = {}
|
| 86 |
+
if want_credit:
|
| 87 |
+
decisions["credit"] = credit_decision(extract, ratios, POLICIES)
|
| 88 |
+
if want_loan:
|
| 89 |
+
decisions["loan"] = loan_decision(extract, ratios, base_rate or BASE_RATE, POLICIES)
|
| 90 |
+
if want_invest:
|
| 91 |
+
# Ask LLM for suggested multiples if industry present
|
| 92 |
+
multiple: Optional[MultipleSuggestion] = None
|
| 93 |
+
try:
|
| 94 |
+
multiple = suggest_multiples_with_llm(
|
| 95 |
+
client=client,
|
| 96 |
+
text_model=TEXT_MODEL,
|
| 97 |
+
industry=extract.industry or industry_hint or "",
|
| 98 |
+
region="ja",
|
| 99 |
+
debug=debug,
|
| 100 |
+
)
|
| 101 |
+
except Exception:
|
| 102 |
+
multiple = None
|
| 103 |
+
decisions["investment"] = invesement_decision(extract, ratios, POLICIES, multiple)
|
| 104 |
+
|
| 105 |
+
# 4) Build a combined report (dict) and return displays
|
| 106 |
+
report = build_report_dict(extract, ratios, decisions)
|
| 107 |
+
report_json = json.dumps(report, ensure_ascii=False, indent=2)
|
| 108 |
+
|
| 109 |
+
#Save a downloadable JSON
|
| 110 |
+
ts = datetime.utcnow().strftime("%Ym%d-%H%M%S")
|
| 111 |
+
out_path =f"/mnt/data/report-{ts}.json"
|
| 112 |
+
with open(out_path,"w", encoding="utf-8") as f:
|
| 113 |
+
f.write(report_json)
|
| 114 |
+
|
| 115 |
+
# Pretty sections for Gradio
|
| 116 |
+
summary_md =[]
|
| 117 |
+
summary_md.append(f"### 企業名\n{extract.company_name or '(不明)'}")
|
| 118 |
+
if extract.industry:
|
| 119 |
+
summary_md.append(f"### 業種(推定/指定)\n{extract.industry}")
|
| 120 |
+
if extract.fiscal_year_end:
|
| 121 |
+
summary_md.append(f"### 決算期末\n{extract.fisxal_year_end}")
|
| 122 |
+
|
| 123 |
+
summary_md.append("### 指標(主要)")
|
| 124 |
+
summary_md.append(
|
| 125 |
+
f"- 売上高:{ratios.get('revenue')}\n"
|
| 126 |
+
f"- 営業利益(EBIT):{ratios.get('ebit')}\n"
|
| 127 |
+
f"- EBITDA:{ratios.get('ebitda')}\n"
|
| 128 |
+
f"- 当期純利益:{ratios.get('net_income')}\n"
|
| 129 |
+
f"- 流動比率:{ratios.get('current_ratio')}\n"
|
| 130 |
+
f"- 当座比率:{ratios.get('quick_ratio')}\n"
|
| 131 |
+
f"- D/Eレシオ:{ratios.get('debt_to_equity')}\n"
|
| 132 |
+
f"- インタレストカバレッジ:{ratios.get('inerest_coverage')}\n"
|
| 133 |
+
f"- 売上成長率:{ratios.get('revenue_growth_pct')}"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
if "credit" in decisions:
|
| 137 |
+
c = decisions["credit"]
|
| 138 |
+
summary_md.append("### 与信判断(提案)")
|
| 139 |
+
summary_md.append(
|
| 140 |
+
f"- 与信ランク:**{c['rationg']}**(スコア {c['risk_score']}/100)\n"
|
| 141 |
+
f"- 取引サイト:**{c['site_days']}日**\n"
|
| 142 |
+
f"- 取引可能上限:**{c{'tansaction_limit_display'}}**\n"
|
| 143 |
+
f"- 見直しタイミング:**{c['review_cycle']}**"
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
if "loan" in decisions:
|
| 147 |
+
l = decisions["loan"]
|
| 148 |
+
summary_md.append("### 融資判断(提案)" )
|
| 149 |
+
summary_md.append(
|
| 150 |
+
f"- 融資上限額(概算):**{l['max_principal_display']}**\n"
|
| 151 |
+
f"- 期間案:**{l['term_yeras']}年**\n"
|
| 152 |
+
f"- 参考金利:**{l['interest_rate_pct']}%**\n"
|
| 153 |
+
f"- 目標DSCR:**{l['target_dscr']}**"
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
if "investment" in decisions:
|
| 157 |
+
inv = decisions["investment"]
|
| 158 |
+
summary_md.append("### 投資判断(提案)")
|
| 159 |
+
summary_md.append(
|
| 160 |
+
f"- 推定企業価値(EV):**{inv['ev_display']}**\n"
|
| 161 |
+
f"- 推定時価総額:**{inv['market_cap_display']}**\n"
|
| 162 |
+
f"- 想定投資レンジ:**{inv['recommended_check_size_display']}**\n"
|
| 163 |
+
f"- 魅力度:**{inv['attractiveness']}/5**(成長性:{inv['growth_label']})"
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
#Return
|
| 167 |
+
return "\n\n".join(summary_md),gr.JSON.update(value=json.loads(report_json)),out_path
|
| 168 |
+
|
| 169 |
+
def build_ui():
|
| 170 |
+
with gr.Blocks(theme = gr.themes.Soft(), css = "footer{visivility: hidden}") as demo:
|
| 171 |
+
gr.Markdown("# 決算書→与信・融資・投資判断(HF+OpenAI)")
|
| 172 |
+
with gr.Row():
|
| 173 |
+
with gr.Column():
|
| 174 |
+
files = gr.File(label="決算書ファイル(PDF/JPG/PNG,複数可)",file_types=[".pdf",".png",".jpg",".jpeg"],file_count="multiple")
|
| 175 |
+
company_name = gr.Textbox(label="会社名(任意)")
|
| 176 |
+
industry_hint = gr.Textbox(label="業種(任意)")
|
| 177 |
+
currency_hint = gr.Textbox(label="通過(任意,例: JPY, USD)")
|
| 178 |
+
base_rate = gr.Number(label="ベース金利(%/年)",value=BASE_RATE)
|
| 179 |
+
want_credit = gr.Checkbox(label="与信判断",value=True)
|
| 180 |
+
want_loan = gr.Checkbox(label="融資判断",value=True)
|
| 181 |
+
want_invest = gr.Checkbox(label="投資判断",value=True)
|
| 182 |
+
debug = gr.Checkbox(label="デバッグモード(LLM抽出JSONを厳密化)",value=False)
|
| 183 |
+
run_btn = gr.Button("分析する", variant="primary")
|
| 184 |
+
with gr.Column():
|
| 185 |
+
summary = gr.Markdown()
|
| 186 |
+
report = gr.JSON(label="詳細レポート(JSON)")
|
| 187 |
+
download = gr.File(label="レポート(JSONダウンロード)")
|
| 188 |
+
|
| 189 |
+
run_btn.chek(
|
| 190 |
+
analyze,
|
| 191 |
+
inputs=[files, company_name, industry_hint, currency_hint, base_rate, want_credit, want_loan, want_invest, debug],
|
| 192 |
+
outputs=[summary, report, download]
|
| 193 |
+
)
|
| 194 |
+
return demo
|
| 195 |
+
|
| 196 |
+
if __name__== "__main__":
|
| 197 |
+
demo =build_ui()
|
| 198 |
+
demo.launch()
|