
    Bfi4                        U d Z ddlmZ ddlZddlZddlZddlmZ ddl	m
Z
 	 ddlZdZ ed       G d d	             Zd
Zded<    G d d      ZddlmZ y# e$ rZdZeZY dZ[<dZ[ww xY w)u  
APEX V16 — Anthropic Claude AI client.

Wraps the Anthropic Python SDK with:
  - Async-native interface (no time.sleep in async loops)
  - Structured error categorization (credit / overload / invalid / unknown)
  - Retry policy with explicit logging (logs to brain_log.jsonl)
  - Configurable model, max_tokens, temperature, retries

Replaces V15's MarketRouter.call_ai() which was sync and called
from async code via asyncio.to_thread(). V16 is async-first.

Environment:
  ANTHROPIC_API_KEY: required, read from env (typically loaded from .env
                    by main.py before instantiating AIClient).

Single-key design: V16 uses ONE Anthropic key (paid plan).
No round-robin (which V15 had only as a Gemini compat shim anyway).
    )annotationsN)	dataclass)OptionalT)frozenc                  F    e Zd ZU dZded<   dZded<   dZded<   d	Zd
ed<   y)
AIResponsez:Result of an AI call. Either text is set, or error is set.Optional[str]textN
error_kindr   intattempts strerror_detail)__name__
__module____qualname____doc____annotations__r   r   r        &/home/work/apex_v16/brain/ai_client.pyr   r   *   s)    D
 $J$HcL#r   r   zzAll Brain decisions must be JSON only. No markdown, no prose outside the JSON object. Output a single valid JSON document.r   SYSTEM_PROMPT_BRAIN_DECISIONSc                      e Zd ZdZdZdZdZdZdZddeeedf	 	 	 	 	 	 	 	 	 	 	 ddZ	dd	Z
dd
Zeddf	 	 	 	 	 	 	 	 	 ddZ	 	 d	 	 	 	 	 	 	 ddZedd       Zedd       ZddZddZy)AIClientz
    Async Anthropic Claude client.

    Usage:
        client = AIClient(model="claude-haiku-4-5-20251001")
        await client.connect()
        resp = await client.ask("What is 2+2?")
        if resp.text:
            print(resp.text)
    zclaude-haiku-4-5-20251001i   g?   g       @Nc                   t         t        dt         d      |xs | j                  | _        |xs t        j                  d      | _        | j                  st        d      || _        || _	        || _
        || _        d| _        d| _        y)a  
        Args:
            model: Claude model string (default: claude-haiku-4-5-20251001).
            api_key: Anthropic key. If None, read from ANTHROPIC_API_KEY env.
            max_tokens: per-call cap.
            max_retries: number of attempts on retryable errors.
            retry_sleep_sec: seconds to wait between retries (async).
            logger: LoggerBundle (optional). If provided, errors logged
                    to error_log.jsonl and system log.
        Nzanthropic SDK not installed: z. Run: pip install anthropicANTHROPIC_API_KEYzeANTHROPIC_API_KEY not set. Either pass api_key= or set the environment variable (typically via .env).F)	anthropicRuntimeError_import_errorDEFAULT_MODELmodelosgetenvapi_key
max_tokensmax_retriesretry_sleep_seclogger_client
_connected)selfr#   r&   r'   r(   r)   r*   s          r   __init__zAIClient.__init__Z   s    & / ?- . 
 0d00
@")),?"@||E 
 %&.6:r   c                   K   	 t        j                  | j                        | _        d| _        y# t
        $ r%}| j                  dt        |             Y d}~yd}~ww xY ww)zv
        Initialize the Anthropic client. Cheap operation (no network call).
        Returns True on success.
        )r&   Tz
ai.connectNF)r   	Anthropicr&   r+   r,   	Exception
_log_errorr   )r-   es     r   connectzAIClient.connect   sO     
	$..t||DDL"DO 	OOL#a&1	s*   A",1 A"	AAA"AA"c                :    | j                   xr | j                  d uS N)r,   r+   )r-   s    r   is_connectedzAIClient.is_connected   s    ;4<<t#;;r   c           
     T  K   | j                         st        dddd      S | j                  |      }|st        dddd      S ||n| j                  }d}| j                  ||d	|d
gd}|r||d<   t        d| j                  dz         D ]^  }		 t        j                  | j                  j                  j                  fi | d{   }
| j                  |
      }t        ||	      c S  | j#                  dd| j                   d |dd         t        dd| j                  |dd       S 7 f# t        $ rk}t        |      }|}|j                         t!        fddD              r1| j#                  dd|dd         t        dd|	|dd       cY d}~c S d|v sdv r1| j#                  dd|dd         t        dd|	|dd       cY d}~c S d|v sdv r_| j%                  dd|	 d| j                          |	| j                  k  r(t        j&                  | j(                         d{  7   Y d}~| j%                  d|dd  d|	 d| j                          |	| j                  k  r(t        j&                  | j(                         d{  7   Y d}~d}~ww xY ww)!aq  
        Send a prompt to Claude and return the response.

        Handles:
          - Empty prompt: returns error_kind='invalid'
          - Credit exhausted: NO retry (429 on /v1/messages with
            credit_balance_too_low or similar) - returns error_kind='credit'
          - Overload (529): retry with backoff up to max_retries
          - Invalid request (400): NO retry - returns error_kind='invalid'
          - Network/transient: retry up to max_retries

        Returns:
            AIResponse. Caller checks resp.text (str on success, None on failure)
            and resp.error_kind for diagnostics.
        Nunknownr   z*client not connected; call connect() first)r
   r   r   r   invalidzempty prompt after sanitizationr   user)rolecontent)r#   r'   temperaturemessagessystem   )r
   r   c              3  &   K   | ]  }|v  
 y wr6   r   ).0slowers     r   	<genexpr>zAIClient.ask.<locals>.<genexpr>   s     LaqEzLs   )creditbillingbalancezai.askzcredit exhausted: x   rG      400invalid_requestzinvalid request: 529
