
    5jTH                   	   U d Z ddlmZ ddlZddlZddlZddlZddlZddlZ	ddl
m
Z
mZ ddlmZ ddlmZ ddd	d
dddddddgddddddddddddddgdddddddddd dd!d!dgdd"dd#Zd$ed%<   dZdZdd&Zdd'Z eej,                  j/                  d( ee      j3                         j4                  d)z              Zd*d*d+d,Zd-ed.<   dd/Zdd0Zdd1Zdd2Z dd3Z!dd4Z"efdd5Z#dd6Z$dd7Z%ddd8Z&dd9dd;Z'	 	 	 	 	 	 	 	 	 	 dd<Z(	 	 	 	 	 	 	 	 	 	 dd=Z)	 	 	 	 	 	 	 	 	 	 dd>Z*dd?Z+dd@Z,dAZ-ddBZ.	 	 	 	 	 	 	 	 	 	 	 	 ddCZ/ddDZ0ddEZ1ddFZ2dGZ3dHZ4ddIZ5	 ddd9	 	 	 	 	 	 	 	 	 	 	 ddJZ6ddKZ7	 ddd9	 	 	 	 	 	 	 	 	 	 	 ddLZ8	 	 	 	 	 	 	 	 	 	 	 	 ddMZ9	 	 	 	 	 	 	 	 	 	 	 	 ddNZ:efddOZ; eej,                  j/                  dPdQ            Z< eej,                  j/                  dRdS            Z= eej,                  j/                  dTdU            Z> ee      j3                         j4                  Z? eej,                  j/                  dV e@e?dWz                    ZA ej                  dX      ZC ej                         ZEddYZFdZd[gd\d]d^d_d`dadbid_gdcdddeg dfdgd^i g dcdddeg dhdid^djd`dkg dldmig dcdddeg dndod^dpd`dqdbidpgdcdddedrZGd$eds<   eGj                         D  cg c]  } | dt   	 c} ZId:edu<   dvd[gdwdxdy dzd{d^d|d`d}dbid|gdcddd~dvd[gdwdd ddd^d`ddbd`ddbdd|dgdcddd~dZd[gdwdd ddd^d|d`ddbid|gdcddd~dvd[gdwdd ddd^d`ddbd`ddbd`ddbdddbdg ddcddd~g dwdd ddd^d`ddbd`ddbd`ddbd`ddbd`ddbdd_gdcddd~dZJd$ed<   i eGeJZKd$ed<   i ZLd-ed<   ddZMddZNddZOddZPddZQddZRddZSePeQeReSdrZTd-ed<   ddZUi ZVd$ed<    ej                         ZWdZXddZY	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZZddZ[dZ\ddZ] G d d      Z^ddZ_ddZ` G d d      Za G d d      Zb G d d      Zc ea        eb        ec       dZdd-ed<   ddZe	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZfddÄZgdd9	 	 	 	 	 	 	 	 	 	 	 ddĄZh	 	 	 	 	 	 	 	 	 	 ddńZiddƄZjddǄZkd dȄZldݐddɄZmddʄZndd˄Zodݐdd̄Zp G d̈́ deq      Zr G dτ deq      Zs G dф det      Zuyc c} w (  aG  Assistant module for Togen LLM chat (DVI-529).

Provides a pluggable provider registry (ASSISTANT_PROVIDERS), settings
persistence for global admin config and per-user connection overrides, an
availability gate (is_assistant_available), a chat proxy, and a vision-based
field-extraction helper used by the Claude-vision OCR backend.

Architecture note (D1): ASSISTANT_PROVIDERS is intentionally open-ended so a
future full-MCP provider can be registered here without touching routes or UI.
The dispatch branch in chat() keys on provider id; MCP providers would add
their own branch.
    )annotationsN)datetimetimezone)Path)Any	anthropiczClaude (Anthropic API)zclaude-opus-4-7zClaude Opus 4.7)idlabelzclaude-sonnet-4-6zClaude Sonnet 4.6zclaude-haiku-4-5-20251001zClaude Haiku 4.5z+https://console.anthropic.com/settings/keys)r	   r
   modelsdefault_modeldocs_urlgooglezGemini (Google AI)zgemini-2.5-prozGemini 2.5 Prozgemini-2.5-flashzGemini 2.5 Flashzgemini-2.0-flashzGemini 2.0 Flashz&https://aistudio.google.com/app/apikeyopenaizOpenAI (GPT)zgpt-4.1zGPT-4.1zgpt-4ozGPT-4ozo4-miniz$https://platform.openai.com/api-keys)r   r   r   zdict[str, dict]ASSISTANT_PROVIDERSc                    t         j                         D ]*  \  }}|j                  dg       D ]  }|d   | k(  s|c c S  , t        S )zJReturn the provider id that owns model_id, defaulting to DEFAULT_PROVIDER.r   r	   )r   itemsgetDEFAULT_PROVIDER)model_idprov_idprovms        /var/www/html/togen/assistant.py_provider_for_modelr   F   sQ    ,224 (B' 	Aw("	     c                z    t         j                         D ]$  }|j                  dg       D ]  }|d   | k(  s  y & y)z>Return True if model_id is a registered model of any provider.r   r	   TF)r   valuesr   )r   r   r   s      r   _is_known_modelr   O   sH    #**, (B' 	Aw("	 r   ASSISTANT_SETTINGS_FILEzassistant_settings.jsonF )enabledaccessible_to_usersdefault_context_mdzdict[str, Any]DEFAULT_ASSISTANT_SETTINGSc                     t        t              } t        j                         rI	 t	        j
                  t        j                               }t        |t               rt        |j                  d      t              r|d   | d<   t        |j                  d      t              r|d   | d<   t        |j                  d      t              r|d   | d<   |j                  d      r|d   | d<   t        |j                  d      t               r|d   | d<   t        |j                  d      t               r|d   | d<   |j                  d      r|d   | d<   |j                  d      r|d   | d<   | S | S # t        j                  t        f$ r Y | S w xY w)	z;Return global assistant settings, falling back to defaults.r!   r"   r#   api_keyapi_keysprovider_enabledr   	ocr_model)dictr$   r   is_filejsonloads	read_text
isinstancer   boolstrJSONDecodeErrorOSError)settingssaveds     r   load_assistant_settingsr6   k   ss   ./H&&(	JJ6@@BCE%&eii	2D9*/	*:HY'eii(=>E6;<Q6RH23eii(<=sC5:;O5PH1299Y'*/	*:HY'eii
3T:+0+<HZ(eii(:;TB389K3LH/099_-05o0FH_-99[),1+,>H[) O8O $$g. 	O	s   EE0 0FFc                    t         j                  t        j                  | d             t         j	                  t
        j                  t
        j                  z         y)z>Persist global assistant settings. File is 0600 (key-bearing).   )indentN)r   
write_textr,   dumpschmodstatS_IRUSRS_IWUSR)r4   s    r   save_assistant_settingsr@      s6    &&tzz(1'EF!!$,,"=>r   c                >    t        | j                  d      xs i       S )zAExtract the assistant sub-dict from user settings (may be empty).	assistant)r*   r   user_settingss    r   get_user_assistantrE      s    !!+.4"55r   c                &    t        |       }||d<   |S )zCReturn a copy of user_settings with the assistant sub-dict updated.rB   r*   )rD   rB   updateds      r   set_user_assistantrI      s    =!G$GKNr   c                z   |ryt               }|j                  d      syt        |      }|j                  d      xs i }t        |j	                               s|j                  d      ry|j                  d      r@|j                  d      xs i }t        |j	                               s|j                  d      ryy)a  Return True if the assistant is available for this user.

    Admins: always available when the feature is enabled.
    Normal users: available when they have their own key (any provider), OR when
    the org key is configured for any provider and accessible_to_users is True.
    Tr!   Fkeysr&   r"   r'   )r6   r   rE   anyr   )
user_emailis_adminrD   
global_cfgua	user_keysorg_keyss          r   is_assistant_availablerS      s     (*J>>)$	M	*Bv$"I
9"&&"3~~+,>>*-3x !Z^^I%>r   c                4    t        | ||      syt        |      S )u]  Return True only when the assistant is both entitled AND has a usable key.

    `is_assistant_available` is an *entitlement* gate — admins pass it
    unconditionally and org-key access passes it on the flag alone. That is not
    enough to actually run a request: without an effective key, chat fails with
    "No API key configured". This stricter check is what the topbar indicator
    (green vs red) and the chat gate use, so an entitled-but-unconfigured user
    (e.g. an admin who never set a key) sees red and the "configure your key"
    prompt instead of an input box that silently errors.
    F)rS   _has_any_effective_key)rM   rN   rD   s      r   is_assistant_connectedrV      s     "*hF!-00r   c                F   t        |       }|j                  d      xs i }|j                  |      r||   S |dk(  r|j                  d      r|d   S t               }|j                  d      xs i }|j                  |      r||   S |dk(  r|j                  d      r|d   S y)zReturn the best API key for provider_id (user key beats org key).

    Checks new per-provider `keys` dict first; falls back to legacy single
    `api_key` field when provider_id is 'anthropic' for back-compat.
    rK   r   r&   r'   NrE   r   r6   )rD   provider_idrP   rQ   rO   rR   s         r   _effective_keyrZ      s     
