
    xKSj              	      |   U d Z ddlmZ ddlZddlZddl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mZ ddlZej(                  j+                  d
d      Zej(                  j+                  dd      Zej(                  j+                  dd      Zej(                  j+                  dd      ZdZej(                  j+                  dd      dk7  Z eej(                  j+                  dd            Z eej(                  j+                  dd            Zda da!de"d<   ddZ#ddZ$ddZ%edd       Z& e
d e&!      Z' ejP                  e'       dd"Z) G d# d$e      Z* G d% d&e      Z+ G d' d(e      Z,e'j+                  d)      d*        Z-	 	 d	 	 	 	 	 	 	 	 	 dd+Z.e'j_                  d,       ed-      fdd.       Z0e'j_                  d/       ed-      fdd0       Z1e'j_                  d1       ed-      fdd2       Z2h d3Z3dd4Z4dd5Z5	 	 d	 	 	 	 	 	 	 dd7Z6dd8Z7dd9Z8dd:Z9e'j+                  d;      d<dd=d6 ed-      f	 	 	 dd>       Z:e'j+                  d?      dd@dd6d ed-      f	 	 	 	 	 ddA       Z; G dB dCe      Z<e'j_                  dD       ed-      fddE       Z=e'j+                  dF      dd6 ed-      f	 ddG       Z> G dH dIe      Z?e'j_                  dJ       ed-      fddK       Z@e'j+                  dL      d ed-      f	 ddM       ZA G dN dOe      ZBe'j_                  dP       ed-      fddQ       ZCe'j_                  dR       ed-      fddS       ZD G dT dUe      ZEe'j_                  dV       ed-      fddW       ZFe'j+                  dV      d ed-      f	 ddX       ZG G dY dZe      ZHe'j_                  d[       ed-      fdd\       ZI G d] d^e      ZJe'j_                  d_       ed-      f	 dd`       ZK G da dbe      ZLe'j_                  dc       ed-      fddd       ZMe'j+                  de      ddfdg ed-      f	 	 	 ddh       ZNe'j+                  di      ddj ed-      f	 ddk       ZO G dl dme      ZPe'j_                  dn       ed-      f	 ddo       ZQ G dp dqe      ZRe'j_                  dr       ed-      f	 dds       ZS G dt due      ZT G dv dwe      ZUe'j_                  dx       ed-      fddy       ZVe'j+                  dz      dd6 ed-      f	 dd{       ZWe'j_                  d|       ed-      fdd}       ZXe'j_                  d~       ed-      fdd       ZYe'j_                  d       ed-      fdd       ZZe'j+                  d      d ed-      f	 dd       Z[y)u[
  Jarvis memory service — the shared brain (deploy on Railway).

DUMB ON PURPOSE. There is NO model on this server. The intelligence lives in
whatever model is already running on a device (Jarvis's Claude on Mac/Windows
now, a local model on the phone later, future screenshot/transcribe daemons) —
each one decides WHAT to remember and WHEN to recall, and structures the fact
itself. This service only:
  • stores each memory as a node with a LOCAL embedding (FastEmbed, CPU — no
    LLM, no API), and
  • answers searches by MEANING (vector similarity) + returns CONNECTED facts
    (1 graph hop), so a vague query pulls back the linked context.

Backed by FalkorDB (a graph DB with native vectors). Every device/daemon is a
thin client that POSTs facts and GETs searches over HTTPS.

Endpoints
  GET  /health                 — liveness + memory count
  POST /remember               — {text, kind?, when?, source?, group?,
                                  links?:[{to,type}]} → {id}
  GET  /search?q=&k=&group=&hops= — connected facts by meaning
  GET  /memories?group=&limit=&offset= — newest-first list (HUD viewer)
  GET  /graph?group=           — whole graph {nodes,edges} for the HUD graph view
  POST /link                   — {from, to, type}
  POST /unlink                 — {from_id, to} cut the edge(s) between two nodes
  POST /backfill_links         — {dry_run?,threshold?} auto-link all existing memories
  GET  /entity?name=          — everything mentioning an entity ("pull up Al Temimi")
  POST /summary                — {name, text} upsert an entity's current-state summary
  POST /retire                 — {id} soft-retire a memory (stale but kept, for dedup)
  POST /attach_entities        — {memory_id, entities} attach entities (backfill hook)
  POST /insight                — {text,title,itype} reflection finding · GET /insights

RECALL is SALIENCE-ranked: relevance × recency × confidence (latest+firm facts
surface first; nothing is deleted). Facts carry a `confidence` (0..1).

AUTO-LINKING (Layer 1): every /remember & /supersede draws edges to the most
similar existing memories (embedding + shared words) — no LLM. AUTOLINK=0 off.
ENTITIES (Layer 2): the device model extracts people/companies/amounts; this
service stores them as shared :Entity nodes + (:Memory)-[:MENTIONS]->(:Entity).
  POST /supersede              — {old_id, text, ...} mark old fact stale + add new
  POST /forget                 — {id} permanently delete a memory + its links

Env: FALKOR_HOST/FALKOR_PORT, MEMORY_API_KEY (bearer the devices send),
     MEMORY_GROUP (default namespace), EMBED_MODEL.
    )annotationsN)asynccontextmanagerdatetimetimezone)FastAPIHeaderHTTPException)	BaseModel)FalkorDB)TextEmbeddingMEMORY_API_KEY MEMORY_GROUPahmedMEMORY_GRAPHjarvisEMBED_MODELzBAAI/bge-small-en-v1.5i  AUTOLINK10AUTOLINK_THRESHOLDz0.85AUTOLINK_MAX5zTextEmbedding | None	_embedderc                x    t        t        j                  | g            D cg c]  }t        |       c}S c c}w N)nextr   embedfloat)textxs     app.py_embedr$   M   s0    "9??D6#:;<;E!H;<<<s   7c                 d    t        j                  t        j                        j	                         S r   )r   nowr   utc	isoformat     r#   _now_isor+   Q   s    <<%//11r*   c            	     x   t        t              at        t        j
                  j                  dd      t        t        j
                  j                  dd            t        j
                  j                  d      xs d       } | j                  t              a
	 t        j                  dt         d       	 t        j                  d       	 t        j                  dt         d       t!        dd       y # t        $ r5}d	t        |      j                         vrt!        d