overloadedzoverloaded, retry /P   z, retry zall z retries failed: )r7   r   _sanitize_promptr'   r#   ranger(   asyncio	to_threadr+   r?   create_extract_textr1   r   rE   anyr2   _log_warningsleepr)   )r-   promptr>   r'   r@   cleanmt
last_errorcreate_kwargsattemptmsgr
   r3   errrE   s                 @r   askzAIClient.ask   s    ,   "$I	  %%f-$>	  &1Zt

 ZZ&"(U;<	
 &,M(#Q 0 01 45 .	>G->#--LL))00# 
 ))#.!tg>>.	>b 	D)9)9(::KJW[X[L\K]"^_ %%#DS)	
 	
_  #>!f 
		 L+KLLOOH0B3t9+.NO%!#+!(%(#Y	  C<#4#=OOH0A#ds).MN%!#,!(%(#Y	  C<<5#8%%h2DWIQtO_O_N`0ab!1!11%mmD,@,@AAA !!(s3Bxj	4K[K[J\,]^T---!--(<(<===G#>s   BJ(7D0D.!D0)AJ(.D00
J%:AJ J%J(1J J%J(AJ 0H31J 6J(<AJ JJ J( J%%J(c                  K   t        j                         }| j                  |d|t               d{   }|~| j                  rt        j                         |z
  dz  }	 | j                  j
                  j                  d|t        |d      |j                  du|j                  |j                         |S |S 7 # t        $ r Y |S w xY ww)aR  
        Specialized ask() for Brain trading decisions.

        - temperature=0.0 (deterministic).
        - system=SYSTEM_PROMPT_BRAIN_DECISIONS forces JSON-only output
          regardless of per-turn prompt phrasing (V16 incident 2026-04-29
          TF entry: missing per-turn JSON guard caused markdown CoT
          replies, parser failure, lost trades). System prompt does not
          consume the output token budget.
        - max_tokens=1200: TF entry CoT (step_1/2/3 + 8 fields) plus
          MR setup descriptions can legitimately reach ~500 tokens.
          The previous 600-cap risked truncation before the closing
          brace.

        Brain modules (brain_tf, brain_mr) MUST use this method, not
        the general ask(), for trade open/exit decisions.

        When `where` is provided (e.g. "manage_exit"), an `ai_call`
        event is emitted to brain_log with latency_ms / ok / attempts /
        error_kind so the operator can audit AI usage post-trade.
        Default None preserves existing behaviour for entry-prompt
        callers that do not yet pass `where`.
        g        )r>   r'   r@   Ng     @@ai_callrA   )where
latency_msokr   r   )_time	monotonicrc   r   r*   	brain_logwriteroundr
   r   r   r1   )r-   r[   r'   rf   startedresp
elapsed_mss          r   ask_for_decisionzAIClient.ask_for_decision  s     : //#XX!0	  
 
 !8//+g5?J
%%++$Z3		-!]]# ,  t'
"  s4   1CB:,C!AB< 6C<	C	CC		Cc                    g }t        | dg       D ]1  }t        |dd      }|dk(  s|j                  t        |dd             3 dj                  |      j                         S )z
        Extract concatenated text from a Claude messages response.
        Claude responses are a list of content blocks; usually we want
        only text blocks (other types: tool_use, etc.).
        r=   typeNr
   r   
)getattrappendjoinstrip)ra   partsblock
block_types       r   rW   zAIClient._extract_textD  sf     S)R0 	9E 5JV#WUFB78	9 yy%%''r   c                L    | sy| j                  dd      j                         }|S )z
        Light sanitization: strip whitespace, remove NUL bytes,
        ensure non-empty. Heavy filtering should happen upstream
        (Brain logic) where context is known.
        r    )replacerx   )r[   cleaneds     r   rR   zAIClient._sanitize_promptR  s(     ..,224r   c                z    | j                   | j                   j                  ||       y t        d| d|        y )N)rf   errorz[AI ERROR] z: )r*   	log_errorprintr-   rf   details      r   r2   zAIClient._log_error^  s:    ;;"KK!!V!< Kwb12r   c                t    | j                   ,| j                   j                  j                  d| d|        y y )N[z] )r*   r@   warningr   s      r   rY   zAIClient._log_warninge  s6    ;;"KK&&5'F8'<= #r   )r#   r	   r&   r	   r'   r   r(   r   r)   floatreturnNone)r   bool)
r[   r   r>   r   r'   zOptional[int]r@   r	   r   r   )i  N)r[   r   r'   r   rf   r	   r   r   )r   r   )r[   r   r   r   )rf   r   r   r   r   r   )r   r   r   r   r"   DEFAULT_MAX_TOKENSDEFAULT_TEMPERATUREDEFAULT_MAX_RETRIESDEFAULT_RETRY_SLEEP_SECr.   r4   r7   rc   rq   staticmethodrW   rR   r2   rY   r   r   r   r   r   H   s1   	 0M!  $!%,.!8' '  '  	' 
 '  '  
' Z< 1$( $l
l
 l
 "	l

 l
 
l
l #	11 1 	1
 
1l ( ( 	 	3>r   r   )extract_json_from_response)r   
__future__r   rT   r$   timeri   dataclassesr   typingr   r   r!   ImportError_er   r   r   r   core.json_parsingr   r   r   r   <module>r      s   ( #  	  ! 
 M $  &+ s _> _>P	 9c
  IMs   A A*A%%A*