M	*Bv$"I}}[!%%k!bffY&7)}(*J~~j)/RH||K $$k!jnnY&?)$$r   c                4     t         fdt        D              S )zLReturn True if there is at least one usable key for any registered provider.c              3  6   K   | ]  }t        |        y wN)rZ   ).0pidrD   s     r   	<genexpr>z)_has_any_effective_key.<locals>.<genexpr>   s     Qc~mS1Q   )rL   r   rC   s   `r   rU   rU      s    Q=PQQQr   c                    t        |       }|j                  d      |d   S t               }|j                  d      xs dS )zGReturn the context markdown to use (user override beats admin default).
context_mdr#   r    rX   )rD   rP   rO   s      r   _effective_context_mdrd      sC    	M	*B	vvl',(*J>>./525r   c                    d}|r&t        |      j                         }t        |      r|}|s7t        |       }|j	                  d      xs |j	                  d      xs t
        }t        |      }t        | |      }|||fS )u  Return (model_id, provider_id, key) for a plain-/agentic-chat request.

    A per-invocation `model_override` (when it names a registered model) beats the
    user's saved default_model; otherwise falls back to the caller default. The
    provider — and therefore the key — is always derived from the resolved model,
    so selecting e.g. a Gemini model routes to the Google key.
    Nr   model)r1   stripr   rE   r   DEFAULT_MODELr   rZ   )rD   model_overriderf   	candidaterP   rY   keys          r   _resolve_chat_modelrl      s|     E'--/	9%E.'K266'?Km%e,K

4C+s""r   )rf   
list[dict]c                   t        ||      \  }}}|st        d      |dk(  rt        | |||      S |dk(  rt        | |||      S |dk(  rt	        | |||      S t        d| d      )a  Send messages to the configured provider and return the reply text.

    Resolves provider from `model` when given (per-invocation override), else from
    the user's selected default model; dispatches to the appropriate backend
    (anthropic/google/openai).  Raises AssistantUnavailable if no key is
    configured for the resolved provider.  Raises AssistantError on API failure.
    No API key configured.r   rf   rk   r   r   z(Chat is not yet available for provider ''.)rl   AssistantUnavailable_chat_google_chat_openai_chat_anthropic)messagessystemrD   rf   rY   rk   s         r   chatrx   	  s     2-GE;"#;<<hHfEsCChHfEsCCk!xu#FF

2;-rB r   c               <   	 dd l }|j                  |      }	 |j                  j                  |d|xs d|       }|j                  d   j                  S # t        $ r}t        d      |d }~ww xY w# |j                  $ r}t        t        |            |d }~ww xY w)Nr   #anthropic package is not installed.r&      r    )rf   
max_tokensrw   rv   )r   ImportErrorrr   	Anthropicrv   createcontenttextAPIErrorAssistantErrorr1   )rv   rw   rf   rk   
_anthropicexcclientresponses           r   ru   ru   !  s    S& !!#!.F	0??))<R	 * 
 "'''  S"#HIsRS  0SX&C/0s.   A ;A2 	A/A**A/2BBBc               4   	 ddl m} ddlm} |j                  |      }g }| D ]p  }	|	j                  dd      }
|
d	k(  rd
}
|j                  |j                  |
|j                  j                  t        |	j                  dd                  g             r 	 |j                  j                  |||j                  |xs dd            }|j                   S # t        $ r}t        d      |d}~ww xY w# t"        $ r}t%        t        |            |d}~ww xY w)zESend a plain (non-tool) chat request to Gemini and return reply text.r   genaitypes&google-genai package is not installed.Nr{   roleuserrB   rf   r   r    r   partsr|   )system_instructionmax_output_tokensrf   contentsconfig)r   r   google.genair   r~   rr   Clientr   appendContentPart	from_textr1   r   generate_contentGenerateContentConfigr   	Exceptionr   )rv   rw   rf   rk   _genai_gtypesr   r   r   msgr   r   s               r   rs   rs   5  s#   V*1 ]]3]'FH 
wwvv&;DOOgll.D.DSQZ\^I_E`.a-bOc	
	
0==1100#)>T"& 1  2 
 }}+  V"#KLRUUV,  0SX&C/0s/   C >C4 	C1 C,,C14	D=DDc          
         	 ddl m}  ||      }g }|r|j	                  d|d       | D ]?  }|j	                  |j                  dd	      t        |j                  d
d            d       A 	 |j                  j                  j                  |d|      }	|	j                  d   j                  j                  xs dS # t        $ r}t        d      |d}~ww xY w# t        $ r}t        t        |            |d}~ww xY w)zVSend a plain (non-tool) chat request to OpenAI Chat Completions and return reply text.r   OpenAI openai package is not installed.Nr{   rw   r   r   r   r   r   r    r|   rf   r}   rv   )r   r   r~   rr   r   r   r1   rx   completionsr   choicesmessager   r   r   )
rv   rw   rf   rk   _OpenAIr   r   oa_messagesr   r   s
             r   rt   rt   V  s
   P, S!F KH@A GGFF+3779b12
 	
0;;**11  2 

 "**228b8%  P"#EFCOP&  0SX&C/0s0   B= .AC =	CCC	C=#C88C=c                     t               } t        | j                  d      xs d      j                         }|rt	        |      r|}t        |      }nt        }t        }t        |      }|||fS )aY  Resolve (model_id, provider_id, key) for title-block OCR.

    Uses the admin ``ocr_model`` setting (a bare registered model id) plus the
    org-level key for that model's provider. Defaults to Anthropic
    (DEFAULT_MODEL + org anthropic key) when the setting is unset or names an
    unknown model, so existing OCR behavior is unchanged.
    r)   r    )	r6   r1   r   rg   r   r   rh   r   _effective_key_system_only)cfgrawr   rY   rk   s        r   _resolve_ocr_modelr   w  sg     "
#C
cggk"(b
)
/
/
1C
s#)#. &
$[
1C[#%%r   c           
     F   t               \  }}}|st        d| d      dj                  d |D              }|dgk(  rd}nd| d}t        |||| |      }	 |j	                         }|j                  d	      r*|j                  d	      d
   }|j                  d      r|dd }t        j                  |      }|D 	ci c]  }	|	t        |j                  |	d              c}	S c c}	w # t        j                  t        t        t        f$ r}
t        d| d|
       |
d}
~
ww xY w)a  Use a vision model to extract named fields from an image.

    image_bytes: raw PNG/JPEG image bytes.
    fields: list of field names to locate in the image (e.g. ["customer_name", "job_no"]).
    Returns a dict mapping field names to extracted string values (empty string
    when not found). Raises AssistantUnavailable or AssistantError on failure.

    Provider/model come from the admin ``ocr_model`` setting (default Anthropic
    org key); the request is routed to the matching provider's vision path.
    No org API key configured for  vision OCR.
c              3  &   K   | ]	  }d |   ywz- N )r^   fs     r   r`   z!extract_fields.<locals>.<genexpr>  s     5bH5   	full_texta  You are an OCR assistant. Transcribe ALL text visible in this image, preserving line breaks and layout as closely as possible. Return ONLY a JSON object with a single key 'full_text' whose value is the full transcribed text. No other keys, no markdown, no commentary.zYou are an OCR assistant. Extract the following fields from the image of an engineering drawing title block. Return ONLY a JSON object mapping field names to their string values. Fields to extract:
zl

