Corin1998 commited on
Commit
a29650c
·
verified ·
1 Parent(s): 8987196

Upload 7 files

Browse files
Files changed (5) hide show
  1. app.py +96 -88
  2. config:risk_policies.yaml +11 -11
  3. finance_core.py +188 -130
  4. llm_extract.py +67 -64
  5. schemas.py +21 -13
app.py CHANGED
@@ -1,65 +1,68 @@
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/画像)をつ以上アップロードしてください。")
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,
@@ -70,16 +73,16 @@ def analyze(files: List[gr.File],company_name: str, industry_hint: str, currency
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 = {}
@@ -89,110 +92,115 @@ def analyze(files: List[gr.File],company_name: str, industry_hint: str, currency
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()
 
 
1
+ #!/usr/bin/env python3
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 FinancialExtract, ExtractedPeriod, MultipleSuggestion
17
+ from finance_core import (
18
+ compute_ratios,
19
  credit_decision,
20
  loan_decision,
21
+ investment_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
+
32
+ HF_SPACE = os.environ.get("HF_SPACE_NAME", "hf-credit-loan-investment-app")
33
+ VISION_MODEL = os.environ.get("OPENAI_VISION_MODEL", "gpt-4o-mini")
34
+ TEXT_MODEL = os.environ.get("OPENAI_TEXT_MODEL", "gpt-4o-mini")
35
+ BASE_RATE = float(os.environ.get("BASE_RATE", "2.0")) # % per annum, configurable
36
+
37
 
38
  def _load_policies() -> dict:
39
+ cfg_path = os.path.join(os.path.dirname(__file__), "config", "risk_policies.yaml")
40
+ with open(cfg_path, "r", encoding="utf-8") as f:
41
  return yaml.safe_load(f)
42
+
43
+
44
  POLICIES = _load_policies()
45
 
46
 
47
+ def analyze(files: List[gr.File], company_name: str, industry_hint: str, currency_hint: str,
48
+ base_rate: float, want_credit: bool, want_loan: bool, want_invest: bool, debug: bool):
49
+ """Main pipeline used by the Gradio UI."""
50
  try:
51
  client = get_client()
52
  except Exception as e:
53
  raise gr.Error(str(e))
54
+
55
  if not files or len(files) == 0:
56
+ raise gr.Error("決算書ファイル(PDF/画像)を1つ以上アップロードしてください。")
57
+
58
+ # 1) Upload files to OpenAI and extract structured financials via vision + Structured Outputs
59
+ try:
60
  file_ids = []
61
+ for f in files:
62
  file_ids.append(upload_file_to_openai(client, f.name, f.read()))
63
  except Exception as e:
64
+ raise gr.Error(f"ファイルのアップロードに失敗しました: {e}")
65
+
66
  try:
67
  extract = extract_financials_from_files(
68
  client=client,
 
73
  debug=debug,
74
  )
75
  except Exception as e:
76
+ raise gr.Error(f"LLM抽出に失敗しました: {e}")
77
+
78
+ # allow override company name / industry if provided
79
  if company_name:
80
+ extract.company_name = company_name
81
  if industry_hint:
82
  extract.industry = industry_hint
83
 
84
+ # 2) Compute derived ratios and risk score
85
+ ratios = compute_ratios(extract)
86
 
87
  # 3) Decisions
88
  decisions = {}
 
92
  decisions["loan"] = loan_decision(extract, ratios, base_rate or BASE_RATE, POLICIES)
93
  if want_invest:
94
  # Ask LLM for suggested multiples if industry present
95
+ multiples: Optional[MultipleSuggestion] = None
96
  try:
97
+ multiples = suggest_multiples_with_llm(
98
  client=client,
99
  text_model=TEXT_MODEL,
100
  industry=extract.industry or industry_hint or "",
101
+ region="JP",
102
  debug=debug,
103
  )
104
  except Exception:
105
+ multiples = None
106
+ decisions["investment"] = investment_decision(extract, ratios, POLICIES, multiples)
107
 
108
  # 4) Build a combined report (dict) and return displays
109
  report = build_report_dict(extract, ratios, decisions)
110
  report_json = json.dumps(report, ensure_ascii=False, indent=2)
111
 
112
+ # Save a downloadable JSON
113
+ ts = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
114
+ out_path = f"/mnt/data/report-{ts}.json"
115
+ with open(out_path, "w", encoding="utf-8") as f:
116
  f.write(report_json)
117
 
118
  # Pretty sections for Gradio
119
+ summary_md = []
120
+ summary_md.append(f"### 企業名\n{extract.company_name or '不明'}")
121
  if extract.industry:
122
  summary_md.append(f"### 業種(推定/指定)\n{extract.industry}")
123
+ if extract.currency:
124
+ summary_md.append(f"### 通貨\n{extract.currency}")
125
  if extract.fiscal_year_end:
126
+ summary_md.append(f"### 決算期末\n{extract.fiscal_year_end}")
127
 
128
  summary_md.append("### 指標(主要)")
129
  summary_md.append(
130
+ f"- 売上高: {ratios.get('revenue')}\n"
131
+ f"- 営業利益EBIT: {ratios.get('ebit')}\n"
132
+ f"- EBITDA: {ratios.get('ebitda')}\n"
133
+ f"- 当期純利益: {ratios.get('net_income')}\n"
134
+ f"- 流動比率: {ratios.get('current_ratio')}\n"
135
+ f"- 当座比率: {ratios.get('quick_ratio')}\n"
136
+ f"- D/Eレシオ: {ratios.get('debt_to_equity')}\n"
137
+ f"- インタレストカバレッジ: {ratios.get('interest_coverage')}\n"
138
+ f"- 売上成長率: {ratios.get('revenue_growth_pct')}"
139
  )
140
 
141
  if "credit" in decisions:
142
  c = decisions["credit"]
143
  summary_md.append("### 与信判断(提案)")
144
  summary_md.append(
145
+ f"- 与信ランク: **{c['rating']}**スコア {c['risk_score']}/100\n"
146
+ f"- 取引サイト: **{c['site_days']}日**\n"
147
+ f"- 取引可能上限: **{c['transaction_limit_display']}**\n"
148
+ f"- 見直しタイミング: **{c['review_cycle']}**"
149
  )
150
+
151
  if "loan" in decisions:
152
  l = decisions["loan"]
153
+ summary_md.append("### 融資判断(提案)")
154
  summary_md.append(
155
+ f"- 融資上限額(概算): **{l['max_principal_display']}**\n"
156
+ f"- 期間案: **{l['term_years']}年**\n"
157
+ f"- 参考金利: **{l['interest_rate_pct']}%**\n"
158
+ f"- 目標DSCR: **{l['target_dscr']}**"
159
  )
160
+
161
  if "investment" in decisions:
162
  inv = decisions["investment"]
163
  summary_md.append("### 投資判断(提案)")
164
  summary_md.append(
165
+ f"- 推定企業価値EV: **{inv['ev_display']}**\n"
166
+ f"- 推定時価総額: **{inv['market_cap_display']}**\n"
167
+ f"- 想定投資レンジ: **{inv['recommended_check_size_display']}**\n"
168
+ f"- 魅力度: **{inv['attractiveness']}/5**成長性: {inv['growth_label']}"
169
  )
170
+
171
+ # Return
172
+ return "\n\n".join(summary_md), gr.JSON.update(value=json.loads(report_json)), out_path
173
+
174
 
175
  def build_ui():
176
+ with gr.Blocks(theme=gr.themes.Soft(), css="footer {visibility: hidden}") as demo:
177
+ gr.Markdown("# 決算書→与信・融資・投資判断(HF+OpenAI")
178
  with gr.Row():
179
  with gr.Column():
180
+ files = gr.File(label="決算書ファイルPDF/JPG/PNG, 複数可", file_types=[".pdf", ".png", ".jpg", ".jpeg"], file_count="multiple")
181
  company_name = gr.Textbox(label="会社名(任意)")
182
+ industry_hint = gr.Textbox(label="業種(任意")
183
+ currency_hint = gr.Textbox(label="通貨(任意, : JPY, USD")
184
+ base_rate = gr.Number(label="ベース金利%/年", value=BASE_RATE)
185
+ want_credit = gr.Checkbox(label="与信判断", value=True)
186
+ want_loan = gr.Checkbox(label="融資判断", value=True)
187
+ want_invest = gr.Checkbox(label="投資判断", value=True)
188
+ debug = gr.Checkbox(label="デバッグモードLLM抽出JSONを厳密化", value=False)
189
  run_btn = gr.Button("分析する", variant="primary")
190
  with gr.Column():
191
  summary = gr.Markdown()
192
+ report = gr.JSON(label="詳細レポートJSON")
193
+ download = gr.File(label="レポートJSONダウンロード")
194
 
195
+ run_btn.click(
196
  analyze,
197
  inputs=[files, company_name, industry_hint, currency_hint, base_rate, want_credit, want_loan, want_invest, debug],
198
+ outputs=[summary, report, download]
199
  )
200
+
201
  return demo
202
 
203
+
204
+ if __name__ == "__main__":
205
+ demo = build_ui()
206
+ demo.launch()
config:risk_policies.yaml CHANGED
@@ -1,20 +1,20 @@
1
  # Basic policy knobs (extend as needed)
2
- liquidity :
3
- current_ratio_good: 2.0
4
- current_ratio_ok: 1.5
5
 
6
  leverage:
7
- debt_to_equity_good: 0.5
8
- debt_to_equity_ok: 1.0
9
 
10
  coverage:
11
- interest_coverage_good: 6.0
12
- interest_coverage_ok: 3.0
13
 
14
  profitability:
15
- net_margin_good: 0.1
16
- net_margin_ok: 0.03
17
 
18
  growth:
19
- revenue_growth_high: 0.2
20
- revenue_growth_ok: 0.05
 
1
  # Basic policy knobs (extend as needed)
2
+ liquidity:
3
+ current_ratio_good: 2.0
4
+ current_ratio_ok: 1.5
5
 
6
  leverage:
7
+ debt_to_equity_good: 0.5
8
+ debt_to_equity_ok: 1.0
9
 
10
  coverage:
11
+ interest_coverage_good: 6.0
12
+ interest_coverage_ok: 3.0
13
 
14
  profitability:
15
+ net_margin_good: 0.1
16
+ net_margin_ok: 0.03
17
 
18
  growth:
19
+ revenue_growth_high: 0.2
20
+ revenue_growth_ok: 0.05
finance_core.py CHANGED
@@ -1,31 +1,33 @@
1
  # -*- coding: utf-8 -*-
2
  from __future__ import annotations
3
  import math
4
- from typing import Dict, Any ,Optional
5
  from dataclasses import dataclass
6
 
7
- from schemas import FinancalExtract, ExtractedPeriod, MultipleSuggestion
8
 
9
- def _fmt_currency(x: Optional[float],currency:str) -> str:
 
10
  if x is None:
11
- return "-"
12
- try :
13
  # compact format
14
- absx=abs(x)
15
- if absx >=1e12:
16
  return f"{x/1e12:.2f}兆{currency}"
17
- if absx >=1e8:
18
  return f"{x/1e8:.2f}億{currency}"
19
- if absx >=1e6:
20
  return f"{x/1e6:.2f}百万{currency}"
21
- return f"{x:,0f}{currency}"
22
  except Exception:
23
- return f"{x}{currency}"
 
24
 
25
  def compute_ratios(extract: FinancialExtract) -> Dict[str, Any]:
26
- """Cmpute derived metrics from the latest period(and prior if exists)."""
27
- latest: Optional[ExtractedPeriod]= extract.latest()
28
- prev: Optional[ExtractedPeriod]= extract.previous()
29
 
30
  currency = extract.currency or ""
31
 
@@ -35,62 +37,66 @@ def compute_ratios(extract: FinancialExtract) -> Dict[str, Any]:
35
  revenue = latest.revenue
36
  cogs = latest.cogs
37
  ebit = latest.ebit
38
- ebitda = latest.ebitda if latest.ebitda is not None else (latest.ebit + (latest.depreciation or 0.0)if latest.ebit is not None else None)
39
  net_income = latest.net_income
40
  cash = latest.cash_and_equivalents
41
  ar = latest.accounts_receivable
42
- inv = latest.accounts_payable
43
- ap = latest.accoutnts_payable
44
  ca = latest.current_assets
45
- cl = latest.current_laiabilities
46
  ta = latest.total_assets
47
  te = latest.total_equity
 
48
  interest = latest.interest_expense
49
 
50
  gross_profit = (revenue - cogs) if revenue is not None and cogs is not None else None
51
- gross_margin = (gross_profit/revenue)if revenue and gross_profit is not None else None
52
- ebit_margin = (ebit/revenue)if revenue and ebit is not None else None
53
- ebitda_margin = (ebitda /revenue) if revenue and ebitda is not None else None
54
 
55
- current_ratio = (ca/cl) if ca and cl else None
56
  quick_assets = (cash or 0.0) + (ar or 0.0)
57
- quick_ratio = (quick_assets/cl)if quick_assets and cl else None
58
 
59
- debt_to_equity = (td/te)if td is not None and te else None
60
- interest_coverage = (ebit / interest)if ebit is not None and interest else None
61
 
62
  ratios.update({
63
- "revenue": _fmt_currency(revenue,currency),
64
- "ebit":_fmt_currency(ebit, currency),
65
- "ebitda":_fmt_currency(ebitda,currency),
66
- "net_income":_fmt_currency(net_income,currency),
67
- "gross_margin_pct":f"{gross_margin*100:.1f}%"if fross_margin is not None else "-",
68
- "ebit_margin_pct": f"{ebit_margin*100:.1f}%"if ebitda_margin si not None else "-",
69
- "quick_ratio":f"{quick_ratio:.2f}"if quick_ratio is not None else "-",
70
- "debt_to_equity":f"{debt_to_equity:.2f}"if debt_to_equity is not None else "-",
71
- "interest_coverage": f"{interest_coverage:.2f}" if interest_coverage is not None else "-",
 
 
72
  })
73
 
74
- if latest and prev and latest.revenue and prev.revenueand and prev.revenue != 0:
75
- growth =(latest.revenue - prev.revenue)/ abs(prev.revenue)
76
  ratios["revenue_growth_pct"] = f"{growth*100:.1f}%"
77
  ratios["_revenue_growth_float"] = growth
78
  else:
79
- ratios["revenue_growth_pct"] = "-"
80
  ratios["_revenue_growth_float"] = None
81
 
82
  ratios["_latest_currency"] = extract.currency or "JPY"
83
  ratios["_latest_revenue"] = latest.revenue if latest else None
84
- ratios["_latest_cebitda"] = (latest.ebitda if latest and latest.ebitda is not None else
85
- (latest.ebit + (latest.depreciation or 0.0)if latest and latest.ebit is not None else None))
86
- ratios["_latest_current_ratio"] = float(ratios["current_ratio"])if isinstance(ratios.get("current_ratio"), str) and ratios["current_ratio"]not in ("-",)else None
87
- ratios["latest_quick_ratio"] = float(ratios["quick_ratio"])if isinstance(ratios.get("quick_ratio"),str) and ratios["quick_ratio"]not in ("-",)else None
88
 
89
- #Raw for computations
90
- ratios["_latest"] = latest.dict()if latest else{}
91
  return ratios
92
 
93
- def _risk_score(latest:ExtractedPeriod, ratios: Dict[str, Any], policies: Dict[str, Any]) -> float:
 
94
  """Simple additive score 0-100 based on policy thresholds."""
95
  score = 50.0
96
 
@@ -102,9 +108,9 @@ def _risk_score(latest:ExtractedPeriod, ratios: Dict[str, Any], policies: Dict[s
102
  score += 10
103
  elif cr >= 1.5:
104
  score += 6
105
- elif cr >= 1.2 :
106
- score +=3
107
- elif cr< 1.0:
108
  score -= 10
109
 
110
  # Leverage
@@ -115,11 +121,11 @@ def _risk_score(latest:ExtractedPeriod, ratios: Dict[str, Any], policies: Dict[s
115
  score += 8
116
  elif de <= 1.0:
117
  score += 4
118
- elif de >2.0:
119
  score -= 8
120
-
121
- #Interest coverage
122
- ebit,interest =latest.ebit, latest.interest_expense
123
  if ebit is not None and interest:
124
  ic = ebit / interest
125
  if ic >= 6:
@@ -128,110 +134,161 @@ def _risk_score(latest:ExtractedPeriod, ratios: Dict[str, Any], policies: Dict[s
128
  score += 6
129
  elif ic >= 1.5:
130
  score += 3
131
- elif ic <1.0:
132
- score -=12
133
 
134
  # Profitability
135
  revenue = latest.revenue
136
- net = latest.net_incoe
137
  if revenue and net is not None:
138
- nm = net /revenue
139
- if nm >= 0.1:
140
- score +=6
141
  elif nm >= 0.03:
142
  score += 3
143
- elif nm< 0:
144
  score -= 8
145
-
146
- #Growth
147
  g = ratios.get("_revenue_growth_float")
148
  if g is not None:
149
  if g >= 0.2:
150
  score += 6
151
  elif g >= 0.05:
152
- score +=3
153
  elif g < 0:
154
  score -= 5
155
 
156
- return max (0.0, min (100.0, score))
 
157
 
158
- def _rating(score:float) -> str:
159
  if score >= 85: return "A"
160
  if score >= 70: return "B"
161
  if score >= 55: return "C"
162
  if score >= 40: return "D"
163
  return "E"
164
 
165
- def credit_decision(extract: FinancialExtract, ratios: Dict[str, Any],policies: Dict[str, Any]) -> Dict[str, Any]:
166
- lateset = extract.latest()
 
167
  currency = extract.currency or "JPY"
168
- if not lateset:
169
- return{
170
- "tating":"E",
171
- "risk_score":0,
172
- "site_days":0,
173
- "transaction_limit_sisplay":_fmt_currency(0.0, currency),
174
- "review_cycle":"取引前入金(与信不可)",
 
175
  }
176
-
177
- score = _risk_score(lateset, ratios, policies)
178
- rationg = _ _rating(score)
179
 
180
- site_map = {"A":90,"B":60,"C":45,"D":30,"E":0}
181
- review_map ={"A":"年1回","B":"半年毎","C":"四半期毎","D":"月次","E":"取引毎"}
 
 
 
182
 
183
  # Transaction limit heuristic
184
  revenue = latest.revenue or 0.0
185
- working_capital = (lateset.current_assets or 0.0) - (latest.current_liabilities or 0.0)
186
- liquidity_factor = 0.4 +0.6 *min (1.0,max(0.0,(float(ratios.get("current_ratio",0)if ratios.get("current_ratio")!="-"else 0)/2.0)))
187
- risk_factor =0.5 + 0.5 *(score/100.0)
188
 
189
- #Base: monthly revenue × (site/30) × buffer
190
- site_days = site_map[ratig]
191
- base_limit = (revenue/12.0) * (site_days/30.0) * 0.6
192
  # Cap by working capital (can't exceed 40% of WC)
193
  cap = max(0.0, working_capital) * 0.4
194
- tx_limit = min(cap,base_limit * liquidity_factor * risk_factor)
195
  tx_limit = max(0.0, tx_limit)
196
 
197
- return{
198
- "rating":rating,
199
- "risk_score":site_days,
200
- "site_days":site_days,
201
- "transaction_limit_display":_fmt_currency(tx_limit, currency),
202
- "review_cycle":review_map[rating],
 
203
  }
204
 
205
- def _annuity_factor(rate:float, years: int) -> float:
206
- """Annual amortization fctor for an annuity loan, rate in %,years int."""
207
- r = rate /100.0
 
208
  n = max(1, years)
209
  if r <= 0:
210
- return 1.0/n
211
- return r/ (1-(1+r)**(-n))
 
212
 
213
- def loan_decision(extract: FinancialExtract, tatios: Dict[str, Any],base_rate_pct: float, policies: Dict[str, Any]) -> Dict[str, Any]:
214
  latest = extract.latest()
215
  currency = extract.currency or "JPY"
216
  if not latest:
217
- return {"ev":0.0, "ev_display":_fmt_currency(0.0, currency),
218
- "market_cap":0.0, "market_cap_display":_fmt_currency(0.0,currency),
219
- "recommended_chek_size":0.0, "recommended_check_size_display":_fmt_currency(0.0,currency),
220
- "attractiveness":1, "growth_label":"Low"}
221
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  # Growth label
223
- g = ratios.get("_revenue_growht_float")
224
  if g is None:
225
  glabel = "Unknown"
226
  elif g >= 0.25:
227
  glabel = "High"
228
- elif g >=0.05:
229
  glabel = "Medium"
230
  else:
231
  glabel = "Low"
232
-
233
  # Choose multiples
234
- ebitda = latest.ebitda if latest.ebitda is not None else (latest.ebit + (latest.depreciation or 0.0)if latest.ebit is not None else None)
235
  revenue = latest.revenue or 0.0
236
  total_debt = latest.total_debt or 0.0
237
  cash = latest.cash_and_equivalents or 0.0
@@ -240,18 +297,18 @@ def loan_decision(extract: FinancialExtract, tatios: Dict[str, Any],base_rate_pc
240
  if multiples and ebitda and ebitda > 0:
241
  ev = ebitda * multiples.ebitda_multiple
242
  elif ebitda and ebitda > 0:
243
- ev = ebitda * 8.0 #default
244
  else:
245
  # fallback to revenue multiple
246
  rev_mult = multiples.revenue_multiple if multiples else 1.5
247
  ev = revenue * rev_mult
248
 
249
- market_cap = max(0.0,ev - net_debt)
250
 
251
- #Check size heuristic: 5-15% of market cap depending or rating
252
- score = _ _risk_score(latest, ratios, policies)
253
  rating = _rating(score)
254
- pct = {"A":0.15,"B":0.12,"C":0.08,"D":0.04,"E":0.0}[rating]
255
  check = market_cap * pct
256
 
257
  # Attractiveness 1-5 based on score + growth
@@ -259,30 +316,31 @@ def loan_decision(extract: FinancialExtract, tatios: Dict[str, Any],base_rate_pc
259
  if rating == "A":
260
  attractiveness = 5 if glabel == "High" else 4
261
  elif rating == "B":
262
- attractiveness = 4 if glabel ! ="Low" else 3
263
  elif rating == "C":
264
- attractiveness = 4 if glabel == "High" else 2
265
  elif rating == "D":
266
  attractiveness = 1
267
 
268
- return{
269
- "ev":ev, "ev_display":_fmt_currency(ev, currency),
270
- "market_cap":market_cap, "market_cap_display":_fmt_currency(market_cap,currency),
271
- "recommended_check_size":check, "recommentded_check_size_display":_fmt_currency(check,currency),
272
- "attractiveness":attractiveness,
273
- "growth_label":glabel,
274
  }
275
 
276
- def build_report_dict(extract: FinancalExtract, ratios: Dict[str, Any], decisions:Dict[str, Any]) -> Dict[str, Any]:
277
- return{
278
- "metadata":{
279
- "company_name":extract.company_name,
280
- "industry":extract.industry,
281
- "currency":extract.currency,
282
- "fiscal_year_end":extract.fiscal_year_end,
 
283
  },
284
- "extracted":extract.dict()
285
- "ratios":ratios,
286
- "decisions":decisions,
287
- "disclaimer":"本ツールはAIによる推定・一般的な計算式に基づく参考提案であり、投資勧誘・融資約定・与信保証を目的としたものではありません。最終判断は自己責任で、必要に応じて専門家の確認を行ってください。"
288
- }
 
1
  # -*- coding: utf-8 -*-
2
  from __future__ import annotations
3
  import math
4
+ from typing import Dict, Any, Optional
5
  from dataclasses import dataclass
6
 
7
+ from schemas import FinancialExtract, ExtractedPeriod, MultipleSuggestion
8
 
9
+
10
+ def _fmt_currency(x: Optional[float], currency: str) -> str:
11
  if x is None:
12
+ return ""
13
+ try:
14
  # compact format
15
+ absx = abs(x)
16
+ if absx >= 1e12:
17
  return f"{x/1e12:.2f}兆{currency}"
18
+ if absx >= 1e8:
19
  return f"{x/1e8:.2f}億{currency}"
20
+ if absx >= 1e6:
21
  return f"{x/1e6:.2f}百万{currency}"
22
+ return f"{x:,.0f}{currency}"
23
  except Exception:
24
+ return f"{x} {currency}"
25
+
26
 
27
  def compute_ratios(extract: FinancialExtract) -> Dict[str, Any]:
28
+ """Compute derived metrics from the latest period (and prior if exists)."""
29
+ latest: Optional[ExtractedPeriod] = extract.latest()
30
+ prev: Optional[ExtractedPeriod] = extract.previous()
31
 
32
  currency = extract.currency or ""
33
 
 
37
  revenue = latest.revenue
38
  cogs = latest.cogs
39
  ebit = latest.ebit
40
+ ebitda = latest.ebitda if latest.ebitda is not None else (latest.ebit + (latest.depreciation or 0.0) if latest.ebit is not None else None)
41
  net_income = latest.net_income
42
  cash = latest.cash_and_equivalents
43
  ar = latest.accounts_receivable
44
+ inv = latest.inventory
45
+ ap = latest.accounts_payable
46
  ca = latest.current_assets
47
+ cl = latest.current_liabilities
48
  ta = latest.total_assets
49
  te = latest.total_equity
50
+ td = latest.total_debt
51
  interest = latest.interest_expense
52
 
53
  gross_profit = (revenue - cogs) if revenue is not None and cogs is not None else None
54
+ gross_margin = (gross_profit / revenue) if revenue and gross_profit is not None else None
55
+ ebit_margin = (ebit / revenue) if revenue and ebit is not None else None
56
+ ebitda_margin = (ebitda / revenue) if revenue and ebitda is not None else None
57
 
58
+ current_ratio = (ca / cl) if ca and cl else None
59
  quick_assets = (cash or 0.0) + (ar or 0.0)
60
+ quick_ratio = (quick_assets / cl) if quick_assets and cl else None
61
 
62
+ debt_to_equity = (td / te) if td is not None and te else None
63
+ interest_coverage = (ebit / interest) if ebit is not None and interest else None
64
 
65
  ratios.update({
66
+ "revenue": _fmt_currency(revenue, currency),
67
+ "ebit": _fmt_currency(ebit, currency),
68
+ "ebitda": _fmt_currency(ebitda, currency),
69
+ "net_income": _fmt_currency(net_income, currency),
70
+ "gross_margin_pct": f"{gross_margin*100:.1f}%" if gross_margin is not None else "",
71
+ "ebit_margin_pct": f"{ebit_margin*100:.1f}%" if ebit_margin is not None else "",
72
+ "ebitda_margin_pct": f"{ebitda_margin*100:.1f}%" if ebitda_margin is not None else "",
73
+ "current_ratio": f"{current_ratio:.2f}" if current_ratio is not None else "",
74
+ "quick_ratio": f"{quick_ratio:.2f}" if quick_ratio is not None else "",
75
+ "debt_to_equity": f"{debt_to_equity:.2f}" if debt_to_equity is not None else "—",
76
+ "interest_coverage": f"{interest_coverage:.2f}" if interest_coverage is not None else "—",
77
  })
78
 
79
+ if latest and prev and latest.revenue and prev.revenue and prev.revenue != 0:
80
+ growth = (latest.revenue - prev.revenue) / abs(prev.revenue)
81
  ratios["revenue_growth_pct"] = f"{growth*100:.1f}%"
82
  ratios["_revenue_growth_float"] = growth
83
  else:
84
+ ratios["revenue_growth_pct"] = ""
85
  ratios["_revenue_growth_float"] = None
86
 
87
  ratios["_latest_currency"] = extract.currency or "JPY"
88
  ratios["_latest_revenue"] = latest.revenue if latest else None
89
+ ratios["_latest_ebitda"] = (latest.ebitda if latest and latest.ebitda is not None else
90
+ (latest.ebit + (latest.depreciation or 0.0) if latest and latest.ebit is not None else None))
91
+ ratios["_latest_current_ratio"] = float(ratios["current_ratio"]) if isinstance(ratios.get("current_ratio"), str) and ratios["current_ratio"] not in ("",) else None
92
+ ratios["_latest_quick_ratio"] = float(ratios["quick_ratio"]) if isinstance(ratios.get("quick_ratio"), str) and ratios["quick_ratio"] not in ("",) else None
93
 
94
+ # Raw for computations
95
+ ratios["_latest"] = latest.dict() if latest else {}
96
  return ratios
97
 
98
+
99
+ def _risk_score(latest: ExtractedPeriod, ratios: Dict[str, Any], policies: Dict[str, Any]) -> float:
100
  """Simple additive score 0-100 based on policy thresholds."""
101
  score = 50.0
102
 
 
108
  score += 10
109
  elif cr >= 1.5:
110
  score += 6
111
+ elif cr >= 1.2:
112
+ score += 3
113
+ elif cr < 1.0:
114
  score -= 10
115
 
116
  # Leverage
 
121
  score += 8
122
  elif de <= 1.0:
123
  score += 4
124
+ elif de > 2.0:
125
  score -= 8
126
+
127
+ # Interest coverage
128
+ ebit, interest = latest.ebit, latest.interest_expense
129
  if ebit is not None and interest:
130
  ic = ebit / interest
131
  if ic >= 6:
 
134
  score += 6
135
  elif ic >= 1.5:
136
  score += 3
137
+ elif ic < 1.0:
138
+ score -= 12
139
 
140
  # Profitability
141
  revenue = latest.revenue
142
+ net = latest.net_income
143
  if revenue and net is not None:
144
+ nm = net / revenue
145
+ if nm >= 0.1:
146
+ score += 6
147
  elif nm >= 0.03:
148
  score += 3
149
+ elif nm < 0:
150
  score -= 8
151
+
152
+ # Growth
153
  g = ratios.get("_revenue_growth_float")
154
  if g is not None:
155
  if g >= 0.2:
156
  score += 6
157
  elif g >= 0.05:
158
+ score += 3
159
  elif g < 0:
160
  score -= 5
161
 
162
+ return max(0.0, min(100.0, score))
163
+
164
 
165
+ def _rating(score: float) -> str:
166
  if score >= 85: return "A"
167
  if score >= 70: return "B"
168
  if score >= 55: return "C"
169
  if score >= 40: return "D"
170
  return "E"
171
 
172
+
173
+ def credit_decision(extract: FinancialExtract, ratios: Dict[str, Any], policies: Dict[str, Any]) -> Dict[str, Any]:
174
+ latest = extract.latest()
175
  currency = extract.currency or "JPY"
176
+ if not latest:
177
+ return {
178
+ "rating": "E",
179
+ "risk_score": 0,
180
+ "site_days": 0,
181
+ "transaction_limit": 0.0,
182
+ "transaction_limit_display": _fmt_currency(0.0, currency),
183
+ "review_cycle": "取引前入金(与信不可)",
184
  }
 
 
 
185
 
186
+ score = _risk_score(latest, ratios, policies)
187
+ rating = _rating(score)
188
+
189
+ site_map = {"A": 90, "B": 60, "C": 45, "D": 30, "E": 0}
190
+ review_map = {"A": "年1回", "B": "半年毎", "C": "四半期毎", "D": "月次", "E": "取引ごと"}
191
 
192
  # Transaction limit heuristic
193
  revenue = latest.revenue or 0.0
194
+ working_capital = (latest.current_assets or 0.0) - (latest.current_liabilities or 0.0)
195
+ liquidity_factor = 0.4 + 0.6 * min(1.0, max(0.0, (float(ratios.get("current_ratio", 0) if ratios.get("current_ratio") != "" else 0) / 2.0)))
196
+ risk_factor = 0.5 + 0.5 * (score / 100.0)
197
 
198
+ # Base: monthly revenue × (site/30) × buffer
199
+ site_days = site_map[rating]
200
+ base_limit = (revenue / 12.0) * (site_days / 30.0) * 0.6
201
  # Cap by working capital (can't exceed 40% of WC)
202
  cap = max(0.0, working_capital) * 0.4
203
+ tx_limit = min(cap, base_limit * liquidity_factor * risk_factor)
204
  tx_limit = max(0.0, tx_limit)
205
 
206
+ return {
207
+ "rating": rating,
208
+ "risk_score": round(score, 1),
209
+ "site_days": site_days,
210
+ "transaction_limit": tx_limit,
211
+ "transaction_limit_display": _fmt_currency(tx_limit, currency),
212
+ "review_cycle": review_map[rating],
213
  }
214
 
215
+
216
+ def _annuity_factor(rate: float, years: int) -> float:
217
+ """Annual amortization factor for an annuity loan, rate in %, years int."""
218
+ r = rate / 100.0
219
  n = max(1, years)
220
  if r <= 0:
221
+ return 1.0 / n
222
+ return r / (1 - (1 + r) ** (-n))
223
+
224
 
225
+ def loan_decision(extract: FinancialExtract, ratios: Dict[str, Any], base_rate_pct: float, policies: Dict[str, Any]) -> Dict[str, Any]:
226
  latest = extract.latest()
227
  currency = extract.currency or "JPY"
228
  if not latest:
229
+ return {"max_principal": 0.0, "max_principal_display": _fmt_currency(0.0, currency),
230
+ "term_years": 0, "interest_rate_pct": base_rate_pct, "target_dscr": 0.0}
231
+
232
+ score = _risk_score(latest, ratios, policies)
233
+ rating = _rating(score)
234
+
235
+ # Risk premium by rating
236
+ rp = {"A": 0.5, "B": 1.0, "C": 2.0, "D": 3.5, "E": 0.0}[rating]
237
+ if rating == "E":
238
+ return {"max_principal": 0.0, "max_principal_display": _fmt_currency(0.0, currency),
239
+ "term_years": 0, "interest_rate_pct": 0.0, "target_dscr": 0.0}
240
+
241
+ interest_rate_pct = base_rate_pct + rp
242
+ term_years = {"A": 5, "B": 4, "C": 3, "D": 1}[rating]
243
+ target_dscr = {"A": 1.6, "B": 1.4, "C": 1.3, "D": 1.2}[rating]
244
+
245
+ ebitda = ratios.get("_latest_ebitda")
246
+ if not ebitda or ebitda <= 0:
247
+ return {"max_principal": 0.0, "max_principal_display": _fmt_currency(0.0, currency),
248
+ "term_years": term_years, "interest_rate_pct": interest_rate_pct, "target_dscr": target_dscr}
249
+
250
+ ann = _annuity_factor(interest_rate_pct, term_years)
251
+ annual_debt_service_cap = ebitda / target_dscr
252
+ max_principal = max(0.0, annual_debt_service_cap / ann)
253
+
254
+ # Cap by net tangible assets (if available): up to 50% of total assets - cash buffer
255
+ ta = latest.total_assets or 0.0
256
+ cash = latest.cash_and_equivalents or 0.0
257
+ nta_cap = max(0.0, (ta - cash) * 0.5)
258
+ max_principal = min(max_principal, nta_cap)
259
+
260
+ return {
261
+ "max_principal": max_principal,
262
+ "max_principal_display": _fmt_currency(max_principal, currency),
263
+ "term_years": term_years,
264
+ "interest_rate_pct": round(interest_rate_pct, 2),
265
+ "target_dscr": target_dscr,
266
+ }
267
+
268
+
269
+ def investment_decision(extract: FinancialExtract, ratios: Dict[str, Any], policies: Dict[str, Any],
270
+ multiples: Optional[MultipleSuggestion]) -> Dict[str, Any]:
271
+ latest = extract.latest()
272
+ currency = extract.currency or "JPY"
273
+ if not latest:
274
+ return {"ev": 0.0, "ev_display": _fmt_currency(0.0, currency),
275
+ "market_cap": 0.0, "market_cap_display": _fmt_currency(0.0, currency),
276
+ "recommended_check_size": 0.0, "recommended_check_size_display": _fmt_currency(0.0, currency),
277
+ "attractiveness": 1, "growth_label": "Low"}
278
+
279
  # Growth label
280
+ g = ratios.get("_revenue_growth_float")
281
  if g is None:
282
  glabel = "Unknown"
283
  elif g >= 0.25:
284
  glabel = "High"
285
+ elif g >= 0.05:
286
  glabel = "Medium"
287
  else:
288
  glabel = "Low"
289
+
290
  # Choose multiples
291
+ ebitda = latest.ebitda if latest.ebitda is not None else (latest.ebit + (latest.depreciation or 0.0) if latest.ebit is not None else None)
292
  revenue = latest.revenue or 0.0
293
  total_debt = latest.total_debt or 0.0
294
  cash = latest.cash_and_equivalents or 0.0
 
297
  if multiples and ebitda and ebitda > 0:
298
  ev = ebitda * multiples.ebitda_multiple
299
  elif ebitda and ebitda > 0:
300
+ ev = ebitda * 8.0 # default
301
  else:
302
  # fallback to revenue multiple
303
  rev_mult = multiples.revenue_multiple if multiples else 1.5
304
  ev = revenue * rev_mult
305
 
306
+ market_cap = max(0.0, ev - net_debt)
307
 
308
+ # Check size heuristic: 515% of market cap depending on rating
309
+ score = _risk_score(latest, ratios, policies)
310
  rating = _rating(score)
311
+ pct = {"A": 0.15, "B": 0.12, "C": 0.08, "D": 0.04, "E": 0.0}[rating]
312
  check = market_cap * pct
313
 
314
  # Attractiveness 1-5 based on score + growth
 
316
  if rating == "A":
317
  attractiveness = 5 if glabel == "High" else 4
318
  elif rating == "B":
319
+ attractiveness = 4 if glabel != "Low" else 3
320
  elif rating == "C":
321
+ attractiveness = 3 if glabel == "High" else 2
322
  elif rating == "D":
323
  attractiveness = 1
324
 
325
+ return {
326
+ "ev": ev, "ev_display": _fmt_currency(ev, currency),
327
+ "market_cap": market_cap, "market_cap_display": _fmt_currency(market_cap, currency),
328
+ "recommended_check_size": check, "recommended_check_size_display": _fmt_currency(check, currency),
329
+ "attractiveness": attractiveness,
330
+ "growth_label": glabel,
331
  }
332
 
333
+
334
+ def build_report_dict(extract: FinancialExtract, ratios: Dict[str, Any], decisions: Dict[str, Any]) -> Dict[str, Any]:
335
+ return {
336
+ "metadata": {
337
+ "company_name": extract.company_name,
338
+ "industry": extract.industry,
339
+ "currency": extract.currency,
340
+ "fiscal_year_end": extract.fiscal_year_end,
341
  },
342
+ "extracted": extract.dict(),
343
+ "ratios": ratios,
344
+ "decisions": decisions,
345
+ "disclaimer": "本ツールはAIによる推定・一般的な計算式に基づく参考提案であり、投資勧誘・融資約定・与信保証を目的としたものではありません。最終判断は自己責任で、必要に応じて専門家の確認を行ってください。",
346
+ }
llm_extract.py CHANGED
@@ -1,7 +1,7 @@
1
  # -*- coding: utf-8 -*-
2
  from __future__ import annotations
3
  import os
4
- import jason
5
  from typing import List, Optional
6
 
7
  from openai import OpenAI
@@ -9,79 +9,82 @@ from pydantic import ValidationError
9
 
10
  from schemas import FinancialExtract, MultipleSuggestion
11
 
12
- VISION_MODEL = os.environ.get("OPENAI_VISION_MODEL","gpt-5")
13
- TEXT_MODEL = os.environ.get("OPENAI_TEXT_MODEL", "gpt-5")
 
 
14
 
15
  def get_client() -> OpenAI:
16
  key = os.environ.get("OPENAI_API_KEY")
17
  if not key:
18
- raise RuntimeError("OPENAI_API_KEYが未設定です。 Spaces → Settings → Variable and secrets で追加してください。")
19
  return OpenAI(api_key=key, timeout=120)
20
 
21
- def upload_file_to_openai(xlient: OpenAI, filename: str, file_byte:bytes) -> str:
 
22
  """Upload file bytes to OpenAI Files API and return file_id."""
23
  from io import BytesIO
24
- bio = BytesIO(file_byte)
25
  # Prefer 'vision', fallback to 'assistants' for SDK compatibility
26
  try:
27
- f = client.siles.creare(file=("uploaded", bio),purpose="vision")
28
  except Exception:
29
- f = client.files.create(file=("uploaded",bio), purpose="assistants")
30
  return f.id
31
 
32
- def _financial_sxhema_json():
33
- #Pydantic -> JSON Schema
 
34
  return FinancialExtract.model_json_schema()
35
 
36
- def extract_financials_from_files(client: OpenAI, file_ids: List[str],company_hint:Optional[str],
37
- currency_hint: Optional[str], model:str = VISION_MODEL,
 
38
  debug: bool = False) -> FinancialExtract:
39
- """Use Vision model to read statements and produce normalized finacials(latest + previous)"""
40
  system = (
41
- " You are a meticulous financial analyst. Read the provided financial statement filise"
42
- "and extraxt the lateser two fiscal periods' key items with units normalized to the statement currency."
43
- "If multiple statements exist, prefer consolidated figures."
44
- "Return strictly valid JSON that conforms to the provided JSON schema."
45
- "IF any time is missing, set it to null rather than guessing."
46
  )
47
-
48
  user_text = (
49
- "Extraxt the company name, currency, fiscal year end, and the latest twp periods' numbers"
50
- "(revenue, COGS, EBIT, depreciation, EBITDA, net income, cash ,receivables, inventory, payable,"
51
- "current assets, current liabilities, total assets, total equity, total debt, expense)."
52
- "If both annual and quarterly are present, prefer annual."
53
- "Do not compute new numbers except EBITDA =EBIT + depreciation when both are available."
54
  )
55
  if company_hint:
56
  user_text += f"\nCompany hint: {company_hint}"
57
  if currency_hint:
58
- user_text += f"\nCurrency hint:{currency_hint}"
59
 
60
- #Using the Responses API with Structured Outputs JSON schema
61
  resp = client.responses.create(
62
  model=model,
63
  input=[
64
  {
65
- "role":"system"
66
- "content":[{"type_text","text":system}],
67
  },
68
  {
69
- "role":"user",
70
- "content":[{"type":"input_text","text":user_text}]+[
71
- {"type":"input_file","file_ids": fid} for fid in file_ids
72
  ],
73
  },
74
- ]
75
- response_fromat={
76
- "type":"json_schema",
77
- "json_schema":{
78
- "name":"FinancialExtract",
79
- "schema":_financial_schema_json(),
80
- "strict":True,
81
- },
82
  },
83
- max_output_tokens=2048,
84
-
85
  )
86
 
87
  try:
@@ -94,65 +97,66 @@ def extract_financials_from_files(client: OpenAI, file_ids: List[str],company_hi
94
 
95
  if not raw:
96
  raise RuntimeError("LLMからの応答が空でした。")
97
-
98
  try:
99
- data =json.loads(raw)
100
  except json.JSONDecodeError as e:
101
  if not debug:
102
  raw2 = raw.strip()
103
  if raw2.startswith("```"):
104
- raw2 = raw2.strip("`").split("\n",1)[-1]
105
- data = json.loads(raw2)
106
  else:
107
  raise
108
 
109
  try:
110
- extract = FinancialExtract.model_validate(date)
111
  except ValidationError as ve:
112
- raise RuntimeError(f"構造化出力の検証に失敗{ve}")
113
-
114
  return extract
115
 
116
- def suggest_multiples_with_llm(client: OpenAI, text_model:str, industry: str, region: str = "JP",debug: bool =False) -> Optional[MultipleSuggestion]:
117
- """Ask a small text model to propose resonable valuation multiples for the given industry/region."""
 
118
  if not industry:
119
  return None
120
-
121
- system = " You are an equity analyst. Provide conservative valuation multiple estimates for the given industry and region."
122
  user = (
123
- f"Industry:{industry}\nRegion: {region}\n"
124
- "Return a short JSON with keys: revenue_multiple(float),ebitda_multiple(float)."
125
  "Use public-market small/mid-cap ranges where applicable and be conservative."
126
  )
127
 
128
  resp = client.responses.create(
129
  model=text_model,
130
  input=[
131
- {"role":"system","content":[{"type":"input_text","text":system}]},
132
- {"role":"user","content":[{"type":"input_text","text":user}]},
133
  ],
134
  response_format={
135
- "type":"json_schema",
136
- "json_schema":{
137
- "name":"MultipleSuggestion",
138
- "schema":MultipleSuggestion.model_json_schema(),
139
- "strict":True,
140
  },
141
  },
142
  max_output_tokens=200,
143
  )
144
 
145
  try:
146
-
147
  raw = resp.output_text
148
  except Exception:
149
  try:
150
  raw = resp.output[0].content[0].text
151
  except Exception:
152
  raw = None
 
153
  if not raw:
154
  return None
155
-
156
  try:
157
  data = json.loads(raw)
158
  return MultipleSuggestion.model_validate(data)
@@ -160,4 +164,3 @@ def suggest_multiples_with_llm(client: OpenAI, text_model:str, industry: str, re
160
  if debug:
161
  raise
162
  return None
163
-
 
1
  # -*- coding: utf-8 -*-
2
  from __future__ import annotations
3
  import os
4
+ import json
5
  from typing import List, Optional
6
 
7
  from openai import OpenAI
 
9
 
10
  from schemas import FinancialExtract, MultipleSuggestion
11
 
12
+
13
+ VISION_MODEL = os.environ.get("OPENAI_VISION_MODEL", "gpt-4o-mini")
14
+ TEXT_MODEL = os.environ.get("OPENAI_TEXT_MODEL", "gpt-4o-mini")
15
+
16
 
17
  def get_client() -> OpenAI:
18
  key = os.environ.get("OPENAI_API_KEY")
19
  if not key:
20
+ raise RuntimeError("OPENAI_API_KEY が未設定です。Spaces → Settings → Variables and secrets で追加してください。")
21
  return OpenAI(api_key=key, timeout=120)
22
 
23
+
24
+ def upload_file_to_openai(client: OpenAI, filename: str, file_bytes: bytes) -> str:
25
  """Upload file bytes to OpenAI Files API and return file_id."""
26
  from io import BytesIO
27
+ bio = BytesIO(file_bytes)
28
  # Prefer 'vision', fallback to 'assistants' for SDK compatibility
29
  try:
30
+ f = client.files.create(file=("uploaded", bio), purpose="vision")
31
  except Exception:
32
+ f = client.files.create(file=("uploaded", bio), purpose="assistants")
33
  return f.id
34
 
35
+
36
+ def _financial_schema_json():
37
+ # Pydantic -> JSON Schema
38
  return FinancialExtract.model_json_schema()
39
 
40
+
41
+ def extract_financials_from_files(client: OpenAI, file_ids: List[str], company_hint: Optional[str],
42
+ currency_hint: Optional[str], model: str = VISION_MODEL,
43
  debug: bool = False) -> FinancialExtract:
44
+ """Use vision model to read statements and produce normalized financials (latest + previous)."""
45
  system = (
46
+ "You are a meticulous financial analyst. Read the provided financial statement files "
47
+ "and extract the latest two fiscal periods' key items with units normalized to the statement currency. "
48
+ "If multiple statements exist, prefer consolidated figures. "
49
+ "Return strictly valid JSON that conforms to the provided JSON schema. "
50
+ "If any item is missing, set it to null rather than guessing."
51
  )
 
52
  user_text = (
53
+ "Extract the company name, currency, fiscal year end, and the latest two periods' numbers "
54
+ "(revenue, COGS, EBIT, depreciation, EBITDA, net income, cash, receivables, inventory, payables, "
55
+ "current assets, current liabilities, total assets, total equity, total debt, interest expense). "
56
+ "If both annual and quarterly are present, prefer annual. "
57
+ "Do not compute new numbers except EBITDA = EBIT + depreciation when both are available."
58
  )
59
  if company_hint:
60
  user_text += f"\nCompany hint: {company_hint}"
61
  if currency_hint:
62
+ user_text += f"\nCurrency hint: {currency_hint}"
63
 
64
+ # Using the Responses API with Structured Outputs JSON schema
65
  resp = client.responses.create(
66
  model=model,
67
  input=[
68
  {
69
+ "role": "system",
70
+ "content": [{"type": "input_text", "text": system}],
71
  },
72
  {
73
+ "role": "user",
74
+ "content": [{"type": "input_text", "text": user_text}] + [
75
+ {"type": "input_file", "file_id": fid} for fid in file_ids
76
  ],
77
  },
78
+ ],
79
+ response_format={
80
+ "type": "json_schema",
81
+ "json_schema": {
82
+ "name": "FinancialExtract",
83
+ "schema": _financial_schema_json(),
84
+ "strict": True,
 
85
  },
86
+ },
87
+ max_output_tokens=2048,
88
  )
89
 
90
  try:
 
97
 
98
  if not raw:
99
  raise RuntimeError("LLMからの応答が空でした。")
100
+
101
  try:
102
+ data = json.loads(raw)
103
  except json.JSONDecodeError as e:
104
  if not debug:
105
  raw2 = raw.strip()
106
  if raw2.startswith("```"):
107
+ raw2 = raw2.strip("`").split("\n", 1)[-1]
108
+ data = json.loads(raw2)
109
  else:
110
  raise
111
 
112
  try:
113
+ extract = FinancialExtract.model_validate(data)
114
  except ValidationError as ve:
115
+ raise RuntimeError(f"構造化出力の検証に失敗: {ve}")
116
+
117
  return extract
118
 
119
+
120
+ def suggest_multiples_with_llm(client: OpenAI, text_model: str, industry: str, region: str = "JP", debug: bool = False) -> Optional[MultipleSuggestion]:
121
+ """Ask a small text model to propose reasonable valuation multiples for the given industry/region."""
122
  if not industry:
123
  return None
124
+
125
+ system = "You are an equity analyst. Provide conservative valuation multiple estimates for the given industry and region."
126
  user = (
127
+ f"Industry: {industry}\nRegion: {region}\n"
128
+ "Return a short JSON with keys: revenue_multiple (float), ebitda_multiple (float). "
129
  "Use public-market small/mid-cap ranges where applicable and be conservative."
130
  )
131
 
132
  resp = client.responses.create(
133
  model=text_model,
134
  input=[
135
+ {"role": "system", "content": [{"type": "input_text", "text": system}]},
136
+ {"role": "user", "content": [{"type": "input_text", "text": user}]},
137
  ],
138
  response_format={
139
+ "type": "json_schema",
140
+ "json_schema": {
141
+ "name": "MultipleSuggestion",
142
+ "schema": MultipleSuggestion.model_json_schema(),
143
+ "strict": True,
144
  },
145
  },
146
  max_output_tokens=200,
147
  )
148
 
149
  try:
 
150
  raw = resp.output_text
151
  except Exception:
152
  try:
153
  raw = resp.output[0].content[0].text
154
  except Exception:
155
  raw = None
156
+
157
  if not raw:
158
  return None
159
+
160
  try:
161
  data = json.loads(raw)
162
  return MultipleSuggestion.model_validate(data)
 
164
  if debug:
165
  raise
166
  return None
 
schemas.py CHANGED
@@ -3,9 +3,10 @@ from __future__ import annotations
3
  from typing import Optional, List
4
  from pydantic import BaseModel, Field
5
 
 
6
  class ExtractedPeriod(BaseModel):
7
- period_label: Optional[str] = Field(None, description = "e.g., FY2024,FY2023" "")
8
- revenue:Optional[float] = None
9
  cogs: Optional[float] = None
10
  ebit: Optional[float] = None
11
  depreciation: Optional[float] = None
@@ -14,28 +15,35 @@ class ExtractedPeriod(BaseModel):
14
 
15
  cash_and_equivalents: Optional[float] = None
16
  accounts_receivable: Optional[float] = None
17
- total_assests: Optional[float] = None
 
 
 
 
 
18
  total_equity: Optional[float] = None
19
  total_debt: Optional[float] = None
20
- interest_expense:Optional[float] = None
 
21
 
22
  class FinancialExtract(BaseModel):
23
  company_name: Optional[str] = None
24
  industry: Optional[str] = None
25
- currency: Optional[str] = None
26
- fiscal_year_end: Optional[str] = None
27
- period: List[ExtractedPeriod]
28
 
29
  def latest(self) -> Optional[ExtractedPeriod]:
30
  if not self.periods:
31
  return None
32
- return self.period[0]
33
-
34
- def prebious(self) -> Optional[ExtractedPeriod]:
35
  if not self.periods or len(self.periods) < 2:
36
  return None
37
- return self.period[1]
38
-
 
39
  class MultipleSuggestion(BaseModel):
40
  revenue_multiple: float = 1.5
41
- ebitda_multiple: float =8.0
 
3
  from typing import Optional, List
4
  from pydantic import BaseModel, Field
5
 
6
+
7
  class ExtractedPeriod(BaseModel):
8
+ period_label: Optional[str] = Field(None, description="e.g., FY2024, FY2023")
9
+ revenue: Optional[float] = None
10
  cogs: Optional[float] = None
11
  ebit: Optional[float] = None
12
  depreciation: Optional[float] = None
 
15
 
16
  cash_and_equivalents: Optional[float] = None
17
  accounts_receivable: Optional[float] = None
18
+ inventory: Optional[float] = None
19
+ accounts_payable: Optional[float] = None
20
+
21
+ current_assets: Optional[float] = None
22
+ current_liabilities: Optional[float] = None
23
+ total_assets: Optional[float] = None
24
  total_equity: Optional[float] = None
25
  total_debt: Optional[float] = None
26
+ interest_expense: Optional[float] = None
27
+
28
 
29
  class FinancialExtract(BaseModel):
30
  company_name: Optional[str] = None
31
  industry: Optional[str] = None
32
+ currency: Optional[str] = None # e.g., JPY, USD
33
+ fiscal_year_end: Optional[str] = None # YYYY-MM-DD if known
34
+ periods: List[ExtractedPeriod]
35
 
36
  def latest(self) -> Optional[ExtractedPeriod]:
37
  if not self.periods:
38
  return None
39
+ return self.periods[0]
40
+
41
+ def previous(self) -> Optional[ExtractedPeriod]:
42
  if not self.periods or len(self.periods) < 2:
43
  return None
44
+ return self.periods[1]
45
+
46
+
47
  class MultipleSuggestion(BaseModel):
48
  revenue_multiple: float = 1.5
49
+ ebitda_multiple: float = 8.0