| d       Y d }~{d }~ww xY w# t        $ r5}d	t        |      j                         vrt!        d| d       Y d }~d }~ww xY w# t        $ r5}d	t        |      j                         vrt!        d| d       Y d }~d }~ww xY w)NFALKOR_HOST	localhostFALKOR_PORT6379FALKOR_PASSWORD)hostportpasswordzHCREATE VECTOR INDEX FOR (m:Memory) ON (m.embedding) OPTIONS {dimension: z, similarityFunction: 'cosine'}alreadyz[startup] vector index note: T)flushz&CREATE INDEX FOR (e:Entity) ON (e.key)z[startup] entity index note: zICREATE VECTOR INDEX FOR (i:Insight) ON (i.embedding) OPTIONS {dimension: z[startup] insight index note: z)memory service ready (no model on server))r   r   r   r   osenvirongetintselect_graph
GRAPH_NAME_graphquery	EMBED_DIM	Exceptionstrlowerprint)dbes     r#   _init_graphrF   U   sz   k*I	rzz~~m[A2::>>-@A::>>*;<D
FB __Z(FC$$-;.NP	QC=>D$$-;.NP	Q 

5TB  CCFLLN*1!5TBC
  CCFLLN*1!5TBC  DCFLLN*21#6dCDsH   C9 8D: E; 9	D7+D22D7:	E8+E33E8;	F9+F44F9c                  K   t                t        j                         4 d {    d  d d d       d {    y 7 7 # 1 d {  7  sw Y   y xY wwr   )rF   
mcp_serverlifespan)apps    r#   	_lifespanrK   s   s8      M""$$ %$$$$$$sA   #AA AAAAAAA
AAAzJarvis Memory)titlerI   c                D    t         r| dt          k7  rt        dd      y y )NzBearer i  zbad or missing API keystatus_codedetail)API_KEYr
   )authorizations    r#   _authrS      s(    =ggY$774LMM 8wr*   c                      e Zd ZU ded<   dZded<   dZded<   dZded	<   dZded
<   dZded<   dZ	ded<   dZ
ded<   dZded<   y)RememberrA   r!   fact
str | NonekindNwhenr   sourcegrouplist[dict] | Nonelinksentitiesfloat | None
confidencetone)__name__
__module____qualname____annotations__rX   rY   rZ   r[   r]   r^   r`   ra   r)   r*   r#   rU   rU      sY    
ID*D*!FJ!E:#E#"&H&#J#D*r*   rU   c                  0    e Zd ZU ded<   ded<   dZded<   y)LinkrA   from_idtorelatedrW   typeN)rb   rc   rd   re   rk   r)   r*   r#   rg   rg      s    LG D* r*   rg   c                      e Zd ZU ded<   ded<   dZded<   dZded<   d	Zded
<   dZded<   dZded<   dZ	ded<   dZ
ded<   y)	SupersederA   old_idr!   rV   rW   rX   NrY   r   rZ   r[   r\   r^   r_   r`   ra   )rb   rc   rd   re   rX   rY   rZ   r[   r^   r`   ra   r)   r*   r#   rm   rm      sS    K
ID*D*!FJ!E:"&H&#J#D*r*   rm   z/healthc                 z    	 t         j                  d      j                  d   d   } d| dS # t        $ r d} Y w xY w)Nz MATCH (m:Memory) RETURN count(m)r   T)okmemories)r=   r>   
result_setr@   )ns    r#   healthru      sI    LL;<GGJ1M A&&  s   %, ::c                    t        j                         j                  }|t        |       }t        j                  d|| |||t               |xs d|t        |      |xs dd
       |S )NzCREATE (m:Memory {id:$id, text:$text, kind:$kind, source:$source, group:$grp, created_at:$now, refers_to:$when, valid:true, confidence:$conf, tone:$tone}) SET m.embedding = vecf32($emb)r   )
idr!   rX   rZ   grpr&   rY   embconfra   )uuiduuid4hexr$   r=   r>   r+   r    )	r!   rX   rY   rZ   r[   ry   r`   ra   mids	            r#   _creater      si     **,

C
{Tl
LL	H D$&hj$*"Sz"DJB	8	9 Jr*   z	/remember)defaultc                   t        |       | j                  j                         st        dd      | j                  xs t
        }| j                  j                         }t        |      }t        || j                  xs d| j                  | j                  xs d||| j                  | j                  nd| j                        }| j                  xs g D ]a  }t        |j                  dd	            j                         }|s/t         j#                  d
||t        |j                  dd            d       c t$        rt'        ||||      ng }t)        || j*                  |      }	|t-        |      |	dS )N  z
empty textrN   rV   r         ?ry   r`   ra   ri   r   MMATCH (a:Memory {id:$a}),(b:Memory {id:$b}) CREATE (a)-[:LINK {type:$t}]->(b)rk   rj   abt)rw   auto_linkedr^   )rS   r!   stripr
   r[   GROUP_DEFAULTr$   r   rX   rY   rZ   r`   ra   r]   rA   r9   r=   r>   r   
_auto_link_attach_entitiesr^   len)
bodyrR   rx   r!   ry   r~   lkri   autoentss
             r#   rememberr      s1   	-99??LAA
**
%C99??D
,C
$		+VTYY++)3C040KT__QUyy"C zzRb!"((*LL4RVVFI-F)GHJ   /7:c4c*BDC4Dc$iTBBr*   z/linkc                    t        |       t        j                  d| j                  | j                  | j
                  xs dd       ddiS )Nr   rj   r   rq   T)rS   r=   r>   rh   ri   rk   r   rR   s     r#   linkr      sC    	-
