
    i                         d Z ddlZddlZddlZddlmZmZmZmZ ddl	m
Z
 ddlmZ  ej                  e      Z ej                  d      j!                  ej"                         da G d d      Z e       Zy)	uj   
Cliente para Neo4j - GraphRAG.
Maneja conexiones y queries Cypher para el grafo de conocimiento médico.
    N)ListDictAnyOptional)GraphDatabase)settingszneo4j.notificationsc                      e Zd ZdZdZd ZdefdZdefdZd Z	d	 Z
d
 ZdAdedeeeef      dee   deeeef      fdZdBdededeeeef      fdZdBdededeeeef      fdZ	 	 	 	 	 	 	 dCdededededee   dee   dededeeef   fdZdDdededeeeef      fdZdDdededeeeef      fdZdedeeef   fdZdEdededeeeef      fdZ	 	 dFdedee   deeeef      fdZdBdedeeeef      fd Zdefd!Zdee   fd"Zd#edefd$Zd#edefd%Z dEd#ededeeeef      fd&Z!defd'Z"defd(Z#dGd)ed*edee   fd+Z$d,eeeef      deeef   fd-Z%deeef   fd.Z&de'fd/Z(defd0Z)defd1Z*defd2Z+defd3Z,d4ee   defd5Z-d6 Z.d7 Z/dGd8edefd9Z0d8edefd:Z1d;eeeef      deeef   fd<Z2d;eeeef      d8ed=edeeef   fd>Z3dHded8ededeeeef      fd?Z4d,eeeef      deeef   fd@Z5y)INeo4jClientz3Cliente para interactuar con Neo4j GraphRAG Retail.x   c                    t         j                  | _        t         j                  | _        t         j
                  | _        t         j                  | _        d | _	        d | _
        t        j                         | _        ddlm} || _        y )N   )embeddings_service)r   	NEO4J_URIuri
NEO4J_USERuserNEO4J_PASSWORDpasswordNEO4J_DATABASEdatabase_driver_keepalive_thread	threadingEvent_keepalive_stopr   )selfr   s     8/opt/lhia/marcimex/agent/app/rag_service/neo4j_client.py__init__zNeo4jClient.__init__   s_    %%''	 // //37(0:"4    r   c                 B    || _         t        j                  d|        y)zCambia la base de datos activa.u   🔄 Base de datos cambiada a: N)r   loggerinfo)r   r   s     r   set_databasezNeo4jClient.set_database(   s     5hZ@Ar   returnc                     | j                   S )z-Retorna el nombre de la base de datos actual.r   r   s    r   get_current_databasez Neo4jClient.get_current_database-   s    }}r   c           	         	 t        j                  | j                  | j                  | j                  fddddd      | _        | j
                  j                          t        j                  d| j                   d| j                   d       | j                          y# t        $ r"}t        j                  d	|        Y d
}~yd
}~ww xY w)u4   Establece conexión con Neo4j e inicia el keepalive.
      2   T)authconnection_acquisition_timeoutmax_connection_lifetimeconnection_timeoutmax_connection_pool_size
keep_aliveu   ✅ Conectado a Neo4j en z (DB: )u   ❌ Error conectando a Neo4j: NF)r   driverr   r   r   r   verify_connectivityr!   r"   r   _start_keepalive	Exceptionerror)r   es     r   connectzNeo4jClient.connect1   s    	(//ii//1(+#%)+DL LL,,.KK3DHH:VDMM?RSTU!!# 	LL9!=>	s   BB 	C	'CC	c                 j     j                   j                           j                  r6 j                  j                         r j                  j	                  d        j                   j                           fd}t        j                  |dd       _         j                  j                          y)uV   Inicia un hilo daemon que ejecuta RETURN 1 cada 2 min para mantener la conexión viva.   timeoutc                     j                   j                  j                        sj                  sy 	 j                  j	                  j
                        5 } | j                  d      j                          d d d        t        j                  d       j                   j                  j                        sy y # 1 sw Y   GxY w# t        $ r"}t        j                  d|        Y d }~[d }~ww xY w)Nr=   r&   zRETURN 1u   💓 Neo4j keepalive OKu   ⚠️ Neo4j keepalive falló: )r   wait_KEEPALIVE_INTERVALr   sessionr   runconsumer!   debugr7   warning)rB   r9   r   s     r   
