From d7399ef4ef5182517fd7001ce765b77e6ad1a0a3 Mon Sep 17 00:00:00 2001 From: Artem Darius Weber Date: Sun, 8 Dec 2024 03:15:22 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=B8=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=B7=D0=BE=D0=B9=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= =?UTF-8?q?,=20=D1=81=D1=85=D0=B5=D0=BC=D1=8B=20=D0=B7=D0=B0=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D0=BE=D0=B2=20=D0=B8=20=D0=BE=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=BE=D0=B2,=20=D0=B0=20=D1=82=D0=B0=D0=BA=D0=B6=D0=B5?= =?UTF-8?q?=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 3 + README.md | 113 +++++++++++++++ app.db | Bin 0 -> 32768 bytes app/__pycache__/agent.cpython-311.pyc | Bin 0 -> 8306 bytes app/__pycache__/database.cpython-311.pyc | Bin 0 -> 897 bytes app/__pycache__/main.cpython-311.pyc | Bin 0 -> 10165 bytes app/__pycache__/models.cpython-311.pyc | Bin 0 -> 2213 bytes app/__pycache__/schemas.cpython-311.pyc | Bin 0 -> 2142 bytes app/__pycache__/tools.cpython-311.pyc | Bin 0 -> 3310 bytes app/agent.py | 160 +++++++++++++++++++++ app/database.py | 15 ++ app/main.py | 175 +++++++++++++++++++++++ app/models.py | 33 +++++ app/schemas.py | 30 ++++ app/tools.py | 55 +++++++ 15 files changed, 584 insertions(+) create mode 100644 .env create mode 100644 README.md create mode 100644 app.db create mode 100644 app/__pycache__/agent.cpython-311.pyc create mode 100644 app/__pycache__/database.cpython-311.pyc create mode 100644 app/__pycache__/main.cpython-311.pyc create mode 100644 app/__pycache__/models.cpython-311.pyc create mode 100644 app/__pycache__/schemas.cpython-311.pyc create mode 100644 app/__pycache__/tools.cpython-311.pyc create mode 100644 app/agent.py create mode 100644 app/database.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/schemas.py create mode 100644 app/tools.py diff --git a/.env b/.env new file mode 100644 index 0000000..a7cdb83 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +LM_STUDIO_API_URL=http://192.168.0.104:1234/v1/chat/completions +LM_STUDIO_MODEL=lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf +DATABASE_URL=sqlite:///./app.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..44b282d --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# Langchain multi-tool LLM service + +## Run + +> First, configure `.env` file for your LM Studio MODEL and HOST! + +```bash +uvicorn app.main:app --reload +``` + +## API + +### List of the Tools + +```bash +GET /tools +Content-Type: application/json +``` + +**Response:** + +```bash +[ + { + "id": 0, + "name": "string", + "description": "string" + } +] +``` + +### Create a Tool + +```bash +POST /tools +Content-Type: application/json + +{ + "name": "Calculator", + "description": "Useful for performing mathematical calculations. Input should be a valid mathematical expression.", + "function_code": "def tool_function(input: str) -> str:\n try:\n aeval = Interpreter()\n result = aeval(input)\n return str(result)\n except Exception as e:\n return f\"Error evaluating expression: {e}\"" +} +``` + +**Response:** + + + +```bash +{ + "id": 0, + "name": "string", + "description": "string" +} +``` + +### Get the Tool + +```bash +GET /tools/{id} +Content-Type: application/json +``` + +**Response:** + + + +```bash +{ + "id": 0, + "name": "string", + "description": "string" +} +``` + + +### Submit a Query + +```bash +POST /query +Content-Type: application/json + +{ + "input": "What is the capital of France and what is 15 multiplied by 3?" +} +``` + +**Response:** + +```bash +{ + "output": "Your request is being processed." +} +``` + +### Get processed Answer + +```bash +GET /answer/{question_id} +Content-Type: application/json +``` + +**Response:** + +```bash +{ + "id": 0, + "query": "string", + "answer": "string", + "timestamp": "string" +} +``` + diff --git a/app.db b/app.db new file mode 100644 index 0000000000000000000000000000000000000000..31f888850d52088dc0a6117711266d24a63bda41 GIT binary patch literal 32768 zcmeI)Pfy!s90zb4L()Vg+0B|b@GCozh!Q1X42Zl5*}MRQuI``bL{rtki!}E6%<16UUFf6=8JZxx?6<9owq6Eo!tr zx1Q2e3EFSbuoM+Mw|Mb&Ds3(-=&w!bJnM;J)^L{|w$rXZXj)g*vKi0iw%gX8Wm9Xv zO|7G5^FC!W-^3!4Uf}bYjPljtPT))7OKMwBx$H!F>78)}=d4d(*6p2#_373Q?8f7| zeN2z6S7peoF1@MX4B^4qJCLP*v6UT$lMg&_%IdTinqjt!k0@ol?GqN&Ex-0FI(I< zc-WYFFmjMV(2Gud=S(EA+2I)UAIzl8k4*h|xf~Q zgYYO4-MFWoWm8{->XEkRKPP^!eJXh4b(|jYCn?M~&ryjQem|BJod$8wrDuX1dg1i= zetxQoIPZrdinvC}72V|Lf|Jp0cEM9E)F_f+kybxd`&yPb$nfPLq7shaW@($dlOp8C zcv~nY#Z)dg9`__?C&dOWPUgxu^i{)!LBTnhpN?Gvcixx zRR1vxu*TKZ)aX0${Zf{{Sv~)!#%}}&KmY;|fB*y_009U<00Izz00eHKfMH}ZgZuwC zTK>%~yg*zM1Rwwb2tWV=5P$##AOHafK;XRheKMa4*IA6{10HdeOCiw1Mg`(e;g)8~2aInTpaiw^MAc-nQm zo^W|GJ@HZszUW95Z9b+$8M|IU^@B!@9=6*DJA+)F;eNpX?Jt}Lk=$OcR5waxepQz5 zQ$GmhcD1}&Db~mq4mZgwFt`-NvP3*%et3YB+nyh-J?TDe-QY`&V| zQ@(ig4yU{qNb-Z>aslc;249IVI6Mtx{W&iMQKGseUTq?h6!;=3MQ1Um_-_SF@WOM2 zTbk^@T3g?$t#7>2|I7X>Tk840k^i0N|L6bU9RdU(009U<00Izz00bZa0SG_<0=Gb5 z&d_zkP}`XqHUD2d|K~;j5g-5o2tWV=5P$##AOHafKmY;|xWxkM`9JReZ}At5OM?Ie VAOHafKmY;|fB*y_009WFz+VF_BbERF literal 0 HcmV?d00001 diff --git a/app/__pycache__/agent.cpython-311.pyc b/app/__pycache__/agent.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39ed510b6a772856f70f25ca01d6a6ac8b0caf67 GIT binary patch literal 8306 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 Q)F+PgiA)ufj&oE006LciL;wH) literal 0 HcmV?d00001 diff --git a/app/__pycache__/main.cpython-311.pyc b/app/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82376695095c48797ef920f53a096a94c8aa1520 GIT binary patch literal 10165 zcmdryYitwOnltv;9(x?yNgT&X12H6NFwKLuKm(;vAcTZaXiK2QEj5m35*++UXC^#S zXK}emb=h3eR;^H?N32Vqo+UB?|!G>69GrT2kPdGx31Q+5G&X6-v6RJtL zLM}39jk^-=kUQZCc@njuTGD5W*Cgsfbp#I^uTRv6>Pg!k_lCSCO7KoJ6p4ejuh^b< z+@>k&EBG0OrpR1)9-|2U>hVyM;FxGGj!ryRY~@;tt;$+(POK~5&vVt7$WAoxoUf_w z53T22SEx{{us#X;vC*5Fnpt69kcTOE0z^BJkK`?oqV&fsW9U? zpe6WJT3P__<|5u_q-7nfd7-$5?85NtSJBZ0dv+IRRn~lqz*g?sR2qE7{qf07eCw(m zd!D*upmM)w++_otBJRZS8&~br`_!GFOwXfRgMc#gl3U>U6D1(e{ z8;D5q-ZzJITfZ)OOpqiP zgC=u0Cdogd%mppFHId?lxTM>p=oujqk#v@i$dS>ABeg{V4)dcTYChDn(g6Vf0e>0lk-b*4uUG8> z%^t|?TQ-?3^?8>k%i`bntm7JcmHpH)>&TM7Cs|Wv$CD>dBs*Zb!MsR)?b~ZJ1INe_ zQJ$KtdOvuwX81kch;ysn&06Ox`e9m-7Tr+Yr5*#ML{#N^O#yirxFsozh;2RmB%GL` zeqdZ5AHH%pL;l1KxWFn3@|2jYBE|{>gqS=uZcyXYOcg;-W0rpE)Uo&BL1yR~lWc*d zg))J-NJ-Oa(*&|2DU&o$&zq_h_>5WRisQhRNC_#pGHsr)74PTGw*l){CCoDnFkI1m z7bh7jnQC)DQ?ny!V9_9RIP_)3FtHmuu8?u36vmG zZUZcP6}gHzg$YpcbTw1VFfs~hp-hyRH*MxkX=W9(S<TPLibfk@MuadyA9L?@+T=gGrwXt5EJY4 z3l8MIV^gDvm>h@%ra<=w$h!^5sepV&2oP=CLuUO_+i|kHBhlT#2Ra6KcD%8(A zEeQ>=9_ZE0$<%qB1<@HpY$Ce#dd*wNSzxcEWMd>gciTLHPa4FR&K?!ZF`8??NN9V{Rq`%1_ccwR+$bxP#m zj2*;7Fp3_I^mhO<6i8BC)Ae;AtRDY$=jX2Lt|d?BqNh{Y^s4IFrFnK`hW==!JU-3c zc4zbbw;%3M+{ab-am{@^Gni+rpA1|c$o8p>S7W>ixM7tW*0^DX z88&PLSA4iIoX;Q-UL~T)wkTwe_W^_50EYqWh51>W3$Pb`#9lBnmZ#VYCT#&$E-`Mw zV!&<{n%|l>=Sl(#bd*Zkx>6WtcU0|D65+JDdghGHM5S%KIZek_Dhf8u@=RHJX6#QB zaM@ELgqkP`Z1p}ldOs9USJhJhN04J(Nt1KuNXD=r4 z2c{bjh+;}C+8{JX#6AS3TUHIA{Ud`r14n{>;tq{==lWIW2FO>>*H+P16}#v7Y% zocZF+t#exI%c^gy=G(gDdv($G>bC>ht~a$^$CP(Q7k7=SzNqGlF8N}MzL@GetNG4m zx%|4eET^%}c?bpVSM8s2vs{(~v+ZB6H3k>XKHUCqrv&X(Og(j0J9T#Hl)QLKUZ$vX z^pNQ%iu!;)Wct~tpwmB$eE^{gVAhE^tyittw92+UFSRPFKu>$=8WqnaNUm>waV_(*nJASFYojJ z*=Muo{$6F>9=KMYQ5Mdr_y=+FlVv*)h5<9$*UJaC9N0?zeyd|}z4`aM_caVQnIAS; z;07WK5;hU8jS@=;)3KlzKs=E;=!IZ`&SCGWB3G=b2pb^4C?{0c4ja~9S@usLJSq7q zYuad}a8;?cmNTZbNvxGiHX4H!-UJ~CP+d-RUZ&R$ekv(|b}96LHu`C#%rh%2E%-TW zhZHOQA_!CHwSAWAXCU)R0Z4IQtXE^K3`N!$MR&VUCQ7~vWCF@wh`D^`RT%w4G;bvHM&1UxhPJ#%KXN^H$E<8UsklO_E2OzX z3L7c}H?8@a+S!wNuV3@F=Y7qZ4|5J4E9iNT74*Evni&K=Z?XON;vGP4smZ>pGCqy* zDU1&yoY_5dXKtQdV85w(*ryGL74Edkoz}S13Uiu-I#GE4rGl!2sFO4_F!zuv4OKSQ zRthVebHuZ%R13`vq{FU&fc39%zkzX0)9r+DF#!#n7>95fv&Mo5&I`UU$zb0q>QVI3 zeSRDQfJwYGb!yWLy>?)hrpt;D+9hhCT~cWCRJA~TZki1OZM1kAHV9f)aA5_Xn{-pp z7Q&MfYTg3R{o>e#c`t~~CwEV;$ELxUJ-gzV_my@-qd0yQmPs!H$WRKGEL@=vetmHE z=$w4>{GE~clMni}{t?A_Ty-AToX1PE$Xf-8C_7ydKm&P!csyt_M1;c=G#nYJl0eAY zmxvP2!m2As1tKDGdws)=O&l(81}38BeHRfpV4iF*4ox=BQWLvjy3zA*f8l?Y*#9O(EV%p zNf!VRhH|>D^f~C$#R@wC>j*99-;%#LQuhJG{iby~w?- zaw8fylIhQLZd9-biwgFD;u=t01Db01F+to);LPrV#aey4mF z80rte?RU!W)C>%LtSl75<|JU4Y6YNtQ63E_-zxVIEGWNKzESQe-x|^})r%AVri}TX z`l+nMHsz-GyVktacMkt*DhSWaLcOsyHt0#=I&l{ zZ(Ve6y>~*}Hl%Gkti1W&;xAk$ zp}9^d>y- zj3}|XOZqS#iQ@kG4x}*5PJ091qLJ|-PV3{W;$v0S?1;@Gk6_b48(wWH=q(=@SGUr=Hik|2!l2_Nw zG5G9mh6zU60X<^ffjWLa`LIshTHOrf5pzydu=pW97&3<#K(GM;eo6g>-34E(rsBeG z@e{ZOwMR*laK_7Knx-LJKp&tLs{c=4d1|9ledVb}rTWTK+mvU!^3*HJ)2}?$s#ITj zYMoMj6H1hW5Y@=pxp6gTX?V7zkgP9T9c7@X@ z?-JFzNVVqP{W1(Ysnj-&(EYu&CNr*hdll;z)w)HqZpoOJtri+Yqy#*a$?{3d<(6zr zrTrT1UpCQnEr<|Z`;(b6HI;f;2ko01S*D;`37Qdw|FS>3Dff!j)|c_C^bU>Qk&m2F z;EJWx$fOpTT&6tqVcLLmose2Gnh$!7T6yP#${9u*N!SUfmQ7@Bdpqr&la?u{7R;o& z-$<&J;0XLDREeZ7_aH?tYxK+coqO+hK6w7Oz5fiG`sfHygK*BK`8ONQ1zY8S(MBS+ z!s=xcS@II1ZoJ%>_2=4i=fAw9ZG0uusM38J-Irh2KKD-U#9xPRhVg_`fFI#!uTBByI|t{FBEnr7y(=Hwa;I^@cei!E6+RHt#{nV2&!R>>cfPXM zXd~j+VCk}n%&#j@tJnhzjoOO`5%Zu%59WKf-#NczS*@y9D)QhyU%a-d!C(n-}h$! z(zG0ccISC>m1Gcl!3X0ej-0)Jzw%n4z zPveZ0wUw44pcJ}}u=pp!5>YnO2t9>6X{}t0$$(|w!PFR20L#6DQd8LWtk><)MdGbONqJa+#qXJ zf#oT&jIQZA-hH$4Qs6TPEVF2Mq&=RU|4O65UI5M$L{Lk>sFlJ(D-8?G5D}+vYA1bY z(^8Dd9K)F%aT8Mh6z-(8vN5UvB28|MCky603Q@5PTg@`1>AKcU7ZZz#U2whTc8aMT zYT5=}(;tvECYlZ=j~dl9lYGZ~=o4Ck_p+?xTb6OxBHIEj!-JJ-o@I62F>IpiET`+X zi+zjxd0l_#8`cQXbiK>}mJBH;mh!Ezl!+;q4$E+e@CizwHs~)v-ZM->WaVZQmwN*QuFsowNPWlIdpwL!TSm21I`VdYe>v2khZ zLRgs}ROX|~d~lO@dS?9Y3FYjU)nkA^S~*-b6S5zvMvnC~i72sZaz`L4mOwQ!(|+=a z$Eiq^qkt;nq1U7ZSY3l20}G0v6~2|@+??R%Bscq%(}NjPH(CI zb*NPbS~b$DL1S1t(UW?=0a>Nmpj3-WwP2a|WCi@?6U6M7^d=x5NsI`o1`0Oeym-pb zjJO?1l!C?gl(%{W!cRL5%UIaS9-6Iy6`(N3)T?o-69VNV?<4yD$nW!6GjT#@obzpg z7491C2YSct_;A`0Z>3uN5RQ2fF?Bqv4wW9WF1KAcS)FdoH$4wQG|MBi*upBxAW~EwSIq#umSnd17UOOOoUc>R$ zX`GS5+n}0D?a}730S6>}Hz@zaCI``3?!_;;$i2yOc$2-HoVDN5c|f_0y#K)LrUgOR zm5^{%2+?;Rat+bNaPk_WQ{m*bn~{XCcG0`0mwCmXzJ3dpB%#hD51ZZ+kF4{^5md?w t*M(hlwC$aL6(g??;uZAig^hgw=YPKmr&b12E78OLf*j0o}R8a130t5 z8Hy_dXB9Y8aTVaI1+J{P8gTUjXSt1E$$s-+biYq!d*uG{vD#2L;RO~hQbY}*NfFm`aeupT{Wi;nAXFA<#|!+zqkAQqj7htF9b1ZMzquiqXz!Jz-l@q#vB+)jiS4BA7# zE#k!W!j2O~9npWrd`BEaBW{9m1v&`g8-Vn^-=x||kJ6*@(d5?7rH=1geP^=$$)%3( zTZJI-TMH|>9HU8mWCk z&*fzxxdo7OP3L=Nw?SCBc4<#@OJGF1Y0ixtJPd=U-r%fzq4OHrsv|T2q!n@>4~y|F z)Txg|d(zDI@LiZRVa&DxR(YFj@4mF~mu>kJ$nHgX-MZ!lF>e9K(FDg;Ykv3+>JTb0 z#1nv(lax%XT6&lsjt@7U(p0C$-6<);73!`x5LvY7E|r(lGuRKDLSSxRO7mxS@r zDSMGH5lh1hq9pEeyl2)w>`T-kU{1w7fZT`Gn(1+RJcf}TXHntWkBq7@k4i+)P)wR( z5`QqRufPpQz$_Hws<+aU^kfVpJ;~z2wYj*KHGc}s=4`PIHLr9!-Y}~A)y-L`{xTG< zC7a&|;!nm;S@uEv4s4PJFVo(w1+yBOTkloWO+^w;lwMM->EI&mA3<51)EAl8& zy`_4c1E_(+1Ogs548@Q)0l@$xPdk@1 z^;5e*E)UN=_wSr@|Gsno*x4CCkaqrkIXBmV(0^#bOZ>j@>}wG2pesm03ZtU5n=@$! z%0AUsVAE`YOLGiTScOyhfeYP+ytExP&Vw;=EXN{-SYLc--m$0tqwc?zXl~pMF3z~ve9i-rF(ZHlc zFmO9;Xqu{L2+rkogREpdV>B1wXW@VL2OnKAqcI?|XR-?!+4Z zKEL^QH(zF!(M|RmT4px?>70?4ooRZ}ewyHh`7fa%dZJ#aw`U>QvHF{U1Zhr_70Ef* z(gM*6l5rhNifqX9vW_LWs7UT<$Aq>NMx8O|b$zLTb*PaIX<@0DHQ?@KvT$dltX4GS ze376}syOWyCc$KT2)D7%9G9f)5{rSF9>7}-QFG)qskQBU-FGA;h z(Q$l2TJ63-h(;t8l;n#!r&fCIn~*LhyNDl_&gS1+!eoU2%pqI645oiUFSyMD8rWP> zE?^?kCJx`kS-aD7%bAkhkyEvKS=DVehYj1WYPlR>0hcc>Xf}_jVOzX#GmA@f6>L7M zYC2B(ZJ#o42OaNSFtIsWutjQ3FUeVeAZo{Euufg1dm%~BFQ=~PnCK}*Cix{jHKS!= zrHh81DiLiFXF-s2P|jyZRJoYTUWc_F0d9;e=`iyVwJ@R^OG;i#$)!??8q`NiD|Udc zOvWjaK`_<@zb*p=t)u@1P$2eU|7QRCRNZ%k@2vSO$H5gYqk zY^)j^w_@WP!CG{njylCz#^oC`wMh5o%I9G-G741Z0!L<aJ zgSUlS!iRy)z=refZTMO%puheTSn93w?93VTO>`IRM~H zr2*$)qmd2(I6yCXt?V-zVFCg4G((I7;4PqCp&lPd0N^cS4uf6_YOO({(qgZv4j}vm z^ZrQ!2?hM~n2+iUG{1@0dstW;rvKEbP zh`T~)}E1KN4d&8xyWOE_m9M*A0?pQQJeuFY@H zzcLj?-<%o*{%tfc9cI7nAD$N3$084Mvd0$lg_1@LH9v26(6|5)Z2OIs66A#5CqkFh z7QA@s#v-&IhL-cc&X=%~m+cN&H}DNvCG;fPB7~VaH^>N?3=J2VjJu0YjAG&GX-6^{ zxd^dPrZGo{c8cnRz5txv`T{)z5jh+AT$qFaw4~ynlG9K};X!{23?PkR>VAMo6LmGH zhQjO4S3@Vw!>EP^&33AxsM$`nHiZKwYNLON$2NK`@!0m6s+h3E#JV56c8_&&I~`^wx5lmRp*jNT2N`)4X8V6H CQ~_}S literal 0 HcmV?d00001 diff --git a/app/agent.py b/app/agent.py new file mode 100644 index 0000000..efa53ea --- /dev/null +++ b/app/agent.py @@ -0,0 +1,160 @@ +import requests +from typing import Any, Dict, List, Optional +from langchain.llms.base import LLM +from langchain.agents import initialize_agent, AgentType, Tool +from pydantic import Field +import os +from dotenv import load_dotenv +from datetime import datetime +import wikipedia +from asteval import Interpreter # For a safer calculator +import logging +from .tools import tools_registry + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +load_dotenv() + +LM_STUDIO_API_URL = os.getenv("LM_STUDIO_API_URL", "http://192.168.0.104:1234/v1/chat/completions") +MODEL_NAME = os.getenv("LM_STUDIO_MODEL", "lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf") +CONTENT_TYPE = "application/json" + +class LMStudioLLM(LLM): + """ + Custom LangChain LLM to interface with LM Studio API. + """ + api_url: str = Field(default=LM_STUDIO_API_URL, description="The API endpoint for LM Studio.") + model: str = Field(default=MODEL_NAME, description="The model path/name.") + temperature: float = Field(default=0.7, description="Sampling temperature.") + max_tokens: Optional[int] = Field(default=4096, description="Maximum number of tokens to generate.") + streaming: bool = Field(default=False, alias="stream", description="Whether to use streaming responses.") + + class Config: + populate_by_name = True + + @property + def _llm_type(self) -> str: + return "lmstudio" + + @property + def identifying_params(self) -> Dict[str, Any]: + return { + "api_url": self.api_url, + "model": self.model, + "temperature": self.temperature, + "max_tokens": self.max_tokens, + "stream": self.streaming, + } + + def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str: + """ + Generate a response from the LM Studio model. + + Args: + prompt (str): The input prompt. + stop (Optional[List[str]]): Stop sequences. + + Returns: + str: The generated response. + """ + headers = { + "Content-Type": CONTENT_TYPE, + } + + payload = { + "model": self.model, + "messages": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": prompt}, + ], + "temperature": self.temperature, + "max_tokens": self.max_tokens if self.max_tokens is not None else -1, + "stream": self.streaming, # Uses alias 'stream' + } + + logger.info(f"Payload: {payload}") + + try: + response = requests.post( + self.api_url, + headers=headers, + json=payload, + timeout=60, + stream=self.streaming + ) + response.raise_for_status() + logger.info(f"Response content: {response.text}") + except requests.RequestException as e: + logger.error(f"Failed to connect to LM Studio API: {e}") + raise RuntimeError(f"Failed to connect to LM Studio API: {e}") + + if self.streaming: + return self._handle_stream(response) + else: + try: + response_json = response.json() + choices = response_json.get("choices", []) + if not choices: + raise ValueError("No choices found in the response.") + + # Extract the first response's content + content = choices[0].get("message", {}).get("content", "") + return content.strip() + except (ValueError, KeyError) as e: + logger.error(f"Invalid response format: {e}") + raise RuntimeError(f"Invalid response format: {e}") + + def _handle_stream(self, response: requests.Response) -> str: + """ + Process streaming responses from the LM Studio API. + + Args: + response (requests.Response): The streaming response object. + + Returns: + str: The concatenated content from the stream. + """ + content = "" + try: + for line in response.iter_lines(): + if line: + decoded_line = line.decode('utf-8') + if decoded_line.startswith("data: "): + data = decoded_line[6:] + if data == "[DONE]": + break + try: + json_data = requests.utils.json.loads(data) + choices = json_data.get("choices", []) + for chunk in choices: + delta = chunk.get("delta", {}) + piece = delta.get("content", "") + content += piece + except requests.utils.json.JSONDecodeError: + continue + return content.strip() + except Exception as e: + logger.error(f"Error processing streaming response: {e}") + raise RuntimeError(f"Error processing streaming response: {e}") + +def create_agent(tools: List[Tool]) -> Any: + """ + Initialize the LangChain agent with the provided tools. + + Args: + tools (List[Tool]): List of LangChain Tool objects. + + Returns: + Any: Initialized agent. + """ + llm = LMStudioLLM() + + agent = initialize_agent( + tools=tools, + llm=llm, + agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, + verbose=False, + handle_parsing_errors=True, + ) + return agent diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..4843c27 --- /dev/null +++ b/app/database.py @@ -0,0 +1,15 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +import os +from dotenv import load_dotenv + +load_dotenv() + +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./app.db") + +engine = create_engine( + DATABASE_URL, connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..f38e8c2 --- /dev/null +++ b/app/main.py @@ -0,0 +1,175 @@ +# app/main.py + +from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks +from sqlalchemy.orm import Session +from typing import List +from . import models, schemas, database, agent, tools +from .tools import load_tools, add_tool, tools_registry +from .agent import create_agent +from datetime import datetime +import logging + +# Initialize logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Create the database tables +models.Base.metadata.create_all(bind=database.engine) + +app = FastAPI(title="LangChain FastAPI Service") + +# Dependency to get DB session +def get_db(): + db = database.SessionLocal() + try: + yield db + finally: + db.close() + +# Load existing tools at startup +@app.on_event("startup") +def startup_event(): + load_tools() + +# Helper function to create LangChain tools from registry +def get_langchain_tools() -> List: + tools_list = [] + for name, func in tools_registry.items(): + tool = schemas.ToolOut(name=name, description=func.__doc__ or "No description provided.") + lc_tool = agent.Tool( + name=name, + func=func, + description=tool.description + ) + tools_list.append(lc_tool) + return tools_list + +# Endpoint to submit a query +@app.post("/query", response_model=schemas.QueryResponse) +def submit_query(request: schemas.QueryRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db)): + """ + Submit a user query to the agent. + """ + user_input = request.input + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Save the question to the database + question = models.QuestionModel(query=user_input, answer="", timestamp=timestamp) + db.add(question) + db.commit() + db.refresh(question) + + # Define background task for processing + background_tasks.add_task(process_query, question.id, user_input, db) + + return schemas.QueryResponse(output="Your request is being processed.") + +def process_query(question_id: int, user_input: str, db: Session): + """ + Process the user query and save the answer. + """ + try: + # Create agent with current tools + lc_tools = [] + for name, func in tools_registry.items(): + tool = agent.Tool( + name=name, + func=func, + description=func.__doc__ or "No description provided." + ) + lc_tools.append(tool) + + langchain_agent = agent.create_agent(lc_tools) + + # Invoke the agent + response = langchain_agent({"input": user_input}) + answer = response["output"] + + # Update the question with the answer + db_question = db.query(models.QuestionModel).filter(models.QuestionModel.id == question_id).first() + db_question.answer = answer + db.commit() + + # Optionally, save each step as separate answers + answer_model = models.AnswerModel(question_id=question_id, content=answer, timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + db.add(answer_model) + db.commit() + + logger.info(f"Processed query {question_id}: {user_input} -> {answer}") + except Exception as e: + logger.error(f"Error processing query {question_id}: {e}") + db_question = db.query(models.QuestionModel).filter(models.QuestionModel.id == question_id).first() + db_question.answer = f"Error processing the query: {e}" + db.commit() + +# Endpoint to register a new tool +@app.post("/tools", response_model=schemas.ToolOut) +def register_tool(tool: schemas.ToolCreate, db: Session = Depends(get_db)): + """ + Register a new tool. + """ + # Check if tool with the same name exists + existing_tool = db.query(models.ToolModel).filter(models.ToolModel.name == tool.name).first() + if existing_tool: + raise HTTPException(status_code=400, detail="Tool with this name already exists.") + + # Create a new tool + new_tool = models.ToolModel( + name=tool.name, + description=tool.description, + function_code=tool.function_code + ) + db.add(new_tool) + db.commit() + db.refresh(new_tool) + + # Add to the registry + add_tool(new_tool) + + return schemas.ToolOut(id=new_tool.id, name=new_tool.name, description=new_tool.description) + +# Endpoint to list all tools +@app.get("/tools", response_model=List[schemas.ToolOut]) +def list_tools(db: Session = Depends(get_db)): + """ + List all registered tools. + """ + tool_models = db.query(models.ToolModel).all() + return [schemas.ToolOut(id=tool.id, name=tool.name, description=tool.description) for tool in tool_models] + +# Endpoint to get a specific tool by ID +@app.get("/tools/{tool_id}", response_model=schemas.ToolOut) +def get_tool(tool_id: int, db: Session = Depends(get_db)): + """ + Get details of a specific tool. + """ + tool = db.query(models.ToolModel).filter(models.ToolModel.id == tool_id).first() + if not tool: + raise HTTPException(status_code=404, detail="Tool not found.") + return schemas.ToolOut(id=tool.id, name=tool.name, description=tool.description) + +@app.get("/answer/{question_id}", response_model=schemas.AnswerResponse) +def get_answer(question_id: int, db: Session = Depends(get_db)): + """ + Получить ответ по ID вопроса. + """ + # Поиск вопроса в базе данных по ID + question = db.query(models.QuestionModel).filter(models.QuestionModel.id == question_id).first() + + if not question: + raise HTTPException(status_code=404, detail="Вопрос не найден.") + + if not question.answer: + return schemas.AnswerResponse( + id=question.id, + query=question.query, + answer="Ответ ещё обрабатывается.", + timestamp=question.timestamp + ) + + return schemas.AnswerResponse( + id=question.id, + query=question.query, + answer=question.answer, + timestamp=question.timestamp + ) \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..2ab9224 --- /dev/null +++ b/app/models.py @@ -0,0 +1,33 @@ +# app/models.py + +from sqlalchemy import Column, Integer, String, Text, ForeignKey +from sqlalchemy.orm import relationship +from .database import Base + +class ToolModel(Base): + __tablename__ = "tools" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, unique=True, index=True, nullable=False) + description = Column(Text, nullable=False) + function_code = Column(Text, nullable=False) # Store function as code (for simplicity) + +class QuestionModel(Base): + __tablename__ = "questions" + + id = Column(Integer, primary_key=True, index=True) + query = Column(Text, nullable=False) + answer = Column(Text, nullable=False) + timestamp = Column(String, nullable=False) + +class AnswerModel(Base): + __tablename__ = "answers" + + id = Column(Integer, primary_key=True, index=True) + question_id = Column(Integer, ForeignKey("questions.id")) + content = Column(Text, nullable=False) + timestamp = Column(String, nullable=False) + + question = relationship("QuestionModel", back_populates="answers") + +QuestionModel.answers = relationship("AnswerModel", back_populates="question") diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..0e2f3af --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel +from typing import Optional + +class ToolCreate(BaseModel): + name: str + description: str + function_code: str # The Python code for the tool function + +class ToolOut(BaseModel): + id: int + name: str + description: str + + class Config: + orm_mode = True + +class QueryRequest(BaseModel): + input: str + +class QueryResponse(BaseModel): + output: str + +class AnswerResponse(BaseModel): + id: int + query: str + answer: str + timestamp: str + + class Config: + orm_mode = True \ No newline at end of file diff --git a/app/tools.py b/app/tools.py new file mode 100644 index 0000000..ed0172d --- /dev/null +++ b/app/tools.py @@ -0,0 +1,55 @@ +# app/tools.py + +import importlib +import logging +from typing import Callable, Dict +from .database import SessionLocal +from . import models +import wikipedia +from asteval import Interpreter # Ensure necessary imports are available + +logger = logging.getLogger(__name__) + +# Dictionary to store tool functions +tools_registry: Dict[str, Callable[[str], str]] = {} + +def load_tools(): + """ + Load tools from the database and register them. + Assumes that function_code contains the body of the function. + """ + db = SessionLocal() + try: + tool_models = db.query(models.ToolModel).all() + for tool in tool_models: + if tool.name not in tools_registry: + # Dynamically create function from code + try: + namespace = {} + exec(tool.function_code, globals(), namespace) + func = namespace.get('tool_function') + if func: + tools_registry[tool.name] = func + logger.info(f"Loaded tool: {tool.name}") + else: + logger.error(f"Function 'tool_function' not defined in tool: {tool.name}") + except Exception as e: + logger.error(f"Error loading tool {tool.name}: {e}") + finally: + db.close() + +def add_tool(tool: models.ToolModel): + """ + Add a tool to the registry. + """ + try: + namespace = {} + exec(tool.function_code, globals(), namespace) + func = namespace.get('tool_function') + if func: + tools_registry[tool.name] = func + logger.info(f"Registered new tool: {tool.name}") + else: + logger.error(f"Function 'tool_function' not defined in tool: {tool.name}") + except Exception as e: + logger.error(f"Error adding tool {tool.name}: {e}")