LL	,lltyy/EIFH $<r*   z
/supersedec           
     <   t        |       | j                  xs t        }t        j	                  d| j
                  t               d       | j                  j                         }t        |      }t        || j                  xs d| j                  | j                  xs d||| j                  | j                  nd| j                        }t        j	                  d|| j
                  d       t         rt#        ||||       t%        || j&                  |      }||d	S )
u   A fact changed: mark the old one stale (kept for history — this is the
    'temporal' bit) and add the new current one, linked to what it replaced.>MATCH (m:Memory {id:$id}) SET m.valid=false, m.invalid_at=$nowrw   r&   rV   r   r   r   zUMATCH (a:Memory {id:$a}),(b:Memory {id:$b}) CREATE (a)-[:LINK {type:'replaces'}]->(b)r   r   )rw   r^   )rS   r[   r   r=   r>   rn   r+   r!   r   r$   r   rX   rY   rZ   r`   ra   r   r   r   r^   )r   rR   rx   r!   ry   r~   r   s          r#   	superseder      s     
-
**
%C
LL %-1[[&LN99??D
,C
$		+VTYY++)3C040KT__QUyy"C LL =-/ 3c3'C4D4((r*   >-   r   iandoisitmemyofri   andaredidforhasherhimhisshesirthewaswhoyoudoesfromhaveintoknowtellthatthemtheythiswerewhatrY   withyouraboutr   wherewhichr   r   c                    ddl }|j                  d| xs dj                               D ch c]  }|t        vr| c}S c c}w )uG   Content words (names/terms) of a fact — used to spot shared entities.r   N[a-z0-9]{3,}r   )refindallrB   _STOP)r!   _rews      r#   _significant_wordsr      sI    {{?TZR4F4F4HI I!~ I   s   ?c                p    t         j                  d| |d      j                  d   d   }t        |      dkD  S )NzDMATCH (a:Memory {id:$a})-[l:LINK]-(b:Memory {id:$b}) RETURN count(l)r   r   )r=   r>   rs   r:   )r   r   rt   s      r#   _edge_existsr     s?    Na	$*Q	(()	+A q6A:r*   Fc           	        |t         n|}|t        n|}t        |      }i }t        j	                  d|dz   ||| d      j
                  }	|	D ]I  \  }
}}t        |t        |xs d      z        }t        |      d|z  z   }|
|vs|||
   d   kD  sC||f||
<   K t        |      dd }|rd	d
j                  d t        t        |            D              z   dz   }|| dt        |      D ci c]  \  }}d| | c}}}t        j	                  ||      j
                  D ]E  \  }
}t        |t        |xs d      z        }|dk\  s'dd|z  z   }|
|vs|||
   d   kD  s?||f||
<   G t        |j                         d d      }g }|D ]h  \  }
\  }}||k  r |S t        |      |k\  r |S t        | |
      r/|st        j	                  d| |
d       |j                  |
|t!        |d      d       j |S c c}}w )u  LAYER 1 — connect a fact to the memories most like it, automatically.

    Candidates come from BOTH vector similarity (meaning) and shared significant
    words (so two notes that both name "Altimi" always link, even when the
    embeddings are only lukewarm). Best `max_links` above `threshold` get a
    :LINK edge of type 'similar' (skipping any pair already connected). No LLM.NzCALL db.idx.vector.queryNodes('Memory','embedding',$k,vecf32($emb)) YIELD node, score WHERE node.group=$grp AND node.valid=true AND node.id <> $nid RETURN node.id, node.text, score   )kry   rx   nidr   gQ?r      zKMATCH (m:Memory) WHERE m.group=$grp AND m.valid=true AND m.id <> $nid AND ( OR c              3  &   K   | ]	  }d |   ywztoLower(m.text) CONTAINS $wNr)   .0r   s     r#   	<genexpr>z_auto_link.<locals>.<genexpr>+        7$5q 9<$5   z) RETURN m.id, m.text)rx   r   r      333333?c                    | d   d   S )N   r   r)   )kvs    r#   <lambda>z_auto_link.<locals>.<lambda>7  s    Aqr*   TkeyreversezTMATCH (a:Memory {id:$a}),(b:Memory {id:$b}) CREATE (a)-[:LINK {type:'similar'}]->(b)r      )rw   r!   score)r   r   r   r=   r>   rs   r   r    listjoinrange	enumeratesorteditemsr   appendround)new_idr!   ry   r[   	threshold	max_linksdry_runmy_wordscandvrowscidctextr   sharedcombinedwordskwqr   r   paramsrankedlinkedscs                          r#   r   r   
  sd    '0&7"YI ) 1yI!$'HD LL	? "nS		
 $ 
 #UEX 25;B ??@<$-/d?hc15!5)DI	 # N2AE({{ 7$)#e*$57 77 '	'
 v ?,5e,<=,<DAqqWaZ,<=? ,,sF3>>JC$6u{$CCDF{.d?hc1&=!)5 1DI ? DJJL&94HFF"[b%	> M v;)# M $LL;3') 	S%%A,GH # M/ >s   *G5c                d    dj                  | xs dj                         j                               S )z=Canonical key for an entity so the same name reuses one node. r   )r   rB   split)names    r#   _norm_entityr  P  s'    88TZR&&(..011r*   c           
        d}|xs g D ]  }t        |xs i j                  dd            j                         }t        |      dk  r?t        |xs i j                  dd            j                         j	                         xs d}t        |      }|st        j                  d||||t               d       t        j                  d	| ||d
       |dz  } |S )Nr   r  r   r   rk   thingzeMERGE (e:Entity {key:$key, group:$grp}) ON CREATE SET e.name=$name, e.etype=$etype, e.created_at=$now)r   rx   r  etyper&   zWMATCH (m:Memory {id:$mid}),(e:Entity {key:$key, group:$grp}) MERGE (m)-[:MENTIONS]->(e))r~   r   rx   r   )	rA   r9   r   r   rB   r  r=   r>   r+   )mem_idr^   r[   rt   rE   r  r  r   s           r#   r   r   U  s    	A^^AG==,-335t9q=ag2]]6734::<BBD  	4 LteJ 	!
 	)3u5	7 	
Q' ( Hr*   c                   ddl m }m} | j                  dd      d| j                  dd      z  z   }d}| j                  d      xs d}|rU	 |j                  |j                        |j                  |      z
  j                         d	z  }d
dt        |d       dz  z  z  }d| j                  dd      dz
  z  }||z   |z   S # t        $ r Y +w xY w)u   Rank facts by relevance + keyword hits + recency + confidence — the
    Generative-Agents recency×importance×relevance idea. Recency and confidence
    only RE-ORDER results; nothing is ever dropped from memory.r   r   r   g        g333333?keyword_hits
created_atr   g     @g      ?g       @g      ^@g?r`   r         ?)	r   r   r9   r&   r'   fromisoformattotal_secondsmaxr@   )f_dt_tzbasereccaage_daysrz   s           r#   	_saliencer  n  s     :55#na(@!@@D
C	
|		"B		(3+<+<R+@@'-/G4H#3x#5"5"=>?C !%%d+c12D#:  		s   AB? ?	C
Cz/search
   r   c                   ddl }t        |       |xs t        }|rdnd}t        |       }	t        j                  d|z   dz   t        |d      |	|d      j                  }
