Update core/ai_client.py
Browse files- core/ai_client.py +8 -121
core/ai_client.py
CHANGED
|
@@ -1,125 +1,12 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
except Exception:
|
| 7 |
-
OpenAI = None # 起動は継続、UIだけでも出す
|
| 8 |
|
| 9 |
-
|
| 10 |
-
OPENAI_MODEL_TEXT = os.environ.get("OPENAI_TEXT_MODEL", "gpt-4o-mini")
|
| 11 |
-
|
| 12 |
-
SYSTEM_JSON = """あなたは有能な財務アナリストです。
|
| 13 |
-
与えられた決算書(画像またはテキスト)から、次の厳密な JSON 構造のみを日本語の単位なし・半角数値で返してください。分からない項目は null。
|
| 14 |
-
{
|
| 15 |
-
"company": {"name": null},
|
| 16 |
-
"period": {"start_date": null, "end_date": null},
|
| 17 |
-
"balance_sheet": {
|
| 18 |
-
"total_assets": null, "total_liabilities": null, "total_equity": null,
|
| 19 |
-
"current_assets": null, "fixed_assets": null,
|
| 20 |
-
"current_liabilities": null, "long_term_liabilities": null
|
| 21 |
-
},
|
| 22 |
-
"income_statement": {
|
| 23 |
-
"sales": null, "cost_of_sales": null, "gross_profit": null,
|
| 24 |
-
"operating_expenses": null, "operating_income": null,
|
| 25 |
-
"ordinary_income": null, "net_income": null
|
| 26 |
-
},
|
| 27 |
-
"cash_flows": {
|
| 28 |
-
"operating_cash_flow": null, "investing_cash_flow": null, "financing_cash_flow": null
|
| 29 |
-
}
|
| 30 |
-
}
|
| 31 |
-
"""
|
| 32 |
-
|
| 33 |
-
def _b64(img: bytes) -> str:
|
| 34 |
-
import base64 as _b
|
| 35 |
-
return _b.b64encode(img).decode("utf-8")
|
| 36 |
-
|
| 37 |
-
def _client() -> Optional[OpenAI]:
|
| 38 |
-
if OpenAI is None:
|
| 39 |
-
return None
|
| 40 |
key = os.environ.get("OPENAI_API_KEY")
|
| 41 |
if not key:
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def extract_financials(images: Optional[List[bytes]], text_blob: Optional[str], company_hint: str) -> Dict[str, Any]:
|
| 46 |
-
"""
|
| 47 |
-
OpenAI が使えない場合は「空の既定JSON」を返す(UIは必ず表示される)
|
| 48 |
-
"""
|
| 49 |
-
base: Dict[str, Any] = {
|
| 50 |
-
"company": {"name": company_hint or None},
|
| 51 |
-
"period": {"start_date": None, "end_date": None},
|
| 52 |
-
"balance_sheet": {
|
| 53 |
-
"total_assets": None, "total_liabilities": None, "total_equity": None,
|
| 54 |
-
"current_assets": None, "fixed_assets": None,
|
| 55 |
-
"current_liabilities": None, "long_term_liabilities": None
|
| 56 |
-
},
|
| 57 |
-
"income_statement": {
|
| 58 |
-
"sales": None, "cost_of_sales": None, "gross_profit": None,
|
| 59 |
-
"operating_expenses": None, "operating_income": None,
|
| 60 |
-
"ordinary_income": None, "net_income": None
|
| 61 |
-
},
|
| 62 |
-
"cash_flows": {
|
| 63 |
-
"operating_cash_flow": None, "investing_cash_flow": None, "financing_cash_flow": None
|
| 64 |
-
}
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
client = _client()
|
| 68 |
-
if client is None:
|
| 69 |
-
return base # キー未設定 or ライブラリ不在でも落とさない
|
| 70 |
-
|
| 71 |
-
if images:
|
| 72 |
-
content = [{"type": "text", "text": SYSTEM_JSON}]
|
| 73 |
-
if company_hint:
|
| 74 |
-
content.append({"type": "text", "text": f"会社名の候補: {company_hint}"})
|
| 75 |
-
for im in images:
|
| 76 |
-
content.append({"type": "input_image", "image_url": f"data:image/png;base64,{_b64(im)}"})
|
| 77 |
-
resp = client.chat.completions.create(
|
| 78 |
-
model=OPENAI_MODEL_VISION,
|
| 79 |
-
messages=[
|
| 80 |
-
{"role": "system", "content": "返答は必ず有効な JSON オブジェクトのみ。説明を含めない。"},
|
| 81 |
-
{"role": "user", "content": content},
|
| 82 |
-
],
|
| 83 |
-
response_format={"type": "json_object"},
|
| 84 |
-
temperature=0.1,
|
| 85 |
-
)
|
| 86 |
-
return json.loads(resp.choices[0].message.content or "{}") or base
|
| 87 |
-
|
| 88 |
-
else:
|
| 89 |
-
prompt = f"{SYSTEM_JSON}\n\n以下は決算書のテキストです。上記の JSON だけを返してください。\n\n{text_blob or ''}"
|
| 90 |
-
resp = client.chat.completions.create(
|
| 91 |
-
model=OPENAI_MODEL_TEXT,
|
| 92 |
-
messages=[
|
| 93 |
-
{"role": "system", "content": "返答は必ず有効な JSON オブジェクトのみ。"},
|
| 94 |
-
{"role": "user", "content": prompt},
|
| 95 |
-
],
|
| 96 |
-
response_format={"type": "json_object"},
|
| 97 |
-
temperature=0.1,
|
| 98 |
-
)
|
| 99 |
-
return json.loads(resp.choices[0].message.content or "{}") or base
|
| 100 |
-
|
| 101 |
-
def short_insight(fin: Dict[str, Any], score: Dict[str, Any]) -> str:
|
| 102 |
-
client = _client()
|
| 103 |
-
if client is None:
|
| 104 |
-
return "(OpenAI キー未設定のため AI 所見は省略)"
|
| 105 |
-
prompt = f"""次の財務データとスコア結果から、箇条書きで短く日本語でコメントしてください。
|
| 106 |
-
- 良い点 3つ
|
| 107 |
-
- 懸念点 3つ
|
| 108 |
-
- 総評(100字以内)
|
| 109 |
-
|
| 110 |
-
[財務データ]
|
| 111 |
-
{json.dumps(fin, ensure_ascii=False)}
|
| 112 |
-
|
| 113 |
-
[スコア]
|
| 114 |
-
{json.dumps(score, ensure_ascii=False)}
|
| 115 |
-
"""
|
| 116 |
-
try:
|
| 117 |
-
resp = client.chat.completions.create(
|
| 118 |
-
model=OPENAI_MODEL_TEXT,
|
| 119 |
-
messages=[{"role": "system", "content": "簡潔で公正な財務アナリスト。"},
|
| 120 |
-
{"role": "user", "content": prompt}],
|
| 121 |
-
temperature=0.3,
|
| 122 |
-
)
|
| 123 |
-
return resp.choices[0].message.content or "(AI 所見なし)"
|
| 124 |
-
except Exception as e:
|
| 125 |
-
return f"AI所見の生成に失敗: {e}"
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from openai import OpenAI
|
| 3 |
|
| 4 |
+
VISION_MODEL = os.environ.get("OPENAI_VISION_MODEL", "gpt-4o-mini")
|
| 5 |
+
TEXT_MODEL = os.environ.get("OPENAI_TEXT_MODEL", "gpt-4o-mini")
|
|
|
|
|
|
|
| 6 |
|
| 7 |
+
def get_client() -> OpenAI:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
key = os.environ.get("OPENAI_API_KEY")
|
| 9 |
if not key:
|
| 10 |
+
raise RuntimeError("OPENAI_API_KEY が未設定です。環境変数に設定してください。")
|
| 11 |
+
# proxies は渡さない(OpenAI 1.x では不正引数になり得る)
|
| 12 |
+
return OpenAI(api_key=key, timeout=30)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|