_ping_loopz0Neo4jClient._start_keepalive.<locals>._ping_loopN   s    **//8P8P/Q||J--t}}-E :J/779:LL!:; **//8P8P/Q: : ! JNN%DQC#HIIJs/   &C  C<C CC 	C9C44C9Tzneo4j-keepalive)targetdaemonnameN)	r   setr   is_alivejoinclearr   Threadstart)r   rG   s   ` r   r6   zNeo4jClient._start_keepaliveE   s     	  "!!d&<&<&E&E&G""'''2""$		J "+!1!1DWh!i$$&r   c                     | j                   j                          | j                  r0| j                  j                          t        j                  d       y y )Nu   🔌 Desconectado de Neo4j)r   rK   r   closer!   r"   r'   s    r   rR   zNeo4jClient.close\   s=      "<<LL KK45 r   Nquery
parametersc                    | j                   s| j                         sg S |xs | j                  }t        d      D ]`  }	 | j                   j	                  |      5 }|j                  ||xs i d      }|D cg c]  }|j                          c}cd d d        c S  g S c c}w # 1 sw Y   nxY ww# t        $ r}	t        |	      }
t        j                  d|	        |dk(  rd|
j                         v s$d|
j                         v sd	|
j                         v rft        j                  d
       	 | j                   j                          n# t        $ r Y nw xY wd | _         | j                         s	g cY d }	~	c S Y d }	~	Dg cY d }	~	c S d }	~	ww xY w)Nr<   r&      r=   u   ❌ Error query: r   defunctzfailed to read
connectionu<   ⚠️ Conexión Neo4j defunta detectada — reconectando...)r   r:   r   rangerB   rC   datar7   strr!   r8   lowerrF   rR   )r   rS   rT   r   	db_to_useattemptrB   resultrecordr9   err_strs              r   execute_queryzNeo4jClient.execute_queryb   s~   ||<<>"9-	Qx 	G\\))9)= @$[[
0@b"[MF8>?fFKKM?@ @	( 	! @@ @ @  a&045a<Y'--/%AEUY`YfYfYhEhlx  }D  }J  }J  }L  mLNN#ab**,$ #'DL<<>!		sx   B1B$7BB$	B1B$$B-	)B11	F:A3F.E	F		EFEF0F>F?FF
query_textlimitc                    d}d}	 | j                  |||d      }t        j                  dt        |       d|D cg c]&  }|j	                  dd      |j	                  dd      f( c}        |S c c}w #  | j                  |||d      }t        j                  d	t        |       d|D cg c]&  }|j	                  dd      |j	                  dd      f( nc c}w c}        |cY S xY w)
uJ   
        Búsqueda difusa de productos por nombre o descripción.
        a?  
        CALL db.index.fulltext.queryNodes('productFulltext', $query_text)
        YIELD node, score
        MATCH (node:Product)
        OPTIONAL MATCH (node)-[:BELONGS_TO]->(c:Category)
        OPTIONAL MATCH (node)-[:MADE_BY]->(b:Brand)
        RETURN node.id as id, node.sku_id as sku_id, node.name as name, node.price as price, node.description as description,
               node.link as link, node.image_link as image_link,
               c.name as category, b.name as brand, score, node.promotion as promotion
        ORDER BY score DESC
        LIMIT $limit
        a  
        MATCH (p:Product)
        WHERE toLower(p.name) CONTAINS toLower($query_text)
           OR toLower(p.description) CONTAINS toLower($query_text)
        OPTIONAL MATCH (p)-[:BELONGS_TO]->(c:Category)
        OPTIONAL MATCH (p)-[:MADE_BY]->(b:Brand)
        RETURN p.id as id, p.sku_id as sku_id, p.name as name, p.price as price, p.description as description,
               p.link as link, p.image_link as image_link,
               c.name as category, b.name as brand, p.promotion as promotion
        LIMIT $limit
        rc   rd   u'   🔍 Neo4j fulltext_search resultados (): id?rJ   u'   🔍 Neo4j fallback_search resultados ()rb   r!   r"   lenget)r   rc   rd   rS   fallback_queryresultsrs          r   fulltext_search_productsz$Neo4jClient.fulltext_search_products~   s3   
	((zTY0Z[GKKA#g,s  CJ  TK}~UVUZUZ[_`cUdfgfkfklrsvfwTx  TK  SL  M  NN TK	)).^c:deW[[B3w<.PS  DK  UL~VWV[V[\`adVeghglglmstwgxUy  UL  UL  TM  N  O^s(   5A6 +A1&
A6 1A6 67C+-+CC+c                 B   	 | j                   j                  |      }|s't        j                  d       | j	                  ||      S d}| j                  |||d      }t        j                  dt        |       d|D cg c]&  }|j                  dd      |j                  dd      f( c}        |s't        j                  d	       | j	                  ||      S |S c c}w # t        $ r4}t        j                  d
|        | j	                  ||      cY d}~S d}~ww xY w)u   
        Búsqueda semántica usando embeddings.
        Fallback a fulltext si el índice no existe o retorna vacío.
        ,   ⚠️ Embedding vacío, fallback a fulltextab  
            CALL db.index.vector.queryNodes('productSearch', $limit, $embedding)
            YIELD node, score
            MATCH (node:Product)
            OPTIONAL MATCH (node)-[:BELONGS_TO]->(c:Category)
            OPTIONAL MATCH (node)-[:MADE_BY]->(b:Brand)
            RETURN node.id as id, node.sku_id as sku_id, node.name as name, node.price as price, node.description as description,
                   node.link as link, node.image_link as image_link,
                   c.name as category, b.name as brand, score, node.promotion as promotion
            ORDER BY score DESC, node.id ASC
            	embeddingrd   u%   🔍 Neo4j vector_search resultados (rg   rh   ri   rJ   u8   ⚠️ Vector search sin resultados, fallback a fulltextu   ❌ Error vector search: N)r   embed_queryr!   rF   ro   rb   r"   rj   rk   r7   r8   )r   rc   rd   rs   rS   rm   rn   r9   s           r   vector_search_productsz"Neo4jClient.vector_search_products   s3   
	D//;;JGI NO55j%HH
E ((iRW0XYGKK?G~S  AH  RI{|STSXSXY]^aSbdedidijpqtduRv  RI  QJ  K  LYZ44ZGGN	 RI
  	DLL4QC8900UCC	Ds<   AC! 7C! =+C(1C! C! C! !	D*)DDDskip	min_scoreexcluded_categoriesrs   promotion_filterorder_by_promotionc	                    t         dz  a t         }	t        j                  d|	 d| d| d|        t        j                         }
	 |dt        j                         }| j                  j                  |      }t        j                  d|dd  d	t        j                         |z
  d
d       |s-t        j                  d       | j                  ||      }|dg dS |g }||z   t        |      dz  z   dz   }|rdnd}d| d}|dz   }t        j                         }| j                  ||||||||d      }t        j                  dt        j                         |z
  d
d       t        |      |kD  }|d| }t        j                  d| d| d| dt        |       d| dt        j                         |
z
  d
d       |||dS # t        $ r<}t        j                  d| d       | j                  ||      }|dg dcY d}~S d}~ww xY w) u  
        Búsqueda vectorial con paginación, filtrado de score y categorías.
        Acepta un embedding pre-calculado para garantizar consistencia entre páginas.

        promotion_filter=True  → solo retorna productos con promotion=true (filtro duro).
        order_by_promotion=True → muestra todos los resultados, promocionados primero.

        Returns:
            Dict con 'products' (lista) y 'has_more' (bool)
        r   u7   📡 [Neo4j] vector_search_products_paginated LLAMADA #z q='z' skip=z limit=Nu   ⏱️ [Neo4j] embed_query('(   z'): z.2fsrq   F)productshas_morers   r*      z5ORDER BY node.promotion DESC, score DESC, node.id ASCz ORDER BY score DESC, node.id ASCa  
            CALL db.index.vector.queryNodes('productSearch', $fetch_count, $embedding)
            YIELD node, score
            WHERE score >= $min_score
            MATCH (node:Product)
            OPTIONAL MATCH (node)-[:BELONGS_TO]->(c:Category)
            OPTIONAL MATCH (node)-[:MADE_BY]->(b:Brand)
            WITH node, score, c, b
            WHERE (c IS NULL OR NOT c.name IN $excluded_categories)
              AND (NOT $promotion_filter OR node.promotion = true)
            RETURN node.id              AS id,
                   node.sku_id          AS sku_id,
                   node.name            AS name,
                   node.price           AS price,
                   node.description     AS description,
                   node.summary         AS summary,
                   node.link            AS link,
                   node.image_link      AS image_link,
                   node.in_stock        AS in_stock,
                   node.availability    AS availability,
                   node.color           AS color,
                   node.specifications  AS specifications,
                   node.global_category AS global_category,
                   node.sub_category    AS sub_category,
                   node.product_type    AS product_type,
                   node.promotion       AS promotion,
                   node.sync_status     AS sync_status,
                   c.name               AS category,
                   b.name               AS brand,
                   score
            zB
            SKIP $skip
            LIMIT $page_limit
            )rs   fetch_countrw   rx   rv   
