
    yjr                        d dl Z d dlmZ d dlmZ d dlmZmZmZm	Z	m
Z
mZ d dlmZ d dlmZ d dlmZ d dlmZmZmZ d d	lmZ d d
lmZ d dlmZ d dlmZ d dlmZ d dl m!Z!m"Z"m#Z# d dl$m%Z%m&Z& d dl'm(Z(m)Z)m*Z* d dl+m,Z, d dl-m.Z. d dl/m0Z0 d dl1m2Z2 d dl3m4Z4  e j5        e6          Z7 eddg          Z8dddede9de9dz  de9de9de9dz  d e:dz  d!e;dz  d"e<d#e:dz  d$e=e;         dz  d%e.fd&Z>dede9de9de9d%e=e9         dz  f
d'Z?dede9d(e9d)e:d*e<d%e@ejA        dz  e=ejB                 f         fd+ZCdede9d(e9d%e@ejA        dz  ejA        dz  f         fd,ZDdede9d(e9d-e:d)e:d%e=ejB                 fd.ZEd/ejA        dz  d0ejA        dz  d)e:d*e<d%e@ejA        dz  e:e:f         f
d1ZFe8G                    d2eejH                  e e&d3                    g4           e	d5           edd67          efde9d8ejI        dz  defd9            ZJe8G                    d:ejH        ;           e	d5           ed5d<7           e e&                      efd=ede9d>ejK        d?e%def
d@            ZLe8M                    dAejH         e e&dd(B                    g4           e	d5           e	d5           ed5dC7          efde9d(e9d>ejN        defdD            ZOe8P                    dAdE e e&dd(B                    gF           e	d5           e	d5          efde9d(e9defdG            ZQe8G                    dHejH        dI e e&dd(B                    gJ           e	d5           e	d5           e
ddK7          efde9d(e9dLe9dz  defdM            ZRe8G                    dNejH         e e&dd(B                    g4           e	d5           e	d5           ed5dO7          efde9d(e9dPeSe9ejT        f         defdQ            ZUe8M                    dNejH         e e&dd(B                    g4           e	d5           e	d5           ed5dR7          efde9d(e9dPeSe9ejT        f         defdS            ZVe8P                    dNejH         e e&dd(B                    g4           e	d5           e	d5           ed5dT7          efde9d(e9dPe=e9         defdU            ZWe8X                    dVejT         e e&dd(B                    g4           e	d5           e	d5           e	d5          efde9d(e9dWe9defdX            ZYe8M                    dVdYd e e&dd(B                    gZ           e	d5           e	d5           e	d5           ed5d[7          efde9d(e9dWe9d\ejT        def
d]            ZZe8X                    dNeej[                  e e&dd(B                    g4           e	d5           e	d5          efde9d(e9defd^            Z\e8X                    d_ej]         e e&dd(B                    g4           e	d5           e	d5          e e
dej^        j_        d`ej^        j_         dab          f e
ddc7           e
dddedfg           e
ddh7           e
ddi7           e
djdkl           e
ddmdndop           e
ddqdrdsp           e
djdtl           e
ddmdndup          dv	de9d(e9dedwe:dz  dxe9dz  d*e<dye9dz  dze9dz  d{e<d e:dz  d!e;dz  d|e<d}e:dz  fd~            Z`e8X                    deja         e e&dd(B                    g4           e	d5           e	d5          efde9d(e9ded%eja        fd            Zbe8G                    de=ejB                  e e&dd(B                    g4           e	d5           e	d5           ed5d7          fde9d(e9dejc        fd            ZddS )    N)suppress)perf_counter)	APIRouterBodyDependsPathQueryResponse)Page)	apaginate)AsyncSession)configcrudschemas)safe_cache_delete)session_cache_key)db)enqueue_deletion)embedding_client)AuthenticationExceptionResourceNotFoundExceptionValidationException)	JWTParamsrequire_auth)EmbeddingCallPurposeGetContextEventemit)
summarizer)Representation)search)estimate_tokens)embedding_call_purposez#/workspaces/{workspace_id}/sessionssessions)prefixtags)	embeddingr   workspace_idlast_messageobserverobservedsession_namesearch_top_ksearch_max_distanceinclude_most_derivedmax_observationsr&   returnc                   K   t          j        || ||||||||
|	|	nt          j        j        j        dt          j                   d{V S )az  
    Get working representation using an externally-provided DB session.

    Args:
        db: Database session to use for queries
        workspace_id: The workspace identifier
        last_message: Optional last message for semantic query
        observer: Name of the observer peer
        observed: Name of the observed peer
        session_name: Optional session to filter by
        search_top_k: Number of semantic-search-retrieved observations to include in the representation
        search_max_distance: Maximum distance to search for semantically relevant observations
        include_most_derived: Whether to include the most derived observations in the representation
        max_observations: Maximum number of observations to include in the representation
        embedding: Pre-computed embedding for the semantic query

    Returns:
        The working representation
    Napi)workspace_namer   r)   r*   r+   include_semantic_querysemantic_search_top_ksemantic_search_max_distancer.   r&   r/   parent_categoryembedding_purpose)r   get_working_representationr   settingsDERIVER'WORKING_REPRESENTATION_MAX_OBSERVATIONSr   SESSION_CONTEXT_SEARCH)r   r'   r(   r)   r*   r+   r,   r-   r.   r/   r&   s              </DATA/AppData/hermes/projects/honcho/src/routers/sessions.py _get_working_representation_taskr?   %   s      B 0#!+*%81' *)_$L.E             c                B   K   t          j        | |||           d{V S )a;  
    Get peer card using an externally-provided DB session.

    Args:
        db: Database session to use for queries
        workspace_id: The workspace identifier
        observer: Name of the observer peer
        observed: Name of the observed peer

    Returns:
        The peer card or None if not found
    )r3   r)   r*   N)r   get_peer_card)r   r'   r)   r*   s       r>   _get_peer_card_taskrC   Y   sL      & #