If a field is not present or not legible, use an empty string. Return only the JSON object, no other text.```   r,      Nr     Unexpected response format from 	 vision: )r   rr   join_ocr_vision_callrg   
startswithsplitr,   r-   r1   r   r2   KeyError
IndexErrorAttributeErrorr   )image_bytesfieldsr   rY   rk   fields_listpromptr   resultr   r   s              r   extract_fieldsr     sE    "4!5Hk3",[MF
 	
 ))5f55K+W 	GGRm T:: 	 ;#{F
KC
fiik>>% ))E"1%C~~f%!"gC39:a3vzz!R()):::  (JG f?}IVYUZ[\beefs+   A$C& ;#C!C& !C& &#D 	DD )
batch_datebatch_numbermixtureyardsrock_moisturesand_moistureinitialsnotesc           
        t               \  }}}|st        d| d      dj                  d |D              xs d}d|z   dz   }t        |||| |      }	 |j	                         }|j                  d      r*|j                  d      d	   }|j                  d
      r|dd }t        j                  |      }t        |t              st        d      	 t         D 
ci c]"  }
|
t#        |j%                  |
d      xs d      $ }}
g }|j%                  d      xs g D ]  }t        |t              st#        |j%                  dd      xs d      j	                         }|sD|j'                  |t#        |j%                  dd      xs d      j	                         d        ||dS # t        j                  t        t        t        f$ r}	t        d| d|	       |	d}	~	ww xY wc c}
w )u  Vision-extract a concrete QC batch ticket into structured fields.

    image_bytes: raw PNG/JPEG of one batch-ticket page.
    material_names: the QC material vocabulary, so the model can match the
        per-material weighed amounts to known materials.

    Returns ``{"fields": {batch_date, batch_number, mixture, yards,
    rock_moisture, sand_moisture, initials, notes}, "materials": [{"name",
    "actual"}, ...]}`` — best effort; a missing/illegible value is an empty
    string and materials may be empty. Raises AssistantUnavailable/AssistantError
    on provider failure (callers treat OCR as best-effort prefill).

    Provider/model come from the admin ``ocr_model`` setting (default Anthropic
    org key), reusing the title-block OCR resolution + per-provider vision path.
    r   r   r   c              3  &   K   | ]	  }d |   ywr   r   )r^   r   s     r   r`   z'extract_batch_ticket.<locals>.<genexpr>  s     6!r!X6r   z- (none configured)a  You are reading a concrete Quality-Control batch ticket. Extract the batch header and the per-material actual (weighed/dispensed) amounts. Return ONLY a JSON object with these keys:
  batch_date (YYYY-MM-DD if you can determine it, else the date text),
  batch_number (the batch or ticket number),
  mixture (the mix design name or code),
  yards (cubic yards produced, number only),
  rock_moisture (rock/stone moisture percent, number only),
  sand_moisture (sand moisture percent, number only),
  initials (the operator initials),
  notes (any free-text notes),
  materials (an array of objects {"name": <material>, "actual": <number>} for each material weighed).
Match each material name to this list where possible:
u   

Use an empty string (or omit) any field that is absent or not legible. Return only the JSON object — no markdown fences, no commentary.r   r   r,   r   Nzexpected a JSON objectr   r   r    	materialsnameactual)r   r   )r   r   )r   rr   r   r   rg   r   r   r,   r-   r/   r*   
ValueErrorr2   r   r   r   _BATCH_TICKET_KEYSr1   r   r   )r   material_namesr   rY   rk   matsr   r   datar   kr   r   itemr   s                  r   extract_batch_ticketr     s     "4!5Hk3",[MF
 	
 996~66O:OD	B EI	ILM	M $ ;#{F
KCiik>>% ))E"1%C~~f%!"gzz#$%566 & 6HHaTXXa_*++HFHI+&," P$%488FB'-2.446$$'2(>(D"$E$K$K$MO 	PP 955   *j.I .{m9SEJ
	 Is   A;F" 'G"#GGGc                    | dk(  rt        ||||      S | dk(  rt        ||||      S | dk7  rt        d|  d      t        ||||      S )a	  Send a single image + prompt to the given provider and return raw text.

    Images are sent at full resolution (no downscale) to preserve fine title-block
    text for OCR. Mirrors the per-provider vision message shapes used by
    interpret_images (DVI-953).
    r   rp   r   r   z+Vision OCR is not configured for provider 'rq   )_ocr_google_ocr_openairr   _ocr_anthropic)rY   r   rk   r   r   s        r   r   r     sj     h;hCHHh;hCHHk!"9+bI
 	
 +vX3GGr   c          
        dd l }	 dd l}|j	                  |      }|j                  |       j                         }	 |j                  j                  |ddddd|d	d
d|dgdg      }	|	j                  d   j                  S # t        $ r}t        d      |d }~ww xY w# |j                  $ r}t        t        |            |d }~ww xY w)Nr   rz   r{      r   imagebase64	image/pngtype
media_typer   r   sourcer   r   r   r   r   )r   r   r~   rr   r   standard_b64encodedecoderv   r   r   r   r   r   r1   )
r   r   rf   rk   r   r   r   r   b64r   s
             r   r   r     s    S& !!#!.F

#
#K
0
7
7
9C0??)) # %,(0.9(+' "(8
  * 
( "'''3  S"#HIsRS4  0SX&C/0s/   B AB  	BBB C	/CC	c          	        dd l }	 ddlm}  ||      }|j                  |       j                         }	 |j                  j                  j                  |ddddd	| id
d|dgdg      }	|	j                  d   j                  j                  xs dS # t        $ r}t	        d      |d }~ww xY w# t        $ r}t        t        |            |d }~ww xY w)Nr   r   r   r{   r   r   	image_urlurlzdata:image/png;base64,r   r   r   r   r   r   r    )r   r   r   r~   rr   r   r   rx   r   r   r   r   r   r   r   r1   )
r   r   rf   rk   r   r   r   r   r   r   s
             r   r   r   <  s    P, S!F

#
#K
0
7
7
9C0;;**11 # %0*/3I#1O)P "(8 	 2 
  "**228b8+  P"#EFCOP,  0SX&C/0s/   B AB2 	B/B**B/2	C;CCc          
        	 ddl m} ddlm} |j                  |      }	 |j                  j                  ||j                  d|j                  j                  | d      |j                  j                  |      g	      g|j                  d
            }|j                  S # t        $ r}t        d      |d }~ww xY w# t        $ r}t!        t#        |            |d }~ww xY w)Nr   r   r   r   r{   r   r   r   	mime_typer   r   r   r   )r   r   r   r   r~   rr   r   r   r   r   r   
from_bytesr   r   r   r   r   r1   )	r   r   rf   rk   r   r   r   r   r   s	            r   r   r   Z  s    V*1 ]]3]'F0==11//[K/X..v6    0040H 2 
 }}#  V"#KLRUUV$  0SX&C/0s/   B# BC  #	B=,B88B= 	C#	CC#i   U   c                   	 ddl m} ddlm} |j	                   ||             }|j
                  \  }}t        ||      }|t        kD  r?t        |z  }|j                  t        ||z        t        ||z        f|j                        }|j                  dvr|j                  d      } |       }|j                  |dt               |j                         S # t         $ r | cY S w xY w)zRe-encode image to JPEG, capped at _INTERPRET_MAX_LONG_EDGE on the long edge.

    Accepts any PIL-readable format; returns JPEG bytes. Falls back to the
    original bytes if Pillow is unavailable or the image is already small enough.
    r   )BytesIO)Image)RGBLr  JPEG)formatquality)ior  PILr  opensizemax_INTERPRET_MAX_LONG_EDGEresizeintLANCZOSmodeconvertsave_INTERPRET_JPEG_QUALITYgetvaluer   )	r   r  r  imgwh	long_edgescalebufs	            r   _downscale_imager  |  s    jj-.xx11I	//,y8E**c!e)nc!e)n=u}}MC88<'++e$CiV-DE||~ s   CC C$#C$c               $    t        | g||||      S )zInterpret a single image using the configured Vision assistant model.

    Delegates to interpret_images with a single-element list; behavior is unchanged.
    )rD   rf   )interpret_images)r   r   historyrD   rf   s        r   interpret_imager!    s     [M67-_deer   c                   |xs d}|j                  d      r%|t        d      d }t        |      }t        |      }n|j                  d      re|t        d      d }t        |      }t	        |       }|j                  d      xs i }|j                  |      xs |dk(  r|j                  d      nd}nt        }t        }t        | |      }|st        }t        }|st        | |      }|||fS )a  Resolve (model_id, provider_id, key) from a ``source:modelId`` string.

    Shared by the saved Vision default and the per-request override. ``system:``
    forces the org key, ``user:`` forces the user's own key, otherwise the default
    provider/key. Falls back to the default model/provider when unparseable and
    always tries a last-resort effective key so a valid model never fails purely
    on prefix mismatch.
    r    zsystem:Nzuser:rK   r   r&   )	r   lenr   r   rE   r   rh   r   rZ   )rD   	raw_modelr   rY   rk   rP   rQ   s          r   _resolve_vision_modelr%    s     RII&S^_-)(3(5			g	&S\]+)(3.FF6N(b	mmK(g+Q\B\RVVI->bf &]K8 &]K8[#%%r   c               L   ddl }| st        d      d}|r9t        |      j                         }t	        |j                  dd      d         r|}|s%|j                  d      xs i j                  d	d      }t        ||      \  }}	}
|
st        d
      |	dk(  rt        | ||||
      S |	dk(  rt        | ||||
      S |	dk7  rt        d|	 d      	 ddl
}t        |       }g }t        | d      D ]f  \  }}t        |      }|j                  |      j!                         }|dkD  r|j#                  dd| d| d       |j#                  ddd|dd       h g }|rd}|D ]  }|j                  d      dk(  r9|s7|j#                  d|dt        |j                  dd            dgz   d        d!}P|j#                  |d   t        |j                  dd            d         |j#                  d|d        n|j#                  d|d|dgz   d        |j%                  |
"      }	 |j&                  j)                  |d#|$      }|j*                  d   j,                  S # t        $ r}t        d      |d}~ww xY w# |j.                  $ r}t1        t        |            |d}~ww xY w)%u  Interpret an ordered sequence of images using the configured Vision assistant model.

    images: ordered list of raw image bytes (any PIL-readable format). Each image
            is preceded by a 'Frame k/N' text label so the model can reference frames.
    prompt: user question / instruction for this turn.
    history: prior conversation turns [{role, content}] — images are pinned on the
             first user turn as context for multi-turn sessions.
    user_settings: caller's settings dict (for key + model resolution).

    Raises AssistantUnavailable if no key is configured or provider not supported.
    Raises AssistantError on API failure.
    r   Nz&No images provided for interpretation.r    :r   visionassistant_modelz0No API key configured for Vision interpretation.r   rp   r   r   z:Vision interpretation is not yet configured for provider 'rq   rz   startr   Frame /r   r   r   