page_limitry   u(   ⏱️ [Neo4j] execute_query vectorial: u$   🔍 [Neo4j] paginated_search (skip=z, limit=z, min_score=rg   z productos, has_more=u    — total: u#   ❌ Error paginated vector search: Texc_info)_vector_search_call_countr!   r"   timer   rt   rF   ro   rj   rb   r7   r8   )r   rc   rv   rd   rw   rx   rs   ry   rz   call_nt_startt0rm   r   order_clauserS   r   r   r~   r9   s                       r    vector_search_products_paginatedz,Neo4jClient.vector_search_products_paginated   st   , 	"Q&!*EfX NWTF'%:	
 ))+Y	M YY[ 33??
K	::cr?:K4PTPYPYP[^`P`adOeefghMN77
EJ$+RPP"*&(# ,-@)AB)FFKK & H7 < N =!EH JB((&*&':($41 G KKB499;QSCSTWBXXYZ[7|e+HvHKK6tfHUG<XaWbbex=/!6xjTYY[[bMbcfLgghj %$&   	MLL>qcBTLR33JFG 'ULL	Ms&   BG C(G 	H	1H>H	H	c                    | j                   j                  |      }|s't        j                  d       | j	                  ||      S d}	 | j                  |||d      }t        j                  dt        |       d| d       |S # t        $ r6}t        j                  d| d	       | j	                  ||      cY d
}~S d
}~ww xY w)u   
        Búsqueda vectorial que retorna TODOS los atributos del nodo Product
        excepto el embedding. Usada por el endpoint de diagnóstico /products/search.
        rq   a  
        CALL db.index.vector.queryNodes('productSearch', $limit, $embedding)
        YIELD node, score
        MATCH (node:Product)
        OPTIONAL MATCH (node)-[:BELONGS_TO]->(c:Category)
        OPTIONAL MATCH (node)-[:MADE_BY]->(b:Brand)
        RETURN
            node.id              AS id,
            node.sku_id          AS sku_id,
            node.name            AS name,
            node.price           AS price,
            node.description     AS description,
            node.summary         AS summary,
            node.link            AS link,
            node.image_link      AS image_link,
            node.in_stock        AS in_stock,
            node.availability    AS availability,
            node.color           AS color,
            node.specifications  AS specifications,
            node.global_category AS global_category,
            node.sub_category    AS sub_category,
            node.product_type    AS product_type,
            node.promotion       AS promotion,
            node.sync_status     AS sync_status,
            c.name               AS category,
            b.name               AS brand,
            score
        ORDER BY score DESC
        LIMIT $limit
        rr   u   🔍 [vector_search_full]  productos para ''u   ❌ Error vector_search_full: Tr   N)
r   rt   r!   rF   ro   rb   r"   rj   r7   r8   )r   rc   rd   rs   rS   rm   r9   s          r   vector_search_products_fullz'Neo4jClient.vector_search_products_full<  s    
 ++77