#	         r@   
session_idtoken_limitinclude_summaryc                 j   K   t          j        | ||||           d{V \  }}d |D             }||fS )aZ  
    Get session context

    Args:
        db: Database session to use for queries
        workspace_id: The workspace identifier
        session_id: The session identifier
        token_limit: Maximum tokens for the context
        include_summary: Whether to include summary if available

    Returns:
        Tuple of (summary, messages)
    )r3   r+   rE   rF   Nc                 L    g | ]!}t           j                            |          "S  r   Messagemodel_validate.0msgs     r>   
<listcomp>z-_get_session_context_task.<locals>.<listcomp>   s(    OOOsw55c::OOOr@   )r   get_session_context)r   r'   rD   rE   rF   summarymessagesmessage_schemass           r>   _get_session_context_taskrU   t   sr      ( )<
#'        GX POhOOOOO##r@   c                    K   t          j        | ||           d{V \  }}|rt          j        |          nd}|rt          j        |          nd}||fS )z
    Fetch both short and long summaries.

    Returns:
        Tuple of (short_summary, long_summary) as Pydantic schemas.
    r3   r+   N)r   get_both_summariesto_schema_summary)r   r'   rD   	short_rawlong_rawshortlongs          r>   _get_both_summaries_taskr^      s       !+ =
<j! ! !      Ix 8AJJ(333dE5=G:'1114D$;r@   start_idc                 l   K   |dk    rg S t          j        | ||||           d{V }d |D             S )a  
    Fetch messages for context.

    Args:
        db: Database session to use for queries
        workspace_id: The workspace identifier
        session_id: The session identifier
        start_id: Internal message PK to start from (messages after summary coverage)
        token_limit: Maximum tokens for the messages

    Returns:
        List of messages as Pydantic schemas
    r   )r_   rE   Nc                 L    g | ]!}t           j                            |          "S rI   rJ   rM   s     r>   rP   z2_get_messages_for_context_task.<locals>.<listcomp>   s(    DDDCGO**3//DDDr@   )r   get_messages_id_range)r   r'   rD   r_   rE   rS   s         r>   _get_messages_for_context_taskrc      ss      ( a	/
        H ED8DDDDr@   short_summarylong_summaryc                     |r|dk    rddt          |d          fS t          |dz            }|r|j        nd}| r| j        nd}|r||k    r||k    r||j        ||z
  fS | r||k    r|dk    r| | j        ||z
  fS dd|fS )a  
    Pick the best summary that fits within the token budget using 40/60 allocation.

    Args:
        short_summary: The short summary, or None
        long_summary: The long summary, or None
        token_limit: Total token budget for summary + messages
        include_summary: Whether summaries should be considered

    Returns:
        Tuple of (chosen_summary, messages_start_id, messages_token_budget)
    r   Ng?)maxinttoken_count