image/jpegr   r   Fr   r   r   r   Tr{      r   )r   rr   r1   rg   r   r   r   r%  _interpret_images_google_interpret_images_openair   r~   r#  	enumerater  r   r   r   r   rv   r   r   r   r   r   )imagesr   r   rD   rf   r   r$  rj   r   rY   rk   r   r   nframe_contentr   	img_bytesscaledr   rv   first_user_injectedturnr   r   s                           r   r  r    s   ( "#KLL
 IJ$$&	9??32267!I"&&x06B;;<MrR	!6}i!PHk3"#UVVh'xUXYYh'xUXYYk!"HUWX
 	
S&
 	FA "M!&2 9!),''/668q5  &F1#Qqc:J!KL'|SQ
 	 H# 	aDxx6)2E",TXXV_acMdIe0f/gg!  '+#f#dhhyZ\F]B^ _`	a 	F;<$(H'II
 	
 !!#!.F0??)) * 

 "'''U  S"#HIsRSV  0SX&C/0s0   I &6I: 	I7&I22I7:J#	JJ#c                  	 ddl m} ddlm} |j                  |      }t        |       }	g }
t        | d      D ]q  \  }}t        |      }|	dkD  r0|
j                  |j                  j                  d	| d
|	              |
j                  |j                  j                  |d             s g }|rd}|D ]  }|j                  dd      }|dk(  rd}|dk(  r]|s[|j                  |j                  d|
|j                  j                  t!        |j                  dd                  gz                d}~|j                  |j                  ||j                  j                  t!        |j                  dd                  g              |j                  |j                  d|j                  j                  |      g             n?|j                  |j                  d|
|j                  j                  |      gz                	 |j"                  j%                  |||j'                  d            }|j(                  S # t        $ r}t        d      |d}~ww xY w# t*        $ r}t-        t!        |            |d}~ww xY w)zEInterpret an image sequence using Gemini vision (inline image parts).r   r   r   r   Nr{   r   r+  r-  r.  r/  r   Fr   r   rB   rf   r   r    r   Tr0  r   r   )r   r   r   r   r~   rr   r   r#  r3  r  r   r   r   r   r   r   r1   r   r   r   r   r   r   )r4  r   r   rf   rk   r   r   r   r   r5  frame_partsr   r7  r8  r   r9  r:  r   r   s                      r   r1  r1  &  st   V*1 ]]3]'FFA  K!&2 Y9!),q5w||55qc1#6FGH7<<22,2WX	Y H# 	D88FF+D{"v~&9%)?)?DHHYXZD[@\)](^^ !0 !  '+#"<<11#dhhy"6M2NOP !0 ! 	 	VGLL<R<RSY<Z;[\]!7!7!? @@ ( 
 	
0==110040H 2 

 }}[  V"#KLRUUV\  0SX&C/0s/   I 	9I  	III 	J)I>>Jc                  ddl }	 ddlm}  ||      }t        |       }	g }
t        | d      D ]g  \  }}t        |      }|j                  |      j                         }|	dkD  r|
j                  dd	| d
|	 d       |
j                  ddd| id       i g }|rd}|D ]  }|j                  d      dk(  r9|s7|j                  d|
dt        |j                  dd            dgz   d       d}P|j                  |d   t        |j                  dd            d        |j                  d|d       n|j                  d|
d|dgz   d       	 |j                  j                  j                  |d|      }|j                   d   j"                  j$                  xs dS # t        $ r}t	        d      |d}~ww xY w# t&        $ r}t)        t        |            |d}~ww xY w)zSInterpret an image sequence using OpenAI vision (Chat Completions image_url parts).r   Nr   r   r{   r   r+  r   r-  r.  r   r   r   zdata:image/jpeg;base64,r   Fr   r   r   r    r   Tr0  r   )r   r   r   r~   rr   r#  r3  r  r   r   r   r   r1   rx   r   r   r   r   r   r   r   )r4  r   r   rf   rk   r   r   r   r   r5  r6  r   r7  r8  r   rv   r9  r:  r   s                      r   r2  r2  d  s    P, S!FFA !#M!&2 9!),''/668q5  &F1#Qqc:J!KL#:3%!@A
 	 H# 	aDxx6)2E",TXXV_acMdIe0f/gg!  '+#f#dhhyZ\F]B^ _`	a 	F;<$(H'II
 	
0;;**11 2 

 "**228b8W  P"#EFCOPX  0SX&C/0s0   F# AG  #	F=,F88F= 	G#	GG#c                    t               }|j                  d      xs i }|j                  |       r||    S | dk(  r|j                  d      r|d   S y)zHReturn the org-level key for provider_id (for system: model selections).r'   r   r&   N)r6   r   )rY   rO   rR   s      r   r   r     sU    (*J~~j)/RH||K $$k!jnnY&?)$$r   PRODRUN_DIRz/var/www/html/prodrunmergeQR_SHARED_DIRz/var/www/html/sharedWOR_DIRz/var/www/html/worTOOL_AUDIT_LOGztool_audit.jsonlz$^[a-zA-Z0-9][a-zA-Z0-9 _\-\.]{0,79}$c                    	 t         5  t        j                  dd      5 }|j                  t	        j
                  |       dz          ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w# t        $ r Y yw xY w)z;Append one JSON line to the audit log; swallows exceptions.azutf-8)encodingr   N)_audit_lockrB  r  writer,   r;   r   )entryfhs     r   _audit_appendrJ    s{     	3$$S7$; 3rE*T123	3 	33 3	3 	3  sE   A1 A%(AA%A1 A"	A%%A.*A1 .A1 1	A=<A=prod_runtogen_adminprodrun_statuszbGet the current status of a production run merge job. Returns status, message, and result summary.objectjob_idstringuS   Production run job ID — alphanumeric, spaces, dashes, or dots; max 80 characters.)r   description)r   
propertiesrequired)r   rQ  input_schema)required_rolesanthropic_defreprint_list_projectszCList available project folders for reprint or lookup, newest first.wor_historyzQList new and in-progress Work Order Request PDFs. Defaults to 'maintenance' type.r   z	WOR type.)maintenancemobilemachinesafety
preventive)r   rQ  enummanuals_lookupzPLook up a manual previously associated with an Equipment, Asset, or System name.	equipmentz!Equipment, asset, or system name.)rM  rW  rX  r^  _TIER0_TOOL_DEFSrV  TIER0_ANTHROPIC_TOOLStakeoff_workflowr   zSubmit QR Merge Jobc                *    d| j                  dd      iS NFolderfolderr    r   rD  s    r   <lambda>ri  8      !%%"2E'F r   	qr_submitzSubmit a QR code merge job for a project folder. Merges shop drawings with carrier sheets and writes results to the shared folder. Requires explicit user confirmation before execution.rf  zGProject folder name (alphanumeric, spaces, dashes, dots; max 80 chars).)rU  tierdisplay_namemake_display_argsrV  zDuplicate Project Folderc                L    | j                  dd      | j                  dd      dS )Nrf  r    
new_folder)SourcezNew namerg  rh  s    r   ri  ri  P  s%    eeHb)lB/(
 r   duplicate_folderzDuplicate a project folder under a new name, copying all documents, CSV, and metadata. Requires explicit user confirmation before execution.zSource project folder name.z)New folder name (must not already exist).)rf  rp  rp  zSubmit Production Runc                *    d| j                  dd      iS rd  rg  rh  s    r   ri  ri  n  rj  r   prodrun_submitzSubmit a Production Run merge job. Starts the merge worker for the given folder. Requires explicit user confirmation before execution.zProduction run job folder name.zSave Carrier Sheet to Projectc                n    | j                  dd      | j                  dd      | j                  dd      dS )NrO  r    folder_nameproduct_code)JobzProject folderProduct coderg  rh  s    r   ri  ri    s3    552&eeM26EE."5(
 r   scs_save_to_projectzSave a Single Carrier Sheet (from an existing creation session) to a project folder, updating the project CSV with the carrier sheet URL. Requires explicit user confirmation before execution.z5SCS job ID from a completed carrier creation session.z3Target project folder name in the shared directory.z3Product code to match or append in the project CSV.booleanzGAppend a new CSV row if the product code is not found (default: false).)rO  rv  rw  