g }t               }|
D ]h  |j                  d          |j                  d   d   d	   d
   d   t        t        d         d
      d   t        d         nddd       j |j                  d| j                               D cg c]  }|t        vr| c}dd }|rd|j!                  dd      z   dz   dj#                  d t%        t'        |            D              z   dz   dz   }d|it)        |      D ci c]  \  }}d| | c}}}t        j                  ||      j                  D ]  d   |v rt+        fd|D              }|j                  d   d   d	   d
   d   t        dd|z  z   d
      d   t        d         nd|dd 	       |j                  d           |j-                  t.        d!"       |dkD  r||rz|D cg c]  }|d#   	 }}t        j                  d$d%|i      j                  }|D ]A  d   |vs|j                  d          |j                  d   d   d	   d
   d!d&       C d'|iS c c}w c c}}w c c}w )(u_  HYBRID recall: vector similarity (meaning) UNIONed with keyword matches
    (names/terms). A long detailed memory has a blurry embedding and can lose a
    short vague one on pure vector search, so we also surface any fact whose
    text literally contains the query's significant words — that's how "Ziyad"
    or "Journey Joy" reliably comes back.r   Nr   zAND node.valid=true zlCALL db.idx.vector.queryNodes('Memory','embedding',$k,vecf32($emb)) YIELD node, score WHERE node.group=$grp z]RETURN node.id, node.text, node.kind, node.refers_to, node.created_at, score, node.confidencer   r   ry   rx   r   r            r   F)rw   rV   rX   	refers_tor
  r   r`   r   r   r   $MATCH (m:Memory) WHERE m.group=$grp znode.zm.zAND (r   c              3  &   K   | ]	  }d |   ywr   r)   r   s     r#   r   zsearch.<locals>.<genexpr>  s      %O<Mq.qc2<Mr   z:) RETURN m.id, m.text, m.kind, m.refers_to, m.created_at, zm.confidencerx   r   c              3  V   K   | ]   }|d    xs dj                         v sd  " yw)r   r   N)rB   )r   r   rs     r#   r   zsearch.<locals>.<genexpr>  s*     E%Q110B0B0D+Dq%s   ))r  g?)	rw   rV   rX   r  r
  r   r`   r	  r   Tr   rw   zuMATCH (m:Memory)-[l:LINK]-(n:Memory) WHERE m.id IN $ids AND n.valid=true RETURN DISTINCT n.id, n.text, n.kind, l.typeids)rw   rV   rX   r   r   facts)r   rS   r   r$   r=   r>   r  rs   setaddr   r   r    r   rB   r   replacer   r   r   r   sumsortr  )qr   r[   hopsinclude_stalerR   r   rx   validry   rowsr#  seenr   r   kwr   r   hitsr  r"  neighr!  s                         @r#   searchr2    s9    	-

 =CB%;E
)C<<	35:	;	2	2 !QiC02
 3=* 	 E5D1AaD!A$!#$Q4qt$U1Q4[!434Q43CE!A$K %	' 	(  OQWWY?  ?1 ?   "$E3emmGT6RR %O<A#e*<M%O OO LL 	
 J	%8H I8H11QC!8H IJb&)44Att|E%EEDLL!adAaD'(t1Q4#(sTz)91#=78t7GadT*.%	A B
 HHQqTN 5 
JJ9dJ+axE %&1qw&cl$ %/J 	 At41AaD!A$!&'dd< = 
 UG  !J" 's   ;K/KKz	/memories(   c                   t        |       | xs t        }|rdnd}|rdnd}|t        |d      t        dt        |d            d}	|r||	d<   t        j                  d	|z   |z   d