message_id)rd   re   rE   rF   summary_budgetlong_len	short_lens          r>   _select_summary_for_contextrn      s    $  ,kQ..QK++++s*++N+7>|''QH-:A))I 
N22x)7K7K#("
 	

  
n44Q$)#
 	
 Kr@   z/list)r3   )response_modeldependencies.z6Filtering and pagination options for the sessions list)descriptionoptionsc                    K   d}|r&t          |d          r|j        r|j        }|i k    rd}t          |t          j        | |           d{V            d{V S )zBGet all Sessions for a Workspace, paginated with optional filters.Nfilters)r3   rt   )hasattrrt   r   r   get_sessions)r'   rr   r   filter_params       r>   rv   rv      s       L  77I..  7?  2L
$#<VVVVVVVVV        r@    )ro   zSession creation parametersresponsesession
jwt_paramsc           	      0  K   |j         s!|j        |j        |k    rt          d          |j        r.|j         s&|j        |j        |j        k    rt          d          n"|j        st          d          |j        |_        	 t          j        |||           d{V }|j        rdnd| _        |j	        S # t          $ rT}t                              d|j         dt          |                      t          t          |                    |d}~ww xY w)	z
    Get a Session by ID or create a new Session with the given ID.

    If Session ID is provided as a parameter, it verifies the Session is in the Workspace.
    Otherwise, it uses the session_id from the JWT for verification.
    NzUnauthorized access to resourcez.Session ID not found in query parameter or JWT)r3   rz         z Failed to get or create session : )adwr   namesr   get_or_create_sessioncreatedstatus_coderesource
ValueErrorloggerwarningstrr   )ry   r'   rz   r{   r   resultes          r>   r   r   
  s[     ( = IZ\5*,,:V:V%&GHHH | $	M(,,)*KLLL| 	)@   "|11|W
 
 
 
 
 
 
 
 
 '-n=ss# 1 1 1R',RR#a&&RRSSS!#a&&))q01s   3B7 7
DADDz/{session_id}rW   zUpdated session parametersc           	         K   	 t          j        || ||           d{V }|S # t          $ rB}t                              d| dt          |                      t          d          |d}~ww xY w)z1Update a Session's metadata and/or configuration.)r3   r+   rz   NzFailed to update session r   Session not found)r   update_sessionr   r   r   r   r   )r'   rD   rz   r   updated_sessionr   s         r>   r   r   <  s       D $ 3|*g!
 !
 !
 
 
 
 
 
 
  D D DI:IIQIIJJJ'(;<<!CDs   $ 
A0=A++A0   )r   rp   c           	        K   	 t          j        |||            d{V }d|_        t          | d||           d{V   |j                     d{V  t          t          | |                     d{V  t                              d|           ddiS # t          $ rB}t          
                    d| d	t          |                      t          d
          |d}~ww xY w)a  
    Delete a Session and all associated messages.

    The Session is marked as inactive immediately and returns 202 Accepted. The actual
    deletion of all related data happens asynchronously via the queue with retry support.

    This action cannot be undone.
    NFrz   )r3   deletion_typeresource_id
db_sessionz0Session %s marked as inactive, deletion enqueuedmessagezSession deleted successfullyzFailed to delete session r   r   )r   get_session	is_activer   commitr   r   r   debugr   r   r   r   )r'   rD   r   rz   r   s        r>   delete_sessionr   V  sH     (D(ZFFFFFFFF! '#"	
 
 
 	
 	
 	
 	
 	
 	
 	
 bikk 1,
 K KLLLLLLLLLGTTT9:: D D DI:IIQIIJJJ'(;<<!CDs   BB 
C#!=CC#z/{session_id}/cloner}   )ro   r   rp   z"Message ID to cut off the clone atrj   c           	        K   	 t          j        || ||           d{V }t                              d|           |S # t          $ rB}t                              d| dt          |                      t          d          |d}~ww xY w)z8Clone a Session, optionally up to a specific message ID.)r3   original_session_namecutoff_message_idNzSession %s cloned successfullyzFailed to clone session r   r   )r   clone_sessionr   r   r   r   r   r   )r'   rD   rj   r   cloned_sessionr   s         r>   r   r     s      "D#1'",(	 
  
  
 
 
 
 
 
 
 	5zBBB D D DH*HHAHHIII'(;<<!CDs   :? 
