2727from typing import Any
2828
2929import json
30- import os
3130import re
3231from pathlib import Path
3332from typing import TypeVar
@@ -107,7 +106,7 @@ def _extract_json(raw: str) -> str:
107106 raw = raw .strip ()
108107 if raw .startswith ("```" ):
109108 # handles ```json, ```JSON, ``` (no lang tag)
110- raw = raw [3 :] # strip opening ```
109+ raw = raw [3 :] # strip opening ```
111110 if raw .startswith ("json" ) or raw .startswith ("JSON" ):
112111 raw = raw [4 :]
113112 # strip closing ```
@@ -136,7 +135,10 @@ def call(self, input: adal.GeneratorOutput, extra_fields: dict | None = None) ->
136135 if self .model_class .__name__ == "InvestmentThesis" :
137136 data ["reasoning_blocks" ] = []
138137 # Clamp time_horizon_days to minimum 1 (schema requires gt=0)
139- if data .get ("time_horizon_days" ) is not None and int (data .get ("time_horizon_days" , 1 )) < 1 :
138+ if (
139+ data .get ("time_horizon_days" ) is not None
140+ and int (data .get ("time_horizon_days" , 1 )) < 1
141+ ):
140142 data ["time_horizon_days" ] = 1
141143 # Strip unknown fields to prevent extra="forbid" crashes from LLM hallucinations.
142144 # The schema is strict by design (no junk in IPFS), but we sanitize at parse time.
@@ -315,12 +317,18 @@ def _repair_and_parse_once(
315317 extra_fields : dict [str , Any ] | None = None ,
316318 ) -> BaseModel | None :
317319 """Run exactly one JSON-repair pass, then parse again. Fail closed on error."""
318- raw = getattr (output , "raw_response" , None ) or getattr (output , "data" , None ) or str (output or "" )
320+ raw = (
321+ getattr (output , "raw_response" , None )
322+ or getattr (output , "data" , None )
323+ or str (output or "" )
324+ )
319325 try :
320326 repaired_output = self .json_repair (
321327 prompt_kwargs = {
322328 "schema_name" : parser .model_class .__name__ ,
323- "schema_json" : json .dumps (parser .model_class .model_json_schema (), ensure_ascii = False ),
329+ "schema_json" : json .dumps (
330+ parser .model_class .model_json_schema (), ensure_ascii = False
331+ ),
324332 "raw_output" : _sanitize_untrusted_text (raw , max_len = 16000 ),
325333 }
326334 )
@@ -384,7 +392,10 @@ async def analyze(self, ticker: str, prior_feedback: str = "") -> InvestmentThes
384392 _wait = 2 ** (_sub_attempt + 2 )
385393 logger .warning (
386394 "Sub-agent 503 for %s/%s — retrying in %ds (attempt %d/3)" ,
387- role .value , safe_ticker , _wait , _sub_attempt + 1 ,
395+ role .value ,
396+ safe_ticker ,
397+ _wait ,
398+ _sub_attempt + 1 ,
388399 )
389400 await asyncio .sleep (_wait )
390401 continue
@@ -398,7 +409,7 @@ async def analyze(self, ticker: str, prior_feedback: str = "") -> InvestmentThes
398409 extra_fields = {"agent_role" : role .value },
399410 )
400411 if not isinstance (block , ReasoningBlock ):
401- raw_snippet = (getattr (block_output , ' raw_response' , None ) or '' )[:300 ]
412+ raw_snippet = (getattr (block_output , " raw_response" , None ) or "" )[:300 ]
402413 raise RuntimeError (
403414 f"{ role } sub-agent failed to produce a ReasoningBlock after one repair pass. "
404415 f"raw: { raw_snippet } "
@@ -430,12 +441,15 @@ async def analyze(self, ticker: str, prior_feedback: str = "") -> InvestmentThes
430441 "learned_guidelines" : _sanitize_untrusted_text (_guidelines_str , max_len = 4000 ),
431442 }
432443 import asyncio as _asyncio
444+
433445 thesis_output = None
434446 for _attempt in range (3 ):
435447 thesis_output = self .synthesizer (prompt_kwargs = synthesis_kwargs )
436448 if getattr (thesis_output , "error" , None ) and "503" in str (thesis_output .error ):
437449 wait = 2 ** (_attempt + 2 ) # 4s, 8s, 16s
438- logger .warning ("Synthesizer 503 — retrying in %ds (attempt %d/3)" , wait , _attempt + 1 )
450+ logger .warning (
451+ "Synthesizer 503 — retrying in %ds (attempt %d/3)" , wait , _attempt + 1
452+ )
439453 await _asyncio .sleep (wait )
440454 continue
441455 break
@@ -450,7 +464,7 @@ async def analyze(self, ticker: str, prior_feedback: str = "") -> InvestmentThes
450464 extra_fields = {"region" : self .region .value , "ticker_or_asset" : safe_ticker },
451465 )
452466 if not isinstance (thesis , InvestmentThesis ):
453- raw_snippet = (getattr (thesis_output , ' raw_response' , None ) or '' )[:300 ]
467+ raw_snippet = (getattr (thesis_output , " raw_response" , None ) or "" )[:300 ]
454468 raise RuntimeError (f"Synthesizer failed after one repair pass. raw: { raw_snippet } " )
455469
456470 # 3. Splice the sub-agent blocks back in (synthesizer may have summarized them).
@@ -463,6 +477,7 @@ async def analyze(self, ticker: str, prior_feedback: str = "") -> InvestmentThes
463477 entry_price_1e8 : int | None = None
464478 try :
465479 from data .yfinance_client import YFinanceClient as _YFC
480+
466481 # Normalize ticker for yfinance:
467482 # - Tushare .SH → Yahoo .SS (Shanghai A-shares)
468483 # - Bare crypto symbols (BTC, ETH) → BTC-USD, ETH-USD
@@ -472,7 +487,12 @@ async def analyze(self, ticker: str, prior_feedback: str = "") -> InvestmentThes
472487 price = await _YFC ().get_current_price (_yf_ticker )
473488 if price and price > 0 :
474489 entry_price_1e8 = int (price * 1e8 )
475- logger .debug ("Live price for %s: %.4f → entry_price_1e8=%d" , _yf_ticker , price , entry_price_1e8 )
490+ logger .debug (
491+ "Live price for %s: %.4f → entry_price_1e8=%d" ,
492+ _yf_ticker ,
493+ price ,
494+ entry_price_1e8 ,
495+ )
476496 except Exception as _price_exc :
477497 logger .debug ("Price fetch skipped for %s: %s" , safe_ticker , _price_exc )
478498
0 commit comments