append_row)rO  rv  rw  zCreate Carrier Sheetc                    | j                  d| j                  dd            | j                  dd      | j                  dd      dS )N
job_numberrO  r    rw  customer_name)z
Job numberry  Customerrg  rh  s    r   ri  ri    s>    %%aeeHb.ABEE."5or2(
 r   create_carrierzGenerate a QR carrier sheet for an existing upload session (job_id). The shop drawing PDF must already have been uploaded via the Create Carrier UI first. Optionally overrides carrier field values. Requires explicit user confirmation before execution.zQJob ID from a completed file-upload session (returned by the UI after uploading).zJob number override.zProduct code override.zCustomer name override.zProject name override.)rO  r~  rw  r  project_name)rk  rr  rt  rz  r  _TIER1_TOOL_DEFS_ALL_TOOL_DEFS_tier1_callbacksc                    |t         | <   y)zFRegister the executor for a Tier-1 tool (called by app.py at startup).N)r  )tool_idfns     r   register_tier1_callbackr    s     "Wr   c                l    t         j                  |       }|y|d   }|syt        fd|D              S )a,  Return True when user holds at least one required role for this tool.

    Deny-by-default: unknown tool_ids return False. Empty required_roles list
    means any authenticated user is permitted (mirrors @login_required).
    Mirrors the same role flags as the underlying Flask route decorators.
    FrU  Tc              3  @   K   | ]  }j                  |        y wr]   rg  )r^   rr   s     r   r`   z"_authorize_tool.<locals>.<genexpr>  s     -qtxx{-s   )r  r   rL   )r  r   defnrS  s    `  r   _authorize_toolr    s?     g&D|$%H-H---r   c                4     t         fdt        D              S )u$  Return True if the user is authorized to call at least one registered tool.

    Used by the chat route (Phase C) to decide whether a non-admin user gets
    agentic mode — if they have access to at least one tool, they use
    chat_agentic(); otherwise they fall back to plain chat().
    c              3  6   K   | ]  }t        |        y wr]   )r  )r^   tidr   s     r   r`   z&has_any_tool_access.<locals>.<genexpr>  s     DcsD)Dra   )rL   r  )r   s   `r   has_any_tool_accessr    s     D^DDDr   c                r   t        | j                  dd            j                         }|rt        j	                  |      rd|v rddiS t
        |z  dz  }|j                         sddd	S 	 t        j                  |j                               }i }d
