| | |
| | models_code = """ |
| | from pydantic import BaseModel,Field |
| | from typing import List,Optional,Dict,Any |
| | |
| | class CompanyMeta(BaseModel): |
| | company_name:str |
| | fiscal_year:int |
| | currency:str="JPY" |
| | ticker:Optional[str] = None |
| | report_title:str = "Integrated Report" |
| | ceo_name:Optional[str] = None |
| | material_topics:List[str]=[] |
| | targets:Dict[str,Any]={} |
| | |
| | class ESGRow(BaseModel): |
| | year:int |
| | metric:str |
| | value:float |
| | unit:str |
| | scope:Optional[str]=None |
| | notes:Optional[str]=None |
| | |
| | class ReportSections(BaseModel): |
| | ceo_message:str = "" |
| | risl_opportunity:str = "" |
| | |
| | class RenderPayload(BaseModel): |
| | meta:CompanyMeta |
| | esg_table:List[Dict[str,Any]] |
| | kpi:Dict[str,float] |
| | sections:ReportSections |
| | generated_at:str |
| | """ |
| | with open("models.py", "w", encoding="utf-8") as f: |
| | f.write(models_code) |
| |
|
| | |
| | templating_code = """ |
| | from jinja2 import Environment,FileSystemLoader,select_autoescape |
| | from pathlib import Path |
| | |
| | def get_env(templates_dir:str): |
| | env = Environment( |
| | loader = FileSystemLoader(templates_dir), |
| | autoescape=select_autoescape(["html","xml"]) |
| | ) |
| | return env |
| | |
| | def render(env,template_name: str,context :dict)->str: |
| | template = env.get_template(template_name) |
| | return template.render(**context) |
| | """ |
| | with open("templating.py", "w", encoding="utf-8") as f: |
| | f.write(templating_code) |
| |
|
| | |
| | render_code = """ |
| | # from weasyprint import HTML # Commenting out due to potential dependency issues |
| | from docx import Document |
| | from docx.shared import Pt |
| | from bs4 import BeautifulSoup |
| | |
| | def html_to_pdf(html_str:str,out_pdf_path:str): |
| | # HTML(string=html_str).write_pdf(out_pdf_path) # Commenting out due to potential dependency issues |
| | print("PDF generation skipped due to weasyprint dependency") |
| | |
| | |
| | def html_to_docx(html_str:str,out_docx_path:str): |
| | doc = Document() |
| | soup = BeautifulSoup(html_str,"html.parser") |
| | for tag in soup.find_all(["h1","h2","h3","p","li"]): |
| | text = tag.get_text(strip=True) |
| | if not text: |
| | continue |
| | if tag.name =="h1": |
| | p = doc.add_heading(text,level=0) |
| | elif tag.name=="h2": |
| | p=doc.add_heading(text,level=1) |
| | elif tag.name=="h3": |
| | p=doc.add_heading(text,level=2) |
| | else: |
| | p= doc.add_paragraph(text) |
| | for run in p.runs: |
| | run.font.size = Pt(11) |
| | doc.save(out_docx_path) |
| | """ |
| | with open("render.py", "w", encoding="utf-8") as f: |
| | f.write(render_code) |
| |
|
| | |
| | core_code = """ |
| | import pandas as pd |
| | import yaml, datetime |
| | from pathlib import Path |
| | from templating import get_env,render |
| | from models import CompanyMeta, ReportSections,RenderPayload |
| | from render import html_to_pdf,html_to_docx |
| | |
| | DISPLAY_NAME={ |
| | "co2_emissions":"CO2排出量", |
| | "energy_renewable_ratio":"再生可能エネルギー比率", |
| | "female_management_ratio":"女性管理職比率" |
| | } |
| | def load_company_meta(path:str)->CompanyMeta: |
| | with open(path, 'r', encoding='utf-8') as f: |
| | data = yaml.safe_load(f) |
| | return CompanyMeta(**data) |
| | |
| | def load_financials(path:str)->pd.DataFrame: |
| | df = pd.read_csv(path) |
| | return df |
| | |
| | def load_esg(path:str)->pd.DataFrame: |
| | df = pd.read_csv(path) |
| | return df |
| | |
| | def compute_kpi(fin_df:pd.DataFrame, fiscal_year:int): |
| | latest = fin_df[fin_df["year"] == fiscal_year].sort_values("quarter").tail(1) |
| | prev = fin_df[fin_df["year"] == fiscal_year - 1].sort_values("quarter").tail(1) |
| | |
| | revenue = float(latest["revenue"].iloc[0]) if not latest.empty and "revenue" in latest.columns else 0.0 |
| | operating_income = float(latest["operating_income"].iloc[0]) if not latest.empty and "operating_income" in latest.columns else 0.0 |
| | ebit = float(latest["ebit"].iloc[0]) if not latest.empty and "ebit" in latest.columns else 0.0 |
| | net_income = float(latest["net_income"].iloc[0]) if not latest.empty and "net_income" in latest.columns else 0.0 |
| | equity = float(latest["equity"].iloc[0]) if not latest.empty and "equity" in latest.columns else 0.0 # Assuming 'equity' is a column in financials |
| | |
| | ebit_margin = ebit / revenue * 100 if revenue else 0.0 |
| | roe = net_income / equity * 100 if equity else 0.0 |
| | revenue_yoy = ((revenue / float(prev["revenue"].iloc[0])) - 1) * 100 if not prev.empty and "revenue" in prev.columns else 0.0 |
| | |
| | return { |
| | "revenue": revenue, |
| | "operating_income": operating_income, |
| | "ebit": ebit, |
| | "net_income": net_income, |
| | "ebit_margin": ebit_margin, |
| | "roe": roe, |
| | "revenue_yoy": revenue_yoy, |
| | } |
| | |
| | def esg_table(df:pd.DataFrame, fiscal_year:int): |
| | dfy = df[df["year"]==fiscal_year].copy() |
| | rows = [] |
| | for _,r in dfy.iterrows(): |
| | display = DISPLAY_NAME.get(r["metric"],r["metric"]) |
| | rows.append({ |
| | "display":display, |
| | "value":r["value"], |
| | "unit":r.get("unit",""), |
| | "scope":r.get("scope",""), |
| | "notes":r.get("notes","") |
| | }) |
| | return rows |
| | |
| | def build_sections(meta:CompanyMeta,kpi:dict,esg_rows:list,llm=None)->ReportSections: |
| | if llm: |
| | ceo_message = llm.generate_ceo_message(meta,kpi,esg_rows) |
| | risk=llm.generate_risk_opportunity(meta,kpi,esg_rows) |
| | else: |
| | ceo_message =f"{meta.fiscal_year}期は、売り上げ成長と収益性の両立に注力しました。今後も中長期的な企業価値向上を目指します。" |
| | risk="主要リスクはマクロ環境と規制動向。機会は生成AI活用と脱炭素需要の拡大です。" |
| | return ReportSections(ceo_message=ceo_message,risk_opportunity=risk) |
| | |
| | def generate_report( |
| | company_yaml:str,financials_csv:str,esg_csv:str, |
| | template_dir:str,template_name:str ="report.html.j2", |
| | out_html="output/report.html",out_pdf="output/report.pdf",out_docx="output/report.docx", |
| | llm=None, |
| | ): |
| | Path("output").mkdir(exist_ok=True) |
| | meta = load_company_meta(company_yaml) |
| | fin =load_financials(financials_csv) |
| | esg= load_esg(esg_csv) |
| | |
| | kpi = compute_kpi(fin,meta.fiscal_year) |
| | esg_rows = esg_table(esg,meta.fiscal_year) |
| | sections = build_sections(meta,kpi,esg_rows,llm=llm) |
| | |
| | env=get_env(template_dir) |
| | payload = RenderPayload( |
| | meta=meta,esg_table=esg_rows,kpi=kpi,sections=sections,generated_at=datetime.datetime.now().strftime("%Y-%m-%d %H:%M") |
| | ).model_dump() |
| | |
| | html = render(env,template_name,payload) |
| | Path(out_html).write_text(html,encoding="utf-8") |
| | # html_to_pdf(html,out_pdf) # Commenting out due to potential weasyprint dependency issues |
| | html_to_docx(html,out_docx) |
| | return out_html,out_pdf,out_docx |
| | """ |
| | with open("core.py", "w", encoding="utf-8") as f: |
| | f.write(core_code) |
| |
|
| | |
| | main_code = """ |
| | from fastapi import FastAPI, UploadFile, Form |
| | from fastapi.responses import FileResponse |
| | from pathlib import Path |
| | import tempfile, shutil |
| | from core import generate_report |
| | |
| | app = FastAPI() |
| | |
| | @app.post("/generate") |
| | async def generate(company: UploadFile, financials: UploadFile, esg: UploadFile): |
| | with tempfile.TemporaryDirectory() as td: |
| | cpath = Path(td)/"company.yaml"; cpath.write_bytes(await company.read()) |
| | fpath = Path(td)/"financials.csv"; fpath.write_bytes(await financials.read()) |
| | epath = Path(td)/"esg.csv"; epath.write_bytes(await esg.read()) |
| | html, pdf, docx = generate_report( |
| | str(cpath), str(fpath), str(epath), templates_dir="app/templates" |
| | ) |
| | return {"html": html, "pdf": pdf, "docx": docx} |
| | |
| | # uvicorn app.main:app --reload |
| | """ |
| | with open("app/main.py", "w", encoding="utf-8") as f: |
| | f.write(main_code) |
| |
|
| | |
| | html_template_content = """ |
| | <!doctype html> |
| | <html lang="ja"> |
| | <head> |
| | <meta charset="utf-8" /> |
| | <title>{{ meta.report_title }} - {{ meta.company_name }}</title> |
| | <style> |
| | body { font-family: system-ui, -apple-system, "Segoe UI", Helvetica, Arial; line-height: 1.6; } |
| | h1,h2,h3 { margin: 0.6em 0; } |
| | .kpi { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } |
| | .card { border: 1px solid #ddd; border-radius: 12px; padding: 12px; } |
| | .small { color: #666; font-size: 0.9em; } |
| | table { border-collapse: collapse; width: 100%; } |
| | th, td { border: 1px solid #eee; padding: 8px; text-align: right; } |
| | th { background: #fafafa; } |
| | .left { text-align: left; } |
| | </style> |
| | </head> |
| | <body> |
| | <h1>{{ meta.report_title }}({{ meta.fiscal_year }})</h1> |
| | <p class="small">{{ meta.company_name }} / Ticker: {{ meta.ticker }} / 通貨: {{ meta.currency }}</p> |
| | |
| | <h2>CEOメッセージ</h2> |
| | <p>{{ sections.ceo_message }}</p> |
| | |
| | <h2>ハイライトKPI</h2> |
| | <div class="kpi"> |
| | <div class="card"> |
| | <div class="small">売上高</div> |
| | <div><strong>{{ kpi.revenue | round(0) | int }} {{ meta.currency }}</strong></div> |
| | <div class="small">前年比: {{ kpi.revenue_yoy | round(1) }}%</div> |
| | </div> |
| | <div class="card"> |
| | <div class="small">EBIT</div> |
| | <div><strong>{{ kpi.ebit | round(0) | int }} {{ meta.currency }}</strong></div> |
| | <div class="small">マージン: {{ kpi.ebit_margin | round(1) }}%</div> |
| | </div> |
| | <div class="card"> |
| | <div class="small">純利益</div> |
| | <div><strong>{{ kpi.net_income | round(0) | int }} {{ meta.currency }}</strong></div> |
| | <div class="small">ROE: {{ kpi.roe | round(1) }}%</div> |
| | </div> |
| | </div> |
| | |
| | <h2>ESGサマリー</h2> |
| | <table> |
| | <thead> |
| | <tr><th class="left">指標</th><th>値</th><th>単位</th><th class="left">備考</th></tr> |
| | </thead> |
| | <tbody> |
| | {% for row in esg_table %} |
| | <tr> |
| | <td class="left">{{ row.display }}</td> |
| | <td>{{ row.value }}</td> |
| | <td>{{ row.unit }}</td> |
| | <td class="left">{{ row.notes }}</td> |
| | </tr> |
| | {% endfor %} |
| | </tbody> |
| | </table> |
| | |
| | <h2>マテリアリティ & ターゲット</h2> |
| | <ul> |
| | {% for topic in meta.material_topics %} |
| | <li>{{ topic }}</li> |
| | {% endfor %} |
| | </ul> |
| | <p class="small">CO₂削減目標: {{ meta.targets.co2_reduction_pct }}% / 女性管理職比率: {{ meta.targets.female_management_ratio }}% / 再エネ比率: {{ meta.targets.renewable_energy_ratio }}%</p> |
| | |
| | <h2>リスク & 機会(要約)</h2> |
| | <p>{{ sections.risk_opportunity }}</p> |
| | |
| | <footer class="small">Generated on {{ generated_at }}</footer> |
| | </body> |
| | </html> |
| | """ |
| | with open("app/templates/report.html.j2", "w", encoding="utf-8") as f: |
| | f.write(html_template_content) |