z   |	      j                  }
t        j                  d	|z   dz   d|i      j                  d   d   }||
D cg c]%  }|d   |d   |d   |d   |d   |d   |d   xs dd' c}dS c c}w )u   List memories newest-first — powers the HUD memory VIEWER (browse &
    curate, not search). `kind` filters (e.g. feedback/preference) so the
    reflection engine can pull just Ahmed's stated preferences to respect.r   AND m.valid=true zAND m.kind=$kind r   r      )rx   offlimrX   r  zxRETURN m.id, m.text, m.kind, m.refers_to, m.created_at, m.source, m.tone ORDER BY m.created_at DESC SKIP $off LIMIT $limzRETURN count(m)rx   r   r   r  r  r  )rw   rV   rX   r  r
  rZ   ra   )totalrr   )rS   r   r  minr=   r>   rs   )r[   limitoffsetr+  rX   rR   rx   r,  kfr   r-  r9  r!  s                r#   rr   rr     s,    
-

 =CB%8E $	"BVQCsO,.Fv<<.6;	:	: 		 
	 	
 LL.69JJ	 j$$%'E JN)PJNQ tQqT1Q4adtqtQqTZR	AJN)P Q Q )Ps   )*Cc                  &    e Zd ZU ded<   dZded<   y)ForgetrA   rw   NrW   r[   rb   rc   rd   re   r[   r)   r*   r#   r?  r?        GE:r*   r?  z/forgetc                    t        |       | j                  j                         st        dd      t        j                  dd| j                  j                         i       ddiS )u   Permanently delete a memory (and its links) — Ahmed removed a WRONG fact
    from the HUD viewer. Unlike /supersede (which keeps the old node as history),
    this is gone for good; use it only for genuinely bad/incorrect memories.r   zno idrN   z)MATCH (m:Memory {id:$id}) DETACH DELETE mrw   rq   T)rS   rw   r   r
   r=   r>   r   s     r#   forgetrC    sQ    
 
-77==?G<<
LL<(*$<r*   z/graphc           	        t        |       | xs t        }|rdnd}t        j                  d|z   dz   d|i      j                  }t        j                  d|rdndz   dz   d|i      j                  }t        j                  d	d|i      j                  }t        j                  d
|rdndz   dz   d|i      j                  }|D 	cg c]  }	|	d   |	d   |	d   |	d   d }
}	|
|D 	cg c]  }	d|	d   z   |	d   d|	d   xs dz   dd c}	z  }
|D 	cg c]  }	|	d   |	d   |	d   xs dd }}	||D 	cg c]  }	|	d   d|	d   z   dd c}	z  }|
|dS c c}	w c c}	w c c}	w c c}	w )u   The WHOLE memory graph for a group — :Memory nodes + :LINK edges, PLUS
    Layer-2 :Entity nodes and (:Memory)-[:MENTIONS]->(:Entity) edges — for the
    HUD's graph view. Ahmed walks/zooms this to see what's connected to what.r   r5  r  z)RETURN m.id, m.text, m.kind, m.created_atrx   zJMATCH (a:Memory)-[l:LINK]->(b:Memory) WHERE a.group=$grp AND b.group=$grp z"AND a.valid=true AND b.valid=true zRETURN a.id, b.id, l.typezAMATCH (e:Entity) WHERE e.group=$grp RETURN e.key, e.name, e.etypezMMATCH (m:Memory)-[:MENTIONS]->(e:Entity) WHERE m.group=$grp AND e.group=$grp zRETURN m.id, e.keyr   r   r   r   )rw   r!   rX   r
  zent:zentity:r  rj   )r   ri   rk   mentions)nodesedgesrS   r   r=   r>   rs   )r[   r+  rR   rx   r,  nrowserowsentrowsmentrowsr!  rF  rG  s               r#   graphrM    s    
-

 =CB%8ELL.63	46;S\CCM: 
 LL	/2$H	J 	$	$ ',S\	3 4>:	 
 llK	 j  ||	/"5	7 		  %cl	, -7J	   aD!A$!AaDI 
 	 ! ad]AaD AaDOG4BH   E  d!A$!0A	B 
 	!! t6AaD=*E! !EU++ !s   E#!EE.Ec                  0    e Zd ZU ded<   ded<   dZded<   y)UnlinkrA   rh   ri   NrW   r[   r@  r)   r*   r#   rO  rO    s    LGE:r*   rO  z/unlinkc                z    t        |       t        j                  d| j                  | j                  d       ddiS )u   Cut the connection(s) between two memories (either direction) — Ahmed
    disconnected two nodes in the graph view. The nodes stay; only the edge goes.z=MATCH (a:Memory {id:$a})-[l:LINK]-(b:Memory {id:$b}) DELETE lr   rq   T)rS   r=   r>   rh   ri   r   s     r#   unlinkrQ     s7     
-
LLGll)+ $<r*   z/entityc           
     ^   t        |       |xs t        }t        |       }|sdg iS t        j	                  d||d      j
                  }i }|D ]C  \  }}}	}
}}}}|j                  |||	|
xs d|xs dg d      }|d   j                  |||d       E dt        |j                               iS )u   Everything hanging off one entity — the 'pull up Al Temimi' view. Matches
    by canonical key with light fuzziness (substring either way).r^   zMATCH (e:Entity)<-[:MENTIONS]-(m:Memory) WHERE e.group=$grp AND m.valid=true AND (e.key=$key OR e.key CONTAINS $key OR $key CONTAINS e.key) RETURN e.key, e.name, e.etype, e.summary, e.read, m.id, m.text, m.created_at ORDER BY e.name, m.created_at)rx   r   r   )r  rk   summaryreadr#  r#  )rw   rV   r
  )
