From 67df2804de84a0d79cae47ce0d58cde068ec7a50 Mon Sep 17 00:00:00 2001 From: minmin-intel Date: Wed, 21 Aug 2024 07:10:22 -0700 Subject: [PATCH] AgentQnA example (#601) * initial code and readme for hierarchical agent example * agent test with openai llm passed * update readme and add test * update test * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * change example name and update docker yaml Signed-off-by: minmin-intel * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * change diagram name and test script name Signed-off-by: minmin-intel * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update test --------- Signed-off-by: minmin-intel Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- AgentQnA/README.md | 106 ++++++ AgentQnA/assets/agent_qna_arch.png | Bin 0 -> 70399 bytes .../openai/docker-compose-agent-openai.yaml | 63 ++++ .../openai/launch_agent_service_openai.sh | 13 + AgentQnA/tests/_test_agentqna_on_xeon.sh | 75 ++++ AgentQnA/tools/pycragapi.py | 330 ++++++++++++++++++ AgentQnA/tools/supervisor_agent_tools.yaml | 59 ++++ AgentQnA/tools/tools.py | 52 +++ AgentQnA/tools/worker_agent_tools.yaml | 5 + 9 files changed, 703 insertions(+) create mode 100644 AgentQnA/README.md create mode 100644 AgentQnA/assets/agent_qna_arch.png create mode 100644 AgentQnA/docker/openai/docker-compose-agent-openai.yaml create mode 100644 AgentQnA/docker/openai/launch_agent_service_openai.sh create mode 100644 AgentQnA/tests/_test_agentqna_on_xeon.sh create mode 100644 AgentQnA/tools/pycragapi.py create mode 100644 AgentQnA/tools/supervisor_agent_tools.yaml create mode 100644 AgentQnA/tools/tools.py create mode 100644 AgentQnA/tools/worker_agent_tools.yaml diff --git a/AgentQnA/README.md b/AgentQnA/README.md new file mode 100644 index 000000000..41eac2b0e --- /dev/null +++ b/AgentQnA/README.md @@ -0,0 +1,106 @@ +# Agents for Question Answering + +## Overview + +This example showcases a hierarchical multi-agent system for question-answering applications. The architecture diagram is shown below. The supervisor agent interfaces with the user and dispatch tasks to the worker agent and other tools to gather information and come up with answers. The worker agent uses the retrieval tool to generate answers to the queries posted by the supervisor agent. Other tools used by the supervisor agent may include APIs to interface knowledge graphs, SQL databases, external knowledge bases, etc. +![Architecture Overview](assets/agent_qna_arch.png) + +### Why Agent for question answering? + +1. Improve relevancy of retrieved context. + Agent can rephrase user queries, decompose user queries, and iterate to get the most relevant context for answering user's questions. Compared to conventional RAG, RAG agent can significantly improve the correctness and relevancy of the answer. +2. Use tools to get additional knowledge. + For example, knowledge graphs and SQL databases can be exposed as APIs for Agents to gather knowledge that may be missing in the retrieval vector database. +3. Hierarchical agent can further improve performance. + Expert worker agents, such as retrieval agent, knowledge graph agent, SQL agent, etc., can provide high-quality output for different aspects of a complex query, and the supervisor agent can aggregate the information together to provide a comprehensive answer. + +### Roadmap + +- v0.9: Worker agent uses open-source websearch tool (duckduckgo), agents use OpenAI GPT-4o-mini as llm backend. +- v1.0: Worker agent uses OPEA retrieval megaservice as tool. +- v1.0 or later: agents use open-source llm backend. +- v1.1 or later: add safeguards + +## Getting started + +1. Build agent docker image
+ First, clone the opea GenAIComps repo + +``` +export WORKDIR= +cd $WORKDIR +git clone https://github.com/opea-project/GenAIComps.git +``` + +Then build the agent docker image. Both the supervisor agent and the worker agent will use the same docker image, but when we launch the two agents we will specify different strategies and register different tools. + +``` +cd GenAIComps +docker build -t opea/comps-agent-langchain:latest --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f comps/agent/langchain/docker/Dockerfile . +``` + +2. Launch tool services
+ In this example, we will use some of the mock APIs provided in the Meta CRAG KDD Challenge to demonstrate the benefits of gaining additional context from mock knowledge graphs. + +``` +docker run -d -p=8080:8000 docker.io/aicrowd/kdd-cup-24-crag-mock-api:v0 +``` + +3. Set up environment for this example
+ First, clone this repo + +``` +cd $WORKDIR +git clone https://github.com/opea-project/GenAIExamples.git +``` + +Second, set up env vars + +``` +export TOOLSET_PATH=$WORKDIR/GenAIExamples/AgentQnA/tools/ +# optional: OPANAI_API_KEY +export OPENAI_API_KEY= +``` + +4. Launch agent services
+ The configurations of the supervisor agent and the worker agent are defined in the docker-compose yaml file. We currently use openAI GPT-4o-mini as LLM, and we plan to add support for llama3.1-70B-instruct (served by TGI-Gaudi) in a subsequent release. + To use openai llm, run command below. + +``` +cd docker/openai/ +bash launch_agent_service_openai.sh +``` + +## Validate services + +First look at logs of the agent docker containers: + +``` +docker logs docgrader-agent-endpoint +``` + +``` +docker logs react-agent-endpoint +``` + +You should see something like "HTTP server setup successful" if the docker containers are started successfully.

+ +Second, validate worker agent: + +``` +curl http://${ip_address}:9095/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ + "query": "Most recent album by Taylor Swift" + }' +``` + +Third, validate supervisor agent: + +``` +curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ + "query": "Most recent album by Taylor Swift" + }' +``` + +## How to register your own tools with agent + +You can take a look at the tools yaml and python files in this example. For more details, please refer to the "Provide your own tools" section in the instructions [here](https://github.com/minmin-intel/GenAIComps/tree/agent-comp-dev/comps/agent/langchain#-4-provide-your-own-tools). diff --git a/AgentQnA/assets/agent_qna_arch.png b/AgentQnA/assets/agent_qna_arch.png new file mode 100644 index 0000000000000000000000000000000000000000..3bebb1d9972e58813811c0fd997d52f015e60fa3 GIT binary patch literal 70399 zcmeFZcT`hdw>OF?N)hQrkfJCcNKt9h1q@0N5D;lWdJ%{Mp@k4onh#AyM5Qif5lpVw)GCN5@)yXy~0lT=-QivvWYiT zm-K1A@l}T1zEUMf{V0{S-qLUW-dn1`c9$K_I&ul(15vlK0UMMV;`T^OjyGrJnu=nAt5u(b442-->sXL)#lNbfnKffX0phdPM*&8e@$1f0hJ~&J7 zTxaGZTBX%d{s$!@CLztYrq@5SjPcGHffP?3zu*(h#QQy;nKKpr&*z^dtAw=z8i=mq zK>ysJZ`}XojcccQMgN5%f2PX-orIo$l96Wuzv8Vkv$>`FuVmH~l+H6MHfP-UC!PEN zrb_@FgBH%@1n2XVl*fD$T?P9b61x5(x#e?OKN z2PXhr_y|_P?*PjA{TX%l1(-Nd@jt}>M=k&3EyrQ;p9PVyeOvXx!Z{4)c6hlRw+KVX z2{4gBo!K!lB4#o0+FzrzO5s%h_I&QI#VbMVR8>RH}WgX@o zPEdVMj{aJWtY@NM;!9K#|uv zbnW)SIRid2sRwoXHXi(kgJm(FA&}vc-%tK zo^Ln|KH@3o*Z1G#xj9%vJieTFee(AnKfhl*Ch>X7{Ka)4i8{yMoR`-A{QmpId+%j0 zJVRnVkmHco;g{1;>fitH*Y{%*L=oYXU#otctt|Ol!k^!%k5@e2DERW8DaI7}w=BQD z6CbZolNDp$e13DOuT;;bw$E(1_6h&b?|3f7nFgaBjDB@-2<%ux5giE!|&>z>SVR~@7>V3%~EfrV4V533&O$hD2V>m z6>ZMU8o6%AP<|s#f;}0&Dq`|0YX0fb@-WUI!8C{D4i~^#8^VRK%lu-ya4g9^(05pT|8_xID)>_kad!GC&nihIvF- zkAFuL!Xo7J&h)yH()FOPV*%X1;f#;gBuU4OYXRQ?nXGLDf3Ig2{h2d^sGrY;QmP*o z3BNwcUnvqSAD!*+N9KLYXFh&Pl)(BEEXp&D)m-``1N%5u$sb}uWB2{P0JQUadBr;M z9RO3gj4$&8Q0n*Nxvv!T|3mzL)bjsRZyEYNOu(K$gU0BIUl3d%`K!xVd=NBnp_u-k zB}H*0k`3{-73}`#jsutdIfK8DCVz(F(D#Y?`Au)!?#(v$6K3bP4UQH;{;{aV3Z!_G zoW&3MFOL(F6VH=S-WtPS{3DEQq+WysSka42x$g)55ylD2I&74!yf*pn`$2y^Cdv#w zT>PjCuJi}EBAQk{-0&~={g;2Pz484gW%_jt-JeC4g}n&NN3D(@|B@giR8gHBAWX$SPwueOkEz^@lg-YCj>ceV z!sm_g%^e|*_zGzGi->9e)xAX>$a%9|CV5qeu|0BjRqmMq>b&8(m@;G3#rf55l^Tw) z-3eWp z7{g6og}IKQs>~FhQ!r^j<95FYxy0cbP42z43^w%?i{(U>kmtz)C|?kOnzu zL3X1bHv%^+1N_GnGL2Egh{^lw z(O#TZE;RAr79OVUiZVhZSJp~{+@(ts>G1sm#1dcU>vyw9>wMfl3?ZDjQLhjaXHc)e zBQPc$z|r+RIu6`^38!asPIK_M^>L%Z%&ZwOb+gb9=ygKwR1!N4%`+pfCN`5*qq6tm zLEtzFImi)N>XVr#VxxYPjfdAPfk(J5UHZBrL}-WPh6cqAp}Z7#KOCLvK3X?Q+z*~_ z?!-&vP+&L?elU#$djiruTep_O_lo%Swd#&qP|CR-krn3qbnY5QS3?E&MX%ukgp1G| zp!vq8v@v{xm&n%llZ+biOx_zuiyse6(b0ep?Tv!Z46pc00}3eHi+00l&+ebYJFqB0 z%D1P+){2fhc8ZqkTOQi`QSEcYIBPjfZ(HSxir@&a0Z)B7YBx}r1zV4!GM4tZx4V7J zz3Fx6kNVaMiT=9_B>#bY_Tn0I%MD< z>wal&LyxeB9HDRp7T#MIaPuAx36S-^y?gb@{e3=lOn$4NM)bf|=MOLsoP5KAf+WWA zKLCsSckFAl`Nb<9Z~WI`5~!di6i^|Wsx_6Blww2>c#WEO{vBuDQThM54%f^Ntecjr zY(H9u-uM6seBHsbg7`c!zgeqs>4;`eYtO{@WEUFmCI|jlv;-!6#a|LI#bwXweYtpb z%`!rL=(sh|&;9vX0F?+sPvCpxc9UQ~*~}Q*Z8T{DDWWT?cn+=|flm2R`&&<)q69w^ zHaXzsn{BBGB_$>M&>}Z48CEj*;)dC-{;}v4A1bz|WQ~2=y$-TgNDenn!7dQ?T1EV1 z57XRQ(Q(lPEt>xPau|9dxAXDXuXPo(-C6p#!T>@4;LmE|^GmJ@BE)t)ylAt{yy?!Z zOP9=o)sz)hO~90L!AD|fivMyXqvS7D!On+f!MquM*u^RNhX^U*7kNCHW`J;uaKuY7 zdovz^&IHGe;8l1do(Uklc-ij=bJ{Q~*Z?vX`E?v^L8(X@8WiO4j{2xSnqW$r2xP`q zT7@I;6ghiQ5eTAdS-Y4LqA4sWQOqKK~o;!s7I)Z`*tjLZVi!9uzU3^Y58U$<< z`~T=h0n{usG{e^}9`y|5>!=mDE}2DN`7Q1HgNc3d)dR8rR0nMV9a(@zb0YqGwWxWo z<5=VM@BH5`{oD6;_7cv>T^^;X+RjH-TsPnM zA6gfP$7WUvPUQa`NbgIiu|lYqSG>9#<ws{bSoQf*eyX< z)=Z9a+?vh}tSLgRkLT9Bl9C}fy20yB|6$r*uP(})#wR|4TxFeSS-YNAl;zfnN_mRy zM6T}jR6wtLn-tHwNfH*i8eK~5tILX({qtP!A=TSo)fMJi|41}|x90IMO$?yTNO2R$ns(#%OE}o(<5IXZbjlN@hb;62gR0p+p1fTQTsD>Ci;e@TKn5K2(S&Y z-EtXJ1l}!gH6vsX{b5yO&oy|t=(X;!EA=*f*gM%_0ln>wL5pv)KS1l^^_@21%ld#f z_QUS^BO4s(TZhwfasB%tSaE^w_0$%(M@y@jT5*U^998&ol3siA&G#qZ$d*^C{XwZSao)b9`G zZt9ScG1z2<_#vx})B`eBdtmU+Ob4kTa>(W~{qPnLgU_BcZ^gOl>oW{3K=fcU!-Wen z8pum8-Y%~>7aPdj4?&@<9v z#Vv|oLPyj{b9@GCN~1oi71+PedfGM|kftC(k~QY)w2aO{DH3K#3ijL*mj@9-tlt(k z2UqpRGmOXh)vjeO^DedYkKQE-V-59=YBe*oV}h=T5&Hy)P8JpK#eP*W_<~>r*5WCSsXo*rF|LPm;VY4!gwUhTAD+s<|X^ zeIq*xb;}qjQK+*B8!nMuwyroS?e9Xau`S*dKkRH+!Nz)F0nRAf1>;!SahuKSN2N_` z)jnAw?OugVgu6K8MYYGn^!ii?`f2CTD z+4|G|#b?$32y1l`|5866UDHWJv+@uID#xXA|5xx9TMFsg3>^!nfd2 zDE);Yhp4^mt^fx)>`n3V#Q~Jzs)^X;l5t=ZA)$A|wk8*p!6z>OFdkNTaSJq#gqsN2 zUprmS!eD6BShR$`ZZrH&Yd;Fd>PgZt8Xj#TZOyT4I!EX4qPvz zFv^fvqkm>^zm90V6YmrX_~S7xlqYRT9^gQp=lK1`c;A*VHlKG-mM|%sEzN~iQTJ;n z+TmGB;*o6$2)A{}0uz2P0T<}_{#pOkO*i_Jx+Ilj=?jI7K&E}Y_^{dyUjas}CO8;^ z9E8xNju0+T8SZ$5vnx#Dg3X&j}c&W~tRuHLT196Cp=6zvO&@?6ZQfz*Wc%x?Gb z@4F3XASoYa>J* z?m*}*-R%G7Ev`wLDE|r#=Kze}MyjYdOytDUDn}ubZH;HMwiu+Fz!% z<8Twjq16uQ^@=sQ#7>Y{Fyl^nzxR(7=%W26hke^&7wTm_(D?nd{EX5f>Jqtxc(|xxIVJ2jvb-FucdaX|#G!_|5o}i2RLxaJL z&Cl(Nakq#1w9(0M^zG#mxv7Q9qUDTNcoLYS;eL!K{Fz@7F=4{h!hS!mG!g}(RS@U@ z(|Mw_FTNs<8;KHNckN$HBhF80>fchxVpg<)Kdgk4WHvzUf9Q)|8~xUL8el4Ws9&(? zVyYdz7kwKqIthBXES6_+$Co-Kr!AB2)c7vw^=+=s$!9H zb~zb=Jn>1^dcCBt70-637gU)*U1QTwyCqMWQ!$E_Rq?LFLwQxu1-x2AsbD45 z>`*i@81~3y)3>r4b;|@5WiV?!GdAtWB@V>kK-#u`^>&Jk)mbMah2QS@^`f8P>_}jx z>I0jeY$R9?ks_SA9fjVXhc4Pw?m0!0@CxqOOZo|h{dj0d-`{Cnu(;$kn+RQW=j-T0 zzgf)DR@SsbCMIYTtm>el-pfqx-)|rSOJfo5;=Ko>N1@pb(X0)t_m^#=AoQPR80#M6 zmd8Ay#W^MJ$qkjJo$V7f*~=b_wj4cfpP)JcTxfJLhaGF-k}R+JFd}%lL&&dhw!NRr zk6rCI!$1)%2x0e}_PppM#6UjOzr!+Zd(tGw#0adzCFbx{Ao#XbE@hoXo=+C+GSuOH^qDr6)M!F9{@g0G4`3B19S+;r27+&MJC*6iu&Y3&y8mm} z%Iex56^b?vT^C38GRFARp2)ovq{OMw6jE^(sq&jyjy+ECCU1hd0muH zl2q6F^ja`$Zq!w0t(nyhSKx%LUAXDFIL)Z60Nd9txZDUzP7F>?+b)Ose}R_d^q?Kl z;vCPwx`uBQJsr|#!qy7dB5hh-!D?sYO*UX0rxz%J* zWBI;$Z4{Y|?D-=R>6_noyepm<Kr)s<&bBOBv?~4f9p>9f z;mVqx{h@8=;ETmQPqxJuR!bHaP@)TcxmoRw{Z|d_IUM$H<#`8Iu4KtQbu)0RcQoB+AjwebHk%^K62dT zqGZL?=kg>&;fDh39S9n2^ zZ)2F0+HSMTp;a|T#X;|jc8Hpv@*@7 zW*?GRfnP0Wh>h@`%Du@e5Vq2{pG8>>4Su*Mwe{gIt{<=rRHDSts5ZiSkd>4CO7nY2G*Lv14g_ZQlT)C%IixgT?LzrZP+pdxLNWCZ1d}Q zk)^4bdX3&5L!KhK@W9vTgM5DacWT~Zz5rG7_~2wKhuw!I*L;XO%p~|3dagd!=5<7B zKg5L&0bZ$^sE&7ya_BRu z+J0IAhv*xQ;4xI?gs|yeSqlhO$5wlJhjrT*Tu8W|`*-{(l8&-B13kBbeUlbmo5k-Yrx%syqCSWFns$f0PP9OdU zJp5-r@!$Gf|JFSPny%uwcE}~5WqSXAv)zBuG98OB3IawPrRx8|YyZ{-oMJ*l)c$iZ zfRs%59uKGcPuqw8g&5w7P8Wf}z<}7FzW9G`)56^T^Vj~(8h{u=UJKFxhqmLdhW4PO z;#_xkV0)!Crp;kz=~2bS1G=N0IH1JMCVvT_X*>+HCEy!Z+`vhn_r_w~mW>AsTn^%5 zRbzVwlf?sdkJbbCXaTpekEHgT{xlhGFjyes+`iORqb(Jlo9*95vRXag#$T)Y&&4Qe z1JjM~ZRW#zuGiSAtY^DEm<=p>*v)p~_?l_NjxUb<(ys$4+|G4=NLwn(byNzrg^6TS z)~kNyp=cd-E=?}~i0-S`CSL7Vm?i+>9c~iXj6TSS%?5mD=yk2tIG!8^0n^yLg^7LL z_=5o0v%~346Zjx5vgCEgM+?nqk*cTV#YL)tqSr4%k6Zb~!2ChaWxaZn&cs=$?Qn=S z=yG`rPqz~o-%xyROGYXz*7fAfU!Qz2OI@a+Vc_!l0cg)(zh(_S4{typ@4XWIY_e5w zv#DVhNlimzILW_KCPbOojz&zl?CPuCf_JY|tPFZ=1uEp8+UevZd=3RN=A0WwA`s1l~0=##=9dyHBwa)ah8MJE=W}u zn$Q0@)a}*rBSq3R_4rQTGWLhNWR#bMb*UhzVGC+qc z9N3;M$?s^Kr*_s5BLwLu*@dDA4Q&EVB!_bvL$5w%R7|g5&GuByTG5)iL)Qz& z+X|E8cgc<}E-{;b0!FE66l;Wi6nYVy0EG7aOu1dWki|t{`X&x2Fm9yCe7*qAb4^D- zG&;;UjizP(?hLo$;?`=}lh?HDzeE=ni$NhgbeYDB;&&@xuMdr8TQjqUlw=YTB*csO zfsH(LcJD2zN}7%XCJM;~EUQJ5Tx7uMm95+4mxMOFZBf1&auz5Z*3Lyz6VTE$i6 z-_qRzJOk3%ie^p3I44g`cLO#q|<}f)9`qv(cqslt?P5(oOm0a0WNm_!u!bR zFgca!TeXP3FcsUFA&;%EiIHpXWhw%XNpz61kbR=q$O&$ajh0;xZCgBtpa2H26JEVM z?P1Ty3W3QYmU2Y0dcPvriV*{rsD3(mryHrnRudN|>ou0I;tmjX@js@f$-iea$B*8* zwNuLmP#M8BQQ<}_$jF3Z^aq?fTNHTQ7F%lPgRN%Hfu&_oxTJZ*(}Mliukjwcs(GCv zlLq)rf4~@iJ+GW;74Y@R1tHu#JT{mqiV*HSD{>XGy9fJx=jLXh1L<;E1=I@Pai@FJ znMITh%qYM|^CoKT5n#9xinmXkUFV;IYbUqV8-+*_$3ss@>4>LOo?shc`3p*&LmwkJ z0hq3_^NHM=fA-nNpM&A*Ge(l|W)DMa#uc~cvo&Cen+PSYbfGwKnW-K2U3#6KVXb1~ zx~7Wz(xZ=w$DvYYk(>xjy)WSc<=iVaho+K#iVy!9NRXvW=;rZe)X0q-&@87EvS9xl znRC-Qm`Poz#Xh#?3U*ERiuB3{8P82&755P!!MUoq|8fy|rdgr53fG0t)`UAuixoCI zf-{G_HZZ}Lp2X%Lw{(#Cwdy;Cn(zQo)INfhN*$g1Tw0%A2Dx{%xWR&6K&u!6EbGW?x(k!pW#F z%O_^;{<#lTet}~*t7LvLqQ<8UOxvfql73~)Js`No+6YZ>+YYZqSk2qhtqANV?O4=% zsd4`ovEfTNRyWpyJ!g*np&`<_pBDmdQPpY~TIz^nC+7E-nOa-eMkI@#4pF>a6duNm zd?{ACyAe0OoNRgdinP`02$`SFgoip!54oyciS%Qq<$C_!{P;iZnm1!R6CR5#1g<2^bA;|*s^uC{5R5z@w zTHqImB=n>LnM7Fnfb5ri!fiH}(!2_55wJCgl$6{$ z%g9d6K83BshsxRTnia>B%9E>vud^eZd6|gq?~^MS^Yv-Bk$#V^Ga7-|_QcaJO|%!H zZ*Rxy&fdz|k-mv|G+w~O*Ke9`Sgi-mW(WwByJ)aM%g&egdPy-fYOMpNRr_m7{y5|o zV!fy2a9Tgbw4<^2gGovpF;UZ&CATM_Wba!^*kz5S6RI4hK!|O%^&e~-_z1}DhP6Y% zJ1-#>t1nv9qe;fxv>U?L_slZLTrh`H6STW7n_={TO1{?_ibO;K9GxSDB$ z$lkK2R(Q;zqyJ^<6q;)v`@vAB{8bK+ljJhuK>}%X)aNq%l*Ra>kKNtepWSv0I6=+W zl@^~9WE=8;R(B({9Y1UL zP3B3Deb|@K&brNV6`wo=5aP@CU1yplH|-s1up?XqjL&S=`clk;4xj)kw{U^_AkPL1(iTsVO}A;fO)mes7= z7^8~g)QXAh&4n&jXkybD(dV~fL!MfhmPp;r)lmQRKI~f@_Tq&a)tr zr7z>ADVn`~m<_H7jXzsm>XfPD8-Hdu+sS$i!Ki6$N9SITnm|O%T15hzBP5!?+#gnuY3E ziZPF<%2DQ?omGD^oY$D)0@>99_TJg*Rok0=Y2-3=4n3@ln3kO{LNqV`->S6l;_ zBts|^XGgBx#mF6>fkYue!UiqX>4J+Hv5n{aTW`ychdlpyx;BA9osV&hn?+=!V_015 z1|?6qFAXK@nNH>KT>=;kpJln7S$O&swDsBgmO%d}OHlxDSXqjVOg4Q|FNGUeDs(wr zJWLppG6LfT=JNrjk!u~TV#k}xFLv@4+y)h}w8wZg#dd}VurhueQS}XB>f3^P;yFs& zu6d0b&!m$zn?>KXzj-1R#G5{mKg&EHG;4HK8; zxk^STW@u?ISlQ6#QCQDF+=xHa*wH6Gzv!SC-osyZ4_~b_e=fAJ28g6^74H;8eNe`D z0Emau*FeHdSYY@@QNH-QrXL5K*xZJEcS_><^(*`g#jxnGi?*ZIv&`D@n}OT1Od3KX z)XnRe9Lc=niRcFy*sn9@y^YA5!AkkDyZ-&AbU9aY>LlfTilA2GWyBxyKM3l4PyTMR zU}0#j6E9ljm9oV@t0|UnzwVoblmoei3s=(OK5%09KOrzPF4i;;O6|Y%E%4p4f`F*+ zh6iZsdFquVmJUi{a7Oo(EqM0f_8KnkE|F{LiI>&oB5>Y0erGh!|Ega;*C$XI%UoJA zKD)Cs_gXAak(`v_vefwZ(`_7r`$3)t-J;Q_S_a!ZOLD<8hKIS}-?|c1Z`dsXp^Vj8W%gtqM}S5$CvTZAtsi zivDx}(&u6VOS^5(?1%D(DR;M_LAfW_E7xxzD(xPHW0@bm1E9u?5;KS_JTkViRQu>0 zB>H+Tbit{vmiALJy`f~&8_)_R?bj48iZjn!7}6Kt9Pfmk`?!39Y0^$pBUp4 z|Caf2J8-X#Nxi*2!y*WK2*s`dUc5PgF$Kcv#cGPlgJXV@?l9%N2xu=S_oL?(L{hpxN1>a8ul;WuHm=yDfs69`aUW7H=m=PR0i zcp+FnE>ScVHK83n><4trdl+_U?`km4Q@+jiYc#^XRo#eMEAS2T zyFlt@g}z<;>_| zBSv#+&r!_WV%dLzLKnF8*v5@m?ygFg#%d)d^He_*tA3h{X|h0HNC@+`)$1NwsCf3M za0Q>@g}>bs4K7{8_!2T8_MXnZWLR8SXbu$s(zV=|wR2S8__;c%Gy z_B+LYnJVE1`Bayd?#dl&Dxj5+vv$jV;si^zfsry-dhNtZ#c(I82ehV73>M5xr?v}3 z-#kn{TaD_}PAmtF1w<*o#C$0{k>dCYsA62&FNbyMxif-Z^*!k0j33Aklbo)5F7LUa zb^d&>IF-gpd+dxVhhE`JLnf;LoW-|<6!Dp^H+Nd2*6!|~|BYKX$aM=lwc8pjA?@h_ zb_4Y+nZ22oPZnDR+0F%Xlgkvw%GN9gGRTYqz+C$4gERW?!QK|8m|GJ$7qd0XCuEmb z+9MP&xKEa5Dp98MX*CEE#3b!kGU9pH1o#7S)vnKFpKznv;%;3(q~^~|KG2X68c+P# z{@ZmDUkA1`@L(j9UwtK61R!1d3|oSXX$7Z{uJOG)U&F)^wO!8o3?% z>6}n_7|ivWOzBX{M=b8Oo_!{1vZd!3OXtj_^iy*LNy`*Faxw79-GbLrBC!1b4 zgc8AeKUb+#qm`7MFUx_uRZ>Co+u-s44Aa$2K}>;niFU|Xb^J10Xjn?c`4Z#)d=;g`P0GsmZomNXX9mz(Lh z!bW0fIu#EhFTKdyL|oy?Bi2LO!>qi&lhh_JDYHUzS2^5~)T*&;Pth{m`Nezt{W6t_ zxtTGEw*{T6l>*~h>!+bty2;EG$*4xJojoq8)rc!Ts|oVCQPRumWq*(&I}xYQD{Q92 zB{TXeF^YWRYldrXAn|v;@sy#fGi%o=A9X!VHOhY@^Pr|UYbiA9Sz5+RO6W;zBW|(F zuN-+QwBD=tNV+|JH3`=*a(GhMK2`m7YGx{5)jegF_t-Mb8uUs2wC7V_Fpxk!m5`y~ zt$e~1T)zD3bMO2eDPRm^0qW|f^l^)PsjNDlm&Xjg45f9;Hw>;CGgP&Vl?zl=iCUyu z4@nhV^mT!C%4V|OFeT)r+J~lj-|Q+U*@E3XJ^j%yEZ&1G+y&)|*}+O1)|%7ug#Y?S zaqoHG1lNC4I~iQqG@KbmY^)n3ESM~F<_hc8M;Q*7uOr-wCb*I@tfxC&JhHtbz1WPi zLnD-0S6J#Coo4SHmai7KtB1M6vh0%AHhAwndGoMLD_OX(HF;y>CFSDBDS>(A`TBs# z8?-vpWDDde44J9pY`#MSi7zFDKE|O_-`v3p@UHZWR=nxek4MW2iaYcucfBSk^8N(4 zzk7?q#9IS)k)5#*n4USDy)9C4vj?>;cmVqtVAV}=)AVMJlIh(@8sB@-Y~i;obkbvF zG0R)w>~UJ|Y~V2ikq1S9?Hk8u&iRT=r8L%0x!vA}6bkik)XP)pb>qckP^!1x-S^Ww zTbB=eR>qQQ$3^z(dy7#k(w_Czyo-t7DR`C0qHQSkLnrqX+Zx~|3@Klw*T;A@VJV4K z-QYNK4YORbY$9QS%JD5G4HCYP&&}VsH|$sI15R~4ZgT8Mf%fk7Z1>1RW~tJqoj$oi zSOaSmjyJk~nKC{iSYmY|E3FCZaJH>b{_!hedAj9L)t-e!H1n=*;y&%(HEo(a-0jUdrpb!E^kSDIuO-2?|A;Wed+l+tU~)dCE-%D zDEzD|(76Xt*=?qYCV!9V#^fU9ILO7_t1m%#-K&tKrw_QS>zb1llefDJlBIx?0hf^u z+Pr$LXR(%>Gr_y39>|An3TdK4JVGi2T(@cDm+h-`S;U5eJf5G#)TyzcyCR4@+J+=E@Vttm4&RAa z#&3m|s%@~0DttYDpk3WP_QJ--Ne9B#vvocw;KpG-`9j#5#h3x%XV9OyCLr{i3fuD7 zyD+St&C=>-39}>lGEjwF-(zc0b@yZ4JWe(PXIcC|+;^QeVcy+&cW_LATw)MiA&(v9nOCQ7PjGeJ$jL3j@v}LEeyw3bvgpa~m^{>-# zCK;nn($8xfb9eV=fx6Uk-`YS=az1)|T9f!@fvI|lQ=_?A-Bw1F{qs^uh2kl3Q(F^O zo+Jkf$APel~d+*Q;SSumNl=`mTztQ;HOtMlZjMwdyei!9Ny zdkV#={LF$r>6$(^UK+TjOF%QFPI@V8Ixm+Bh_c$qEB*CTFzQo9c7S2BqhZ|{` zbcK!$#7lhBLUqkqZU-7H6zz1sTeIm*_$76apZ(XJWN{uQ-c;o&UayB4=X!(BMSpOo z=cE$5-hD9s%)_P3AwA43vUAC>o0gA4{LnKPY2Q`8E_S7RDQxz*nCj{p%hXpLs_vJR zCGibLnLJb}40ay-5ATqr7zV@OP zdV5fwAAwGs^vRL8KmVvilVWaV#NDXqRv$KkRnL3 z^P#_1j)C}$ydqBf%X2isxSiLJR4DL-t&^Fe|LC|wlHz_ecTx_QaI<5=NQrxU*p)aI zTbdzMuiG1(_bMj}X{j5p0^v?eed-EfjkCltl?eafDG-3QBb=U<R92ArIiuByW)~h@W#$hs1oxsjVEd%on5xOIA%E_Yhq-ZGU zTm6&*a7Y6nfSOtt?gu_GHB#2{oof0XPkffD&)(R-XeviPWmWg@
GqmY%nbUUid&jQ{4zf3)m{zn8~WvD&dX=mtJ%DTRnLiXP^

Uz6zd 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�kr9B;(Sbd5 zpj6Y)N#PR~xNAjIsE;B0gQt~-QkI{px?p_z^z;(N`Qg2;)TsP)SHWQhs$4VsKi|uA+3C_jw$SGL!OV!8X$Ppvy~@&Nmx- zHu~;KhxJ%d4dt#cU1nFX-<6vax-Z7yb%Nhh5nJQmFO4&*_I)y^)01sX*9ox#<+ z6VzLxZ+e~z;%T>Uc~zQzq9tL(Jb8P|f=qh)H%OR&Kf5Jfc{nT7adJ%4v*$tnz+y_= zelZ z`Fo=!F<00mbPI7;6oZ0|#-}tPnn@#^Du7VDqS2b@RsV}F*Fb1Z2Hp|D@WACGratt6 z^hco+Lb$mo^biOzMd#7`poap>44o(qRa{Fx7kjo0+>ef}5e;*RWH+WQ=|cQ`>2c{z zA54V8==9m_>PROO{TE_3=sPzGj<6DuU+2lD%h6t747Q3`jOsmX1Qw%3&Ln9-!oUY( zSk73KQ2F(eZ@XMEiyqbR9R*>gC@9q2F#avJi)6;`7r%LFPmKBj%MvB(B+eemEh{=W zp9-S#)K{A&NHAEd+>M<5f+@3vG&LolH{+%JidlSkzTGA)c|z>>{Dc_@$ANH}oU*(W zY#n`Um|0^I^C>TKqj&wvQX(@K1kq zTSs>6xoO(~l-#_h)Z|x@DjIl4V$X+-yb5HfQ<&EvHLm=-}E?dCa#w z83yWRH{sYAv^-PCh#)*Kd43Nti~0Ci&i=|f8xOA8eESFbGt#^v^LVXJ!YGs-l@NjlhrxgcQg3(Kj!Ki?%{RNChd`Ey& ztJ~@A2{jGXOXRLHk zMRwLD;qGW6BVeb~>R_Cp*BF#x{3Hgt8u+9TeIK6vV=#nV{g4X}w;!=qJ`3SBfXEp* zc`sAV-RWEiUFf%ySLHT)vgGfgRtOYQpby3O)%P!y%?OlIG(COmxn%C0eAmB9kK^xk z93HdH!iThu=a~!VuS1ZG@)-us-<4Y|Yj!hBGQPC1#rqSW3K? zjH@hPB&K$?MGw%Fc`pfFFnpJ_)$VbZ=YSbl6W96=S%owksl6Y?o=3s^Rv%eB5MD^A zq|B7gs5rN8sZ!wZlDpuP>1P=}R$9}++ebBEty5&h{`rG)YBr<(n{|u|RRR#@6p8Oc z_k6k9us<*f?o*1cN%HVb3H4!h9`^U`koeAI)oo5d!Apx6d7oM`U2^ymal$NM`eXw$ z+5t~I)on^~A>PlFu-k;|G1z1o_@Vz`FF9vuVT)-5rL-)dZrJz zx7}8cyBH5Z#sXCY+0)nndMmL|QBhJHWOaStpJ4y(-A@6H>}v}%`R0eh@pDNB(gFfl z^CuRvr_QI`pZRjz_7WXa_2J<(U%0Ai+cu@_KL`MQ8Cm2U~1|f3Vb*= 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 literal 0 HcmV?d00001 diff --git a/AgentQnA/docker/openai/docker-compose-agent-openai.yaml b/AgentQnA/docker/openai/docker-compose-agent-openai.yaml new file mode 100644 index 000000000..1dc46cd74 --- /dev/null +++ b/AgentQnA/docker/openai/docker-compose-agent-openai.yaml @@ -0,0 +1,63 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +services: + worker-docgrader-agent: + image: opea/comps-agent-langchain:latest + container_name: docgrader-agent-endpoint + volumes: + - ${WORKDIR}/GenAIComps/comps/agent/langchain/:/home/user/comps/agent/langchain/ + - ${TOOLSET_PATH}:/home/user/tools/ + ports: + - "9095:9095" + ipc: host + environment: + ip_address: ${ip_address} + strategy: rag_agent + recursion_limit: ${recursion_limit} + llm_engine: openai + OPENAI_API_KEY: ${OPENAI_API_KEY} + model: ${model} + temperature: ${temperature} + max_new_tokens: ${max_new_tokens} + streaming: false + tools: /home/user/tools/worker_agent_tools.yaml + require_human_feedback: false + no_proxy: ${no_proxy} + http_proxy: ${http_proxy} + https_proxy: ${https_proxy} + LANGCHAIN_API_KEY: ${LANGCHAIN_API_KEY} + LANGCHAIN_TRACING_V2: ${LANGCHAIN_TRACING_V2} + LANGCHAIN_PROJECT: "opea-worker-agent-service" + port: 9095 + + supervisor-react-agent: + image: opea/comps-agent-langchain:latest + container_name: react-agent-endpoint + volumes: + - ${WORKDIR}/GenAIComps/comps/agent/langchain/:/home/user/comps/agent/langchain/ + - ${TOOLSET_PATH}:/home/user/tools/ + ports: + - "9090:9090" + ipc: host + environment: + ip_address: ${ip_address} + strategy: react_langgraph + recursion_limit: ${recursion_limit} + llm_engine: openai + OPENAI_API_KEY: ${OPENAI_API_KEY} + model: ${model} + temperature: ${temperature} + max_new_tokens: ${max_new_tokens} + streaming: ${streaming} + tools: /home/user/tools/supervisor_agent_tools.yaml + require_human_feedback: false + no_proxy: ${no_proxy} + http_proxy: ${http_proxy} + https_proxy: ${https_proxy} + LANGCHAIN_API_KEY: ${LANGCHAIN_API_KEY} + LANGCHAIN_TRACING_V2: ${LANGCHAIN_TRACING_V2} + LANGCHAIN_PROJECT: "opea-supervisor-agent-service" + CRAG_SERVER: $CRAG_SERVER + WORKER_AGENT_URL: $WORKER_AGENT_URL + port: 9090 diff --git a/AgentQnA/docker/openai/launch_agent_service_openai.sh b/AgentQnA/docker/openai/launch_agent_service_openai.sh new file mode 100644 index 000000000..7cc197ccc --- /dev/null +++ b/AgentQnA/docker/openai/launch_agent_service_openai.sh @@ -0,0 +1,13 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +export ip_address=$(hostname -I | awk '{print $1}') +export recursion_limit=12 +export model="gpt-4o-mini-2024-07-18" +export temperature=0 +export max_new_tokens=512 +export OPENAI_API_KEY=${OPENAI_API_KEY} +export WORKER_AGENT_URL="http://${ip_address}:9095/v1/chat/completions" +export CRAG_SERVER=http://${ip_address}:8080 + +docker compose -f docker-compose-agent-openai.yaml up -d diff --git a/AgentQnA/tests/_test_agentqna_on_xeon.sh b/AgentQnA/tests/_test_agentqna_on_xeon.sh new file mode 100644 index 000000000..489a827b3 --- /dev/null +++ b/AgentQnA/tests/_test_agentqna_on_xeon.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e +echo "IMAGE_REPO=${IMAGE_REPO}" +echo "OPENAI_API_KEY=${OPENAI_API_KEY}" + +WORKPATH=$(dirname "$PWD") +export WORKDIR=$WORKPATH/../../ +echo "WORKDIR=${WORKDIR}" +export ip_address=$(hostname -I | awk '{print $1}') +export TOOLSET_PATH=$WORKDIR/GenAIExamples/AgentQnA/tools/ + +function build_agent_docker_image() { + cd $WORKDIR + if [ ! -d "GenAIComps" ] ; then + git clone https://github.com/opea-project/GenAIComps.git + fi + cd GenAIComps + echo PWD: $(pwd) + docker build -t opea/comps-agent-langchain:latest --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f comps/agent/langchain/docker/Dockerfile . +} + +function start_services() { + echo "Starting CRAG server" + docker run -d -p=8080:8000 docker.io/aicrowd/kdd-cup-24-crag-mock-api:v0 + echo "Starting Agent services" + cd $WORKDIR/GenAIExamples/AgentQnA/docker/openai + bash launch_agent_service_openai.sh +} + +function validate() { + local CONTENT="$1" + local EXPECTED_RESULT="$2" + local SERVICE_NAME="$3" + + if echo "$CONTENT" | grep -q "$EXPECTED_RESULT"; then + echo "[ $SERVICE_NAME ] Content is as expected: $CONTENT" + echo 0 + else + echo "[ $SERVICE_NAME ] Content does not match the expected result: $CONTENT" + echo 1 + fi +} + + +function run_tests() { + echo "----------------Test supervisor agent ----------------" + local CONTENT=$(http_proxy="" curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ + "query": "Most recent album by Taylor Swift" + }') + local EXIT_CODE=$(validate "$CONTENT" "Taylor" "react-agent-endpoint") + docker logs react-agent-endpoint + if [ "$EXIT_CODE" == "1" ]; then + exit 1 + fi + +} + +function stop_services() { + echo "Stopping CRAG server" + docker stop $(docker ps -q --filter ancestor=docker.io/aicrowd/kdd-cup-24-crag-mock-api:v0) + echo "Stopping Agent services" + docker stop $(docker ps -q --filter ancestor=opea/comps-agent-langchain:latest) +} + +function main() { + build_agent_docker_image + start_services + run_tests + stop_services +} + +main diff --git a/AgentQnA/tools/pycragapi.py b/AgentQnA/tools/pycragapi.py new file mode 100644 index 000000000..52cdd9b06 --- /dev/null +++ b/AgentQnA/tools/pycragapi.py @@ -0,0 +1,330 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import json +import os +from typing import List + +import requests + + +class CRAG(object): + """A client for interacting with the CRAG server, offering methods to query various domains such as Open, Movie, Finance, Music, and Sports. Each method corresponds to an API endpoint on the CRAG server. + + Attributes: + server (str): The base URL of the CRAG server. Defaults to "http://127.0.0.1:8080". + + Methods: + open_search_entity_by_name(query: str) -> dict: Search for entities by name in the Open domain. + open_get_entity(entity: str) -> dict: Retrieve detailed information about an entity in the Open domain. + movie_get_person_info(person_name: str) -> dict: Get information about a person related to movies. + movie_get_movie_info(movie_name: str) -> dict: Get information about a movie. + movie_get_year_info(year: str) -> dict: Get information about movies released in a specific year. + movie_get_movie_info_by_id(movie_id: int) -> dict: Get movie information by its unique ID. + movie_get_person_info_by_id(person_id: int) -> dict: Get person information by their unique ID. + finance_get_company_name(query: str) -> dict: Search for company names in the finance domain. + finance_get_ticker_by_name(query: str) -> dict: Retrieve the ticker symbol for a given company name. + finance_get_price_history(ticker_name: str) -> dict: Get the price history for a given ticker symbol. + finance_get_detailed_price_history(ticker_name: str) -> dict: Get detailed price history for a ticker symbol. + finance_get_dividends_history(ticker_name: str) -> dict: Get dividend history for a ticker symbol. + finance_get_market_capitalization(ticker_name: str) -> dict: Retrieve market capitalization for a ticker symbol. + finance_get_eps(ticker_name: str) -> dict: Get earnings per share (EPS) for a ticker symbol. + finance_get_pe_ratio(ticker_name: str) -> dict: Get the price-to-earnings (PE) ratio for a ticker symbol. + finance_get_info(ticker_name: str) -> dict: Get financial information for a ticker symbol. + music_search_artist_entity_by_name(artist_name: str) -> dict: Search for music artists by name. + music_search_song_entity_by_name(song_name: str) -> dict: Search for songs by name. + music_get_billboard_rank_date(rank: int, date: str = None) -> dict: Get Billboard ranking for a specific rank and date. + music_get_billboard_attributes(date: str, attribute: str, song_name: str) -> dict: Get attributes of a song from Billboard rankings. + music_grammy_get_best_artist_by_year(year: int) -> dict: Get the Grammy Best New Artist for a specific year. + music_grammy_get_award_count_by_artist(artist_name: str) -> dict: Get the total Grammy awards won by an artist. + music_grammy_get_award_count_by_song(song_name: str) -> dict: Get the total Grammy awards won by a song. + music_grammy_get_best_song_by_year(year: int) -> dict: Get the Grammy Song of the Year for a specific year. + music_grammy_get_award_date_by_artist(artist_name: str) -> dict: Get the years an artist won a Grammy award. + music_grammy_get_best_album_by_year(year: int) -> dict: Get the Grammy Album of the Year for a specific year. + music_grammy_get_all_awarded_artists() -> dict: Get all artists awarded the Grammy Best New Artist. + music_get_artist_birth_place(artist_name: str) -> dict: Get the birthplace of an artist. + music_get_artist_birth_date(artist_name: str) -> dict: Get the birth date of an artist. + music_get_members(band_name: str) -> dict: Get the member list of a band. + music_get_lifespan(artist_name: str) -> dict: Get the lifespan of an artist. + music_get_song_author(song_name: str) -> dict: Get the author of a song. + music_get_song_release_country(song_name: str) -> dict: Get the release country of a song. + music_get_song_release_date(song_name: str) -> dict: Get the release date of a song. + music_get_artist_all_works(artist_name: str) -> dict: Get all works by an artist. + sports_soccer_get_games_on_date(team_name: str, date: str) -> dict: Get soccer games on a specific date. + sports_nba_get_games_on_date(team_name: str, date: str) -> dict: Get NBA games on a specific date. + sports_nba_get_play_by_play_data_by_game_ids(game_ids: List[str]) -> dict: Get NBA play by play data for a set of game ids. + + Note: + Each method performs a POST request to the corresponding API endpoint and returns the response as a JSON dictionary. + """ + + def __init__(self): + self.server = os.environ.get("CRAG_SERVER", "http://127.0.0.1:8080") + + def open_search_entity_by_name(self, query: str): + url = self.server + "/open/search_entity_by_name" + headers = {"accept": "application/json"} + data = {"query": query} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def open_get_entity(self, entity: str): + url = self.server + "/open/get_entity" + headers = {"accept": "application/json"} + data = {"query": entity} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def movie_get_person_info(self, person_name: str): + url = self.server + "/movie/get_person_info" + headers = {"accept": "application/json"} + data = {"query": person_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def movie_get_movie_info(self, movie_name: str): + url = self.server + "/movie/get_movie_info" + headers = {"accept": "application/json"} + data = {"query": movie_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def movie_get_year_info(self, year: str): + url = self.server + "/movie/get_year_info" + headers = {"accept": "application/json"} + data = {"query": year} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def movie_get_movie_info_by_id(self, movid_id: int): + url = self.server + "/movie/get_movie_info_by_id" + headers = {"accept": "application/json"} + data = {"query": movid_id} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def movie_get_person_info_by_id(self, person_id: int): + url = self.server + "/movie/get_person_info_by_id" + headers = {"accept": "application/json"} + data = {"query": person_id} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def finance_get_company_name(self, query: str): + url = self.server + "/finance/get_company_name" + headers = {"accept": "application/json"} + data = {"query": query} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def finance_get_ticker_by_name(self, query: str): + url = self.server + "/finance/get_ticker_by_name" + headers = {"accept": "application/json"} + data = {"query": query} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def finance_get_price_history(self, ticker_name: str): + url = self.server + "/finance/get_price_history" + headers = {"accept": "application/json"} + data = {"query": ticker_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def finance_get_detailed_price_history(self, ticker_name: str): + url = self.server + "/finance/get_detailed_price_history" + headers = {"accept": "application/json"} + data = {"query": ticker_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def finance_get_dividends_history(self, ticker_name: str): + url = self.server + "/finance/get_dividends_history" + headers = {"accept": "application/json"} + data = {"query": ticker_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def finance_get_market_capitalization(self, ticker_name: str): + url = self.server + "/finance/get_market_capitalization" + headers = {"accept": "application/json"} + data = {"query": ticker_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def finance_get_eps(self, ticker_name: str): + url = self.server + "/finance/get_eps" + headers = {"accept": "application/json"} + data = {"query": ticker_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def finance_get_pe_ratio(self, ticker_name: str): + url = self.server + "/finance/get_pe_ratio" + headers = {"accept": "application/json"} + data = {"query": ticker_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def finance_get_info(self, ticker_name: str): + url = self.server + "/finance/get_info" + headers = {"accept": "application/json"} + data = {"query": ticker_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_search_artist_entity_by_name(self, artist_name: str): + url = self.server + "/music/search_artist_entity_by_name" + headers = {"accept": "application/json"} + data = {"query": artist_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_search_song_entity_by_name(self, song_name: str): + url = self.server + "/music/search_song_entity_by_name" + headers = {"accept": "application/json"} + data = {"query": song_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_get_billboard_rank_date(self, rank: int, date: str = None): + url = self.server + "/music/get_billboard_rank_date" + headers = {"accept": "application/json"} + data = {"rank": rank, "date": date} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_get_billboard_attributes(self, date: str, attribute: str, song_name: str): + url = self.server + "/music/get_billboard_attributes" + headers = {"accept": "application/json"} + data = {"date": date, "attribute": attribute, "song_name": song_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_grammy_get_best_artist_by_year(self, year: int): + url = self.server + "/music/grammy_get_best_artist_by_year" + headers = {"accept": "application/json"} + data = {"query": year} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_grammy_get_award_count_by_artist(self, artist_name: str): + url = self.server + "/music/grammy_get_award_count_by_artist" + headers = {"accept": "application/json"} + data = {"query": artist_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_grammy_get_award_count_by_song(self, song_name: str): + url = self.server + "/music/grammy_get_award_count_by_song" + headers = {"accept": "application/json"} + data = {"query": song_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_grammy_get_best_song_by_year(self, year: int): + url = self.server + "/music/grammy_get_best_song_by_year" + headers = {"accept": "application/json"} + data = {"query": year} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_grammy_get_award_date_by_artist(self, artist_name: str): + url = self.server + "/music/grammy_get_award_date_by_artist" + headers = {"accept": "application/json"} + data = {"query": artist_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_grammy_get_best_album_by_year(self, year: int): + url = self.server + "/music/grammy_get_best_album_by_year" + headers = {"accept": "application/json"} + data = {"query": year} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_grammy_get_all_awarded_artists(self): + url = self.server + "/music/grammy_get_all_awarded_artists" + headers = {"accept": "application/json"} + result = requests.post(url, headers=headers) + return json.loads(result.text) + + def music_get_artist_birth_place(self, artist_name: str): + url = self.server + "/music/get_artist_birth_place" + headers = {"accept": "application/json"} + data = {"query": artist_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_get_artist_birth_date(self, artist_name: str): + url = self.server + "/music/get_artist_birth_date" + headers = {"accept": "application/json"} + data = {"query": artist_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_get_members(self, band_name: str): + url = self.server + "/music/get_members" + headers = {"accept": "application/json"} + data = {"query": band_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_get_lifespan(self, artist_name: str): + url = self.server + "/music/get_lifespan" + headers = {"accept": "application/json"} + data = {"query": artist_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_get_song_author(self, song_name: str): + url = self.server + "/music/get_song_author" + headers = {"accept": "application/json"} + data = {"query": song_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_get_song_release_country(self, song_name: str): + url = self.server + "/music/get_song_release_country" + headers = {"accept": "application/json"} + data = {"query": song_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_get_song_release_date(self, song_name: str): + url = self.server + "/music/get_song_release_date" + headers = {"accept": "application/json"} + data = {"query": song_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def music_get_artist_all_works(self, song_name: str): + url = self.server + "/music/get_artist_all_works" + headers = {"accept": "application/json"} + data = {"query": song_name} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def sports_soccer_get_games_on_date(self, date: str, team_name: str = None): + url = self.server + "/sports/soccer/get_games_on_date" + headers = {"accept": "application/json"} + data = {"team_name": team_name, "date": date} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def sports_nba_get_games_on_date(self, date: str, team_name: str = None): + url = self.server + "/sports/nba/get_games_on_date" + headers = {"accept": "application/json"} + data = {"team_name": team_name, "date": date} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) + + def sports_nba_get_play_by_play_data_by_game_ids(self, game_ids: List[str]): + url = self.server + "/sports/nba/get_play_by_play_data_by_game_ids" + headers = {"accept": "application/json"} + data = {"game_ids": game_ids} + result = requests.post(url, json=data, headers=headers) + return json.loads(result.text) diff --git a/AgentQnA/tools/supervisor_agent_tools.yaml b/AgentQnA/tools/supervisor_agent_tools.yaml new file mode 100644 index 000000000..58110e529 --- /dev/null +++ b/AgentQnA/tools/supervisor_agent_tools.yaml @@ -0,0 +1,59 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +search_knowledge_base: + description: Search knowledge base for a given query. Returns text related to the query. + callable_api: tools.py:search_knowledge_base + args_schema: + query: + type: str + description: query + return_output: retrieved_data + +get_artist_birth_place: + description: Get the birth place of an artist. + callable_api: tools.py:get_artist_birth_place + args_schema: + artist_name: + type: str + description: artist name + return_output: birth_place + +get_billboard_rank_date: + description: Get Billboard ranking for a specific rank and date. + callable_api: tools.py:get_billboard_rank_date + args_schema: + rank: + type: int + description: song name + date: + type: str + description: date + return_output: billboard_info + +get_song_release_date: + description: Get the release date of a song. + callable_api: tools.py:get_song_release_date + args_schema: + song_name: + type: str + description: song name + return_output: release_date + +get_members: + description: Get the member list of a band. + callable_api: tools.py:get_members + args_schema: + band_name: + type: str + description: band name + return_output: members + +get_grammy_best_artist_by_year: + description: Get the Grammy Best New Artist for a specific year. + callable_api: tools.py:get_grammy_best_artist_by_year + args_schema: + year: + type: int + description: year + return_output: grammy_best_new_artist diff --git a/AgentQnA/tools/tools.py b/AgentQnA/tools/tools.py new file mode 100644 index 000000000..94a44f678 --- /dev/null +++ b/AgentQnA/tools/tools.py @@ -0,0 +1,52 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import os + +import requests +from tools.pycragapi import CRAG + + +def search_knowledge_base(query: str) -> str: + """Search the knowledge base for a specific query.""" + # use worker agent (DocGrader) to search the knowledge base + url = os.environ.get("WORKER_AGENT_URL") + print(url) + proxies = {"http": ""} + payload = { + "query": query, + } + response = requests.post(url, json=payload, proxies=proxies) + return response.json()["text"] + + +def get_grammy_best_artist_by_year(year: int) -> dict: + """Get the Grammy Best New Artist for a specific year.""" + api = CRAG() + year = int(year) + return api.music_grammy_get_best_artist_by_year(year) + + +def get_members(band_name: str) -> dict: + """Get the member list of a band.""" + api = CRAG() + return api.music_get_members(band_name) + + +def get_artist_birth_place(artist_name: str) -> dict: + """Get the birthplace of an artist.""" + api = CRAG() + return api.music_get_artist_birth_place(artist_name) + + +def get_billboard_rank_date(rank: int, date: str = None) -> dict: + """Get Billboard ranking for a specific rank and date.""" + api = CRAG() + rank = int(rank) + return api.music_get_billboard_rank_date(rank, date) + + +def get_song_release_date(song_name: str) -> dict: + """Get the release date of a song.""" + api = CRAG() + return api.music_get_song_release_date(song_name) diff --git a/AgentQnA/tools/worker_agent_tools.yaml b/AgentQnA/tools/worker_agent_tools.yaml new file mode 100644 index 000000000..905106ee3 --- /dev/null +++ b/AgentQnA/tools/worker_agent_tools.yaml @@ -0,0 +1,5 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +duckduckgo_search: + callable_api: ddg-search