B	=BBz/{session_id}/peerszIList of peer IDs (with session-level configuration) to add to the sessionpeersc           	        K   	 t          j        |t          j        ||          |            d{V }|j        S # t
          $ rB}t                              d| dt          |                      t          d          |d}~ww xY w)zWAdd Peers to a Session. If a Peer does not yet exist, it will be created automatically.)r   r   rz   r3   NzFailed to add peers to session r   r   )
r   r   r   SessionCreater   r   r   r   r   r   r'   rD   r   r   r   r   s         r>   add_peers_to_sessionr     s      "D1)   (
 
 
 
 
 
 
 
 
  D D DOOOs1vvOOPPP'(;<<!CDs   7< 
B=BBzJList of peer IDs (with session-level configuration) to set for the sessionc           	        K   	 t          j        || ||           d{V  t          j        |t          j        |          |            d{V }t
                              d|           |j        S # t          $ rB}t
          	                    d| dt          |                      t          d          |d}~ww xY w)	z
    Set the Peers in a Session. If a Peer does not yet exist, it will be created automatically.

    This will fully replace the current set of Peers in the Session.
    r3   r+   
peer_namesNr   r   z%Set peers for session %s successfullyz Failed to set peers for session r   zFailed to set peers for session)r   set_peers_for_sessionr   r   r   r   r   r   r   r   r   r   r   s         r>   set_session_peersr     s     *R('#	
 
 
 	
 	
 	
 	
 	
 	
 	
 1)z:::'
 
 
 
 
 
 
 
 

 	<jIII R R RP*PPAPPQQQ'(IJJPQQRs   A/A4 4
C >=B;;C z+List of peer IDs to remove from the sessionc           	        K   	 t          j        || |t          |                     d{V  t          j        |t	          j        |          |            d{V }t                              d|           |j        S # t          $ rB}t          
                    d| dt          |                      t          d          |d}~ww xY w)	z"Remove Peers by ID from a Session.r   Nr   r   z*Removed peers from session %s successfullyz$Failed to remove peers from session r   r   )r   remove_peers_from_sessionsetr   r   r   r   r   r   r   r   r   r   r   s         r>   r   r     s!      D,'#5zz	
 
 
 	
 	
 	
 	
 	
 	
 	
 1)z:::'
 
 
 
 
 
 
 
 

 	A:NNN D D DTjTTCPQFFTTUUU'(;<<!CDs   A<B 
C=CCz$/{session_id}/peers/{peer_id}/configpeer_idc                 B   K   t          j        || ||           d{V S )z.Get the configuration for a Peer in a Session.)r3   r+   r   N)r   get_peer_config)r'   rD   r   r   s       r>   r   r     sL       %
#	         r@      )r   ro   rp   zNew peer configurationr   c                 &  K   	 t          j        || |||           d{V  t                              d||           dS # t          $ rE}t                              d| d| dt          |                      t          d          |d}~ww xY w)z.Set the configuration for a Peer in a Session.)r3   r+   	peer_namer   Nz1Set peer config for %s in session %s successfullyzFailed to set peer config for z in session r   r   )r   set_peer_configr   r   r   r   r   r   )r'   rD   r   r   r   r   s         r>   r   r   &  s       D"'#
 
 
 	
 	
 	
 	
 	
 	
 	
 	?*	
 	
 	
 	
 	
  D D DXWXX*XXPSTUPVPVXX	
 	
 	
 ((;<<!C	Ds   ;A 
BA BBc           	      
  K   	 t          j        | |           d{V }t          ||           d{V S # t          $ rB}t                              d| dt          |                      t          d          |d}~ww xY w)z2Get all Peers in a Session. Results are paginated.rW   Nz!Failed to get peers from session r   r   )r   get_peers_from_sessionr   r   r   r   r   r   )r'   rD   r   peers_queryr   s        r>   get_session_peersr   H  s      D 7'j
 
 
 
 
 
 
 
 
 r;///////// D D DQ:QQQQQRRR'(;<<!CDs   16 
B =A==Bz/{session_id}/contextzNumber of tokens to use for the context. Includes summary if set to true. Includes representation and peer card if they are included in the response. If not provided, the context will be exhaustive (within z tokens))lerq   z>A query string used to fetch semantically relevant conclusionsTzIWhether or not to include a summary *if* one is available for the sessionrR   )defaultrq   aliasa  The target of the perspective. If given without `peer_perspective`, will get the Honcho-level representation and peer card for this peer. If given with `peer_perspective`, will get the representation and card for this peer *from the perspective of that peer*.zA peer to get context for. If given, response will attempt to include representation and card from the perspective of that peer. Must be provided with `peer_target`.FzOnly used if `search_query` is provided. Whether to limit the representation to the session (as opposed to everything known about the target peer))r   rq      d   z}Only used if `search_query` is provided. The number of semantic-search-retrieved conclusions to include in the representation)ger   rq   g        g      ?zmOnly used if `search_query` is provided. The maximum distance to search for semantically relevant conclusionszoOnly used if `search_query` is provided. Whether to include the most frequent conclusions in the representationzkOnly used if `search_query` is provided. The maximum number of conclusions to include in the representation)	search_queryrF   peer_targetpeer_perspectivelimit_to_sessionr,   r-   include_most_frequentmax_conclusionstokensr   r   r   r   r   r   c       	         b  K   ||nt           j        j        }t                      }|r|st	          d          |st          || |||           d{V \  }}t          j        |||          }t          t          | d||t          |          |du|du|	|
||||t                      |z
  dz                       |S |p|}|}d}|rt          t                    5  t          t          j        j        | d          5  t#          j        |           d{V }ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   t'          || ||||r|nd|	|