rS   r   r  r=   r>   rs   
setdefaultr   r   values)r  r[   rR   rx   r   r-  r   ekeyenamer  rS  ereadr~   mtextmcreatedrE   s                   r#   entityr\  +  s    
 
-

 =C
t
CB<<	5
 C " #-* 	 DDH@eUGUCOOD5%.5mU[b,.#0 1 	
'
e8LM	 EI
 T[[]+,,r*   c                  0    e Zd ZU ded<   ded<   dZded<   y)SummaryrA   r  r!   NrW   r[   r@  r)   r*   r#   r^  r^  E  s    
I
IE:r*   r^  z/summaryc           	     l   t        |       | j                  xs t        }t        | j                        }|r| j
                  j                         st        dd      t        j                  d||| j                  j                         | j
                  j                         t               d       ddiS )u   Upsert a current-state SUMMARY onto an entity — the reflection engine's
    rolling 'profile' so 'pull up Al Temimi' returns the gist, not just raw facts.r   name and text requiredrN   zMERGE (e:Entity {key:$key, group:$grp}) ON CREATE SET e.name=$name, e.etype='thing', e.created_at=$now SET e.summary=$text, e.summary_at=$nowr   rx   r  r!   r&   rq   TrS   r[   r   r  r  r!   r   r
   r=   r>   r+   r   rR   rx   r   s       r#   summary_setrd  K  s     
-
**
%C
tyy
!Cdiioo'4LMM
LL	1 C):"8:	7	8 $<r*   z/readc           	     l   t        |       | j                  xs t        }t        | j                        }|r| j
                  j                         st        dd      t        j                  d||| j                  j                         | j
                  j                         t               d       ddiS )u  THEORY OF MIND — upsert a psychological 'read' onto a person/company:
    what they seem to want, how they decide, the state of the relationship, and
    how Ahmed should approach them. Models THEM, for Ahmed. (Reuses the Summary
    shape: {name, text}.)r   r`  rN   zMERGE (e:Entity {key:$key, group:$grp}) ON CREATE SET e.name=$name, e.etype='thing', e.created_at=$now SET e.read=$text, e.read_at=$nowra  rq   Trb  rc  s       r#   read_setrf  ]  s     
-
**
%C
tyy
!Cdiioo'4LMM
LL	+ C):"8:	7	8 $<r*   c                  &    e Zd ZU ded<   dZded<   y)ValuesrA   r!   NrW   r[   r@  r)   r*   r#   rh  rh  q  s    
IE:r*   rh  z/valuesc                
   t        |       | j                  xs t        }| j                  j	                         st        dd      t        j                  d|| j                  j	                         t               d       ddiS )u   VALUE MODEL — upsert Ahmed's north-star: his priorities, what he chases,
    what he avoids. A singleton profile the reflection engine and the brain
    consult so advice fits HIM, not generic best-practice.r   ztext requiredrN   zOMERGE (p:Profile {id:'values', group:$grp}) SET p.text=$text, p.updated_at=$now)rx   r!   r&   rq   T)	rS   r[   r   r!   r   r
   r=   r>   r+   )r   rR   rx   s      r#   
values_setrj  v  sj    
 
-
**
%C99??ODD
LL	.TYY__.xzBD $<r*   c                    t        |       | xs t        }t        j                  dd|i      j                  }|sdddS |d   d   xs d|d   d   xs ddS )NzGMATCH (p:Profile {id:'values', group:$grp}) RETURN p.text, p.updated_atrx   r   )r!   
updated_atr   r   rH  )r[   rR   rx   r-  s       r#   
values_getrm    sk     
-

 =C<<Q	 j 	 "--GAJ$"DGAJ4D"EEr*   c                  &    e Zd ZU ded<   dZded<   y)RetirerA   rw   NrW   r[   r@  r)   r*   r#   ro  ro    rA  r*   ro  z/retirec                    t        |       t        j                  d| j                  j	                         t               d       ddiS )u   Soft-retire a memory — mark it stale (hidden from recall) but KEEP it as
    history, unlike /forget which deletes. Used by the consolidation pass to fold
    away near-duplicate facts without losing anything.r   r   rq   T)rS   r=   r>   rw   r   r+   r   s     r#   retirerq    s9    
 
-
LLQ
;=$<r*   c                  0    e Zd ZU ded<   ded<   dZded<   y)AttachEntitiesrA   	memory_id
list[dict]r^   NrW   r[   r@  r)   r*   r#   rs  rs    s    NE:r*   rs  z/attach_entitiesc                    t        |       | j                  xs t        }t        | j                  | j
                  |      }d|iS )u   Backfill hook — attach extracted entities to an EXISTING memory (used by
    the engine-side entity backfill over the memories saved before Layer 2).attached)rS   r[   r   r   rt  r^   )r   rR   rx   rt   s       r#   attach_entities_eprx    s:    
 
-
**
%C<A?r*   c                  B    e Zd ZU ded<   dZded<   dZded<   dZded	<   y)
InsightrA   r!   r   rW   rL   observationitypeNr[   )rb   rc   rd   re   rL   r|  r[   r)   r*   r#   rz  rz    s%    
IE:%E:%E:r*   rz  z/insightc                   t        |       | j                  j                         }|st        dd      | j                  xs t
        }t        |      }	 t        j                  d||d      j                  }|rt        |d   d         dkD  rddd	S t        j                         j                  }t        j                  d
||| j                  xs d| j                   xs d|t#               |d       |ddS # t        $ r Y pw xY w)Nr   zempty insightrN   zCALL db.idx.vector.queryNodes('Insight','embedding',3,vecf32($e)) YIELD node, score WHERE node.group=$grp AND score>0.92 RETURN count(node))rE   rx   r   F	duplicate)createdreasonzCREATE (i:Insight {id:$id, text:$text, title:$title, itype:$itype, group:$grp, created_at:$now, surfaced:false}) SET i.embedding = vecf32($emb)r   r{  )rw   r!   rL   r|  rx   r&   ry   T)rw   r  )rS   r!   r   r
   r[   r   r$   r=   r>   rs   r:   r@   r{   r|   r}   rL   r|  r+   )r   rR   r!   rx   ry   dupiids          r#   insight_addr    s    	-99??DODD
**
%C
,Cll!(+C"8: ;E* 	 3s1vay>A%$<< **,

C
LL	) D4::+;::.
3	(	) $''  s   =C8 8	DDz	/insightsT   c                
   t        |       | xs t        }|rdnd}t        j                  d|z   dz   |t	        dt        |d            d      j                  }d|D cg c]  }|d	   |d   |d
   |d   |d   d c}iS c c}w )NzAND i.surfaced=false r   z%MATCH (i:Insight) WHERE i.group=$grp zYRETURN i.id, i.text, i.title, i.itype, i.created_at ORDER BY i.created_at DESC LIMIT $limr   d   )rx   r8  insightsr   r   r   r  )rw   r!   rL   r|  r
  )rS   r   r=   r>   r  r:  rs   )r[   unsurfaced_onlyr;  rR   rx   condr-  r!  s           r#   insights_listr    s     
-

 =C&5"2D<</$6	0	0 C3uc?34	6 7Aj	 	
 FJLFJ !"!adQqT#$Q4qt=FJL M M Ls   B z/insights/searchr   c                   ddl }t        |       |xs t        }t        |       }t	               }g }	 t
        j                  dt        |d      dz   ||d      j                  D ]W  }	|	d   |vs|j                  |	d          |j                  |	d   |	d   |	d   |	d   t        t        |	d         d      d	       Y 	 |j                  d
| j                               D 
cg c]  }
|
t         vr|
 c}
dd }|rddj#                  d t%        t'        |            D              z   dz   }d|it)        |      D 
ci c]  \  }}
d| |
 c}
}}	 t
        j                  ||      j                  D ]A  }	|	d   |vs|j                  |	d          |j                  |	d   |	d   |	d   |	d   dd	       C 	 d|dt        |d       iS # t        $ r Y w xY wc c}
w c c}
}w # t        $ r Y 7w xY w)u   Unsurfaced insights RELEVANT to a query — used to raise a reflection
    contextually (only when Ahmed asks about something related), never on a timer.r   NzCALL db.idx.vector.queryNodes('Insight','embedding',$k,vecf32($emb)) YIELD node, score WHERE node.group=$grp AND node.surfaced=false AND score > 0.5 RETURN node.id, node.text, node.title, node.itype, scorer   r   r  r   r  )rw   r!   rL   r|  r   r   r   z?MATCH (i:Insight) WHERE i.group=$grp AND i.surfaced=false AND (r   c              3  &   K   | ]	  }d |   yw)ztoLower(i.text) CONTAINS $wNr)   )r   rt   s     r#   r   z"insights_search.<locals>.<genexpr>  r   r   z') RETURN i.id, i.text, i.title, i.ityperx   r   r   r  )r   rS   r   r$   r$  r=   r>   r  rs   r%  r   r   r    r@   r   rB   r   r   r   r   r   )r)  r[   r   rR   r   rx   ry   r.  outr!  r   r   r   rt   r   s                  r#   insights_searchr    s-   
 	-

 =C
)CDC  #1ay1}SE	G HRz	RA
 t41

!A$!qt%&qTE%!+q4IK LR OQWWY?  ?1 ?   "$EP{{ 7$)#e*$57 77 99 J	%8H I8H11QC!8H IJ	\\#v.99Q4t#HHQqTNJJadAaD1Q4)*1 = > : JSAY(('    !J  		s<   ;G .AG  G:G*G! ;9G! 	GG!	G-,G-c                  &    e Zd ZU ded<   dZded<   y)InsightSeenrA   rw   NrW   r[   r@  r)   r*   r#   r  r    rA  r*   r  z/insight/seenc                d    t        |       t        j                  dd| j                  i       ddiS )Nz.MATCH (i:Insight {id:$id}) SET i.surfaced=truerw   rq   TrS   r=   r>   rw   r   s     r#   insight_seenr    s0     
-
LLA"$<r*   c                  F    e Zd ZU dZded<   dZded<   dZded<   dZd	ed
<   y)BackfillNr_   r   z
int | Noner   Fboolr   rW   r[   )rb   rc   rd   r   re   r   r   r[   r)   r*   r#   r  r  (  s*    "I|" Iz GTE:r*   r  z/backfill_linksc           
        t        |       | j                  xs t        }| j                  | j                  nt        }| j
                  xs t        }t        j                  dd|i      j                  }t               }g }|D ]  \  }}	t        |	xs d      }
t        ||	xs d|
|||| j                        D ]`  }t        ||d   f      }||v r|j                  |       t!        |      dk  s8|j#                  |	xs ddd |d	   xs ddd |d
   d       b  t!        |      t!        |      || j                  |dS )u   LAYER 1 backfill — run auto-linking over EVERY existing memory once, so
    today's loose cloud gets wired up. `dry_run` reports what it WOULD link (with
    samples) without touching the graph, so the threshold can be tuned first.NzHMATCH (m:Memory) WHERE m.group=$grp AND m.valid=true RETURN m.id, m.textrx   r   )r   rw      <   r!   r   )r   r   r   )rF  linked_pairsr   r   samples)rS   r[   r   r   r   r   r   r=   r>   rs   r$  r$   r   r   	frozensetr%  r   r   )r   rR   rx   thrmxrF  pairsr  r   ntextry   r   pairs                r#   backfill_linksr  /  s@    