D ]  }||v s||   ||<    |S # t        $ r ddicY S w xY w)uG   Return production run job status (safe summary only — no file paths).rO  r    z..errorzInvalid job_id.zstatus.jsonpendingu   Initializing…)statusr   )r  r   drawing_mergelabel_mergezCould not read status.)r1   r   rg   _SAFE_TOOL_ARG_REmatch_PRODRUN_DIRexistsr,   r-   r.   r   )args_userrO  status_pathr   safer   s          r   _tool_prodrun_statusr    s    (B'(..0F*008DFN*++'-7K#0ABB	3zz+//12!F 	"ADyq'Q	"  31223s   +.B& B& &B65B6c                B   g }t         j                         rt	 t        t         j                         d d      D ]J  }|j                         s|j                  j                  d      r0|j                  |j                         L 	 d|iS d|iS # t        $ r Y d|iS w xY w)z6Return project folder names from the shared directory.c                6    | j                         j                  S r]   )r=   st_mtime)es    r   ri  z-_tool_reprint_list_projects.<locals>.<lambda>  s    affh// r   Trk   reverse.folders)_SHARED_DIRis_dirsortediterdirr   r   r   r3   )_argsr  r  rH  s       r   _tool_reprint_list_projectsr    s    G		##%/ /
 <<>%***?*?*DNN5::./ wIw  	w	s   4B B )B 	BBc                   t        | j                  dd            j                         }|dvrd}t        |z  }|df|dz  dfg}g }|D ]  \  }}|j	                         s|j                  d      D ]^  }	 t        j                  |j                         j                        }	|j                  |j                  |	j                  d      |d       `  |j                  d	 d
       d|iS # t        $ r Y w xY w)z@Return new and in-progress WOR PDF names for the requested type.r   rY  >   r[  r\  rY  rZ  newin_progressz*.pdfz%Y-%m-%dT%H:%M:%SZ)r   dater  c                    | d   S )Nr  r   )xs    r   ri  z#_tool_wor_history.<locals>.<lambda>4  s
    QvY r   Tr  files)r1   r   rg   _WOR_DIRr  globr   utcfromtimestampr=   r  r   r   strftimer3   sort)
r  r  wor_typewor_basedirsr  	directoryr  r   mtimes
             r   _tool_wor_historyr    s   488FM2399;HOO ("H	5	M	!=1$D E! 	6!( 		A 11!&&(2C2CDFF!NN+?@$ 		 
JJ&J5U  s   4AC++	C76C7c                F   t        | j                  dd            j                         }|sddiS dj                  |j	                         j                               }t        dz  }|j                         sddiS 	 t        j                  |j                               }|j                  |      }|r|j                  d      sddiS d|j                  dd      |j                  d	|d         |j                  d
d      dS # t        j                  t        f$ r ddicY S w xY w)zILook up a manual by equipment name; returns name + association date only.r_  r    foundF zpm_manuals_index.jsonfilenameToriginal_name
updated_at)r  r_  r  r  )r1   r   rg   r   lowerr   _APP_DIRr+   r,   r-   r.   r2   r3   )r  r  r_  rk   
index_filer   rH  s          r   _tool_manuals_lookupr  8  s   DHH["-.446I
((9??$**,
-C33J zz*..01 HHSME		*- YY{B/?E*4EFiib1	    '*   s   ;#D   D D _TOOL_HANDLERSc                   t        j                  t        j                        j	                         }|xs i j                  dd      }t        | |      }||| ||d}|sd|d<   t        |       dd|  diS t        j                  |       }|d
|d<   t        |       dd|  diS 	  |||      }d|v rdnd|d<   t        |       |S # t        $ r!}	d|	 |d<   t        |       ddicY d	}	~	S d	}	~	ww xY w)u   Authorize, execute, and audit one tool call.

    Returns a result dict suitable for a tool_result message block.
    Never raises — errors are returned as {"error": "..."} dicts and audited.
    emailunknowntsr   toolr  authzdeniedoutcomer  z2Access denied: insufficient permissions for tool 'rq   Nunknown_toolzUnknown tool: 'input_errorsuccesshandler_error: Tool execution failed.)
r   nowr   utc	isoformatr   r  rJ  r  r   )
r  r  r   r  r  r  audithandlerr   r   s
             r   dispatch_toolr  Y  s)    
hll	#	-	-	/BZRWi0EGT*E E #ieMgYVXYZZ  )G)ie?7)2677
3t$ -4v,==9ie 3,SE2ie1223s   & C 	C1C,&C1,C1_pending_actionsiX  c                     t        j                  t        j                        j	                         } t
        j                         D cg c]  \  }}| |d   z
  t        kD  s| }}}|D ]	  }t
        |=  yc c}}w )z7Remove expired tokens (caller must hold _pending_lock).r  N)r   r  r   r  	timestampr  r   _PENDING_ACTION_TTL)r  r   vexpireds       r   _pending_purge_expiredr    sj    
,,x||
$
.
.
0C-335]TQqwI\9\q]G]  Q  ^s   	A7!A7c                   t        t        j                               }t        5  t	                t        j                  t        j                        j                         | |||||||||	|
dt        |<   ddd       |S # 1 sw Y   |S xY w)a  Store conversation state keyed by a fresh UUID token; return the token.

    provider_id + working_messages are provider-native so resume_agentic can
    rebuild the correct client/adapter.  extra_tier1 holds any additional Tier-1
    calls emitted in the same turn beyond the first; they are auto-cancelled on
    resume so every tool call in the paused assistant turn gets a result (required
    by OpenAI/Gemini, and correct for Anthropic too).
    )r  r  r  rM   working_messagestool_use_idtier0_resultsrw   rf   authorized_toolsrY   extra_tier1N)r1   _uuiduuid4_pending_lockr  r   r  r   r  r  r  )r  r  rM   r  r  r  rw   rf   r  rY   r  tokens               r   _pending_creater    s    * E	 
 ,,x||,668$ 0&* 0&&#

  L!
  Ls   AA>>Bc                    t         5  t                t        j                  |       }|
	 ddd       y|j                  d      |k7  r
	 ddd       yt        | = ddd       |S # 1 sw Y   S xY w)zPop and return a pending action; None if missing, expired, or wrong user.

    Leaves the entry in place when the email doesn't match so the correct user
    can still consume it.
    NrM   )r  r  r  r   )r  rM   rH  s      r   _pending_consumer    sw     
 $  $$U+=	$ $
 99\"j0$ $ U#$ L$ Ls   #A"A"A""A,
   c                   | j                   dk(  rd| j                  dS | j                   dk(  r.d| j                  | j                  t	        | j
                        dS 	 | j                         S # t        $ r d| j                   icY S w xY w)z7Convert an Anthropic SDK content block to a plain dict.r   r   tool_use)r   r	   r   inputr   )r   r   r	   r   r*   r  
model_dumpr   )blocks    r   _content_block_to_dictr    s    zzV

33zzZ((JJ%++&	
 	
$!! $

##$s   A- -BBc                      e Zd ZdZdZddZy)_AgenticResultz/Normalized result of one adapter.create() call.r   
tool_callsstop_reasonr   c                <    || _         || _        || _        || _        y r]   r  )selfr   r  r   r   s        r   __init__z_AgenticResult.__init__  s    	$&r   N)
r   r1   r  listr   r1   r   r   returnNone__name__
__module____qualname____doc__	__slots__r  r   r   r   r  r    s    9<Ir   r  c                    | d   S )a  Return the canonical JSON-Schema tool spec for a tool definition.

    The `anthropic_def` field ({name, description, input_schema}) is the
    canonical spec reused across all providers; each adapter's build_tools()
    translates it into the provider-native format.
    rV  r   )r  s    r   
_tool_specr    s       r   c           	     z   t        | t              s| S i }| j                         D ]  \  }}|dk(  r$t        |t              r|j	                         |d<   /|dk(  rAt        |t              r1|j                         D ci c]  \  }}|t        |       c}}|d<   u|dk(  rt        |      |d<   |||<    |S c c}}w )zBest-effort convert a JSON-Schema object into a Gemini types.Schema-friendly
    dict (uppercased `type` values, recursively) so FunctionDeclaration accepts it.

    Falls back to returning the input unchanged for non-dict nodes.
    r   rR  r   )r/   r*   r   r1   upper_json_schema_to_gemini)schemaoutr   r  pkpvs         r   r  r    s     fd#C 1;:a-'')CK,:a#6NOggi XFB%;B%?!? XC'\1!4CLCF J !Ys   <B7c                  @    e Zd ZdZd	dZd
dZddZddZddZddZ	y)_AnthropicAgentAdapterr   c                j    	 dd l }|j                  |      S # t        $ r}t        d      |d }~ww xY w)Nr   rz   r{   )r   r~   rr   r   )r  rk   r   r   s       r   make_clientz"_AnthropicAgentAdapter.make_client(  sD    	W* ##C#00  	W&'LMSVV	Ws    	2-2c                >    |D cg c]  }t        |       c}S c c}w r]   rG   r  rv   r   s      r   init_messagesz$_AnthropicAgentAdapter.init_messages/  s    !)*AQ***   c                >    |D cg c]  }t        |       c}S c c}w r]   rG   r  specsss      r   build_toolsz"_AnthropicAgentAdapter.build_tools2  s    !&'AQ'''r  c                   	 dd l }	 |j                  j	                  |d|xs d||      }d}	g }
|j                  D ]m  }|j                  dk(  r|	|j                  z  }	"|j                  dk(  s2|
j                  |j                  |j                  t        |j                        d       o |j                  D cg c]  }t!        |       }}|j"                  dk(  rdn|j"                  d	k(  rd	n|j"                  }t%        |	|
||      S # t        $ r}t        d      |d }~ww xY w# |j
                  $ r}t        t        |            |d }~ww xY wc c}w )
Nr   rz   r|   r    )rf   r}   rw   rv   toolsr   r  r	   r   r  end_turn)r   r~   rr   rv   r   r   r   r1   r   r   r   r   r	   r   r*   r  r  r   r  )r  r   rv   rw   rf   r$  r   r   respr   callsr  br   stops                  r   r   z_AnthropicAgentAdapter.create5  sY   	W*		4??))|! * D \\ 	_EzzV#

"z)EHHejj4PUP[P[K\]^		_
 37,,?Q%a(??!--;z**j8Jd>N>N 	 dE455/  	W&'LMSVV	W "" 	4 S*3	4 @s4   D	 $D& 9E		D#DD#&E5E

Ec                     d|j                   dS )NrB   r   r   r  r'  s     r   format_assistant_turnz,_AnthropicAgentAdapter.format_assistant_turnQ  s    #99r   c                J    |D cg c]  }d|d   |d   d }}d|dgS c c}w )Ntool_resultr  r   )r   r  r   r   r   r   )r  resultsr  blockss       r   format_tool_resultsz*_AnthropicAgentAdapter.format_tool_resultsT  sI     
 #1]3CPQR[P\]
 
  F344	
s    Nrk   r1   r  r   rv   rm   r  r  r   rm   r  r   r  r  r'  r  r  r   r1  rm   r  r  
r  r	  r
  rY   r  r  r"  r   r.  r3  r   r   r   r  r  %  s%    K1+(68:5r   r  c                  @    e Zd ZdZd	dZd
dZddZddZddZddZ	y)_OpenAIAgentAdapterr   c                \    	 ddl m}  ||      S # t        $ r}t        d      |d }~ww xY w)Nr   r   r   r{   )r   r   r~   rr   )r  rk   r   r   s       r   r  z_OpenAIAgentAdapter.make_client_  s;    	T0 s##  	T&'IJPSS	Ts    	+&+c           
         |D cg c]0  }|j                  dd      t        |j                  dd            d2 c}S c c}w )Nr   r   r   r    r   )r   r1   r  s      r   r  z!_OpenAIAgentAdapter.init_messagesf  s8    _ghZ[vv.3quuYPR?S;TUhhhs   5=c           	     f    |D cg c]!  }d|d   |j                  dd      |d   dd# c}S c c}w )Nfunctionr   rQ  r    rT  r   rQ  
parameters)r   r@  rg  r  s      r   r"  z_OpenAIAgentAdapter.build_toolsi  sM     

  #fI#$55#;"#N"3

 
	
 

s   &.c                X   g }|r|j                  d|d       |j                  |       	 |j                  j                  j	                  |d||xs d       }|j                  d   }	|	j                  }
|
j                  xs d}g }g }t        |
dd       xs g D ]  }|j                  j                  xs d}	 t        j                  |      }|j                  |j"                  |j                  j$                  |d	       |j                  |j"                  d
|j                  j$                  |dd        |	j&                  }|dk(  s|rdn|dk(  rdn|}|xs d |d}t)        ||||      S # t
        $ r}t        t        |            |d }~ww xY w# t        j                   $ r i }Y w xY w)Nrw   r   r|   )rf   r}   rv   r$  r   r    r  z{}r%  r@  )r   	arguments)r	   r   r@  r  r*  r&  )r   r  )r   extendrx   r   r   r   r   r1   r   r   r   getattrr@  rD  r,   r-   r2   r	   r   finish_reasonr  )r  r   rv   rw   rf   r$  r   r'  r   choicer   r   r(  raw_tool_callstcraw_argsr  finishr*  r   s                       r   r   z_OpenAIAgentAdapter.createv  s   VDE8$	4;;**11$mt	 2 D ann{{ b!3d39r 	B{{,,4Hzz(+ LLr{{/?/?$OP!!ee"%'[[%5%5HM# 	 %%$4z F*J 	 ,$nEdE4551  	4 S*3	4 '' s)   -E+ 6F+	F4F		FF)(F)c                l    d|j                   d   d}|j                   d   r|j                   d   |d<   |S )NrB   r   r   r  r,  )r  r'  r   s      r   r.  z)_OpenAIAgentAdapter.format_assistant_turn  s9    (TXXi5HI88L! $ 6C
r   c                @    |D cg c]  }d|d   |d   d c}S c c}w )Nr  r  r   )r   tool_call_idr   r   )r  r1  r  s      r   r3  z'_OpenAIAgentAdapter.format_tool_results  s4     
 Q}-=!I,W
 	
 
s   Nr4  r5  r6  r7  r8  r9  r:  r   r   r   r<  r<  \  s'    K$i
$6L
r   r<  c                  @    e Zd ZdZd	dZd
dZddZddZddZddZ	y)_GoogleAgentAdapterr   c                n    	 ddl m} |j	                  |      S # t        $ r}t        d      |d }~ww xY w)Nr   r   r   r{   )r   r   r~   rr   r   )r  rk   r   r   s       r   r  z_GoogleAgentAdapter.make_client  sA    	Z. }}S}))  	Z&'OPVYY	Zs    	4/4c                    ddl m} g }|D ]p  }|j                  dd      }|dk(  rd}|j                  |j	                  ||j
                  j                  t        |j                  dd                  g	             r |S )
Nr   r   r   r   rB   rf   r   r    r   )r   r   r   r   r   r   r   r1   )r  rv   r   r   r   r   s         r   r  z!_GoogleAgentAdapter.init_messages  s    1 	sA55(D{"OOGOOgll>T>TUXYZY^Y^_hjlYmUn>o=pOqr		s
 r   c                    ddl m} |D cg c]5  }|j                  |d   |j                  dd      t	        |d               7 }}|j                  |      gS c c}w )	Nr   r   r   rQ  r    rT  rA  )function_declarations)r   r   FunctionDeclarationr   r  Tool)r  r   r   r!  declss        r   r"  z_GoogleAgentAdapter.build_tools  ss    1 
  ''vYEE-41!N2CD ( 
 
 59::
s   :Ac           	     V   ddl m} 	 |j                  j                  |||j	                  |xs d |d            }|j                  d   }	d}
g }t        |	j                  j                        D ]v  \  }}t        |dd       r|
|j                  z  }
t        |dd       }|2|j                  |j                   d	| |j                  t!        |j"                  xs i       d
       x |rdnd}t%        |
|||	j                        S # t
        $ r}t        t        |            |d }~ww xY w)Nr   r   r|   )r   r$  r   r   r    r   function_callz::r%  r  r&  )r   r   r   r   r   r   r   r1   
candidatesr3  r   r   rF  r   r   r   r*   r  r  )r  r   rv   rw   rf   r$  r   r'  r   candr   r(  ipartfcr*  s                   r   r   z_GoogleAgentAdapter.create  s2   1	4==11!44'-~&* 5  2 D q! !3!34 	gGAttVT*		!5B~ rwwir!$5rwwQUVXV]V]VcacQdef	g #z
dE4>>  	4 S*3	4s   4D 	D(D##D(c                    |j                   S r]   r,  r-  s     r   r.  z)_GoogleAgentAdapter.format_assistant_turn  s    xxr   c                L   ddl m} g }|D ]^  }	 t        j                  |d         }t        |t              sd|i}|j                  |j                  j                  |d   |             ` |j                  d|      gS # t        j                  t
        f$ r
 d|d   i}Y {w xY w)	Nr   r   r   r   	tool_name)r   r   r   r   )r   r   r,   r-   r2   	TypeErrorr/   r*   r   r   from_function_responser   )r  r1  r   r   r  payloads         r   r3  z'_GoogleAgentAdapter.format_tool_results  s    1 	eA3**Qy\2 gt,#W-LL<<!K.[b<cd	e V59:: (()4 3#Qy\23s   B   B#"B#Nr4  r5  r6  r7  r8  r9  r:  r   r   r   rQ  rQ    s%    K*