C	NNIJ00UCC<	D((iRW0XYGKK4S\NBST^S__`abN 	DLL9!=LM00UCC	Ds   ;B 	C+B>8C>Cc                     d}	 | j                  |||d      }t        j                  dt        |       d| d       |S # t        $ r&}t        j                  d| d       g cY d	}~S d	}~ww xY w)
us   
        Búsqueda fulltext que retorna TODOS los atributos del nodo Product
        excepto el embedding.
        a  
        CALL db.index.fulltext.queryNodes('productFulltext', $query_text)
        YIELD node, score
        MATCH (node:Product)
        OPTIONAL MATCH (node)-[:BELONGS_TO]->(c:Category)
        OPTIONAL MATCH (node)-[:MADE_BY]->(b:Brand)
        RETURN
            node.id              AS id,
            node.sku_id          AS sku_id,
            node.name            AS name,
            node.price           AS price,
            node.description     AS description,
            node.summary         AS summary,
            node.link            AS link,
            node.image_link      AS image_link,
            node.in_stock        AS in_stock,
            node.availability    AS availability,
            node.color           AS color,
            node.specifications  AS specifications,
            node.global_category AS global_category,
            node.sub_category    AS sub_category,
            node.product_type    AS product_type,
            node.promotion       AS promotion,
            node.sync_status     AS sync_status,
            c.name               AS category,
            b.name               AS brand,
            score
        ORDER BY score DESC
        LIMIT $limit
        rf   u   🔍 [fulltext_search_full] r   r   u    ❌ Error fulltext_search_full: Tr   N)rb   r!   r"   rj   r7   r8   )r   rc   rd   rS   rm   r9   s         r   fulltext_search_products_fullz)Neo4jClient.fulltext_search_products_fulll  s|    
<	((zTY0Z[GKK6s7|nDUV`UaabcdN 	LL;A3?$LOI	s   ;A   	A/	A*$A/*A/
product_idc                 @    d}| j                  |d|i      }|r|d   S i S )zF
        Obtiene detalles completos de un producto por su ID.
        ar  
        MATCH (p:Product {id: $product_id})
        OPTIONAL MATCH (p)-[:BELONGS_TO]->(c:Category)
        OPTIONAL MATCH (p)-[:MADE_BY]->(b:Brand)
        RETURN p.id as id, p.sku_id as sku_id, p.name as name, p.price as price,
               p.description as description, p.summary as summary,
               p.link as link, p.image_link as image_link,
               p.in_stock as in_stock, p.availability as availability,
               p.color as color, p.specifications as specifications,
               p.promotion as promotion, p.sync_status as sync_status,
               c.name as category, b.name as brand
        r   r   rb   )r   r   rS   rm   s       r   get_product_contextzNeo4jClient.get_product_context  s3     $$U\:,FG$wqz,",r   c                 0    d}| j                  |||d      S )u-   
        Busca políticas de tienda.
        z
        MATCH (pol:Policy)
        WHERE toLower(pol.text) CONTAINS toLower($query_text) 
           OR toLower(pol.title) CONTAINS toLower($query_text)
        RETURN pol as policy
        LIMIT $limit
        rf   r   )r   rc   rd   rS   s       r   search_policieszNeo4jClient.search_policies  s#     !!%
U)STTr   c                     |g }d}| j                  |||d      }t        j                  dt        |       d       |S )u   
        Retorna productos con promotion=true sin búsqueda vectorial.
        Se usa cuando el usuario pide ver las ofertas/promociones de forma genérica.
        a  
        MATCH (p:Product)
        WHERE p.promotion = true AND p.sync_status <> 'eliminado'
        OPTIONAL MATCH (p)-[:BELONGS_TO]->(c:Category)
        OPTIONAL MATCH (p)-[:MADE_BY]->(b:Brand)
        WITH p, c, b
        WHERE c IS NULL OR NOT c.name IN $excluded_categories
        RETURN p.id              AS id,
               p.sku_id          AS sku_id,
               p.name            AS name,
               p.price           AS price,
               p.description     AS description,
               p.summary         AS summary,
               p.link            AS link,
               p.image_link      AS image_link,
               p.in_stock        AS in_stock,
               p.availability    AS availability,
               p.color           AS color,
               p.specifications  AS specifications,
               p.global_category AS global_category,
               p.sub_category    AS sub_category,
               p.product_type    AS product_type,
               p.promotion       AS promotion,
               p.sync_status     AS sync_status,
               c.name            AS category,
               b.name            AS brand
        LIMIT $limit
        )rx   rd   u    🏷️ [get_promoted_products] u    productos en promociónrb   r!   r"   rj   )r   rd   rx   rS   rm   s        r   get_promoted_productsz!Neo4jClient.get_promoted_products  sX     &"$8 $$U#6-
  	6s7|nD\]^r   c                 v    d}| j                  |d|i      }t        j                  dt        |       d       |S )u}   
        Obtiene productos destacados/aleatorios para mostrar cuando
        el usuario hace una consulta genérica.
        a  
        MATCH (p:Product)
        WHERE p.sync_status <> 'eliminado'
        OPTIONAL MATCH (p)-[:BELONGS_TO]->(c:Category)
        OPTIONAL MATCH (p)-[:MADE_BY]->(b:Brand)
        RETURN p.id as id, p.sku_id as sku_id, p.name as name, p.price as price,
               p.description as description, p.link as link,
               p.image_link as image_link, c.name as category,
               b.name as brand, 1.0 as score, p.promotion as promotion
        ORDER BY rand()
        LIMIT $limit
        rd   u   🌟 Productos destacados: 
 productosr   )r   rd   rS   rm   s       r   get_featured_productsz!Neo4jClient.get_featured_products  sA    
 $$UWe,<=1#g,zJKr   c                     d}| j                  |      }|r|d   j                  dd      nd}t        j                  d| d       |S )z
        Asigna promotion=false a todos los productos que no tengan el campo.
        Retorna la cantidad de productos actualizados.
        z
        MATCH (p:Product)
        WHERE p.promotion IS NULL
        SET p.promotion = false
        RETURN count(p) as updated
        r   updatedu"   🏷️ promotion inicializado en r   rb   rk   r!   r"   )r   rS   rm   r   s       r   initialize_promotion_fieldz&Neo4jClient.initialize_promotion_field  sN    
 $$U+29'!*..A.q8	LMr   c                 |    d}| j                  |      }|D cg c]  }|j                  d      s|d    c}S c c}w )uD   
        Obtiene las categorías disponibles con productos.
        z
        MATCH (p:Product)-[:BELONGS_TO]->(c:Category)
        WHERE p.sync_status <> 'eliminado'
        RETURN DISTINCT c.name as category, count(p) as count
        ORDER BY count DESC
        LIMIT 10
        categoryrb   rk   r   rS   rm   rn   s       r   get_available_categoriesz$Neo4jClient.get_available_categories
  s>     $$U+'.D!!%%
2C*DDD   99customer_idc                     |r|syd}	 | j                  |||d       y# t        $ r"}t        j                  d|        Y d}~yd}~ww xY w)z(Registra que un usuario vio un producto.Nz
        MERGE (u:Customer {id: $cid})
        MERGE (p:Product {id: $pid})
        MERGE (u)-[r:VIEWED]->(p)
        SET r.timestamp = datetime()
        )cidpidu   ⚠️ Error logging view: rb   r7   r!   rF   )r   r   r   rS   r9   s        r   log_user_viewzNeo4jClient.log_user_view  sQ    *f	=e[%LM 	=>>7s;<<	=    	A
AA
c                     |r|syd}	 | j                  |||d       y# t        $ r"}t        j                  d|        Y d}~yd}~ww xY w)u    Registra qué buscó el usuario.Nz
        MERGE (u:Customer {id: $cid})
        MERGE (s:SearchTerm {text: toLower($text)})
        MERGE (u)-[r:SEARCHED]->(s)
        SET r.timestamp = datetime()
        )r   textu   ⚠️ Error logging search: r   )r   r   rc   rS   r9   s        r   log_user_searchzNeo4jClient.log_user_search'  sT    *f	@uk:&NO 	@NN:1#>??	@r   c                 n    |sg S d}| j                  |||d      }|sd}| j                  |||d      }|S )u   
        Genera recomendaciones basadas en productos vistos previamente.
        Lógica: "Usuarios que vieron lo que tú viste, también vieron..."
        as  
        MATCH (current:Customer {id: $cid})-[:VIEWED]->(p:Product)
        MATCH (other:Customer)-[:VIEWED]->(p)
        WHERE other.id <> $cid
        MATCH (other)-[:VIEWED]->(reco:Product)
        WHERE NOT (current)-[:VIEWED]->(reco)
        RETURN reco.id as id, reco.name as name, count(*) as frequency
        ORDER BY frequency DESC
        LIMIT $limit
        )r   rd   a1  
             MATCH (u:Customer {id: $cid})-[:VIEWED]->(p:Product)-[:BELONGS_TO]->(c:Category)
             MATCH (reco:Product)-[:BELONGS_TO]->(c)
             WHERE NOT (u)-[:VIEWED]->(reco) AND reco.id <> p.id
             RETURN reco.id as id, reco.name as name
             LIMIT $limit
             r   )r   r   rd   rS   rm   query_contents         r   get_user_recommendationsz$Neo4jClient.get_user_recommendations6  sV    
 2I	 $$UK%,PQ ] ))-W\9]^Wr   c                 @    d}| j                  |      }|r|d   d   S dS )u9   Cuenta el número total de productos en la base de datos.z*MATCH (p:Product) RETURN count(p) as totalr   totalr   )r   rS   rm   s      r   count_productszNeo4jClient.count_productsY  s-    <$$U+&-wqz'"414r   c                 (    | j                         dk(  S )u7   Verifica si la base de datos de productos está vacía.r   )r   r'   s    r   is_database_emptyzNeo4jClient.is_database_empty_  s    ""$))r   r   
dimensionsc                    ddl }|t        j                  }|j                  |j	                               j                         }g }t        |      D ]6  }||t        |      z     }|dz  dz
  }|j                  t        |d             8 |S )u}   
        Genera un embedding fake para pruebas.
        Usa un hash del texto para generar valores determinísticos.
        r   Ng     _@g      ?   )
hashlibr   EMBEDDING_DIMENSIONSsha256encodedigestrY   rj   appendround)	r   r   r   r   
hash_bytesfake_embeddingibyte_val
normalizeds	            r   generate_fake_embeddingz#Neo4jClient.generate_fake_embeddingc  s    
 	!66J^^DKKM299;
z" 	8A!!c*o"56H"U*c1J!!%
A"67		8
 r   r~   c                    |sddddS d}t        |      D cg c]  \  }}|j                  dd      dk(  s||f! }}}t        |      D cg c]  \  }}|j                  dd      dk7  s||f! }}}i }|r|D cg c]?  \  }}|j                  dd       d	|j                  d
d       d	|j                  dd       A }	}}t        j                  dt	        |	       d       | j
                  j                  |	      }
t        |      D ]]  \  }\  }}|t	        |
      k  r|
|   r	|
|   ||<   %t        j                  d|        | j
                  j                  |	|         ||<   _ d}d}d'dt        t        t        f   dt        t           dt        t        t        f   fd}d}d}|D cg c]  \  }} |||j                  |g               }}}t        dt	        |      |      D ]l  }||||z    }	 | j                  |d|i      }|r|d   d   nd}||z  }|t	        |      |z
  z  }t        j                  d| d|t	        |      z    d| d       n |D cg c]  \  }} ||       }}}t        dt	        |      |      D ]l  }||||z    }	 | j                  |d|i      }|r|d   d   nd}||z  }|t	        |      |z
  z  }t        j                  d| d|t	        |      z    d| d       n t        j                  d | d!| d"       ||t	        |      d#| d$| d%d&S c c}}w c c}}w c c}}w c c}}w # t         $ r4}t        j#                  d| d|        |t	        |      z  }Y d}~d}~ww xY wc c}}w # t         $ r4}t        j#                  d| d|        |t	        |      z  }Y d}~8d}~ww xY w)(u5  
        Crea o actualiza productos en batch en Neo4j.

        Solo genera embeddings para productos NUEVOS (sync_status='nuevo').
        Los productos existentes (sync_status='normal') mantienen su embedding.

        Optimizaciones:
        - Embeddings en batch (1 llamada OpenAI para todos los nuevos, no 1 por producto)
        - Escrituras en Neo4j por chunks con UNWIND (no 1 query por producto)

        Args:
            products: Lista de diccionarios con datos de productos

        Returns:
            Dict con estadísticas de la operación
        r   zNo products to createcreatederrorsmessager+   sync_statusnuevotitle  descriptionr   u(   🔢 Generando embeddings en batch para z productos nuevos...u1   ⚠️ Fallback embed_query para producto en pos a}  
        UNWIND $products AS prod
        MERGE (p:Product {id: prod.product_id})
        SET p.name            = prod.title,
            p.description     = prod.description,
            p.summary         = prod.summary,
            p.price           = prod.price,
            p.link            = prod.link,
            p.image_link      = prod.image_link,
            p.sku_id          = prod.sku_id,
            p.rpx_code        = prod.rpx_code,
            p.in_stock        = prod.in_stock,
            p.availability    = prod.availability,
            p.status          = prod.status,
            p.color           = prod.color,
            p.specifications  = prod.specifications,
            p.global_category = prod.global_category,
            p.sub_category    = prod.sub_category,
            p.product_type    = prod.product_type,
            p.promotion       = prod.promotion,
            p.sync_status     = prod.sync_status,
            p.embedding       = prod.embedding,
            p.updated_at      = datetime()
        WITH p, prod
        FOREACH (_ IN CASE WHEN prod.category IS NOT NULL THEN [1] ELSE [] END |
            MERGE (c:Category {name: prod.category})
            MERGE (p)-[:BELONGS_TO]->(c)
        )
        FOREACH (_ IN CASE WHEN prod.sub_category IS NOT NULL THEN [1] ELSE [] END |
            MERGE (sc:SubCategory {name: prod.sub_category})
            MERGE (p)-[:IN_SUBCATEGORY]->(sc)
        )
        FOREACH (_ IN CASE WHEN prod.brand IS NOT NULL THEN [1] ELSE [] END |
            MERGE (b:Brand {name: prod.brand})
            MERGE (p)-[:MADE_BY]->(b)
        )
        RETURN count(p) AS processed
        aM  
        UNWIND $products AS prod
        MATCH (p:Product {id: prod.product_id})
        SET p.name            = prod.title,
            p.description     = prod.description,
            p.summary         = prod.summary,
            p.price           = prod.price,
            p.link            = prod.link,
            p.image_link      = prod.image_link,
            p.sku_id          = prod.sku_id,
            p.rpx_code        = prod.rpx_code,
            p.in_stock        = prod.in_stock,
            p.availability    = prod.availability,
            p.status          = prod.status,
            p.color           = prod.color,
            p.specifications  = prod.specifications,
            p.global_category = prod.global_category,
            p.sub_category    = prod.sub_category,
            p.product_type    = prod.product_type,
            p.promotion       = prod.promotion,
            p.sync_status     = prod.sync_status,
            p.updated_at      = datetime()
        WITH p, prod
        FOREACH (_ IN CASE WHEN prod.category IS NOT NULL THEN [1] ELSE [] END |
            MERGE (c:Category {name: prod.category})
            MERGE (p)-[:BELONGS_TO]->(c)
        )
        FOREACH (_ IN CASE WHEN prod.sub_category IS NOT NULL THEN [1] ELSE [] END |
            MERGE (sc:SubCategory {name: prod.sub_category})
            MERGE (p)-[:IN_SUBCATEGORY]->(sc)
        )
        FOREACH (_ IN CASE WHEN prod.brand IS NOT NULL THEN [1] ELSE [] END |
            MERGE (b:Brand {name: prod.brand})
            MERGE (p)-[:MADE_BY]->(b)
        )
        RETURN count(p) AS processed
        Nproductrs   r$   c                 
   | j                  dd      }i d| j                  d      d| j                  d      d| j                  d      d| j                  d      d| j                  d      d| j                  d      d	| j                  d	      d
| j                  d
      d| j                  d      d| j                  d      d| j                  d      d| j                  d      d| j                  d      d| j                  d      d| j                  d      d| j                  d      d| j                  d      | j                  d      | j                  d      | j                  dd      |d}|||d<   |S )Nr   r   r   r   r   summarypricelink
image_linksku_idrpx_coder   global_categorysub_categoryproduct_typebrandin_stockavailabilitystatuscolorspecifications	promotionF)r   r   r   r   rs   )rk   )r   rs   r   ds       r   _build_paramz6Neo4jClient.bulk_create_products.<locals>._build_param  s   !++mW=K\!:W!5 ]!; Y!7	
 W!5 V!4 \!: X!6 Z!8 7;;z#: "7;;/@#A 7;;~#> 7;;~#> 7;;w#7 Z!8  ^!<!" X!6#$ ")W!5")++.>"?!([%!@!,+A. $!*+Hr   r~   	processedu     ✅ Nuevos [u   –z]: z procesadosu   ❌ Error batch nuevos chunk : u     ✅ Actualizados [u   ❌ Error batch updates chunk u   ✅ Sync completado: z procesados,  erroreszSync completed: z processed,  errorsr   r   r   r   N)	enumeraterk   r!   r"   rj   r   embed_documentsrF   rt   r   r[   r   r   floatrY   rb   r7   r8   )r   r~   
CHUNK_SIZEr   pnew_productsupdate_productsembeddings_map_textsbatch_embeddingsjidxquery_new_batchquery_update_batchr   r   r   
new_paramschunk_startchunkr_   r   r9   update_paramss                            r   bulk_create_productsz Neo4jClient.bulk_create_productsw  s]   "  A:QRR
 .7x-@mTQAEE-Y`DaelDl1a&mm.7.AndaQUU=ZaEbfmEmAq6nn 24 )Aq 55"%&amR(@'A155UWCXBYZE  KKB3u:,Nbcd#66FFuM(6 X8Cs+,,1A!1D*:1*=N3' NN%VWXVY#Z[*.*A*A*M*MeTUh*WN3'X%P$L	$sCx. 	T%[ 	TXY\^aYaTb 	:  NZZTQl1n&8&8B&?@Z
Z C
OZ@ 
	%K{K*,DEE%++Oj%=PQ6<F1Ik2!	9$#e*y00n[M[3u:=U<VVYZcYddopq
	% 6EETQaEE C$6
C 
	%K!+{Z/GHE%++,>U@ST6<F1Ik2!	9$#e*y002;-s;QTUZQ[C[B\\_`i_jjuvw
	% 	+G9M&RS])',vhgN	
 	
A nn
| [  %<[MA3OP#e*$%
 F  %=k]"QCPQ#e*$%s[   L/L/L5,L5>AL;2#M:A!M"NA!N	N)M??N	O
)OO
c                     | j                         }|dk(  rdddS d}| j                  |      }|r|d   d   nd}d}| j                  |       t        j                  d| d       |d	| d
dS )u   
        Elimina todos los productos y sus relaciones de la base de datos.

        Returns:
            Dict con estadísticas de eliminación
        r   zNo hay productos para eliminar)deletedr   z|
        MATCH (p:Product)
        WITH p, p.id as pid
        DETACH DELETE p
        RETURN count(pid) as deleted
        r  z~
        MATCH (n)
        WHERE (n:Category OR n:SubCategory OR n:Brand)
        AND NOT (n)<-[]-()
        DELETE n
           🗑️ Eliminados z productos de Neo4jzEliminados z productos correctamente)r   rb   r!   r"   )r   count_beforerS   rm   r  cleanup_querys         r   delete_all_productszNeo4jClient.delete_all_products5  s     **,1;  $$U++2'!*Y' 	=))'2EFG$WI-EF
 	
r   c                 |    d}| j                  |      }|D ch c]  }|j                  d      s|d    c}S c c}w )zBObtiene todos los IDs de productos existentes en la base de datos.z#MATCH (p:Product) RETURN p.id as idrh   r   r   s       r   get_all_product_idszNeo4jClient.get_all_product_ids^  s9    5$$U+!(8AAEE$K$888r   c                 v    d}| j                  |      }|r|d   d   nd}t        j                  d| d       |S )z
        Marca todos los productos existentes como PENDING.

        Returns:
            Cantidad de productos marcados
        zn
        MATCH (p:Product)
        SET p.sync_status = 'pendiente'
        RETURN count(p) as updated
        r   r   u   📋 Marcados z productos como PENDINGrb   r!   r"   r   rS   rm   counts       r   mark_all_products_as_pendingz(Neo4jClient.mark_all_products_as_pendingd  sH    
 $$U+)0
9%anUG+BCDr   c                 v    d}| j                  |      }|r|d   d   nd}t        j                  d| d       |S )u   
        Marca como DELETED todos los productos que quedaron en estado PENDING.
        Estos son productos que existían en Neo4j pero no están en el nuevo XML.

        Returns:
            Cantidad de productos marcados como eliminados
        z
        MATCH (p:Product)
        WHERE p.sync_status = 'pendiente'
        SET p.sync_status = 'eliminado'
        RETURN count(p) as deleted
        r   r  u   🗑️ Marcados z. productos como DELETED (no estaban en el XML)r  r  s       r   mark_pending_as_deletedz#Neo4jClient.mark_pending_as_deletedu  sI     $$U+)0
9%a'w.\]^r   c                 v    d}| j                  |      }|r|d   d   nd}t        j                  d| d       |S )u   
        Elimina físicamente de Neo4j todos los productos con sync_status='eliminado'.
        Usa DETACH DELETE para eliminar también sus relaciones.

        Returns:
            Cantidad de productos eliminados
        z
        MATCH (p:Product)
        WHERE p.sync_status = 'eliminado'
        WITH p, p.id as pid
        DETACH DELETE p
        RETURN count(pid) as deleted
        r   r  u    🗑️ Eliminados físicamente z! productos con estado 'eliminado'r  r  s       r   delete_eliminated_productsz&Neo4jClient.delete_eliminated_products  sI     $$U+)0
9%a6ug=^_`r   c                     d}| j                  |      }|r|d   j                  dd      nd}t        j                  d| d       |S )u   
        Pone promotion=false en todos los productos que estaban en true.
        Se llama antes de aplicar el nuevo catálogo de promociones.

        Returns:
            Cantidad de productos reseteados
        z
        MATCH (p:Product)
        WHERE p.promotion = true
        SET p.promotion = false
        RETURN count(p) as updated
        r   r   u'   🔄 Promociones reseteadas a false en r   r   r  s       r   reset_all_promotionsz Neo4jClient.reset_all_promotions  sN     $$U+07
y!,Q=eWJOPr   product_idsc                     |syd}| j                  |d|i      }|r|d   j                  dd      nd}t        j                  d| dt	        |       d       |S )u   
        Activa promotion=true para los productos cuyo id esté en la lista.

        Args:
            product_ids: Lista de product_id que están en promoción

        Returns:
            Cantidad de productos actualizados a promotion=true
        r   z
        UNWIND $ids AS pid
        MATCH (p:Product {id: pid})
        SET p.promotion = true
        RETURN count(p) as updated
        idsr   u!   🏷️ Promociones activadas en z de r   )rb   rk   r!   r"   rj   )r   r  rS   rm   r  s        r   set_promotions_by_idsz!Neo4jClient.set_promotions_by_ids  sj      $$UUK,@A07
y!,Q7wd3{CSBTT^_`r   c                     t         j                  }d| d}	 | j                  |       t        j	                  d| d       y# t
        $ r"}t        j                  d|        Y d}~yd}~ww xY w)u   
        Crea el índice vectorial para productos si no existe.
        Nota: CREATE INDEX no acepta parámetros en Neo4j, se usa valor literal.
        z
        CREATE VECTOR INDEX productSearch IF NOT EXISTS
        FOR (p:Product)
        ON (p.embedding)
        OPTIONS {indexConfig: {
            `vector.dimensions`: H,
            `vector.similarity_function`: 'cosine'
        }}
        u>   ✅ Índice vectorial 'productSearch' verificado/creado (dims=r3   u?   ⚠️ Error creando índice productSearch (puede ya existir): Nr   r   rb   r!   r"   r7   rF   r   r   rS   r9   s       r   ensure_product_vector_indexz'Neo4jClient.ensure_product_vector_index  s    
 22
"
 #- .		bu%KKXYcXddefg 	bNN\]^\_`aa	b   *A 	A.A))A.c                     t         j                  }d| d}	 | j                  |       t        j	                  d| d       y# t
        $ r"}t        j                  d|        Y d}~yd}~ww xY w)u   
        Crea el índice vectorial para documentos si no existe.
        Separado del índice de productos (productSearch).
        Nota: CREATE INDEX no acepta parámetros en Neo4j, se usa valor literal.
        z
        CREATE VECTOR INDEX documentSearch IF NOT EXISTS
        FOR (d:Document)
        ON (d.embedding)
        OPTIONS {indexConfig: {
            `vector.dimensions`: r  u?   ✅ Índice vectorial 'documentSearch' verificado/creado (dims=r3   u@   ⚠️ Error creando índice documentSearch (puede ya existir): Nr  r  s       r   ensure_document_vector_indexz(Neo4jClient.ensure_document_vector_index  s     22
"
 #- .		cu%KKYZdYeefgh 	cNN]^_]`abb	cr  doc_typec                 r    |rd}| j                  |d|i      }nd}| j                  |      }|r|d   d   S dS )z3Cuenta documentos, opcionalmente filtrado por tipo.zAMATCH (d:Document {doc_type: $doc_type}) RETURN count(d) as totalr"  z+MATCH (d:Document) RETURN count(d) as totalr   r   r   )r   r"  rS   rm   s       r   count_documentszNeo4jClient.count_documents  sL    WE((X0FGGAE((/G&-wqz'"414r   c                     d}| j                  |d|i      }|r|d   d   nd}t        j                  d| d| d       |S )u4   Elimina todos los documentos de un tipo específico.zu
        MATCH (d:Document {doc_type: $doc_type})
        DETACH DELETE d
        RETURN count(d) as deleted
        r"  r   r  r  z documentos de tipo 'r   r  )r   r"  rS   rm   r  s        r   delete_documentszNeo4jClient.delete_documents  sW    
 $$UZ,BC+2'!*Y')'2GzQRSTr   	documentsc                 J   |sddddS d}d}d}|D ]  }	 |j                  dd       d|j                  dd       }| j                  j                  |      }|s9t        j	                  d	|j                  d
       d       | j                  |      }|j                  d
      |j                  d      |j                  d      |j                  d      |j                  dd      |j                  dd      |j                  dd      |d}| j                  ||      }	|	r|dz  }n|dz  } t        j                  d| d| d       ||t        |      d| d| ddS # t        $ r:}
t        j                  d|j                  d
       d|
        |dz  }Y d}
~
d}
~
ww xY w)uC  
        Crea documentos en batch en Neo4j con embeddings.

        Cada documento debe tener: id, doc_type, title, content, source,
        chunk_index, total_chunks.

        Args:
            documents: Lista de dicts con datos de documentos

        Returns:
            Dict con estadísticas de la operación
        r   zNo documents to creater   a  
        MERGE (d:Document {id: $id})
        ON CREATE SET d.created_at = datetime()
        SET d.doc_type = $doc_type,
            d.title = $title,
            d.content = $content,
            d.source = $source,
            d.chunk_index = $chunk_index,
            d.total_chunks = $total_chunks,
            d.embedding = $embedding,
            d.updated_at = datetime()
        RETURN d.id as id
        r   r   r   contentu'   ⚠️ Embedding vacío para documento rh   z, usando faker"  sourcechunk_indextotal_chunksr   )rh   r"  r   r)  r*  r+  r,  rs   u   ❌ Error procesando documento r   Nu    ✅ Documentos sync completado: z
 creados, r   zDocuments sync: z
 created, r   r   )rk   r   rt   r!   rF   r   rb   r7   r8   r"   rj   )r   r'  r   r   rS   doctext_for_embeddingrs   paramsr_   r9   s              r   bulk_create_documentsz!Neo4jClient.bulk_create_documents  s     A:RSS  	C(+(<'=QswwyRT?U>V%W" 33??@RS	 NN%LSWWUY]O[h#ij $ < <=O PI ''$- #
 3 WWW-"wwy1!ggh3#&77=!#<$'GGNA$>!*	 ++E6:qLGaKF3	> 	6wiz&QYZ[^)'*VHGL	
 	
  >swwt}oRPQsST!s   DE	F"(/FF"
source_urlc                    | j                          | j                  |      }t        j                  d| d|        |dkD  r| j	                  |       |D ]  }||d<   ||d<    | j                  |      }||d<   ||d<   |S )u  
        Sincroniza documentos de un tipo específico a Neo4j.
        Estrategia: eliminar todos los del tipo y re-insertar (idempotente).

        Args:
            documents: Lista de documentos a sincronizar
            doc_type: Tipo de documento (faq, policy, etc.)
            source_url: URL de origen de los documentos

        Returns:
            Dict con resultado de la sincronización
        u   📊 Documentos 'z' existentes: r   r"  r*  previous_count)r!  r$  r!   r"   r&  r0  )r   r'  r"  r1  r3  r-  r_   s          r   sync_documentszNeo4jClient.sync_documentsS  s     	))+--h7'z?OPQ A!!(+  	'C&C
O&CM	' ++I6%z#1 r   c                 r   	 | j                   j                  |      }|st        j                  d       g S |rd}||||dz   d}nd}||d}| j	                  ||      }t        j                  d|xs d d	t        |       d
       |S # t        $ r$}t        j                  d|        g cY d}~S d}~ww xY w)u+  
        Búsqueda semántica de documentos por similitud vectorial.

        Args:
            query_text: Texto de búsqueda
            doc_type: Filtrar por tipo (opcional)
            limit: Máximo de resultados

        Returns:
            Lista de documentos con score de similitud
        u6   ⚠️ Embedding vacío, no se puede buscar documentosa  
                CALL db.index.vector.queryNodes('documentSearch', $fetch_count, $embedding)
                YIELD node, score
                WHERE node.doc_type = $doc_type
                RETURN node.id as id, node.doc_type as doc_type,
                       node.title as title, node.content as content,
                       node.source as source, score
                ORDER BY score DESC
                LIMIT $limit
                r*   )rs   r"  rd   r   ag  
                CALL db.index.vector.queryNodes('documentSearch', $limit, $embedding)
                YIELD node, score
                RETURN node.id as id, node.doc_type as doc_type,
                       node.title as title, node.content as content,
                       node.source as source, score
                ORDER BY score DESC
                rr   u   🔍 Document search (allrg   z resultadosu&   ❌ Error en vector_search_documents: N)	r   rt   r!   rF   rb   r"   rj   r7   r8   )	r   rc   r"  rd   rs   rS   r/  rm   r9   s	            r   vector_search_documentsz#Neo4jClient.vector_search_documentst  s    #	//;;JGIWX		 (1hQVgloqgqr (15A((7GKK01BU0C3s7|nT_`aN 	LLA!EFI	s#   3B	 AB	 		B6B1+B61B6c                 |   | j                          | j                         }|rdn| j                         }t        j	                  d|rdn| d        |rKt        j	                  dt        |       d       |D ]  }d|d<   	 | j                  |      }d	|d
<   d|d<   |S t        j	                  d| d       | j                         }| j                          d}d}d}	|D ]4  }|j                  d      }
|
s|	dz  }	|
|v rd|d<   |dz  }+d|d<   |dz  }6 t        j	                  d| d| d       | j                  |      }| j                         }| j                         }d|d
<   ||d<   ||d<   ||d<   ||d<   d| d| d| d|j                  dd       d	|d<   t        j	                  d|d           |S )u~  
        Sincroniza productos desde XML a Neo4j.

        Lógica de sincronización:
        - Si la BD está vacía: sube todos con estado 'nuevo'
        - Si ya hay datos:
          1. Marca todos los productos existentes como 'pendiente'
          2. Por cada producto del XML:
             - Si no existe en Neo4j → estado 'nuevo' (crear)
             - Si ya existe en Neo4j → estado 'normal' (actualizar)
          3. Los que quedaron en 'pendiente' → estado 'eliminado'

        Args:
            products: Lista de productos parseados del XML

        Returns:
            Dict con resultado de la sincronización
        r   u   📊 Estado BD: u   VACÍAz productos existentesu   🆕 BD vacía - Subiendo z productos como NUEVOr   r   initial_loadmoder3  u   🔄 BD con u2    productos - Iniciando sincronización incrementalr   r   normalu   📦 XML procesado: z	 nuevos, z existentesincremental_syncnewr   r  zSync incremental: z actualizados, z eliminados, r   r   r   u,   ✅ Sincronización incremental completada: )r  r   r   r!   r"   rj   r  r
  r  rk   r  r  )r   r~   is_emptycurrent_countr   r_   existing_ids	new_countupdated_countr   r   deleted_countphysically_deleteds                r   sync_products_from_xmlz"Neo4jClient.sync_products_from_xml  s4   & 	((*))+%4+>+>+@&8xM?RgAh&ijkKK4S]OCXYZ $ 1)0&1 ..x8F+F6N'(F#$M KK,}o5ghi  335L--/ IMF# #$[[6
!aKF--5GM*!Q&M .5GM*NI# KK.yk=/Q\]^ ..x8F !88:M "&!@!@!B/F6N'4F#$%F5M -F9 2F9$YKy%&mFJJx4K3LHV 9
 KKFviGXFYZ[Mr   )NN)   )r   rF  gffffff?NNFF)r*   )   )rF  Nr   )NrF  )6__name__
__module____qualname____doc__rA   r   r[   r#   r(   r:   r6   rR   r   r   r   r   rb   intro   ru   r   boolr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  rK   r
  r  r  r  r  r  r  r!  r$  r&  r0  r4  r7  rE   r   r   r
   r
      s   = 
5BS B
c ('.63 HT#s(^4L _ghk_l x|  ~B  CF  HK  CK  ~L  yM 8$3 $s $4PTUXZ]U]P^K_ $LD DS DdSVX[S[nI] DF )-!%!&#(vMvM vM 	vM
 vM "#YvM ;vM vM !vM 
c3hvMp.Dc .D# .DtTXY\^aYaTbOc .D`) )C )QUVZ[^`c[cVdQe )V-c -d38n -&U# Uc U$tCQTH~BV U )-,, "#Y, 
d38n		,\3 tDcN7K *C  E$s) E= =# =@3 @C @C  DQUVY[^V^Q_L` F5 5*4 *C S DQVK (|
T$sCx.-A |
d3PS8n |
|'
T#s(^ '
R9S 9c " &C (c &c s 2b.c,5 5s 5
 
 
F
tDcN/C F
SRUX F
PT#s(^(<  Y\ aefiknfnao B/# / /TW /`deijmorjres`t /fWtDcN/C WSRUX Wr   r
   )rK  loggingr   r   typingr   r   r   r   neo4jr   config.settingsr   	getLoggerrH  r!   setLevelERRORr   r
   neo4j_clientrN  r   r   <module>rW     sq   
    , ,  $			8	$   ' ( 1 1'-- @ h hX' }r   