-
**
%C NN6$..<NC		'<BLL	 %s|--7Z 
 EG
UU[b!S%+2sCb%)\\3Bc2d8_-Du}IIdO7|b ekr3B%7&(j&6B%<)+G 6 73  ZU'K Kr*   c                  B    e Zd ZU ded<   dZded<   dZded<   dZded<   y)	TaskrA   r!   NrW   duer[   r   rZ   )rb   rc   rd   re   r  r[   rZ   r)   r*   r#   r  r  R  s%    
ICE:!FJ!r*   r  c                  &    e Zd ZU ded<   dZded<   y)TaskIdrA   rw   NrW   r[   r@  r)   r*   r#   r  r  Y  rA  r*   r  z/taskc           
        t        |       | j                  j                         st        dd      t	        j
                         j                  }t        j                  d|| j                  j                         | j                  xs d| j                  xs d| j                  xs t        t               d       d|iS )	Nr   z
empty taskrN   znCREATE (t:Task {id:$id, text:$text, due:$due, done:false, source:$src, grp:$grp, created_at:$now, done_at:''})r   r   )rw   r!   r  srcrx   r&   rw   )rS   r!   r   r
   r{   r|   r}   r=   r>   r  rZ   r[   r   r+   )r   rR   tids      r#   task_addr  ^  s    	-99??LAA