|||	           d{V }t)          || ||
           d{V }t+          || |           d{V \  }}|t-          t/          |                    z
  t-          |          z
  }t1          ||||          \  }}}t3          || |||           d{V }t          j        ||||                                |          }t          t          di d| ddd|d|d|d|dt          |          d|dudt7          |j                  d|dud|dud|	d|
d|d|d|d|d|dudt                      |z
  dz             |S ) a  
    Produce a context object from the Session. The caller provides an optional token limit which the entire context must fit into.
    If not provided, the context will be exhaustive (within configured max tokens). To do this, we allocate 40% of the token limit
    to the summary, and 60% to recent messages -- as many as can fit. Note that the summary will usually take up less space than
    this. If the caller does not want a summary, we allocate all the tokens to recent messages.
    Nz<peer_target must be provided if peer_perspective is provided)r   rS   rR   rz   i  )r3   context_scoper+   tokens_requestedmessage_counthas_summarysearch_query_providedr,   r-   r   r   rF   r   total_duration_msr2   )r3   r7   )r)   r*   r+   r,   r-   r.   r/   r&   )r)   r*   )r   rS   rR   peer_representation	peer_cardr3   r   r+   r   target_namer   r   r   has_representationhas_peer_cardr   r,   r-   r   r   rF   r   peer_perspective_providedr   rI   )r   r:   GET_CONTEXT_MAX_TOKENSr   r   rU   r   SessionContextr   r   lenr   	Exceptionr"   r   r=   valuer   embedr?   rC   r^   r!   r   rn   rc   format_as_markdownboolr   )r'   rD   r   r   r   rF   r   r   r   r,   r-   r   r   rE   context_startedrR   rS   ry   r)   r*   r&   representationcardrd   re   adjusted_limitmessages_start_idmessages_budgets                               r>   rQ   rQ   _  sK     H $&/*P  #nnO 
 
!J
 
 	
  ";j+#
 #
 
 
 
 
 
 
 )
 
 

 	+''!'!(mm#4/&2$&>)$7&; / /!1#/>>O#Ct"K  	
 	
 	
$ .;HH %)I 	CY	C 	C"$;A+ %  	C 	C /4\BBBBBBBBI	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C <
#3=ZZ!/2(        N %
L8h        D )A
L*) ) # # # # # #M< 	oc.&9&9:::_T=R=RR 
 3N|^_3 3/G
 4
L*&7       H %*==??  H 	 	
 	
 	
'<	
#)	
 $	
 h		

 !	
 $V	
 h---	
  t++	
  $H$@AAA	
 d**	
 #/d":":	
 &	
 !4 3	
 #8"7	
 ,O	
  ,O!	
