| |
| from __future__ import annotations |
| import json |
| from typing import Dict, Any, List, Tuple |
|
|
| import gradio as gr |
| import plotly.graph_objects as go |
| import pandas as pd |
|
|
| from core.extract import parse_pdf |
| from core.market_infer import infer_market_metrics |
| from core.external_scoring import ( |
| get_external_template_df, |
| fill_missing_with_external, |
| merge_market_into_external_df, |
| score_external_from_df, |
| ) |
| from core.ai_judgement import suggest_external_with_llm, ai_evaluate |
|
|
| |
| def _radar(title: str, cat_scores: Dict[str, float]) -> go.Figure: |
| if not cat_scores: |
| cat_scores = {"N/A": 0.0} |
| labels = list(cat_scores.keys()) |
| vals = [float(cat_scores[k] or 0.0) for k in labels] |
| fig = go.Figure() |
| fig.add_trace(go.Scatterpolar(r=vals + [vals[0]], theta=labels + [labels[0]], fill="toself", name=title)) |
| fig.update_layout(polar=dict(radialaxis=dict(visible=True, range=[0, 100])), |
| showlegend=False, height=360, margin=dict(l=30, r=30, t=40, b=30), title=title) |
| return fig |
|
|
| def _diff_bar(ext: Dict[str, float], ai: Dict[str, float]) -> go.Figure: |
| ks = sorted(set(ext.keys()) | set(ai.keys())) |
| diffs = [(float(ai.get(k, 0.0)) - float(ext.get(k, 0.0))) for k in ks] |
| fig = go.Figure(data=[go.Bar(x=ks, y=diffs)]) |
| fig.update_layout(title="AI評点 - 外部評価(カテゴリ差分)", |
| height=320, margin=dict(l=30, r=30, t=40, b=30)) |
| return fig |
|
|
| def _fmt(x): |
| try: |
| f = float(x) |
| if abs(f) >= 1e8: return f"{f/1e8:.2f}億" |
| if abs(f) >= 1e6: return f"{f/1e6:.2f}百万円" |
| if abs(f) >= 1e3: return f"{f/1e3:.1f}千" |
| return f"{f:.0f}" |
| except Exception: |
| return str(x) if x not in (None, "") else "—" |
|
|
| def _cards(company, meta, fin, ext_total, ai_total) -> str: |
| bs = fin.get("balance_sheet", {}) or {}; is_ = fin.get("income_statement", {}) or {} |
| ta = bs.get("total_assets") or 0; te = bs.get("total_equity") or 0 |
| er = "—" |
| try: |
| ta = float(ta); te = float(te) |
| er = f"{(te/ta*100):.1f}%" if ta>0 else "—" |
| except Exception: |
| pass |
| period = "" |
| if meta and isinstance(meta.get("period"), dict): |
| period = f"{meta['period'].get('start_date','')} ~ {meta['period'].get('end_date','')}" |
| unit = (meta.get("unit") or "円").replace("JPY","円") |
| return f""" |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;"> |
| <div style="background:#F8FAFF;border:1px solid #E5E7EB;border-radius:12px;padding:12px;"> |
| <div style="font-size:12px;color:#6B7280;">企業名</div> |
| <div style="font-size:18px;font-weight:700;">{company or fin.get('company',{}).get('name','—')}</div> |
| <div style="font-size:12px;color:#6B7280;margin-top:4px;">期間: {period or '—'} / 単位: {unit}</div> |
| </div> |
| <div style="background:#F8FAFF;border:1px solid #E5E7EB;border-radius:12px;padding:12px;"> |
| <div style="font-size:12px;color:#6B7280;">売上高</div> |
| <div style="font-size:18px;font-weight:700;">{_fmt(is_.get('sales'))}</div> |
| </div> |
| <div style="background:#F8FAFF;border:1px solid #E5E7EB;border-radius:12px;padding:12px;"> |
| <div style="font-size:12px;color:#6B7280;">営業利益</div> |
| <div style="font-size:18px;font-weight:700;">{_fmt(is_.get('operating_income'))}</div> |
| </div> |
| <div style="background:#F8FAFF;border:1px solid #E5E7EB;border-radius:12px;padding:12px;"> |
| <div style="font-size:12px;color:#6B7280;">自己資本比率</div> |
| <div style="font-size:18px;font-weight:700;">{er}</div> |
| </div> |
| <div style="background:#F0FDF4;border:1px solid #DCFCE7;border-radius:12px;padding:12px;"> |
| <div style="font-size:12px;color:#047857;">外部評価(定量)</div> |
| <div style="font-size:22px;font-weight:800;color:#065F46;">{ext_total:.1f}</div> |
| </div> |
| <div style="background:#EFF6FF;border:1px solid #DBEAFE;border-radius:12px;padding:12px;"> |
| <div style="font-size:12px;color:#1D4ED8;">AI評点</div> |
| <div style="font-size:22px;font-weight:800;color:#1E40AF;">{ai_total:.1f}</div> |
| </div> |
| </div> |
| """ |
|
|
| |
| def _market_df_from_dict(d: Dict[str, Any]) -> pd.DataFrame: |
| rows = [] |
| order = [ |
| "市場の年成長率(%)","市場成熟度(0-1)","競争強度(0-10)","参入障壁(0-10)","価格決定力(0-10)", |
| "サイクル感応度(0-10)","規制リスク(0-10)","技術破壊リスク(0-10)","TAM_億円","SAM_億円","SOM_億円" |
| ] |
| for k in order: |
| rows.append([k, d.get(k,"")]) |
| return pd.DataFrame(rows, columns=["指標","値"]) |
|
|
| def _dict_from_market_df(df: pd.DataFrame) -> Dict[str, Any]: |
| out = {} |
| try: |
| for _, r in df.iterrows(): |
| k = str(r["指標"]); v = r["値"] |
| try: |
| out[k] = float(v) |
| except Exception: |
| out[k] = None |
| except Exception: |
| |
| return {} |
| return out |
|
|
| |
| def on_analyze(company: str, use_vision: bool, files: List[str]): |
| """ |
| 例外はcatchしてUIに出す。常に所定の型で返す。 |
| """ |
| try: |
| if not files: |
| raise RuntimeError("PDF をアップロードしてください。") |
| fin, df_fin, meta, log = parse_pdf(files, company, use_vision) |
|
|
| |
| ext_df = get_external_template_df() |
| ext_df = fill_missing_with_external(ext_df, suggest_external_with_llm(fin, company)) |
|
|
| |
| market_df = _market_df_from_dict({}) |
| ext_res = score_external_from_df(ext_df) |
| ai_res = ai_evaluate(fin, {}) |
|
|
| cards = _cards(company, meta, fin, ext_res["external_total"], ai_res["ai_total"]) |
| ext_fig = _radar("外部評価(カテゴリ)", ext_res.get("category_scores", {})) |
| ai_fig = _radar("AI評点(カテゴリ)", ai_res.get("category_scores", {})) |
| diff = _diff_bar(ext_res.get("category_scores", {}), ai_res.get("category_scores", {})) |
|
|
| return (cards, df_fin, ext_df, market_df, |
| float(ext_res["external_total"]), float(ai_res["ai_total"]), |
| ext_fig, ai_fig, diff, |
| json.dumps(fin, ensure_ascii=False, indent=2), |
| json.dumps(ext_res, ensure_ascii=False, indent=2), |
| json.dumps(ai_res, ensure_ascii=False, indent=2), |
| "\n".join([str(x) for x in (log if isinstance(log,list) else [log])])) |
| except Exception as e: |
| |
| empty_df = pd.DataFrame(columns=["カテゴリー","入力項目","値"]) |
| return ( |
| f"<div style='color:#b91c1c'>解析に失敗: {e}</div>", |
| pd.DataFrame(columns=["category","item","value"]), |
| empty_df, |
| _market_df_from_dict({}), |
| 0.0, 0.0, |
| _radar("外部評価(カテゴリ)", {}), |
| _radar("AI評点(カテゴリ)", {}), |
| _diff_bar({}, {}), |
| "{}", "{}","{}", |
| f"TRACE: {type(e).__name__}: {e}" |
| ) |
|
|
| def on_market_infer(industry: str, products_text: str, country: str, horizon: int, |
| ext_df: pd.DataFrame, fin_json: str): |
| try: |
| prods = [p.strip() for p in (products_text or "").splitlines() if p.strip()] |
| market = infer_market_metrics(industry or "", prods, country or "JP", int(horizon or 3)) |
| market_df = _market_df_from_dict(market) |
|
|
| |
| ext_df2 = merge_market_into_external_df(ext_df, market, prods) |
|
|
| |
| fin = json.loads(fin_json or "{}") |
| ext_res = score_external_from_df(ext_df2) |
|
|
| ext_like = { |
| "市場の年成長率(%)": market.get("市場の年成長率(%)"), |
| "主力商品数": len(prods), |
| "成長中主力商品数": sum(1 for p in prods if (market.get("製品別年成長率(%)",{}).get(p,0) or 0)>10) |
| } |
| ai_res = ai_evaluate(fin, ext_like) |
|
|
| ext_fig = _radar("外部評価(カテゴリ)", ext_res.get("category_scores", {})) |
| ai_fig = _radar("AI評点(カテゴリ)", ai_res.get("category_scores", {})) |
| diff = _diff_bar(ext_res.get("category_scores", {}), ai_res.get("category_scores", {})) |
|
|
| return (market_df, ext_df2, |
| float(ext_res["external_total"]), float(ai_res["ai_total"]), |
| ext_fig, ai_fig, diff, |
| json.dumps(ext_res, ensure_ascii=False, indent=2), |
| json.dumps(ai_res, ensure_ascii=False, indent=2), |
| "市場推定OK: " + "; ".join(market.get("注記", [])[:3])) |
| except Exception as e: |
| return (_market_df_from_dict({}), |
| ext_df, |
| 0.0, 0.0, |
| _radar("外部評価(カテゴリ)", {}), |
| _radar("AI評点(カテゴリ)", {}), |
| _diff_bar({}, {}), |
| "{}", "{}", f"市場推定に失敗: {e}") |
|
|
| def on_rescore_all(ext_df: pd.DataFrame, market_df: pd.DataFrame, fin_json: str, products_text: str): |
| try: |
| fin = json.loads(fin_json or "{}") |
| prods = [p.strip() for p in (products_text or "").splitlines() if p.strip()] |
| market = _dict_from_market_df(market_df) |
| ext_df2 = merge_market_into_external_df(ext_df, market, prods) |
|
|
| ext_res = score_external_from_df(ext_df2) |
| ext_like = { |
| "市場の年成長率(%)": market.get("市場の年成長率(%)"), |
| "主力商品数": len(prods), |
| "成長中主力商品数": sum(1 for p in prods if (market.get("製品別年成長率(%)",{}).get(p,0) or 0)>10) |
| } |
| ai_res = ai_evaluate(fin, ext_like) |
|
|
| ext_fig = _radar("外部評価(カテゴリ)", ext_res.get("category_scores", {})) |
| ai_fig = _radar("AI評点(カテゴリ)", ai_res.get("category_scores", {})) |
| diff = _diff_bar(ext_res.get("category_scores", {}), ai_res.get("category_scores", {})) |
|
|
| return (ext_df2, |
| float(ext_res["external_total"]), float(ai_res["ai_total"]), |
| ext_fig, ai_fig, diff, |
| json.dumps(ext_res, ensure_ascii=False, indent=2), |
| json.dumps(ai_res, ensure_ascii=False, indent=2), |
| "再計算完了") |
| except Exception as e: |
| return (ext_df, 0.0, 0.0, |
| _radar("外部評価(カテゴリ)", {}), |
| _radar("AI評点(カテゴリ)", {}), |
| _diff_bar({}, {}), |
| "{}", "{}", f"再計算に失敗: {e}") |
|
|
| def build_ui(): |
| with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo"), analytics_enabled=False) as demo: |
| gr.Markdown("## 🧮 企業スコアリング:PDF抽出 × 市場推定(LLM)× 外部定量 × AI評点") |
|
|
| with gr.Row(): |
| with gr.Column(scale=1): |
| company = gr.Textbox(label="企業名(任意)") |
| use_vision = gr.Checkbox(value=True, label="OpenAI VisionでPDF表を補完") |
| files = gr.File(label="決算書PDF(複数可)", file_count="multiple", type="filepath") |
| run_btn = gr.Button("📄 PDFを解析", variant="primary") |
| with gr.Column(scale=2): |
| cards = gr.HTML(label="サマリー") |
|
|
| with gr.Tab("入力/市場推定"): |
| with gr.Row(): |
| industry = gr.Textbox(label="事業領域(業界・カテゴリ)", placeholder="例)ヘルスケアIT / 産業ロボット 等") |
| products = gr.Textbox(label="主力商品(1行1件)", lines=4, placeholder="製品A\n製品B\n…") |
| with gr.Row(): |
| country = gr.Dropdown(choices=["JP","US","EU","APAC","GLOBAL"], value="JP", label="対象地域") |
| horizon = gr.Slider(1, 7, value=3, step=1, label="予測年数") |
| infer_btn = gr.Button("🔎 市場を推定(LLM)", variant="secondary") |
| market_df = gr.Dataframe(label="市場メトリクス(編集可)", interactive=True, wrap=True) |
|
|
| with gr.Tab("外部入力/財務"): |
| df_fin = gr.Dataframe(label="抽出テーブル(編集可)", interactive=True, wrap=True) |
| ext_df = gr.Dataframe(label="外部入力(編集可)", interactive=True, wrap=True) |
|
|
| with gr.Tab("スコア"): |
| with gr.Row(): |
| ext_total = gr.Number(label="外部評価 合計(0-100)", value=0.0, precision=1, interactive=False) |
| ai_total = gr.Number(label="AI評点 合計(0-100)", value=0.0, precision=1, interactive=False) |
| with gr.Row(): |
| ext_plot = gr.Plot(label="外部評価(レーダー)") |
| ai_plot = gr.Plot(label="AI評点(レーダー)") |
| diff_plot = gr.Plot(label="差分(棒)") |
| rescore_btn = gr.Button("🔁 すべて再計算", variant="secondary") |
|
|
| with gr.Tab("詳細"): |
| fin_json = gr.Code(label="抽出JSON", language="json") |
| ext_json = gr.Code(label="外部評価JSON", language="json") |
| ai_json = gr.Code(label="AI評点JSON", language="json") |
| debug = gr.Textbox(label="ログ", lines=8) |
|
|
| |
| fin_state = gr.State("") |
|
|
| |
| run_btn.click( |
| on_analyze, |
| inputs=[company, use_vision, files], |
| outputs=[cards, df_fin, ext_df, market_df, ext_total, ai_total, |
| ext_plot, ai_plot, diff_plot, fin_json, ext_json, ai_json, debug], |
| ).then(lambda x: x, inputs=[fin_json], outputs=[fin_state]) |
|
|
| infer_btn.click( |
| on_market_infer, |
| inputs=[industry, products, country, horizon, ext_df, fin_state], |
| outputs=[market_df, ext_df, ext_total, ai_total, ext_plot, ai_plot, diff_plot, ext_json, ai_json, debug], |
| ) |
|
|
| rescore_btn.click( |
| on_rescore_all, |
| inputs=[ext_df, market_df, fin_state, products], |
| outputs=[ext_df, ext_total, ai_total, ext_plot, ai_plot, diff_plot, ext_json, ai_json, debug], |
| ) |
| return demo |
|
|