**,

C
LL	?DIIOO-dhhn"'x

0Km
	 #;r*   z/tasksc                   t        |       | xs t        }t        j                  d|rdndz   dz   d|i      j                  }d|D cg c].  }|d   |d   |d	   xs d t        |d
         |d   |d   xs d d0 c}iS c c}w )Nz MATCH (t:Task) WHERE t.grp=$grp r   zAND t.done=false z`RETURN t.id, t.text, t.due, t.done, t.created_at, t.done_at ORDER BY t.done, t.due, t.created_atrx   tasksr   r   r   r   r  r  )rw   r!   r  doner
  done_at)rS   r   r=   r>   rs   r  )r[   include_donerR   rx   r-  r!  s         r#   	task_listr  m  s     
-

 =C<<*2#6	8	/	/ 

 !j 	 9=?9=A qT1Q4!#AaDz1"#A$,$09=? @ @ ?s   3A>z
/task/donec                v    t        |       t        j                  d| j                  t	               d       ddiS )Nz7MATCH (t:Task {id:$id}) SET t.done=true, t.done_at=$nowr   rq   T)rS   r=   r>   rw   r+   r   s     r#   	task_doner  }  s1    	-
LLJ
35$<r*   z/task/reopenc                d    t        |       t        j                  dd| j                  i       ddiS )Nz6MATCH (t:Task {id:$id}) SET t.done=false, t.done_at=''rw   rq   Tr  r   s     r#   task_reopenr    s.    	-
LLI"$<r*   z/task/deletec                d    t        |       t        j                  dd| j                  i       ddiS )Nz MATCH (t:Task {id:$id}) DELETE trw   rq   Tr  r   s     r#   task_deleter    s+    	-
LL3dDGG_E$<r*   z/reminders/duec                   t        |       | xs t        }t               }t        j	                  d||d      j
                  }|D ]  }t        j	                  dd|d   i        d|D cg c]  }|d   |d   |d   d	 c}iS c c}w )
uu   Tasks whose reminder time has passed and aren't done/fired — the
    scheduler polls this to know what to announce.zMATCH (t:Task) WHERE t.grp=$grp AND t.done=false AND t.due<>'' AND t.due<=$now AND coalesce(t.fired,false)=false RETURN t.id, t.text, t.due)rx   r&   z(MATCH (t:Task {id:$id}) SET t.fired=truerw   r   r  r   r   )rw   r!   r  )rS   r   r+   r=   r>   rs   )r[   rR   rx   r&   r-  r!  s         r#   reminders_duer    s    
 
-

 =C
*C<<	%.1#&>@ AK
 	
 ?$!N TJT1Q41ad;TJKKJs   *B)r!   rA   returnlist[float])r  rA   )r  None)rJ   r   )rR   rW   r  r  )Nr   N)r!   rA   rX   rA   rY   rW   rZ   rA   r[   rA   ry   zlist[float] | Noner`   r    ra   rW   r  rA   )r   rU   rR   rW   )r   rg   rR   rW   )r   rm   rR   rW   )r!   rA   r  r$  )r   rA   r   rA   r  r  )NNF)r   rA   r!   rA   ry   r  r[   rA   r   r    r   r:   r   r  r  ru  )r  rA   r  rA   )r  rA   r^   zlist | Noner[   rA   r  r:   )r  dictr  r    )r)  rA   r   r:   r[   rW   r*  r:   r+  r  rR   rW   )r[   rW   r;  r:   r<  r:   r+  r  rX   rW   rR   rW   )r   r?  rR   rW   )r[   rW   r+  r  rR   rW   )r   rO  rR   rW   )r  rA   r[   rW   rR   rW   )r   r^  rR   rW   )r   rh  rR   rW   )r[   rW   rR   rW   )r   ro  rR   rW   )r   rs  rR   rW   )r   rz  rR   rW   )r[   rW   r  r  r;  r:   rR   rW   )r)  rA   r[   rW   r   r:   rR   rW   )r   r  rR   rW   )r   r  rR   rW   )r   r  rR   rW   )r[   rW   r  r  rR   rW   )r   r  rR   rW   )\__doc__
__future__r   r7   r{   
contextlibr   r   r   fastapir   r	   r
   pydanticr   falkordbr   	fastembedr   rH   r8   r9   rQ   r   r<   r   r?   r   r    r   r:   r   r=   r   re   r$   r+   rF   rK   rJ   mountrS   rU   rg   rm   ru   r   postr   r   r   r   r   r   r   r  r   r  r2  rr   r?  rC  rM  rO  rQ  r\  r^  rd  rf  rh  rj  rm  ro  rq  rs  rx  rz  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r)   r*   r#   <module>r     s  +X # 	  * ' 2 2   # 
**..)2
.

~w7ZZ^^NH5
jjnn],DE	
 ::>>*c*c12::>>*>GH 2::>>.#67	"&	 &=2C<   Oi8 
   N
	y 	!9 !		 	 ' ' 379=/,6BE  +9?9M C C. '171E   ,;A$;O ) ),	M :>$<<36<<)3<L2
2& Da %&,T&:AA#A AH !%Ba#(T)/)=Q Q0:Q&Q Q6Y 
 )5;D5I 	 	 "%&,T&:",#", ",JY  )5;D5I   *.'-d';-$- -2i  *;A$;O  " '8>t8L  &Y 
 )9?9M   #'+1$+?	F(	F 	FY 
 )5;D5I  Y  
39$3G&0 i  *;A$;O ( (: &*D!.4T.BMM!+M M 	04q06t0D()#-() ()V) 
 /-3D-A * y  
/5d/CK",K KD"9 "Y 
 '5;D5I   "&U*0*>@'@ @ ,8>t8L   .:@:N   .:@:N   	&*.4T.BL!+L Lr*   