;?8;r   rQ  )r   r   r   _AGENT_ADAPTERSc                R    t         j                  |       }|t        d|  d      |S )zJReturn the agentic adapter for provider_id, or raise AssistantUnavailable.z0Agentic tool-use is not available for provider 'rq   )rf  r   rr   )rY   adapters     r   _get_agentic_adapterri    s7    !!+.G">{m2N
 	
 Nr   c                   |xs i j                  dd      }t        t              D ]  }| j                  |||||      }	|	j                  dk7  s|	j
                  s|	j                  c S |j                  | j                  |	             g }
g }|	j
                  D ]0  }|d   t        v r|j                  |        |
j                  |       2 g }|
D ]C  }t        |d   |d   |      }|j                  |d   |d   t        j                  |      d       E |r|d   }t        |d      } |d	   |d         }|d
d D cg c]  }|d   |d   d }}t        |d   |d   |||d   ||||| j                  |      }t        ||d   |d   ||	j                        |j!                  | j#                  |              yc c}w )a  Run the provider-agnostic agentic tool-calling loop.

    Executes Tier-0 tools inline.  Raises PendingActionRequired when the model
    calls a Tier-1 (mutating) tool so the caller can request user confirmation.
    Raises AssistantError on provider API failure.
    r  r    r  r   r  r	   r  rb  r   r   rn  r   N)r  rb  )r  r  rM   r  r  r  rw   rf   r  rY   r  rm  r  r  rm  display_argspartial_replyz0

[Response truncated: tool-call limit reached.])r   range_AGENTIC_MAX_ROUNDSr   r   r  r   r   r.  r  r  r,   r;   r  rY   PendingActionRequiredrE  r3  )rh  r   r  rw   rf   r$  r   rM   _roundr'  tier0_callstier1_callscallr  r   t1r  rm  cr  r  s                        r   _agentic_looprx    s    *"!!'2.J+, ;L~~f&6uMz)99 	 = =d CDOO 	)DF|//""4(""4(		)  	D"4<gEF  #Dz!&\::f-" 	  QB#BvJ/D44 34R[AL %QR !"$ai@K  $Z[%!1tH+!&#//'E (Z!.1)"ii  	 ; ;M JKw;Lz @7s   =F>c                <   t         j                         D cg c]  \  }}t        ||       rt        |       }}}t        D cg c]  }t        ||       s| }}|r1dj                  |      }d| d}|r|dz   |z   j                         n|}||fS c c}}w c c}w )a_  Build the authorized JSON-Schema tool specs + confirmation preamble.

    dispatch_tool() still enforces authz per-call; restricting the surfaced tool
    list just prevents the model from requesting tools the user can't use.
    Returns (tool_specs, system) where system has the Tier-1 preamble prepended
    when any mutating tool is available.
    z, uH   IMPORTANT — mutating tools: when you call any of the following tools (a  ), the user will see a confirmation prompt before the action executes.  If they decline you will receive {"cancelled": true, "message": "Action cancelled by user."} as the tool result; acknowledge this gracefully.  Propose at most one mutating action per response turn.z

)r  r   r  r  r  r   rg   )	r   rw   r  r  
tool_specsr  tier1_names	names_strpreambles	            r   _build_agentic_contextr~  X  s     ,113GT7D) 	4J  #3Q3oc46P3QKQIIk*	{ EE 	 :@(V#f,335Xv' Rs   !BBBc          	     
   t        ||      \  }}}|st        d      t        |      }|j                  |      }t	        ||      \  }	}|j                  |	      }
|j                  t        |             }t        ||||||
|      S )u  Agentic chat: provider-agnostic tool-use loop with per-tool authz and audit.

    Resolves the provider from the user's selected default model and runs the
    tool-use loop on Claude, Gemini, or OpenAI.  Tier-0 (read-only) tools execute
    inline.  Tier-1 (mutating) tools pause the loop and raise PendingActionRequired;
    call resume_agentic() after the user decides.

    user:          session["user"] dict — used for per-tool authorization.
    user_settings: caller's user-settings dict — used for key resolution.
    Raises AssistantUnavailable / AssistantError on API failure.
    Raises PendingActionRequired when a mutating tool awaits user confirmation.
    ro   )	rl   rr   ri  r  r~  r"  r  r  rx  )rv   rw   r   rD   rf   rY   rk   rh  r   rz  r$  r  s               r   chat_agenticr  v  s    ( 2-GE;"#;<<";/G  %F/f=J
+E,,T(^<&*:FE5RVWWr   c           
     0   |xs i j                  dd      }t        | |      }|y|d   }|d   }|d   }|d   }	|}
|d	   }|d
   }|d   }|d   }|j                  dt              }|j                  d      xs g }t        j                  t
        j                        j                         }||||dd}|sd|d<   t        |       ddd}nPt        j                  |      }|d|d<   t        |       dd| di}n 	  |||      }d|v rdnd|d<   t        |       t        |      |	|
t        j                  |      dgz   }|D ]3  }|j                  |d   |d   t        j                  ddd      d       5 t        |      }t!        ||      }|st#        d       |j%                  |      }t        |      }|j'                  |j)                  |             t+        |||||||      S # t        $ r!}d| |d<   t        |       ddi}Y d}~d}~ww xY w)!a  Resume the agentic loop after the user confirms or cancels a Tier-1 action.

    Retrieves the pending conversation state (including the provider id +
    provider-native working messages), executes (or skips) the Tier-1 tool, and
    continues the loop until the model emits a final reply.

    Raises AssistantUnavailable / AssistantError on API failure.
    Raises PendingActionRequired if the resumed loop hits another Tier-1 tool.
    r  r    NzKThis action has expired or was already handled. Please start a new request.r  r  r  r  r  rw   rf   r  rY   r  Tr  cancelled_by_userr  zAction cancelled by user.)	cancelledr   no_callbackr  zNo handler registered for 'z'. Contact your administrator.r  r  r  r  rk  rb  z=Not executed: only one mutating action is processed per turn.ro   )r   r  r   r   r  r   r  r  rJ  r  r   r  r,   r;   r   ri  rZ   rr   r  rE  r3  rx  )r  	confirmedr   rD   rM   rH  r  r  working_msgsr  rb  r  rw   rf   r$  rY   r  r  r  tier1_resultcbr   combined_resultsextrarh  rk   r   resumed_messagess                               r   resume_agenticr    s    *"!!'2.JUJ/E}\FmDFmD./LM*KIO,MHoFGnE./Eii/?@Kii.4"K	hll	#	-	-	/BE .ie+/<WX!!$':,E)% #'B4&Hf%ghLC!$~4;|4K=QZi e$ M*"::l+/ . 
   /{+zz!Z# !
 	 #;/G

4C"#;<<  %FL)G778HIJ&*:FE5RVWWA  C%4SE#:i e$ ')ABCs   =G+ +	H4HHc                J    | sdddS t        |       dk\  r| dd nd}dd	| dS )
zCReturn a safe public representation of a key (never the raw value).Fr    )
configuredmaskedr   Nz****Tu   ••••)r#  )rk   last4s     r   mask_keyr    s9    #r22CACH6EL*@AAr   c           	     r    | xs i }t         D ci c]  }|t        |j                  |             c}S c c}w )zVReturn masked representation of a per-provider keys dict for all registered providers.)r   r  r   )	keys_dictsrcr_   s      r   	mask_keysr    s1    
/rC3FGCC#''#,''GGGs   "4c           
     t    | xs i }t         D ci c]  }|t        |j                  |d              c}S c c}w )zJReturn {provider_id: bool} for every registered provider, defaulting True.T)r   r0   r   )enabled_dictr  r_   s      r   provider_enabled_mapr    s5    

