From ba2e479902ca8387d0de879ba1754c76cfd536bc Mon Sep 17 00:00:00 2001 From: Katarina Supe <61758502+katarinasupe@users.noreply.github.com> Date: Tue, 22 Apr 2025 12:57:24 +0200 Subject: [PATCH] Add Memgraph integration (#2537) --- Makefile | 2 +- docs/open-source/graph_memory/overview.mdx | 71 ++- examples/graph-db-demo/alice-memories.png | Bin 0 -> 68811 bytes examples/graph-db-demo/memgraph-example.ipynb | 230 ++++++++ mem0/graphs/configs.py | 18 + mem0/memory/main.py | 5 +- mem0/memory/memgraph_memory.py | 516 ++++++++++++++++++ poetry.lock | 135 +++-- pyproject.toml | 3 +- server/main.py | 4 + 10 files changed, 940 insertions(+), 44 deletions(-) create mode 100644 examples/graph-db-demo/alice-memories.png create mode 100644 examples/graph-db-demo/memgraph-example.ipynb create mode 100644 mem0/memory/memgraph_memory.py diff --git a/Makefile b/Makefile index 9460b0a9..f07c17d6 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ install_all: poetry install poetry run pip install groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \ google-generativeai elasticsearch opensearch-py vecs pinecone pinecone-text faiss-cpu langchain-community \ - upstash-vector azure-search-documents + upstash-vector azure-search-documents langchain-memgraph # Format code with ruff format: diff --git a/docs/open-source/graph_memory/overview.mdx b/docs/open-source/graph_memory/overview.mdx index 1867539b..d59f1b36 100644 --- a/docs/open-source/graph_memory/overview.mdx +++ b/docs/open-source/graph_memory/overview.mdx @@ -47,8 +47,14 @@ allowfullscreen ## Initialize Graph Memory -To initialize Graph Memory you'll need to set up your configuration with graph store providers. -Currently, we support Neo4j as a graph store provider. You can setup [Neo4j](https://neo4j.com/) locally or use the hosted [Neo4j AuraDB](https://neo4j.com/product/auradb/). +To initialize Graph Memory you'll need to set up your configuration with graph +store providers. Currently, we support [Neo4j](#initialize-neo4j) and +[Memgraph](#initialize-memgraph) as graph store providers. + + +### Initialize Neo4j + +You can setup [Neo4j](https://neo4j.com/) locally or use the hosted [Neo4j AuraDB](https://neo4j.com/product/auradb/). If you are using Neo4j locally, then you need to install [APOC plugins](https://neo4j.com/labs/apoc/4.1/installation/). @@ -163,6 +169,67 @@ const memory = new Memory(config); If you are using NodeSDK, you need to pass `enableGraph` as `true` in the `config` object. +### Initialize Memgraph + +Run Memgraph with Docker: + +```bash +docker run -p 7687:7687 memgraph/memgraph-mage:latest --schema-info-enabled=True +``` + +The `--schema-info-enabled` flag is set to `True` for more performant schema +generation. + +Additional information can be found on [Memgraph +documentation](https://memgraph.com/docs). + +User can also customize the LLM for Graph Memory from the [Supported LLM list](https://docs.mem0.ai/components/llms/overview) with three levels of configuration: + +1. **Main Configuration**: If `llm` is set in the main config, it will be used for all graph operations. +2. **Graph Store Configuration**: If `llm` is set in the graph_store config, it will override the main config `llm` and be used specifically for graph operations. +3. **Default Configuration**: If no custom LLM is set, the default LLM (`gpt-4o-2024-08-06`) will be used for all graph operations. + +Here's how you can do it: + + + +```python Python +from mem0 import Memory + +config = { + "graph_store": { + "provider": "memgraph", + "config": { + "url": "bolt://localhost:7687", + "username": "memgraph", + "password": "xxx", + }, + }, +} + +m = Memory.from_config(config_dict=config) +``` + +```python Python (Advanced) +config = { + "embedder": { + "provider": "openai", + "config": {"model": "text-embedding-3-large", "embedding_dims": 1536}, + }, + "graph_store": { + "provider": "memgraph", + "config": { + "url": "bolt://localhost:7687", + "username": "memgraph", + "password": "xxx" + } + } +} + +m = Memory.from_config(config_dict=config) +``` + + ## Graph Operations The Mem0's graph supports the following operations: diff --git a/examples/graph-db-demo/alice-memories.png b/examples/graph-db-demo/alice-memories.png new file mode 100644 index 0000000000000000000000000000000000000000..c1fe6d1984199f8ef73dd5a326d622b89e040328 GIT binary patch literal 68811 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

*EC!~OSYy-#G>-^4=IY7W zyG9rZ%0>W-&BQrStIrdI?a%{2|Llq#BT!6w$K9B$q`UuH~)=~fN5s102Ckm>+&@`RHe+gkSK|JNsng}DzMcz&pU_aFFMtv8U zZ_#?IW2`DRO@E!3Qlc(8y&A^K^SyrrTw&8^#x`oq%vgE1jeHP#w_aMFcRj^#4R0`F zQHP#DUDR*L%n!MkBl(aNoOWGt<%7waN{T-K2Yr)39<)m1w{tydx!yfdP@96B+&1R2 z{K`v7@3*3H+VW%^9uOK{viXSUYXrcw$a(W@y+)~xS^{?;>9tf8ny0_vI-{P8x!|FK zp&*=q@?M;PNX%Ch&VIkqS9G* zwS0)i!#CqQzsv=9pssX2JB@O< zKv47`Agh?xn%lUIWKQRO(k~S)s6`{LfMR-f4e~N8bc94I$;xPL?YThZLaJGkCm5*4 zTFIOEfU5l+7m!{&S97 zpK)g`78$&7rFiPAWneD*WI!?kOY#GsmiF&4EFeX8D z3ooGbSeijOag>NAI+4K}oY>HPZD}H8Kgo&74MSKY2ag#PT~FP=K_TI~prQX>OK(i$ zpHJRNQ4+Tdl2*f!O!ohzpReJFFk|0uKzaJa{0D@25z=#SMNbGgf@6E`DZhXt(NR7k z72FqrCsQ(`Pl+gl5}{l!4NDcocbJ2O9^tPO35>u=ZW*f&XaTxOhjur0bI;BU1!Q8G z)~Thf9-D`>(8H*}GVz=FR@(=}*BC#+U8)Y>L4<3saJER=H3kYG@H|BjCnI)Goj)Cl zR=pG<<~j^&U=1e4Ru)`$u_0141+hlnxNzGhcn!kEBo+2;kcXX=pwyyk@xn}`eu{{T?;|)w z-;m1Bd%g_Xq8*$g*l{G~Jv0v8@*iJME@UIweuOkaSVR|*;Gx$p1S6?)NbO-%hs?UP z(eNWOI@<(OPeQ;&)e1`z14GP(QR4L*NMZ;0brII+Q>BRXBXH6r?|{W!Xq;rE$V{n$ zVsF4AWq_z6!sSNG+cQunc*c(~G-ygZqee<2j7U}$Wp?j6{Irq%mX1#R8VUwwbw(^6 z7gz(ts&O;t>i0^~^%EZ}0>bv*6zM0|k}P=ZAihVleD({_gpW|fvm`6P=<(tY*Y+Yz z#Aq3=aHc{l=#AZ?G|4NGEPMfjZBh2y7qr< zE!$MrWqkI6z{Kv>FodjU1RPnuWWA4bizH=n;dy*maq352>;14>9A10o5s{eDb7|aT zh#i4}dI*oqA54PSq>18m!;hN{{?F%M-3&d+wnRweM*{ywhpNx|)kp%12y;OsjL`$U z_9xax=(=%|moSI3p5h^p?Glx!tH?7F&+f)dvEBPvl7ej%K$Wh0d)~W;fNZd8=rz># zT*n75{NY`PNEI=3%^cT*hf1tWP6}J%)b3+ySm4M(UF$Z)wGp#@q))WKCP3ZsCcdfw zl6e;cj#3FiZ1m**3DEq)klGdjS1X3`a^I2lgP}?qp6uqI9g}Tat?F*P>I|ka$2)E) zJ_5a3E zb-!-aY;RZX1-@{1i+l0n1yk0;QE(vh@DJD5|#@_`1$1?gE5FQo1i?c>`<_U+FtpdWH5%^znz_v6V=(N?(CY_aP2w~ zp`9^xR&*nURy&9Yb^AyNXfA*$)5!XJKwi0#HCHy1KeX%Rid2v-Y|UN{Y-) zL`^>>aO;MYQ%$6_;9__I?XRDuGUwpw>}|4<&u58 ze2|)*H?9rZ-DSE_Ik&r~u-Hdn`y7}j~W3$JY> zX0uxDD<~-V%^irJJycr=g_MLS9_cmjV?V@1;QPix@nI;KnkLT6!*ih&Pe(_`DJ5m7 z3e)T8jI73PP*l|c<HWt>jh1pmh>f7M3RxS9Wd(Y5cA2^I20`8sbTv|;{jbxz3s1~}Ys7RT`fiEec z=NCmY%!{oWh_=H@;~G)uTA8D>E*KnM+8Z4i6=epgc|n+SgH+hF1ycn+&o}D;7q*D2 z|B&;;Ej!)1n7*NPhA+KyZqUdVW=W)soB%r94pp^9!*Mn|2%LQGnFA0?KF_9nRb2;j z?qi|)VJIx1+3e6kF?{xw+Npmkb1Fs{-F+%!ZLAdy3|`vAI-s$bi^Tq;SZ~~2YDBoX zcr9jtF)4_WpWk>sCS{yt*w*$kiINJSSZi-gp{? zLF?nkMEfU_Y2f^0@c6CE)Tb^fL;P&wFL16nQ=kEQCv;pQD9^>+w5i3<+$|T5)K^p_ z7XTkCo*jpEgm>fVoKdi!I1kjXW63k96xUwe$bjmuPsTT(GDa+VM=tR1ABU`k3hmgw7M?w6ma;MN0AqTITO0|WL8Pf z?y3U)YU-wMPn*cc65qujZ%`w__mA5%6V_-!{3#ZnV$Oj0?T)pNyOlC}z~rJV#&;PR zvL%cYz>&$zq0yJX(O_MvhrAWOa+F}#;1aMJqgQ{OK5>wd;&gp?j)3#=dL z4C7a9+H+KtQmkqOK%fnBAkc>|k81~mKzD9PE4hZk@aGIEOEF<)T2*ht@6AQ--Q8WT z_^wW?j_x-(IVzkX@864egl7r8s}X7MF+9DI-u0_vxpaDJN|ugpJ6Kv41L~vh)R_s@ zcVw`?M1)HemaX|)sMVqJwh+wginoIl!+-z?S|aI<+xzv1Go$D^%Em6I~r2D<<=6csumA+Wf?9O$htJLJ@si~-l43+G%6&!(+s0l0h zXE>P>If;_Q%SQ&@yXG*_Uk{L?+CdnL-pIPF4C}hog*e)uY_wi28ySMw#}u5lnV-w$ zqhDb(q4G3^;fh+$mAQh>#Pewa$E_8g`U+!uiFBp7Pg1qUITz8uSu@0smG3_}O$=`d z!hpi>Wobb!811<5T#r|MBK0fDq;dL2E28`WFH3M0By66FE|2#fb0FA5IlXM2wzp%; zrJc5gte2ct7(VoWZy#0=1zl(mve8|(*29NcUVvG+4ec(iYr z3fz6yj;>DZZ0}`Y9K!v0K&4tIpnBdhq`Ozltc4>hH}E;|rKp{+*}4BScauQoKcw79 zjoG<}AM0DqI1OVKu^2yMmE$@9rl6;%k3X&J)1+kh1o)h~l!+Lv;R2N}l6x6?FN^3F z7}b-pUb9sW$jB6ySDYxsiH`7&q}Hz#hRd7lRl9usC#pN+DDT;@s1KYM)peQqN;kit z#s}YSr%+_)?jNo3QC`9GZM631l71x#qdFo4hXHJrU#BSK^ff~nSgHB2m^8SX z*g~=6pdGPcC_N@)@Y{=k%1r1?^42eEzzeRguPbi`%y(;5I;Z*jDzJ$YQZo4VJU zGBE{VM`}V+XL&7Kb{8benX7rPBPP~(%Hsx!3GWAQ}w0#alkJR+0S6ve5v5&J~B5_ttOhI8VajbuZ~9Y4{d*HCa^!4U_via zJl4MZ;PGAW<A)o0;M$fb@OIH9WoysVuuI^Q z!w5e@A~C<|%4}G>Nm3T3+PZRqJAC{3DVkm8W_t~Ntq9a~$f$Z-w3xy7lUBTiON z_onVvhkY)6b*Z%Sg4Rn9J{YWc=2H`e+Z(ydn(V_voN;&00O3~(kd)@!tL1se8fAsX z7JzZGqnQEiON<`mftx#<;&=Y>%I<{k7Ju8DwKFp;!&#+VdkeQO40MroVcwMC$D&K*>-dc4(lu4uMRn-&gNH{bL+jUwb-Wy zpOjlEFE15p{BitXJU>|2+Vs?>cXyR4{J=cU(9EvN-8UWX03 zrXy|W1*nz;OEST>nKw&gzua>BjCAEfZFr@0?(n?y`{ViZ!E*X2Q|p{}g^uR_4uc&>c-zBN z*FiFN8f>T?OM}Pc=g_RPk1V9M@7!U}ul@5L0s084D!hpsI`7QrX?NF6A}_Kc|>QfS@&1R zh(Q~RA!FqEzEpRp1f&VLadI;Ab)_~#^v4D>fXcldi+pikf2UsYd*OZ`+nKFO=Rhxu zo$0f>Rb0A?d?yWEy6Uv$`n#-|{+!_FxKCVh5nY8%9SmTUxAljm;Kp9yd&kZJyEH2^ zdJ^D_<~*h<5859Z_LVPMp1iCUf1w`T4t=MQIeG6Dp;EM}ckV$UmEcM;`EE1w4cehe z?>+PHT(S1@k@_>H3J)swNe4&&Y?TW_o$ylJGv_P9Q5e~3umIf$7$QV;XjN%%=hC|g zM?ap#*wo}_DOiRT%upxm_|m@Z={Q4%b4-2eZC>3(57IF^5FahFpRPnM4rYfdRkH3R254#fX4H1| z24n(ddl_d>>IZCm&5P0Y)92ZLZ^KkDXX}2oOJT$ykxRs$y?%3P;LeEeZ@YP@WV)m| z_F+oyr2gn@a$^bl4w?s!@Y+pjXaC}&B2&OJIYEn1@C=oJVa@Phsfkzhw~EbM8l*Mk zf+h>bOrGYMLY?|1WWu>b7l8$L()`uM0L02*_q;xRym@GH2-tSO_fCA$V%GPmU#>}E zjbTl`%^Y;+NzcWoV>ei8TbMqr(B~3$^q0)KgSiZ)h`&ZeoDbKjI_v&*#1|y?qo@KO zr3iw+#e&(Cdu&l(Vs)F-Jh`qE%douDQmPW!`m^vXfJSm7LPJACR)}gl46gRrH{V3!T zh<@MFIRL`${s{O-TU9-Zq(zUy9V>;33WO3W%*9Yj{GaGMsypnVg zZd@(~yH|$#XnnZc1ByK;)g8;#8?)Bya{u}`(@BxB(xR*M<*Es)_I;JkMjU$k64Z(B z2o3l!A@41MGuM3Stqihr5LWf>`an>c%WmQRHcJ1G_S4dkge;WU_k}6H@V+?WPc?w$r=VZj=i z)$ZE*?8y*ua%p4&9eIe@YxRlArS8D#=C~9`RxOAe#cKB`4;3p~!3eaex}M>iH>j zS+t|@DB8=sr+L2mP3{Xx@7~;)ZxAHXrq0H(q7g9@QZWe6x44>~Q#ECCPEqW|u1kcA z0n3Ig5}`wG!euvc;;<>{lVZ+saPR6A7EpjpzRPN|!cRJ!umT^KpX1 zQsp3>5DRxqzs#5lxg&U15`_1bIromZOd`p|2jyAnEqRAT?OcHx2pLn&V5PBkI&if? ziOLbTjg>n;VScy!L*usdKQq1r$UwSx4@-##;|1W})%;Tku@87uNhC<^!bigU9IVBI zvpxCM>tb!v`|Rj6c@TCm%cXK8eoR&-6cq_BZfxx7oxa-QB3M1^Rw1NjkwX1V6(d^! zM;k45*B*8ii{T`!uRAR|CH{`8E+rPqCC|9bUd_n~H;4SwQOrB!EE`{VihLsW1mV{& zuk7qXK022sj@dreLEVI{X8~^K%>0rGCOE7veC6{^iHcLmUUQHnQZ$NZ3`oYZd5#-9 z-hLT&{CG$r$koIh+m`_8zN4YEx*#C6>rGqZ2@SqXTP6_UA`wP*HGciZo1Vy;DZ}bAc}-Y_0kTRxewQw^-A^eVlJ}Ig=|lU+TEt zjP1Ug#Y2r!h7vjC6n6}tKc1NW7Pz=%@blwGSp*Fg z=UvQ%>fo#Mdyuw0;5>`SvYiAeyu2uuxjz-Pxs|e#OBHXWmD=igd##JAsrNnP4T5KR zyn2rwUTxgYG%1oAkj7b}fRUA8Nt5Z_#iH!v`D~`T?#VLQgqVCILd>;FQx}VoErd^_ zmwIT=jC@*|w7Gd!2=kR*@#RPX^^B}uy^r6>&>M`8Y5E3vX&yXyAZXQ@LAUe?6!ia3 z$(zO9Gk@{AL{Mu)H&$^qKeKaP*)y|I>dy;vy9*v`Mja<>!Ev^P_`lqhz3i_ukd7c? zIM~@gh)*Po3S|}z>U~reLJcnd?woKDbNrYWuAJ{6q{DwLl>~N)!AK|MUw1GggQEg~ z=D7i@S5FA!`y}5)Ugt6+Zx!;*=XF5oHNWR;ODg4;jE)kc`9Z}$3t&3bb4D0oS=Nk< zTDB%ptA4?2)|#?m)k0yJzv@gOl<{tur4wOy>3kKyj1~1m$DYCC$+3Iu&d5E&VO

P zI6KHSPfR5o#IcD79a)aYXI)tm$8zWMmwEe$^o#?XE07EcsifD2ZeF+~cj#cH)Jbj+ zvm?}o)H~$|D>$1nuHY@VPT8|>WLNf-EVA?LmT1F6Lu243DnUhn?HZ)SV4Y>OAuWHu zn!6p42^EWZm9hb1+BwCE(W(`KI`Ym4yk z71UJ<;Xi$!XRjBqgS21?H57MUdo&|cQXH7216QieR2g}B9hR0$E(Q5BsqA}rTfM2; zskzrA6By_V0IVBwuIX%e=^@Y_L-wCp4xix55D!|wQlP+lXvZgVZ);ao{oZ7auy~fp zv`KE%uLq-};`*X*iC1A{6_0q@DUkj{NMmodgjWAG&phn_lS~k?iM7nn^8VddB>Z|- z=(=_K9LZ&s&Hg(lxN9BNrG!a7_T0aJ-?YEL*a^bcN`@ccGhGD@`V0U0{NY#pVSG22 z_Dq)4HYmJ2aOK4h#Vr2s5&WB*f21sUSPI_?-pp!QIg|Knd<0ml*`PD0bUpohpAbA{ z1aVboKWZI5MF#7!R!?dMwNb6BHUS?S^Ixv;k1hp=&mFTIoYY@I*U(6e-mV#ZZfa$t z*ocgp&JcW3N-G2^DMng3psQ*yGW2j!Sd72)jT?{eI=6@v!AQN=<>iHyqC&Z zd{}?O(Xn!xxT9wxlx%J8S!=QFb|{G3H;GIQMeVO!~GP8 zIs%UZh^@#GBbY1miPyciUyhD-2v-Z74|^MXf(m6_0Y^MU2$CgQuKm+fJ0lWTl*;J) zMKeDzwk3<*fjTXH|Ff|Fl zBM?wfIweO1;b83Xfyy>i3;V0(EeKHPZUP(=;*+*gIJmVG*FY}Jp;nAX?7KDRMVW>) zC_0pBYJF)0tu!@o&}y3&^YZ0+D84-h5PU(CI0JmqZ{(~xG}0dK3yS}imKGOeaibvL zrRZAPWwy|(9<^Y$@QKrD|3+qHf|dYTYqR64c=Fq`Rh6%0Ru|5-V(ju=qgAdf?-?*p z3~2Tk0}|EVy^#Yh{~gGYrX-&JyE{fG5fPE~ev|MffSmJ8Pfx3~c4c0FG3;ekc=zIa z<<7`c%GhDk^r!-93N4MEkCz3CKQ+JY6Phj#i`u! z6?SDM0-9eGG&MD&tg&8S31U4?t5fWPw$i0!_{qtW9Ic)g&qwgmCJv^GRhPCFe3Vl% znG#qvZ?s{xkaxA~O4XJtYX0^unv}&u`h4x)IuXwUd9Ws`U`@o1#~#M3kE-z0_=|pj z9!8yoa(zDsW54SW*iX~$R7ECNw-`#+TW0EKgt5IW##B#+x=P&pjxQ~2yL(Sg7w1Qs z4SeQ+PT8_;LEO5QC{ArsV#(qSkSh=AohIJG}s?$uz*ApPL_v; zY>W(Mgx>+!DfepC&#-xtt+(VI!!aD6`;x@#2bdQ#?%aG~>iSO{zr}l{z@trH-$r#$ zd5Jk*Rt*lGOqSufpV_#3`s~xFhV(Bp;jyJJZ=Ggj#A zET8@2>OL5?_{(x7Pqos|!qimW*SCg}Nl+HNCe^F?6k!hNYq!9YMzfN~A+8|~t3%8z z2#EETA|PfMsP(J0oEN5w*qjzqiD{>u-bgp=7B1*v3*78_b)8g*Tpw!4vQh|URN1WtCJM#a{&>TMfQ3@#D9Z8|Q z6Mr8AKd+Mm_0BrpkHj{ApN`vu-T1JY@Zi(m#~{DFO8)WpJJ!Fs9vt>Ks;8%?v%MXs zprBwd>R@M=J14iTq^cSlAAdqfqsW(=i_6RB^d(s$;sXk(@%!Bg1GE5$Q3fSYy%CA9 z5^#BJ&Xg@IEKH3a{3+i9M%*Ysv}r=1mXVe9`Sc{Dnzm-Jwh-wN(+559<+Fn+8e+nK zmt}nJg88=-P>4U&5$5LRIom}LTO%F%^W!}u6TmN}=;l^N33U!9C#NZdrF!#(Ev$h+ zhg)WIEIZAFtQ3kl(D5h$iRH+Mh_RmUmTNi7!F+iUqkA)=od|CI=amN&vIu3L_7@{| zuu{n&xiMQYLF6Oq6tL}g^;IO>rEEWc`jnt%Yx`5zcXM--OH{j7gjMwF)e+lO-vQ8) z`3XHHrM(+f&?0a#3Alu%B4OT2!lX4MQNksA2VwXr5g-I01|v?mbGSihH6v(aSqDTT zi1sOj3SR-DtNdQZS$hpGL(|t7zG z6y$@FP8;5+}I zJPJ_8pq0ge*z@1Toej@CA_lT43F(-+?#X zj)CWNOp#GNO1IjpR03wMbV2}M0kb+(h!(}$z3prm zTD%J2+u6bH#Z@T6Xd3uzYBB++a6m5n^-nKqq&gi2n>8mvk@q(d&Wuuu<-h1X3Q%Sz zU?NLOy<@1=m3&V{)C2m7TD(*OVKOVXGWiF z(qVAFPst9-$>s)&d!2B8T;P&ipVf8bJQU+H){%$>E%-}+xYiTzwR2)Z6WFm?0ZnI(HP@fF%6KdwO zwZIl3OB&P8*fu-_sGM)M0G{|gG1&!esY!bqb$`i6-$ znAe;g!%4GMzr`UlhATlY`BlGiwYz(mm_d*RCj?D{=4K%hOt;)5QF-7o87evj5{rT! zggXdEd49N}SZoHe^g}MTeV>KQzhU5&3@KA)JyNkEO`Z~_ z(06w0H5Zf>gt4khN+=e>Da?-7i0>ul=por*}De;)jQ|DgW_*N4v{$RlZ;sn^Zb|FwQPAW8;O2xb0l z!=Cd)hNej^&Ks#V{r!LfxOnHNAu##(U(V=)t&U7Nzx`j6X$0|<-f2j0{M!`DBH3vU qse3E`HFI^xa66^{&jeI=K-xwBBvf~jBAE#ODcn$%&AVpg|9=1$Ib+BG literal 0 HcmV?d00001 diff --git a/examples/graph-db-demo/memgraph-example.ipynb b/examples/graph-db-demo/memgraph-example.ipynb new file mode 100644 index 00000000..b559b6e2 --- /dev/null +++ b/examples/graph-db-demo/memgraph-example.ipynb @@ -0,0 +1,230 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Memgraph as Graph Memory" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "### 1. Install Mem0 with Graph Memory support \n", + "\n", + "To use Mem0 with Graph Memory support, install it using pip:\n", + "\n", + "```bash\n", + "pip install \"mem0ai[graph]\"\n", + "```\n", + "\n", + "This command installs Mem0 along with the necessary dependencies for graph functionality.\n", + "\n", + "### 2. Install Memgraph\n", + "\n", + "To utilize Memgraph as Graph Memory, run it with Docker:\n", + "\n", + "```bash\n", + "docker run -p 7687:7687 memgraph/memgraph-mage:latest --schema-info-enabled=True\n", + "```\n", + "\n", + "The `--schema-info-enabled` flag is set to `True` for more performant schema\n", + "generation.\n", + "\n", + "Additional information can be found on [Memgraph documentation](https://memgraph.com/docs). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration\n", + "\n", + "Do all the imports and configure OpenAI (enter your OpenAI API key):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from mem0 import Memory\n", + "\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = (\n", + " \"\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set up configuration to use the embedder model and Memgraph as a graph store:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "config = {\n", + " \"embedder\": {\n", + " \"provider\": \"openai\",\n", + " \"config\": {\"model\": \"text-embedding-3-large\", \"embedding_dims\": 1536},\n", + " },\n", + " \"graph_store\": {\n", + " \"provider\": \"memgraph\",\n", + " \"config\": {\n", + " \"url\": \"bolt://localhost:7687\",\n", + " \"username\": \"memgraph\",\n", + " \"password\": \"mem0graph\",\n", + " },\n", + " },\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Graph Memory initializiation \n", + "\n", + "Initialize Memgraph as a Graph Memory store: " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/katelatte/repos/forks/mem0/.venv/lib/python3.13/site-packages/neo4j/_sync/driver.py:547: DeprecationWarning: Relying on Driver's destructor to close the session is deprecated. Please make sure to close the session. Use it as a context (`with` statement) or make sure to call `.close()` explicitly. Future versions of the driver will not close drivers automatically.\n", + " _deprecation_warn(\n" + ] + } + ], + "source": [ + "m = Memory.from_config(config_dict=config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Store memories \n", + "\n", + "Create memories:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": \"I'm planning to watch a movie tonight. Any recommendations?\",\n", + " },\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"How about a thriller movies? They can be quite engaging.\",\n", + " },\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": \"I'm not a big fan of thriller movies but I love sci-fi movies.\",\n", + " },\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.\",\n", + " },\n", + "]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Store memories in Memgraph:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "# Store inferred memories (default behavior)\n", + "result = m.add(\n", + " messages, user_id=\"alice\", metadata={\"category\": \"movie_recommendations\"}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](./alice-memories.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Search memories" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loves sci-fi movies 0.31536642873408993\n", + "Planning to watch a movie tonight 0.09684523796547778\n", + "Not a big fan of thriller movies 0.09468540071789475\n" + ] + } + ], + "source": [ + "for result in m.search(\"what does alice love?\", user_id=\"alice\")[\"results\"]:\n", + " print(result[\"memory\"], result[\"score\"])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/mem0/graphs/configs.py b/mem0/graphs/configs.py index c14249ad..e0e98274 100644 --- a/mem0/graphs/configs.py +++ b/mem0/graphs/configs.py @@ -20,6 +20,22 @@ class Neo4jConfig(BaseModel): if not url or not username or not password: raise ValueError("Please provide 'url', 'username' and 'password'.") return values + +class MemgraphConfig(BaseModel): + url: Optional[str] = Field(None, description="Host address for the graph database") + username: Optional[str] = Field(None, description="Username for the graph database") + password: Optional[str] = Field(None, description="Password for the graph database") + + @model_validator(mode="before") + def check_host_port_or_path(cls, values): + url, username, password = ( + values.get("url"), + values.get("username"), + values.get("password"), + ) + if not url or not username or not password: + raise ValueError("Please provide 'url', 'username' and 'password'.") + return values class GraphStoreConfig(BaseModel): @@ -35,5 +51,7 @@ class GraphStoreConfig(BaseModel): provider = values.data.get("provider") if provider == "neo4j": return Neo4jConfig(**v.model_dump()) + elif provider == "memgraph": + return MemgraphConfig(**v.model_dump()) else: raise ValueError(f"Unsupported graph store provider: {provider}") diff --git a/mem0/memory/main.py b/mem0/memory/main.py index 50dfe8ab..5b35a40f 100644 --- a/mem0/memory/main.py +++ b/mem0/memory/main.py @@ -59,7 +59,10 @@ class Memory(MemoryBase): self.enable_graph = False if self.config.graph_store.config: - from mem0.memory.graph_memory import MemoryGraph + if self.config.graph_store.provider == "memgraph": + from mem0.memory.memgraph_memory import MemoryGraph + else: + from mem0.memory.graph_memory import MemoryGraph self.graph = MemoryGraph(self.config) self.enable_graph = True diff --git a/mem0/memory/memgraph_memory.py b/mem0/memory/memgraph_memory.py new file mode 100644 index 00000000..e425ba55 --- /dev/null +++ b/mem0/memory/memgraph_memory.py @@ -0,0 +1,516 @@ +import logging + +from mem0.memory.utils import format_entities + +try: + from langchain_memgraph import Memgraph +except ImportError: + raise ImportError( + "langchain_memgraph is not installed. Please install it using pip install langchain-memgraph" + ) + +try: + from rank_bm25 import BM25Okapi +except ImportError: + raise ImportError( + "rank_bm25 is not installed. Please install it using pip install rank-bm25" + ) + +from mem0.graphs.tools import ( + DELETE_MEMORY_STRUCT_TOOL_GRAPH, + DELETE_MEMORY_TOOL_GRAPH, + EXTRACT_ENTITIES_STRUCT_TOOL, + EXTRACT_ENTITIES_TOOL, + RELATIONS_STRUCT_TOOL, + RELATIONS_TOOL, +) +from mem0.graphs.utils import EXTRACT_RELATIONS_PROMPT, get_delete_messages +from mem0.utils.factory import EmbedderFactory, LlmFactory + +logger = logging.getLogger(__name__) + + +class MemoryGraph: + def __init__(self, config): + self.config = config + self.graph = Memgraph( + self.config.graph_store.config.url, + self.config.graph_store.config.username, + self.config.graph_store.config.password, + ) + self.embedding_model = EmbedderFactory.create( + self.config.embedder.provider, + self.config.embedder.config, + {"enable_embeddings": True}, + ) + + self.llm_provider = "openai_structured" + if self.config.llm.provider: + self.llm_provider = self.config.llm.provider + if self.config.graph_store.llm: + self.llm_provider = self.config.graph_store.llm.provider + + self.llm = LlmFactory.create(self.llm_provider, self.config.llm.config) + self.user_id = None + self.threshold = 0.7 + + # Setup Memgraph: + # 1. Create vector index (created Entity label on all nodes) + # 2. Create label property index for performance optimizations + embedding_dims = self.config.embedder.config["embedding_dims"] + create_vector_index_query = f"CREATE VECTOR INDEX memzero ON :Entity(embedding) WITH CONFIG {{'dimension': {embedding_dims}, 'capacity': 1000, 'metric': 'cos'}};" + self.graph.query(create_vector_index_query, params={}) + create_label_prop_index_query = f"CREATE INDEX ON :Entity(user_id);" + self.graph.query(create_label_prop_index_query, params={}) + create_label_index_query = f"CREATE INDEX ON :Entity;" + self.graph.query(create_label_index_query, params={}) + + def add(self, data, filters): + """ + Adds data to the graph. + + Args: + data (str): The data to add to the graph. + filters (dict): A dictionary containing filters to be applied during the addition. + """ + entity_type_map = self._retrieve_nodes_from_data(data, filters) + to_be_added = self._establish_nodes_relations_from_data( + data, filters, entity_type_map + ) + search_output = self._search_graph_db( + node_list=list(entity_type_map.keys()), filters=filters + ) + to_be_deleted = self._get_delete_entities_from_search_output( + search_output, data, filters + ) + + # TODO: Batch queries with APOC plugin + # TODO: Add more filter support + deleted_entities = self._delete_entities(to_be_deleted, filters["user_id"]) + added_entities = self._add_entities( + to_be_added, filters["user_id"], entity_type_map + ) + + return {"deleted_entities": deleted_entities, "added_entities": added_entities} + + def search(self, query, filters, limit=100): + """ + Search for memories and related graph data. + + Args: + query (str): Query to search for. + filters (dict): A dictionary containing filters to be applied during the search. + limit (int): The maximum number of nodes and relationships to retrieve. Defaults to 100. + + Returns: + dict: A dictionary containing: + - "contexts": List of search results from the base data store. + - "entities": List of related graph data based on the query. + """ + entity_type_map = self._retrieve_nodes_from_data(query, filters) + search_output = self._search_graph_db( + node_list=list(entity_type_map.keys()), filters=filters + ) + + if not search_output: + return [] + + search_outputs_sequence = [ + [item["source"], item["relationship"], item["destination"]] + for item in search_output + ] + bm25 = BM25Okapi(search_outputs_sequence) + + tokenized_query = query.split(" ") + reranked_results = bm25.get_top_n(tokenized_query, search_outputs_sequence, n=5) + + search_results = [] + for item in reranked_results: + search_results.append( + {"source": item[0], "relationship": item[1], "destination": item[2]} + ) + + logger.info(f"Returned {len(search_results)} search results") + + return search_results + + def delete_all(self, filters): + cypher = """ + MATCH (n {user_id: $user_id}) + DETACH DELETE n + """ + params = {"user_id": filters["user_id"]} + self.graph.query(cypher, params=params) + + def get_all(self, filters, limit=100): + """ + Retrieves all nodes and relationships from the graph database based on optional filtering criteria. + + Args: + filters (dict): A dictionary containing filters to be applied during the retrieval. + limit (int): The maximum number of nodes and relationships to retrieve. Defaults to 100. + Returns: + list: A list of dictionaries, each containing: + - 'contexts': The base data store response for each memory. + - 'entities': A list of strings representing the nodes and relationships + """ + + # return all nodes and relationships + query = """ + MATCH (n:Entity {user_id: $user_id})-[r]->(m:Entity {user_id: $user_id}) + RETURN n.name AS source, type(r) AS relationship, m.name AS target + LIMIT $limit + """ + results = self.graph.query( + query, params={"user_id": filters["user_id"], "limit": limit} + ) + + final_results = [] + for result in results: + final_results.append( + { + "source": result["source"], + "relationship": result["relationship"], + "target": result["target"], + } + ) + + logger.info(f"Retrieved {len(final_results)} relationships") + + return final_results + + def _retrieve_nodes_from_data(self, data, filters): + """Extracts all the entities mentioned in the query.""" + _tools = [EXTRACT_ENTITIES_TOOL] + if self.llm_provider in ["azure_openai_structured", "openai_structured"]: + _tools = [EXTRACT_ENTITIES_STRUCT_TOOL] + search_results = self.llm.generate_response( + messages=[ + { + "role": "system", + "content": f"You are a smart assistant who understands entities and their types in a given text. If user message contains self reference such as 'I', 'me', 'my' etc. then use {filters['user_id']} as the source entity. Extract all the entities from the text. ***DO NOT*** answer the question itself if the given text is a question.", + }, + {"role": "user", "content": data}, + ], + tools=_tools, + ) + + entity_type_map = {} + + try: + for tool_call in search_results["tool_calls"]: + if tool_call["name"] != "extract_entities": + continue + for item in tool_call["arguments"]["entities"]: + entity_type_map[item["entity"]] = item["entity_type"] + except Exception as e: + logger.exception( + f"Error in search tool: {e}, llm_provider={self.llm_provider}, search_results={search_results}" + ) + + entity_type_map = { + k.lower().replace(" ", "_"): v.lower().replace(" ", "_") + for k, v in entity_type_map.items() + } + logger.debug( + f"Entity type map: {entity_type_map}\n search_results={search_results}" + ) + return entity_type_map + + def _establish_nodes_relations_from_data(self, data, filters, entity_type_map): + """Eshtablish relations among the extracted nodes.""" + if self.config.graph_store.custom_prompt: + messages = [ + { + "role": "system", + "content": EXTRACT_RELATIONS_PROMPT.replace( + "USER_ID", filters["user_id"] + ).replace( + "CUSTOM_PROMPT", f"4. {self.config.graph_store.custom_prompt}" + ), + }, + {"role": "user", "content": data}, + ] + else: + messages = [ + { + "role": "system", + "content": EXTRACT_RELATIONS_PROMPT.replace( + "USER_ID", filters["user_id"] + ), + }, + { + "role": "user", + "content": f"List of entities: {list(entity_type_map.keys())}. \n\nText: {data}", + }, + ] + + _tools = [RELATIONS_TOOL] + if self.llm_provider in ["azure_openai_structured", "openai_structured"]: + _tools = [RELATIONS_STRUCT_TOOL] + + extracted_entities = self.llm.generate_response( + messages=messages, + tools=_tools, + ) + + entities = [] + if extracted_entities["tool_calls"]: + entities = extracted_entities["tool_calls"][0]["arguments"]["entities"] + + entities = self._remove_spaces_from_entities(entities) + logger.debug(f"Extracted entities: {entities}") + return entities + + def _search_graph_db(self, node_list, filters, limit=100): + """Search similar nodes among and their respective incoming and outgoing relations.""" + result_relations = [] + + for node in node_list: + n_embedding = self.embedding_model.embed(node) + + cypher_query = f""" + MATCH (n:Entity {{user_id: $user_id}})-[r]->(m:Entity) + WHERE n.embedding IS NOT NULL + WITH collect(n) AS nodes1, collect(m) AS nodes2, r + CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2) + YIELD node1, node2, similarity + WITH node1, node2, similarity, r + WHERE similarity >= $threshold + RETURN node1.user_id AS source, id(node1) AS source_id, type(r) AS relationship, id(r) AS relation_id, node2.user_id AS destination, id(node2) AS destination_id, similarity + UNION + MATCH (n:Entity {{user_id: $user_id}})<-[r]-(m:Entity) + WHERE n.embedding IS NOT NULL + WITH collect(n) AS nodes1, collect(m) AS nodes2, r + CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2) + YIELD node1, node2, similarity + WITH node1, node2, similarity, r + WHERE similarity >= $threshold + RETURN node2.name AS source, id(node2) AS source_id, type(r) AS relationship, id(r) AS relation_id, node1.name AS destination, id(node1) AS destination_id, similarity + ORDER BY similarity DESC + LIMIT $limit; + """ + params = { + "n_embedding": n_embedding, + "threshold": self.threshold, + "user_id": filters["user_id"], + "limit": limit, + } + ans = self.graph.query(cypher_query, params=params) + result_relations.extend(ans) + + return result_relations + + def _get_delete_entities_from_search_output(self, search_output, data, filters): + """Get the entities to be deleted from the search output.""" + search_output_string = format_entities(search_output) + system_prompt, user_prompt = get_delete_messages( + search_output_string, data, filters["user_id"] + ) + + _tools = [DELETE_MEMORY_TOOL_GRAPH] + if self.llm_provider in ["azure_openai_structured", "openai_structured"]: + _tools = [ + DELETE_MEMORY_STRUCT_TOOL_GRAPH, + ] + + memory_updates = self.llm.generate_response( + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ], + tools=_tools, + ) + to_be_deleted = [] + for item in memory_updates["tool_calls"]: + if item["name"] == "delete_graph_memory": + to_be_deleted.append(item["arguments"]) + # in case if it is not in the correct format + to_be_deleted = self._remove_spaces_from_entities(to_be_deleted) + logger.debug(f"Deleted relationships: {to_be_deleted}") + return to_be_deleted + + def _delete_entities(self, to_be_deleted, user_id): + """Delete the entities from the graph.""" + results = [] + for item in to_be_deleted: + source = item["source"] + destination = item["destination"] + relationship = item["relationship"] + + # Delete the specific relationship between nodes + cypher = f""" + MATCH (n:Entity {{name: $source_name, user_id: $user_id}}) + -[r:{relationship}]-> + (m {{name: $dest_name, user_id: $user_id}}) + DELETE r + RETURN + n.name AS source, + m.name AS target, + type(r) AS relationship + """ + params = { + "source_name": source, + "dest_name": destination, + "user_id": user_id, + } + result = self.graph.query(cypher, params=params) + results.append(result) + return results + + # added Entity label to all nodes for vector search to work + def _add_entities(self, to_be_added, user_id, entity_type_map): + """Add the new entities to the graph. Merge the nodes if they already exist.""" + results = [] + for item in to_be_added: + # entities + source = item["source"] + destination = item["destination"] + relationship = item["relationship"] + + # types + source_type = entity_type_map.get(source, "unknown") + destination_type = entity_type_map.get(destination, "unknown") + + # embeddings + source_embedding = self.embedding_model.embed(source) + dest_embedding = self.embedding_model.embed(destination) + + # search for the nodes with the closest embeddings; this is basically + # comparison of one embedding to all embeddings in a graph -> vector + # search with cosine similarity metric + source_node_search_result = self._search_source_node( + source_embedding, user_id, threshold=0.9 + ) + destination_node_search_result = self._search_destination_node( + dest_embedding, user_id, threshold=0.9 + ) + + # TODO: Create a cypher query and common params for all the cases + if not destination_node_search_result and source_node_search_result: + cypher = f""" + MATCH (source:Entity) + WHERE id(source) = $source_id + MERGE (destination:{destination_type}:Entity {{name: $destination_name, user_id: $user_id}}) + ON CREATE SET + destination.created = timestamp(), + destination.embedding = $destination_embedding, + destination:Entity + MERGE (source)-[r:{relationship}]->(destination) + ON CREATE SET + r.created = timestamp() + RETURN source.name AS source, type(r) AS relationship, destination.name AS target + """ + + params = { + "source_id": source_node_search_result[0]["id(source_candidate)"], + "destination_name": destination, + "destination_embedding": dest_embedding, + "user_id": user_id, + } + elif destination_node_search_result and not source_node_search_result: + cypher = f""" + MATCH (destination:Entity) + WHERE id(destination) = $destination_id + MERGE (source:{source_type}:Entity {{name: $source_name, user_id: $user_id}}) + ON CREATE SET + source.created = timestamp(), + source.embedding = $source_embedding, + source:Entity + MERGE (source)-[r:{relationship}]->(destination) + ON CREATE SET + r.created = timestamp() + RETURN source.name AS source, type(r) AS relationship, destination.name AS target + """ + + params = { + "destination_id": destination_node_search_result[0][ + "id(destination_candidate)" + ], + "source_name": source, + "source_embedding": source_embedding, + "user_id": user_id, + } + elif source_node_search_result and destination_node_search_result: + cypher = f""" + MATCH (source:Entity) + WHERE id(source) = $source_id + MATCH (destination:Entity) + WHERE id(destination) = $destination_id + MERGE (source)-[r:{relationship}]->(destination) + ON CREATE SET + r.created_at = timestamp(), + r.updated_at = timestamp() + RETURN source.name AS source, type(r) AS relationship, destination.name AS target + """ + params = { + "source_id": source_node_search_result[0]["id(source_candidate)"], + "destination_id": destination_node_search_result[0][ + "id(destination_candidate)" + ], + "user_id": user_id, + } + else: + cypher = f""" + MERGE (n:{source_type}:Entity {{name: $source_name, user_id: $user_id}}) + ON CREATE SET n.created = timestamp(), n.embedding = $source_embedding, n:Entity + ON MATCH SET n.embedding = $source_embedding + MERGE (m:{destination_type}:Entity {{name: $dest_name, user_id: $user_id}}) + ON CREATE SET m.created = timestamp(), m.embedding = $dest_embedding, m:Entity + ON MATCH SET m.embedding = $dest_embedding + MERGE (n)-[rel:{relationship}]->(m) + ON CREATE SET rel.created = timestamp() + RETURN n.name AS source, type(rel) AS relationship, m.name AS target + """ + params = { + "source_name": source, + "dest_name": destination, + "source_embedding": source_embedding, + "dest_embedding": dest_embedding, + "user_id": user_id, + } + result = self.graph.query(cypher, params=params) + results.append(result) + return results + + def _remove_spaces_from_entities(self, entity_list): + for item in entity_list: + item["source"] = item["source"].lower().replace(" ", "_") + item["relationship"] = item["relationship"].lower().replace(" ", "_") + item["destination"] = item["destination"].lower().replace(" ", "_") + return entity_list + + def _search_source_node(self, source_embedding, user_id, threshold=0.9): + cypher = f""" + CALL vector_search.search("memzero", 1, $source_embedding) + YIELD distance, node, similarity + WITH node AS source_candidate, similarity + WHERE source_candidate.user_id = $user_id AND similarity >= $threshold + RETURN id(source_candidate); + """ + + params = { + "source_embedding": source_embedding, + "user_id": user_id, + "threshold": threshold, + } + + result = self.graph.query(cypher, params=params) + return result + + def _search_destination_node(self, destination_embedding, user_id, threshold=0.9): + cypher = f""" + CALL vector_search.search("memzero", 1, $destination_embedding) + YIELD distance, node, similarity + WITH node AS destination_candidate, similarity + WHERE node.user_id = $user_id AND similarity >= $threshold + RETURN id(destination_candidate); + """ + params = { + "destination_embedding": destination_embedding, + "user_id": user_id, + "threshold": threshold, + } + + result = self.graph.query(cypher, params=params) + return result diff --git a/poetry.lock b/poetry.lock index 831ca4e9..ceacb6f4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -7,6 +7,7 @@ description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -19,6 +20,7 @@ description = "High level compatibility layer for multiple asynchronous event lo optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, @@ -32,7 +34,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -55,6 +57,7 @@ description = "Function decoration for backoff and retry" optional = false python-versions = ">=3.7,<4.0" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, @@ -67,6 +70,7 @@ description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, @@ -79,7 +83,7 @@ description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"graph\" and platform_python_implementation == \"PyPy\"" +markers = "platform_python_implementation == \"PyPy\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -160,6 +164,7 @@ description = "The Real First Universal Charset Detector. Open, modern and activ optional = false python-versions = ">=3.7" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -266,7 +271,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\"", test = "sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\" and (python_version <= \"3.12\" or python_version >= \"3.13\")", dev = "sys_platform == \"win32\" and (python_version <= \"3.12\" or python_version >= \"3.13\")", test = "sys_platform == \"win32\" and (python_version <= \"3.12\" or python_version >= \"3.13\")"} [[package]] name = "distro" @@ -275,6 +280,7 @@ description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, @@ -303,7 +309,7 @@ description = "File-system specification" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"graph\"" +markers = "extra == \"graph\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "fsspec-2024.12.0-py3-none-any.whl", hash = "sha256:b520aed47ad9804237ff878b504267a3b0b441e97508bd6d2d8774e3db85cee2"}, {file = "fsspec-2024.12.0.tar.gz", hash = "sha256:670700c977ed2fb51e0d9f9253177ed20cbde4a3e5c0283cc5385b5870c8533f"}, @@ -344,7 +350,7 @@ description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "(platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and python_version < \"3.14\"" +markers = "(platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and python_version < \"3.14\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -432,6 +438,7 @@ description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "grpcio-1.71.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:c200cb6f2393468142eb50ab19613229dcc7829b5ccee8b658a36005f6669fdd"}, {file = "grpcio-1.71.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b2266862c5ad664a380fbbcdbdb8289d71464c42a8c29053820ee78ba0119e5d"}, @@ -496,6 +503,7 @@ description = "Protobuf code generator for gRPC" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "grpcio_tools-1.71.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:f4ad7f0d756546902597053d70b3af2606fbd70d7972876cd75c1e241d22ae00"}, {file = "grpcio_tools-1.71.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:64bdb291df61cf570b5256777ad5fe2b1db6d67bc46e55dc56a0a862722ae329"}, @@ -562,6 +570,7 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -574,6 +583,7 @@ description = "Pure-Python HTTP/2 protocol implementation" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, @@ -590,6 +600,7 @@ description = "Pure-Python HPACK header encoding" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, @@ -602,6 +613,7 @@ description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, @@ -624,6 +636,7 @@ description = "The next generation HTTP client." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -637,7 +650,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -650,6 +663,7 @@ description = "Pure-Python HTTP/2 framing" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, @@ -662,6 +676,7 @@ description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -677,6 +692,7 @@ description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["dev", "test"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -689,6 +705,7 @@ description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" groups = ["dev"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -704,6 +721,7 @@ description = "Fast iterable JSON parser." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "jiter-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:816ec9b60fdfd1fec87da1d7ed46c66c44ffec37ab2ef7de5b147b2fce3fd5ad"}, {file = "jiter-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b1d3086f8a3ee0194ecf2008cf81286a5c3e540d977fa038ff23576c023c0ea"}, @@ -790,7 +808,7 @@ description = "A package to repair broken json strings" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"graph\"" +markers = "extra == \"graph\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "json_repair-0.39.1-py3-none-any.whl", hash = "sha256:3001409a2f319249f13e13d6c622117a5b70ea7e0c6f43864a0233cdffc3a599"}, {file = "json_repair-0.39.1.tar.gz", hash = "sha256:e90a489f247e1a8fc86612a5c719872a3dbf9cbaffd6d55f238ec571a77740fa"}, @@ -803,7 +821,7 @@ description = "Apply JSON-Patches (RFC 6902)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" groups = ["main"] -markers = "extra == \"graph\"" +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, @@ -819,7 +837,7 @@ description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"graph\"" +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, @@ -832,7 +850,7 @@ description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] -markers = "extra == \"graph\"" +markers = "extra == \"graph\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "langchain-0.3.21-py3-none-any.whl", hash = "sha256:c8bd2372440cc5d48cb50b2d532c2e24036124f1c467002ceb15bc7b86c92579"}, {file = "langchain-0.3.21.tar.gz", hash = "sha256:a10c81f8c450158af90bf37190298d996208cfd15dd3accc1c585f068473d619"}, @@ -873,7 +891,7 @@ description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] -markers = "extra == \"graph\"" +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "langchain_core-0.3.48-py3-none-any.whl", hash = "sha256:21e4fe84262b9c7ad8aefe7816439ede130893f8a64b8c965cd9695c2be91c73"}, {file = "langchain_core-0.3.48.tar.gz", hash = "sha256:be4b2fe36d8a11fb4b6b13e0808b12aea9f25e345624ffafe1d606afb6059f21"}, @@ -891,6 +909,23 @@ PyYAML = ">=5.3" tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0" typing-extensions = ">=4.7" +[[package]] +name = "langchain-memgraph" +version = "0.1.1" +description = "An integration package connecting Memgraph and LangChain" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" +files = [ + {file = "langchain_memgraph-0.1.1-py3-none-any.whl", hash = "sha256:656e272a317d596c01016210fe5adb7ca5a9485cf733bdfe65e23cb80c360b52"}, + {file = "langchain_memgraph-0.1.1.tar.gz", hash = "sha256:64e8560720a4382db230bcbc45d9e5dcbd329da4ea1c192ab867dc0157724554"}, +] + +[package.dependencies] +langchain-core = ">=0.3.15,<0.4.0" +neo4j = ">=5.28.1,<6.0.0" + [[package]] name = "langchain-neo4j" version = "0.4.0" @@ -898,7 +933,7 @@ description = "An integration package connecting Neo4j and LangChain" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] -markers = "extra == \"graph\"" +markers = "extra == \"graph\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "langchain_neo4j-0.4.0-py3-none-any.whl", hash = "sha256:2760b5757e7a402884cf3419830217651df97fe4f44b3fec6c96b14b6d7fd18e"}, {file = "langchain_neo4j-0.4.0.tar.gz", hash = "sha256:3f059a66411cec1062a2b8c44953a70d0fff9e123e9fb1d6b3f17a0bef6d6114"}, @@ -917,7 +952,7 @@ description = "LangChain text splitting utilities" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] -markers = "extra == \"graph\"" +markers = "extra == \"graph\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "langchain_text_splitters-0.3.7-py3-none-any.whl", hash = "sha256:31ba826013e3f563359d7c7f1e99b1cdb94897f665675ee505718c116e7e20ad"}, {file = "langchain_text_splitters-0.3.7.tar.gz", hash = "sha256:7dbf0fb98e10bb91792a1d33f540e2287f9cc1dc30ade45b7aedd2d5cd3dc70b"}, @@ -933,7 +968,7 @@ description = "Client library to connect to the LangSmith LLM Tracing and Evalua optional = false python-versions = "<4.0,>=3.9" groups = ["main"] -markers = "extra == \"graph\"" +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "langsmith-0.3.18-py3-none-any.whl", hash = "sha256:7ad65ec26084312a039885ef625ae72a69ad089818b64bacf7ce6daff672353a"}, {file = "langsmith-0.3.18.tar.gz", hash = "sha256:18ff2d8f2e77b375485e4fb3d0dbf7b30fabbd438c7347c3534470e9b7d187b8"}, @@ -964,6 +999,7 @@ description = "An implementation of time.monotonic() for Python 2 & < 3.3" optional = false python-versions = "*" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c"}, {file = "monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7"}, @@ -976,7 +1012,7 @@ description = "Neo4j Bolt driver for Python" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"graph\"" +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "neo4j-5.28.1-py3-none-any.whl", hash = "sha256:6755ef9e5f4e14b403aef1138fb6315b120631a0075c138b5ddb2a06b87b09fd"}, {file = "neo4j-5.28.1.tar.gz", hash = "sha256:ae8e37a1d895099062c75bc359b2cce62099baac7be768d0eba7180c1298e214"}, @@ -997,7 +1033,7 @@ description = "Python package to allow easy integration to Neo4j's GraphRAG feat optional = false python-versions = "<4.0.0,>=3.9.0" groups = ["main"] -markers = "extra == \"graph\"" +markers = "extra == \"graph\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "neo4j_graphrag-1.6.0-py3-none-any.whl", hash = "sha256:e5f3ee7eae2fa48bf9627498274d2fb5e7a29445e51845d97614bf0b2d8dca55"}, {file = "neo4j_graphrag-1.6.0.tar.gz", hash = "sha256:21e5c5171293e00233be81631778bace2a3f6c0d063d2712415e54cdc37d17ff"}, @@ -1015,9 +1051,9 @@ types-pyyaml = ">=6.0.12.20240917,<7.0.0.0" [package.extras] anthropic = ["anthropic (>=0.49.0,<0.50.0)"] cohere = ["cohere (>=5.9.0,<6.0.0)"] -experimental = ["langchain-text-splitters (>=0.3.0,<0.4.0)", "llama-index (>=0.12.0,<0.13.0)", "pygraphviz (>=1.0.0,<2.0.0) ; python_version < \"3.10\"", "pygraphviz (>=1.13.0,<2.0.0) ; python_version >= \"3.10\" and python_full_version < \"4.0.0\""] +experimental = ["langchain-text-splitters (>=0.3.0,<0.4.0)", "llama-index (>=0.12.0,<0.13.0)", "pygraphviz (>=1.0.0,<2.0.0)", "pygraphviz (>=1.13.0,<2.0.0)"] google = ["google-cloud-aiplatform (>=1.66.0,<2.0.0)"] -kg-creation-tools = ["pygraphviz (>=1.0.0,<2.0.0) ; python_version < \"3.10\"", "pygraphviz (>=1.13.0,<2.0.0) ; python_version >= \"3.10\" and python_full_version < \"4.0.0\""] +kg-creation-tools = ["pygraphviz (>=1.0.0,<2.0.0)", "pygraphviz (>=1.13.0,<2.0.0)"] mistralai = ["mistralai (>=1.0.3,<2.0.0)"] ollama = ["ollama (>=0.4.4,<0.5.0)"] openai = ["openai (>=1.51.1,<2.0.0)"] @@ -1089,7 +1125,7 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.10\" and python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9"}, {file = "numpy-2.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae"}, @@ -1155,6 +1191,7 @@ description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "openai-1.68.2-py3-none-any.whl", hash = "sha256:24484cb5c9a33b58576fdc5acf0e5f92603024a4e39d0b99793dfa1eb14c2b36"}, {file = "openai-1.68.2.tar.gz", hash = "sha256:b720f0a95a1dbe1429c0d9bb62096a0d98057bcda82516f6e8af10284bdd5b19"}, @@ -1182,7 +1219,7 @@ description = "Fast, correct Python JSON library supporting dataclasses, datetim optional = false python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"graph\" and platform_python_implementation != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8"}, {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00"}, @@ -1261,11 +1298,11 @@ description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev", "test"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] -markers = {main = "extra == \"graph\""} [[package]] name = "pluggy" @@ -1274,6 +1311,7 @@ description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" groups = ["dev", "test"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1290,6 +1328,7 @@ description = "Wraps the portalocker recipe for easy usage" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf"}, {file = "portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f"}, @@ -1310,6 +1349,7 @@ description = "Integrate PostHog into any python application." optional = false python-versions = "*" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "posthog-3.21.0-py2.py3-none-any.whl", hash = "sha256:1e07626bb5219369dd36826881fa61711713e8175d3557db4657e64ecb351467"}, {file = "posthog-3.21.0.tar.gz", hash = "sha256:62e339789f6f018b6a892357f5703d1f1e63c97aee75061b3dc97c5e5c6a5304"}, @@ -1336,6 +1376,7 @@ description = "" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "protobuf-5.29.4-cp310-abi3-win32.whl", hash = "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7"}, {file = "protobuf-5.29.4-cp310-abi3-win_amd64.whl", hash = "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d"}, @@ -1357,6 +1398,7 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, @@ -1435,7 +1477,7 @@ description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"graph\" and platform_python_implementation == \"PyPy\"" +markers = "platform_python_implementation == \"PyPy\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -1448,6 +1490,7 @@ description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, @@ -1460,7 +1503,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] +timezone = ["tzdata"] [[package]] name = "pydantic-core" @@ -1469,6 +1512,7 @@ description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, @@ -1582,7 +1626,7 @@ description = "A pure-python PDF library capable of splitting, merging, cropping optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"graph\"" +markers = "extra == \"graph\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "pypdf-5.4.0-py3-none-any.whl", hash = "sha256:db994ab47cadc81057ea1591b90e5b543e2b7ef2d0e31ef41a9bfe763c119dab"}, {file = "pypdf-5.4.0.tar.gz", hash = "sha256:9af476a9dc30fcb137659b0dec747ea94aa954933c52cf02ee33e39a16fe9175"}, @@ -1606,6 +1650,7 @@ description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" groups = ["dev", "test"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, @@ -1629,6 +1674,7 @@ description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1644,6 +1690,7 @@ description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -1656,7 +1703,7 @@ description = "Python for Window Extensions" optional = false python-versions = "*" groups = ["main"] -markers = "platform_system == \"Windows\"" +markers = "platform_system == \"Windows\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1"}, {file = "pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d"}, @@ -1683,7 +1730,7 @@ description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"graph\"" +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1747,6 +1794,7 @@ description = "Client library for the Qdrant vector search engine" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "qdrant_client-1.13.3-py3-none-any.whl", hash = "sha256:f52cacbb936e547d3fceb1aaed3e3c56be0ebfd48e8ea495ea3dbc89c671d1d2"}, {file = "qdrant_client-1.13.3.tar.gz", hash = "sha256:61ca09e07c6d7ac0dfbdeb13dca4fe5f3e08fa430cb0d74d66ef5d023a70adfc"}, @@ -1777,7 +1825,7 @@ description = "Various BM25 algorithms for document ranking" optional = false python-versions = "*" groups = ["main"] -markers = "extra == \"graph\"" +markers = "extra == \"graph\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "rank_bm25-0.2.2-py3-none-any.whl", hash = "sha256:7bd4a95571adadfc271746fa146a4bcfd89c0cf731e49c3d1ad863290adbe8ae"}, {file = "rank_bm25-0.2.2.tar.gz", hash = "sha256:096ccef76f8188563419aaf384a02f0ea459503fdf77901378d4fd9d87e5e51d"}, @@ -1796,6 +1844,7 @@ description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1818,7 +1867,7 @@ description = "A utility belt for advanced users of python-requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["main"] -markers = "extra == \"graph\"" +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, @@ -1834,6 +1883,7 @@ description = "An extremely fast Python linter and code formatter, written in Ru optional = false python-versions = ">=3.7" groups = ["dev"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, @@ -1862,19 +1912,20 @@ description = "Easily download, build, install, upgrade, and uninstall Python pa optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8"}, {file = "setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -1883,6 +1934,7 @@ description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1895,6 +1947,7 @@ description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -1907,6 +1960,7 @@ description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "SQLAlchemy-2.0.39-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:66a40003bc244e4ad86b72abb9965d304726d05a939e8c09ce844d27af9e6d37"}, {file = "SQLAlchemy-2.0.39-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67de057fbcb04a066171bd9ee6bcb58738d89378ee3cabff0bffbf343ae1c787"}, @@ -2003,7 +2057,7 @@ description = "Retry code until it succeeds" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"graph\"" +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, @@ -2063,6 +2117,7 @@ description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -2085,7 +2140,7 @@ description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"graph\"" +markers = "extra == \"graph\" and (python_version <= \"3.12\" or python_version >= \"3.13\")" files = [ {file = "types_pyyaml-6.0.12.20250326-py3-none-any.whl", hash = "sha256:961871cfbdc1ad8ae3cb6ae3f13007262bcfc168adc513119755a6e4d5d7ed65"}, {file = "types_pyyaml-6.0.12.20250326.tar.gz", hash = "sha256:5e2d86d8706697803f361ba0b8188eef2999e1c372cd4faee4ebb0844b8a4190"}, @@ -2098,6 +2153,7 @@ description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5"}, {file = "typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b"}, @@ -2110,13 +2166,14 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2128,7 +2185,7 @@ description = "Zstandard bindings for Python" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"graph\"" +markers = "python_version <= \"3.12\" or python_version >= \"3.13\"" files = [ {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, {file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, @@ -2236,9 +2293,9 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [extras] -graph = ["langchain-neo4j", "neo4j", "rank-bm25"] +graph = ["langchain-memgraph", "langchain-neo4j", "neo4j", "rank-bm25"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "5848e23bdd7b453f938c9b5f6171866faa01bdcc2651bedb83ee9f4fe90e8bc8" +content-hash = "2f2496320b637ae8b74ee70707a8ea3431b3cd0ed045af847bb9b660bca334ac" diff --git a/pyproject.toml b/pyproject.toml index a7e0564d..778e70af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,9 +31,10 @@ langchain-neo4j = "^0.4.0" neo4j = "^5.23.1" rank-bm25 = "^0.2.2" psycopg2-binary = "^2.9.10" +langchain-memgraph = "^0.1.1" [tool.poetry.extras] -graph = ["langchain-neo4j", "neo4j", "rank-bm25"] +graph = ["langchain-neo4j", "neo4j", "rank-bm25", "langchain-memgraph"] [tool.poetry.group.test.dependencies] pytest = "^8.2.2" diff --git a/server/main.py b/server/main.py index 165c1af2..0e1723f2 100644 --- a/server/main.py +++ b/server/main.py @@ -25,6 +25,10 @@ NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://neo4j:7687") NEO4J_USERNAME = os.environ.get("NEO4J_USERNAME", "neo4j") NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "mem0graph") +MEMGRAPH_URI = os.environ.get("MEMGRAPH_URI", "bolt://localhost:7687") +MEMGRAPH_USERNAME = os.environ.get("MEMGRAPH_USERNAME", "memgraph") +MEMGRAPH_PASSWORD = os.environ.get("MEMGRAPH_PASSWORD", "mem0graph") + OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") HISTORY_DB_PATH = os.environ.get("HISTORY_DB_PATH", "/app/history/history.db")