" .-#	
$ '7d&B&B%	
&  ,~~?4GG'	
  . Os6   "D;=D$D;$D(	(D;+D(	,D;;D?D?z/{session_id}/summariesc                    K   t          j        || |           d{V \  }}d}|rt          j        |          }d}|rt          j        |          }t          j        |||          S )z
    Get available summaries for a Session.

    Returns both short and long summaries if available, including metadata like
    the message ID they cover up to, creation timestamp, and token count.
    rW   N)r   rd   re   )r   rX   rY   r   SessionSummaries)r'   rD   r   rd   re   short_summary_schemalong_summary_schemas          r>   get_session_summariesr     s      & )3(E
#) ) ) # # # # # #M<   K);MJJ I(:<HH#*(   r@   z/{session_id}/searchzMessage search parametersbodyc                 p   K   |j         pi }| |d<   ||d<   t          |j        ||j                   d{V S )zh
    Search a Session with optional filters. Use `limit` to control the number of results returned.
    r'   rD   )rt   limitN)rt   r    queryr   )r'   rD   r   rt   s       r>   search_sessionr   H  sj      $ l bG*GN&GL
j         r@   )elogging
contextlibr   timer   fastapir   r   r   r   r	   r
   fastapi_paginationr   !fastapi_pagination.ext.sqlalchemyr   sqlalchemy.ext.asyncior   srcr   r   r   src.cache.clientr   src.crud.sessionr   src.dependenciesr   src.deriver.enqueuer   src.embedding_clientr   src.exceptionsr   r   r   src.securityr   r   src.telemetry.eventsr   r   r   	src.utilsr   src.utils.representationr   src.utils.searchr    src.utils.tokensr!   src.utils.typesr"   	getLogger__name__r   routerr   rh   floatr   listr?   rC   tupleSummaryrK   rU   r^   rc   rn   postSession
SessionGetrv   r   r   putSessionUpdater   deleter   r   dictSessionPeerConfigr   r   r   getr   r   Peerr   r   r:   r   rQ   r   r   MessageSearchOptionsr   rI   r@   r>   <module>r     s                C C C C C C C C C C C C C C C C # # # # # # 7 7 7 7 7 7 / / / / / / % % % % % % % % % % . . . . . . . . . . . .       0 0 0 0 0 0 1 1 1 1 1 1         
 1 0 0 0 0 0 0 0 L L L L L L L L L L             3 3 3 3 3 3 # # # # # # , , , , , , 2 2 2 2 2 2		8	$	$	0

 
 
$ %)1 1 111 *1
 1 1 *1 *1 1 1 Dj1 E{T!1 1 1 1 1h 	
  
#Y   6$$$ $ 	$
 $ 7?T!4#889$ $ $ $@  7?T!7?T#99:	   &EEE E 	E
 E 
'/E E E E@' ?T)' /D('  '  	' 
 7?T!3+,'  '  '  ' T (',,nEEEFFG    S		)-R* * *  $&
 	   
( ?    S		%)T6& & & $GLLNN33+1 +1+1+1 "+1 +1 	+1 +1 +1	 +1\ ?NVVVWW    S		d3ii%)T5& & & D DDD "D 	D D D D& NVVVWW    S		d3ii#D #D#D#D 	#D #D #D #DL ?NVVVWW	    S		d3ii"U>   D DDD d
D 	D D D D0 ?NVVVWW    S		d3ii26$_3 3 3 D DDD W../D 	D D D D2 ?NVVVWW    S		d3ii26$`3 3 3 R RRR W../R 	R R R RD ?NVVVWW    S		d3iitF   D DDD 9D 	D D D D: *,NVVVWW    S		d3ii499	   		    *NVVVWW	    S		d3ii499(,S>V(W(W(WD DDD D %	D
 	D D D D4 %NVVVWW    S		d3iiD DDD 	D D D D  )NVVVWW    S		d3ii?1 V  fl  fu  fL  V  V  V  	v  %uT      "E_  
 $e Z   $)5 |$ $ $ #U i    %u T	      ). D	) ) ) #(% F# # # #(% B	# # #_v v vvv 	v $J	v *v v& t'v. Dj/v6 7v> *?vJ KvV  Wv^ 4Z_v v v vr +NVVVWW    S		d3ii  	 	   D (NVVVWW    S		d3ii)-4* * *  
&     r@   