"C5HIcCcggc4())IIIs   #5c                R    | | n	t               }t        |j                  d            S )zIReturn the org-level enabled map (admin 'available system-wide' toggles).r(   )r6   r  r   )rO   r   s     r   org_provider_enabled_mapr    s'    ".*4K4MC(: ;<<r   c                L    t        |       }t        |j                  d            S )z:Return the user-level enabled map (user 'active' toggles).r(   )rE   r  r   )rD   rP   s     r   user_provider_enabled_mapr    s!    	M	*B'9 :;;r   c           
        t               }t        |       }t        |       }|j                  d      xs |j                  d      xs t        }g }t
        j                         D ]t  \  }}|j                  |d      r|j                  |d      s+t        | |      s8|j                  dg       D ](  }|j                  ||d   |d   |d   |d   |k(  d       * v ||dS )	a.  List the models a user can pick per-invocation for chat.

    A model is offered only when its provider is org-enabled AND user-enabled AND
    has an effective key (user or org). Returns {models: [...], default_model}.
    Each model carries provider id/label so the picker can group by provider.
    r   rf   Tr   r
   r	   )rY   provider_labelr	   r
   default)r   r   )	r  r  rE   r   rh   r   r   rZ   r   )	rD   org_enuser_enrP   r   r   r_   r   r   s	            r   available_modelsr     s     &'F'6G	M	*BFF?+OrvvgO-MF(..0 	T

3%'++c4*@mS1(B' 	AMM""&w-g7T7m3 	 }==r   c           	     J   | | n	t               }t        |j                  d      xs d      j                         }g }t        j                         D ]G  \  }}t        |      s|j                  dg       D ]!  }|j                  ||d   |d   |d   d       # I ||t        dS )a"  List models an admin can pick for title-block OCR.

    A model is offered only when its provider has an org-level key configured
    (OCR always runs on the org key). Returns {models: [...], selected} where
    ``selected`` is the saved ``ocr_model`` ("" means the Anthropic default).
    r)   r    r   r
   r	   )rY   r  r	   r
   )r   selectedr   )	r6   r1   r   rg   r   r   r   r   rh   )rO   r   r  r   r_   r   r   s          r   org_ocr_modelsr  <  s     #.*4K4MC377;'-2.446HF(..0 		T)#.(B' 	AMM""&w-g7	 		 (]SSr   c                      e Zd ZdZy)rr   zFRaised when the assistant cannot be used (not configured / gated out).Nr  r	  r
  r  r   r   r   rr   rr   W  s    Pr   rr   c                      e Zd ZdZy)r   z;Raised on API or provider errors during a chat/vision call.Nr  r   r   r   r   r   [  s    Er   r   c                  6    e Zd ZdZdZ	 d	 	 	 	 	 	 	 	 	 	 	 ddZy)rq  a  Raised by the agentic loop when a Tier-1 tool awaits user confirmation.

    Attributes:
        token:        UUID string identifying the stored conversation state.
        tool:         Tier-1 tool name.
        display_name: Human-readable action label for the confirmation card.
        display_args: Key-value pairs to render in the confirmation card.
        partial_reply: Any model text emitted before the tool call (may be "").
    rl  c                J    || _         || _        || _        || _        || _        y r]   rl  )r  r  r  rm  rm  rn  s         r   r  zPendingActionRequired.__init__k  s*     
	((*r   N)r    )r  r1   r  r1   rm  r1   rm  r*   rn  r1   r  r  r  r   r   r   rq  rq  _  sS     SI  ++ + 	+
 + + 
+r   rq  )r   r1   r  r1   )r   r1   r  r0   )r  r*   )r4   r*   r  r  )rD   r*   r  r*   )rD   r*   rB   r*   r  r*   )rM   r1   rN   r0   rD   r*   r  r0   )rD   r*   rY   r1   r  
str | None)rD   r*   r  r0   )rD   r*   r  r1   r]   )rD   r*   ri   r  r  tuple[str, str, str | None])
rv   rm   rw   r1   rD   r*   rf   r  r  r1   )
rv   rm   rw   r1   rf   r1   rk   r1   r  r1   )r  r  )r   bytesr   	list[str]r  zdict[str, str])r   r  r   r  r  r*   )rY   r1   r   r1   rk   r1   r   r  r   r1   r  r1   )
r   r  r   r1   rf   r1   rk   r1   r  r1   )r   r  r  r  )r   r  r   r1   r   list[dict] | NonerD   r*   rf   r  r  r1   )rD   r*   r$  r1   r  r  )r4  list[bytes]r   r1   r   r  rD   r*   rf   r  r  r1   )r4  r  r   r1   r   r  rf   r1   rk   r1   r  r1   )rY   r1   r  r  )rH  r*   r  r  )r  r1   r  r   r  r  )r  r1   r   r*   r  r0   )r   r*   r  r0   )r  r*   r  r*   r  r*   )r  r*   r  r*   r  r*   )r  r1   r  r*   r   r*   r  r*   )r  r  )r  r1   r  r*   rM   r1   r  r  r  r1   r  r  rw   r1   rf   r1   r  r   rY   r1   r  r  r  r1   )r  r1   rM   r1   r  dict | None)r  r   r  r*   )r  r*   r  r*   )r  r   r  r   )rY   r1   r  r   )rh  r   r   r   r  r  rw   r1   rf   r1   r$  r   r   r*   r  r1   )r   r*   rw   r1   r  ztuple[list, str])rv   rm   rw   r1   r   r*   rD   r*   rf   r  r  r1   )
r  r1   r  r0   r   r*   rD   r*   r  r1   )rk   r  r  r*   )r  r  r  r*   )r  r  r  r*   )rO   r  r  r*   )vr  
__future__r   r,   osrer=   	threadinguuidr  r   r   pathlibr   typingr   r   __annotations__r   rh   r   r   environr   __file__resolveparentr   r$   r6   r@   rE   rI   rS   rV   rZ   rU   rd   rl   rx   ru   rs   rt   r   r   r   r   r   r   r   r   r  r  r  r!  r%  r  r1  r2  r   r  r  r  r  r1   rB  compiler  LockrF  rJ  r`  r   ra  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  rp  r  r  r  r  r  r<  rQ  rf  ri  rx  r~  r  r  r  r  r  r  r  r  r  RuntimeErrorrr   r   r   rq  )ds   0r   <module>r     sC
   #  	 	    '   )$/@A&1DE.9KL

 -A
 %#.>?%0BC%0BC

 ,<
 y1h/y1

 ":
/"( _ "H  # JJNN!X ''*CC   . N 8?6
.1( <L *R
6#* Y] 000"%0140;>00(00"%0140;>00B00"%0140;>00B&(-fd @6FHH #H*-H<AHKNHH(0D0<0<    6 "&f fff f
 f f 	f&F "&`0 `0`0`0 `0
 `0 `0 	`0F;0;0;0 ;0
 ;0 
;0 	;0|:0:0:0 :0
 :0 
:0 	:0z 3C : BJJNN=5QRSBJJNN?5KLMBJJNN95HIJH~%%'..JJNN#S4F)F%GH BJJFG inn &}5$? ! (B &J
0 +`  
 !2 ! ('2 X 

, $) ! ('J" )M	
sM% / Mb !1 7 7 9%Ao% z  .}=-FH
 ! ('p &J	
2 .}=2

 'H ! !)'D
 !)'R#	 &|4
> &}5/F$H ! ('H &J	
0 .}=7
 *H
 ! !)'^
 !)'\$
 !)'\%
 !*'p#$ F)
'R .
 %H ! !)'z /7G]%^.6G_%`.6G`%a.6G_%`	 &J
 mW% / Wt #K%5"J9I"J J $& . %#
. E3( "84 +8$*	" '3b %' / &	   &
&
& & 	&
 & & & & & & & 	&RL  $$ !*45 45nJ
 J
ZJ; J;\ ()!#!## N@N@N@ N@ 	N@
 N@ N@ N@ 	N@bH XXX 	X
 X X 	XDXXXXXX XX 	XX
 	XX~BHJ=<>8T6Q< QF\ F+I +q!%s   )S(