diff --git a/docs/source/tutorials/logging.rst b/docs/source/tutorials/logging.rst index 04d31de9..1f566758 100644 --- a/docs/source/tutorials/logging.rst +++ b/docs/source/tutorials/logging.rst @@ -1,3 +1,19 @@ +Logger Example +------------- + +.. raw:: html + +
+ + Open In Colab + + + GitHub + Open Source Code + +
+ + Logging ==================== diff --git a/docs/source/tutorials/logging_tracing.rst b/docs/source/tutorials/logging_tracing.rst index 26d8f605..7cf5ba5f 100644 --- a/docs/source/tutorials/logging_tracing.rst +++ b/docs/source/tutorials/logging_tracing.rst @@ -1,3 +1,17 @@ +```rst +.. raw:: html + +
+ + Open In Colab + + + GitHub + Open Source Code + +
+``` + .. _logging_tracing: Tracing diff --git a/docs/source/tutorials/rag_playbook.rst b/docs/source/tutorials/rag_playbook.rst index 685bb4ea..9175a09f 100644 --- a/docs/source/tutorials/rag_playbook.rst +++ b/docs/source/tutorials/rag_playbook.rst @@ -1,11 +1,9 @@ -.. -.. Try Quickstart in Colab -.. - .. raw:: html
- + + Try RAG playbook in Colab + GitHub Open Source Code diff --git a/docs/source/tutorials/tool_helper.rst b/docs/source/tutorials/tool_helper.rst index 6b3736fd..4b607a26 100644 --- a/docs/source/tutorials/tool_helper.rst +++ b/docs/source/tutorials/tool_helper.rst @@ -1,3 +1,15 @@ +.. raw:: html + +
+ + Open In Colab + + + GitHub + Open Source Code + +
+ .. _tool_helper: Function calls diff --git a/docs/source/use_cases/classification.rst b/docs/source/use_cases/classification.rst index d0aaa489..0ba09159 100644 --- a/docs/source/use_cases/classification.rst +++ b/docs/source/use_cases/classification.rst @@ -1,10 +1,9 @@ -.. -.. Try Quickstart in Colab -.. - .. raw:: html
+ + Open In Colab + GitHub diff --git a/docs/source/use_cases/rag_opt.rst b/docs/source/use_cases/rag_opt.rst index 73b824bf..072fe3f5 100644 --- a/docs/source/use_cases/rag_opt.rst +++ b/docs/source/use_cases/rag_opt.rst @@ -1,10 +1,9 @@ -.. -.. Try Quickstart in Colab -.. - .. raw:: html
+ + Open In Colab + GitHub diff --git a/notebooks/tutorials/adalflow_classification_optimization.ipynb b/notebooks/tutorials/adalflow_classification_optimization.ipynb new file mode 100644 index 00000000..0afb97df --- /dev/null +++ b/notebooks/tutorials/adalflow_classification_optimization.ipynb @@ -0,0 +1,463 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# 🤗 Welcome to AdalFlow!\n", + "## The PyTorch library to auto-optimize any LLM task pipelines\n", + "\n", + "Thanks for trying us out, we're here to provide you with the best LLM application development experience you can dream of 😊 any questions or concerns you may have, [come talk to us on discord,](https://discord.gg/ezzszrRZvT) we're always here to help! ⭐ Star us on Github ⭐\n", + "\n", + "\n", + "# Quick Links\n", + "\n", + "Github repo: https://github.com/SylphAI-Inc/AdalFlow\n", + "\n", + "Full Tutorials: https://adalflow.sylph.ai/index.html#.\n", + "\n", + "Deep dive on each API: check out the [developer notes](https://adalflow.sylph.ai/tutorials/index.html).\n", + "\n", + "Common use cases along with the auto-optimization: check out [Use cases](https://adalflow.sylph.ai/use_cases/index.html).\n", + "\n", + "## 📖 Outline\n", + "\n", + "This is the code for a classification optimization tutorial ![image.png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAJYCAIAAAB+fFtyAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAD6KADAAQAAAABAAACWAAAAADDsFQWAABAAElEQVR4AeydB5gURRqGe5clLJJUMnqIiCiKYBbBnOMZThEDYMCcc1bMeurpKYZTDHcqYsSM2RMFwawcZhEUliRZ4rJ772xJ0fSEnZ2Znunu+ebhWaqrq6v+eqt75qu//64uqa6udvQRAREQAREQAREQAREQAREINoHSYJsn60RABERABERABERABERABGIEJNx1HoiACIiACIiACIiACIhACAhIuIdgkGSiCIiACIiACIiACIiACEi46xwQAREQAREQAREQAREQgRAQkHAPwSDJRBEQAREQAREQAREQARGQcNc5IAIiIAIiIAIiIAIiIAIhICDhHoJBkokiIAIiIAIiIAIiIAIiIOGuc0AEREAEREAEREAEREAEQkBAwj0EgyQTRUAEREAEREAEREAEREDCXeeACIiACIiACIiACIiACISAgIR7CAZJJoqACIiACIiACIiACIiAhLvOAREQAREQAREQAREQAREIAQEJ9xAMkkwUAREQAREQAREQAREQAQl3nQMiIAIiIAIiIAIiIAIiEAICEu4hGCSZKAIiIAIiIAIiIAIiIAIS7joHREAEREAEREAEREAERCAEBCTcQzBIMlEEREAEREAEREAEREAEJNx1DoiACIiACIiACIiACIhACAhIuIdgkGSiCIiACIiACIiACIiACEi46xwQAREQAREQAREQAREQgRAQkHAPwSDJRBEQAREQAREQAREQARGQcNc5IAIiIAIiIAIiIAIiIAIhICDhHoJBkokiIAIiIAIiIAIiIAIiIOGuc0AEREAEREAEREAEREAEQkBAwj0EgyQTRUAEREAEREAEREAEREDCXeeACIiACIiACIiACIiACISAgIR7CAZJJoqACIiACIiACIiACIiAhLvOAREQAREQAREQAREQAREIAQEJ9xAMkkwUAREQAREQAREQAREQAQl3nQMiIAIiIAIiIAIiIAIiEAICEu4hGCSZKAIiIAIiIAIiIAIiIAIS7joHREAEREAEREAEREAERCAEBCTcQzBIMlEEREAEREAEREAEREAEJNx1DoiACIiACIiACIiACIhACAhIuIdgkGSiCIiACIiACIiACIiACEi46xwQAREQAREQAREQAREQgRAQkHAPwSDJRBEQAREQAREQAREQARGQcNc5IAIiIAIiIAIiIAIiIAIhICDhHoJBkokiIAIiIAIiIAIiIAIiIOGuc0AEREAEREAEREAEREAEQkBAwj0EgyQTRUAEREAEREAEREAEREDCXeeACIiACIiACIiACIiACISAgIR7CAZJJoqACIiACIiACIiACIiAhLvOAREQAREQAREQAREQAREIAQEJ9xAMkkwUAREQAREQAREQAREQAQl3nQMiIAIiIAIiIAIiIAIiEAICEu4hGCSZKAIiIAIiIAIiIAIiIAIS7joHREAEREAEREAEREAERCAEBCTcQzBIMlEEREAEREAEREAEREAEJNx1DoiACOSYwC+//FJSUvLII4/kuN5wVrdzzSdN2+F29dVXp1k4V8WyHK/33nsPs/mbK3tsPf7VbJtQok4E/D4/PRfL9OnT//a3v6299tq0e8cdd/h6Pqy33noDBw6sEw0VFoGCEJBwLwh2NZohAb6+U3wykA6LFi1CJ6V54Kuvvkrr7du3r6qqyrADITnM/EA+88wzHntPP/10CHgy87+J0Dz22GM7d+7cqFGjtm3b7rjjjldddVX+zYhvccKECZxOmBe/K1c5v//++wUXXNC1a1f6vtZaa+21114vv/xynSp/4okn0EB1OiQ/he+5554wTvYY8RRfSihR6KEIbZmGDRtuuOGGV1555ZIlS9xgbQF34uSTT3aX4cI85JBDOOcbNGjQunXrAw444LnnnnMXiE//9NNPRx55JIXLy8u7dOly2WWXxZdx53zxxRdHH330uuuui52cYLvvvvvDDz+8YsUKd5m8pc8555zXX3/9kksu+c9//rP33nvnqt3Ro0czanPnzs1VhapHBPJJoCyfjaktEciSAF/ftoZ///vfb775pjtn4403tnvTTCDcBw8eTGHz+5r6qMcffxyvDLLsnXfe4fcsdeFi3tuxY8fFixfXr1/fDwg//vjj1ltvjQo57rjjGI6KiorPPvvs5ptvNuPoR4vp14lwxwzOJQyzR73xxhs2nWXiu+++22233WbOnMm8ZauttkJ5cE6i3s4///y///3vaVaOcB8/fvzZZ59t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aOHKSCuJC3bEiBFpFU2v0I477vjEE0+kVzbQpTLjmU6XJkyYsM466/zxxx/pFFaZ/BBAuL9Q8yGRnxbVigiIgAiIQJYEJNyzBFjgw1dUVY/56fcXvpjCX9I5sWbnnXc+++yzs6kq+xqStb7eeuvdcccddm/fvn2///57u5lmgjVG9tlnnzQL11rsxRdfnD59+hFHHPHee+/htlxzzTX5y9zA/WFXrfXYAumoZx6B/fvf/77FFlusscYazZs379Gjx+WXX84jmLaS3CZ++eWX448/nkUVy8vLO3fufNVVV2FAwiZYw/60007jAWuW1jn00EMhY4p169Ztu+22u/322xMepUwREAEREAEREIF0CChUJh1KAS0zcnzF4JcmVMxbYuxr17zRVQd023vTdgE11wez0JF86loxS63X9ZD48jyTgJ+S5Tj++c9/HnvssTznt/3220+ZMmXBggVNmzY955xz5s+f//DDD5sD11prrfgaMs5ZunTpnnvu+dVXXw0ePLh3796smjJx4sRhw4bdddddN954o6daFHb2S5h/++23LJhz//33b7DBBuPHjx80aBC+81tvvdXTFpt0/JVXXnn66aeZTpx++umHHHLIhx9+aIpBiQMvueQSrWESz005IiACIiACIpAOAXnc06EUxDKo9lMe+8yqdkycNm8JOeRnY+7AgQP/+9//3nnnncZhjKuV2tBquKjxobZp0+aYY46ZNWsWmXiRUYSjRo0yzd1yyy2sZYmHNWENpoznLw1ts802DRs2bNeu3cUXX2zW1aEMDns0Hx/EX8uWLa+44grz5C75kyZNQhoa2yjpdk5fffXVPXv2fOihh1ixG1NPPfVUhDVWIdMx7Prrr7etc7gJleEQt1+cNBVSDJGKAjYOZpzZzzzzjDmWLlPmtdde23LLLTH7gw8+mDlz5jvvvHPAAQdQABq0BSL+Mp2gAAk++OAvvfTSDh064B3fdtttjfcdz/Qmm2xy4oknmpp/+ukn5D7GsxeBO2/ePGMYFpoC7r//+Mc/aJp2zzzzTCyhvzvttNN99913ww03mGIGILdNoLfXXnuRiaubOGYMWHfddSHDYvO2QrpMDSxndPDBB//+++82353Ye++9mYQwW1h//fUPPPDA888//7nnnnMXMGnMHjp0KG3tuuuuGMYho0eP/uijj8zePfbYY/bs2Qx6/IHK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IAJFRUDCvaiGW50VAREQARFIg4BevZQGJBURARHIPwEJ9/wzV4siIAIiIALBJqAA92CPj6wTgaIlIOFetEOvjouACIiACCQhIOGeBIyyRUAECktA67gXlr9aFwEREIHCECgtLd1qq61om0RhLAhsqwumOwsqHKck9vYlfURABEQgSAQk3IM0GrJFBERABPJFoKysbL/99stXa6FqxwS4t+ziNGwSKrtlrAiIQPQJyNES/TFWD0VABERABOpAQHEydYCloiIgAnklII97XnGrMREQAREICIHq6upFixZhTOPGjUtKSgJiVSDMkHAPxDDICBEQgQQE5HFPAEVZIiACIhB5AsuXL7+15kMi8p2tWweNcG+7Wd2OUmkREAER8J+AhLv/jNWCCIiACIhAWAgsnuPMnRQztp2Ee1jGTHaKQBERkHAvosFWV0VABERABGohUPFVrECLjk75mrWU1G4REAERyDsBCfe8I1eDIiACIiACgSWgd6YGdmhkmAiIAAv4CoIIiIAIiIAIiMCfBPRkqk4FERCBABOQcA/w4Mg0ERABERCBPBOQcM8zcDUnAiJQFwIS7nWhpbIiIAIiIAIRJrB0oTPrh1j/2vWIcC/VNREQgfAS0Dru4R07WS4CIiACmRMoLS3t0SMmT0lkXkvEjpw+3nGqnabtnCatI9YzdUcERCAaBCTcozGO6oUIiIAI1I1AWVnZQQcdVLdjIl/aLCkjd3vkB1odFIHQEpCjJbRDJ8NFQAREQARyS0AB7rnlqdpEQARyTUAe91wTVX0iIAIiEAYC1dXV5p2p9evXLykpCYPJ/tuod6b6z1gtiIAIZENAHvds6OlYERABEQgrAVT7jTUfI9/D2o0c2l251Jn5Taw+hcrkkKqqEgERyCkBCfec4lRlIiACIiACISUwY4JTVemUr+U0XyekPZDZIiACkScg4R75IVYHRUAEREAE0iBgA9wVOJQGLRURAREoCAEJ94JgV6MiIAIiIAIBI/CncN8sYGbJHBEQARFYRUDCfRULpURABERABIqXgPW4Fy8C9VwERCDoBCTcgz5Csk8EREAERMB3Aisqnen/i7XSrqfvbakBERABEciUgIR7jNyQIUPWW2+9Ro0abbvttuPGjUsI84477ujatWt5efm66657zjnnLFmyxBTjQFZSc39OO+20hDUoUwREQAREIKAEZn3vVC5xGjR11uwUUAtllgiIgAg4jtZxd4YPH37uuefed999qHbU+V577fXdd9+1br3a+66feOKJiy+++KGHHtp+++2///77gQMHotRvv/12TqGPP/54xYoV5lwaP378Hnvscdhhh+nUEgEREIGAEygtLe3WrRtGkgi4qfkwzwa4i0Y+cKsNERCBDAlIuDvo70GDBh177LEgRL6/8sorCHRkupvo6NGje/fufeSRR5KJi71fv35jx441BVq1amVL3nTTTZ07d95pp51sjhIiIAIiEEwCZWVl8jKsGhoj3NvqydRVSJQSAREIIIFiF+7Lli379NNPL7nkEjM2eJ523333MWPGeIYKR/tjjz1GFM0222zz888/v/rqq8ccc4ynDFVRBuc9znjPLjaX1nxM/vz580nw0hPz3hP33/gDlRMiAhrKEA1WalM1lKn5hGhvmkNZb+rn3HeobL0pr5MNUe+KytQ0h7KomIS0s56hNJsh7Uv+zS524T5r1iwCXdq0aWPRk/7222/tpknga6dknz59eEl4ZWXlySeffOmll3rKjBgxYu7cuUTRePLNJi8oHDx4sHvXG2+80bhxY5vz5ptv2rQSoSagoQz18LmN11C6aYQ6XctQVlftN+ULhPv7P8xb8Nuroe5p5I2vZSgj3/8IddAO5aJFiyLULd+7UuzCPU3A77333g033HDPPfcQB//jjz+eddZZ11577RVXXOE+fOjQofvss0/79u3dmTaNUx9nvNnE484TrnvuuWezZs3IYa7J6UtwfP369W15JcJIQEMZxlFLaHMxDCU3CW+99Va6f/755zdo0CAhhwhkpjWUs38q+2JJdVmjHQ4+zinVz2JAhz2toQyo7TJrNQKeoTRhCKuV0EZyAsX+DdWyZct69epNnz7dIiLdtm1bu2kSaHRiY0444QQ2u3fv/scff5x44omXXXaZfahr0qRJb7311nPPPec50G42rPnYTRLIdLdS92y6SyodLgIaynCNVwproz2U3D80fY92N9Pq48zYQpAlbTat37A8xfmgXUEgUAynaxA458EGO5Qk8tBcZJoo9sUE8DNtueWWb7/9thnRqqoq0r169fIMMPdxrEZnF1qfv/Znj/TDDz/MQjT77bef50BtioAIiIAIBJ2AXVIm6IbKPhEQgWInUOwed8afCJYBAwZstdVWPHjKcpB4080KM/37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BxfHOIa1l1rEPawjJ7tFQAREQAREQATyTiA6wn3KlCmPPPIIr2GaO3funDlznnjiicMPP5xXA+YdqRqsA4GVMe4KlakDNBUVgZwQqF+//imnnEJVJHJSoSoRAREQARHwm0AUVpV59tln9913365duxLdftttt02dOrW0tJQYd6l2v8+eLOtfsnzFrIXLqKRDC60qkyVLHS4CdSbANySr6PLRV2Wd2ekAERABESgQgSh43Pv27XvRRRfxZGrTpk0LhFHNZkKgYt4SDmvcoF7zcjn8MgGoY0RABERABERABIqKQBQ87scff/yQIUP23nvv++67jyCZohq/UHd2ZZxMuRx+oR5HGR9SArzv4r2aD4mQdkFmi4AIiECxEYiCcL///vsrKipOPPHEYcOGtWvX7q9//Wt1dTXruBfbWIauvyvXglScTOiGTgZHgQB6/b81Hwn3KAyn+iACIlAcBKIg3BkpFm4fMGAAv0Fff/31Jpts0qZNm969ex955JGsLVMc4xjKXhqPe4cWejI1lMMno0VABERABERABPJMICLC3VJjGfcbbrjh119/feyxxxYtWtSvXz+7S4mgETCLuLdrLo970EZG9oiACIiACIiACASRQBQeTo3nyqoyB9R8ZsyYEb9XOQEhYBZxb68lZQIyHjJDBERABERABEQg2ASi5nH30GalM0+ONoNDYGWMu0JlgjMmskQEREAEREAERCC4BCIu3IMLvugt4wHilTHuCpUp+rNBAERABERABERABNIgIOGeBiQV8YHAnEXLlyyPrfzTtrk87j7wVZUiIAIiIAIiIAKRIxDNGPfIDVMEO2Tc7a2aNmxYVi+C3VOXRCDwBMrKyk444QTMJBF4Y2WgCIiACIhAjEB0PO7rr7/+77//7h7VuXPnkunOUTo4BFYGuCtOJjhjIkuKiwAP8Xeo+ZAorp6rtyIgAiIQWgLR+b7+5ZdfPK8RWbp06ZQpU0I7NBE3vGLuYnqoRdwjPszqngiIgAiIgAiIQO4IROEO6YsvvmiAvP76682bNzdpRPzbb7+93nrr5Y6VasolganzllCdFnHPJVPVJQJ1IcCX5EcffcQR2223Xb16ilirCzuVFQEREIECEYiCcD/ooIOgV1JSwstTLcb69euj2m+77Tabo0SgCChUJlDDIWOKkADC/a233qLjW2+9tYR7EZ4A6rIIiEAYCURBuFdVxRYn6dSp08cff9yyZcswDkMR2rxyLUgtKVOEg68ui4AIiIAIiIAIZEIgOjHuEydOdKt2nkxNn8eQIUNwzzdq1GjbbbcdN25cwgPvuOOOrl27lpeXr7vuuuecc86SJbFIDz5XX301zn772WijjUy+/qYmYIS7XpuampL2ioAIiIAIiIAIiIAlEB3hfvPNNw8fPtx07LDDDltrrf+3d+dhUlRnvMeZlRm2GVRkF9wVZRMEEYwmsoiGgCsKV8QoBJFHkAcTQIEgAsYFd8RrULxBBTRREx8XiAkaAwSFYFDBB9xQGDYJ2wzDrPfHFFNpexZm6eWcU9/+Q6urq0695/N2D++cOXX6OK2X8Mknn/hdrWhDZ40fP37atGlr167t2LFjv379du7cGXbwSy+9NHHiRB2zYcOG+fPn65TJkyf7x5xzzjlZpY8PP/zQ389GRQJ5BUU7DxzWqxTuFRGxHwEEEEAAAQQQCBNwp3CfN2+exsLVvWXLlmni5jvvvNO/f/+77rorrMNln86ZM2fEiBE333xzu3bt1Ei9evWee+65sMNWrFjRs2fPIUOGaGC+b9++N9xwQ+jAvFZBblb6CB31D2uEp77Ajv25xcV1UpMTj6+f6u9kAwEEEEAAAQQQQKASARfmuHvd2759u1e4v/nmm9ddd53KaxXZmvpSSef1Ul5e3po1ayZNmuQdpvWMe/fuvXLlyrCzLrzwwoULF6pY79at21dfffXWW2/deOON/jGbNm1q0aKFZtr06NFj9uzZJ510kv+Sv6G1KfXwnu7fv18b+SUPb8P/r3eA8//9dvcB9bFFRlpBQYFLnVVK1R3vvy71K4B9CUIq/TeqNjTZz9UsByGVruYurF+kMgzE3qdhqfSe2tudGEfuTuHeuHHj7777TrW7xtrvu+8+ORYXF4et7F4Wd/fu3TqmadOm/kva3rhxo//U29BYu47s1auX2lStOWrUKH+qjH43WLBggaa/a7LM9OnTL7rook8//bRhw4ZhLaig16uhO5cuXarRfX+P/lDgbzu/8dEuVQlJqQUH9SuQe50NVCrdS19oj9xOpf/jUQvpOr+qjNupDH3TOr9NKp1JsZ/KnJwcZzoVg464U7hfddVVKq9PP/10fX+qJsnI7t///vdpp50WEcTly5fPmjVr7ty5KtM3b948duzYGTNmTJkyRY1719JGhw4d9GqbNm2WLFlyyy23hF1Xg/qaSe/t1Ii7fsHQ3wQaNWqkPfpdU2/fPn36aAnLsLNcffrt+1/V2bz53FNaXX75uS71MYCpdCl9oX0JQiq1Hpd+aqnX+nHk8JenBiGVoW9dh7dJpTPJDUulNw3Bmd5FuyPuFO6PPPKI5sZo0P2BBx5o0KCB4DQEPnr06MoFNSVdQ007duzwD9O25qv7T70N1eiaG3Prrbfqafv27bOzs0eOHHn33XeH/WuXmZl5xhlnqLIPO11P65Y8QverTA+t1MOehh7p3vb2A3nqVKvj6ocKONPNQKXSmayV2xHnUxmpoY1y9Yza6XwqjdKOajCkMqq8sWzcT6U2Ynld26/lTuGuxE+YMCE0H1q0MfRpudupqaldunTRd6x63+KkIShtjxkzJuxg/R0ntEb3/qysaTNhhx08ePDLL78Mnf4edgBPPQEWceedgAACCCCAAAIIVFfAnVVl1PM//OEPmoau+0S//fZbPdXK62+88cYxRTSD5dlnn33hhRe01ONtt92m0XStMKOzhg0b5t+0OmDAgKeffnrRokVaLV7TWjQArz1e+a7fFt5///1vvvlGK89ceeWV2qk1Z4550YAfwCLuAX8D0H0TBDTHXTfc6+FPdjchKmJAAAEEEKhEwJ0RdxXWU6dOHTdu3MyZM71/hzRxRbX7wIEDK+m/Xho8ePCuXbt0rtal6dSpk+5t9e5V3bJliz/Kfs8992jVBf1369atTZo0UdWuq3jNfv/996rUNbFe+/Vrw6pVq7RR+RUD/qr+UrH1v4eEwCLuAX8n0P34Cujn5Ntvv60Y9HPPG4aIbzxcHQEEEEDgmALuFO5PPPGEBs414+X+++/3ut21a9ewyTMVcWhuTNnpMboh1T9eK7Xr25f08Pf4GxqG97fZqIrA/tyC7LxCHdkiI70qx3MMAggggAACCCCAgATcmSqjSSydO3cOTapuB9W8l9A9bJsg4M2TOa5+anpqkgnxEAMCCCCAAAIIIGCFgDuF+8knn7xu3bpQdE16Ofvss0P3sG2CQNY+b55MmgnBEAMCCCCAAAIIIGCLgAtTZe69915NidE9prfffntubq6mUOt2q5dfflnfefT73//elkwEJ86te3PV2ebMkwlOyukpAggggAACCERCwIXCXd9Iqq8y1SLr6enpun9USzfqm5i0tsxjjz12/fXXR0KJNiIpULoWJBPcI6lKWwgggAACCCDgvIALhbu/nvrQkocKd62nfuKJJzqfPEs7WLoWJFNlLE0gYSOAAAIIIIBAfARcKNwlp7Uafb96JQ//KRumCZQW7oy4m5YZ4gmWgBbL8r50QhvB6jm9RQABBKwVcOTn9RlnnBFau4emY8+ePaFP2Y67wLaSOe4s4h73RBBAwAX0PRX6yRlwBLqPAAII2CXgSOGuae4ZGRl20Qcz2oLCou37j9yc2jKTEfdgvgXoNQIIIIAAAgjUUMCRwl03oTKpvYZvgdietvPA4cKi4pSkhCYN6sb2ylwNAQR+JKBvTl2/fr12tW/fnm9O/RENTxBAAAFTBVwo3CuaJGOqeaDj8hZxb5aRlpj4v9sSAi1C5xGIk4AK9zfeeEMXb9euHYV7nJLAZRFAAIHqCbjwBUz+qjLV6zpHx0OARdzjoc41EUAAAQQQQMAFARdG3IuKilxIRTD6wCLuwcgzvUQAAQQQQACByAu4MOIeeRVajJpA6VqQLOIeNWIaRgABBBBAAAFHBSjcHU2sqd0qLdxZUsbUDBEXAggggAACCJgqQOFuamYcjcub484i7o6ml24hgAACCCCAQBQFKNyjiEvTZQWY417WhD0IIIAAAggggEBVBFy4ObUq/eQYEwSyDxfsO5SvSJpnMMfdhIQQQ6AFkpOTr7nmGhFoI9AQdB4BBBCwR4Cf1/bkyv5IvUXcG6UlN0xLsb839AABuwUSExPPOeccu/tA9AgggEDABJgqE7CEx7W7THCPKz8XRwABBBBAAAG7BRhxtzt/dkXPkjJ25Yto3RbQN2Bs2LBBfTz77LM1+u52Z+kdAggg4IYAP6zdyKMdvSgt3Jngbke+iNJtgYKCgldLHtpwu6f0DgEEEHBGgMLdmVRa0JGtew8pStaCtCBVhIgAAggggAAC5glQuJuXE3cjYi1Id3NLzxBAAAEEEEAg6gIU7lEn5gK+wLa9udpmxN0HYQMBBBBAAAEEEKi6AIV71a04slYCRUXF3nKQFO61cuRkBBBAAAEEEAiqAIV7UDMf837vzj6cX1icmFCnacO6Mb84F0QAAQQQQAABBKwXoHC3PoW2dMCbJ9NUX7+UxLvOlqQRJwIIIIAAAggYJMA67gYlw+1QSteCTHe7m/QOAVsEkpKSBg4cqGi1YUvMxIkAAggEXIDCPeBvgNh1n8I9dtZcCYEqCKhe79SpUxUO5BAEEEAAAVMEmLRgSiacj6N0EXe+fcn5VNNBBBBAAAEEEIiKACPuUWGl0bICLOJe1oQ9CMRRoKioaPPmzQrgtNNOS0xkECeOqeDSCCCAQFUF+GFdVSmOq6XA0UXcM5jjXktITkcgMgIFBQUvlzy0EZkWaQUBBBBAIMoCFO5RBqb5UgHmuJdK8H8EEEAAAQQQQKAmAhTuNVHjnOoK5OYX/pCdp7NaZDLHvbp4HI8AAggggAACCBwRoHDnfRALgax9ubpMvdSkjPSUWFyPayCAAAIIIIAAAs4JULg7l1IjO+TPk0lISDAyQIJCAAEEEEAAAQRMF6BwNz1DbsRXuhYkd6a6kU96gQACCCCAAAJxEKBwjwN6AC9ZuhYkE9wDmHy6jAACCCCAAAKREWAd98g40krlAkenyrAWZOVMvIpADAX0zan9+/fXBbURw8tyKQQQQACBmgtQuNfcjjOrLnB0EfdMpspU3YwjEYiugOr1bt26RfcatI4AAgggEFEBpspElJPGKhDYtu+QXmlB4V6BD7sRQAABBBBAAIFjCjDifkwiDqitQHFxcemqMsxxry0m5yMQKYGioqItW7aotZNOOikxkUGcSLnSDgIIIBBFAX5YRxGXpj2B/+bk5+YXabtZBoU7bwoETBEoKCh4oeShDVNiIg4EEEAAgUoFKNwr5eHFSAh4w+1NGtatm8w9cJEApQ0EEEAAAQQQCKQAhXsg0x7bTrOIe2y9uRoCCCCAAAIIuClA4e5mXo3qFYu4G5UOgkEAAQQQQAABSwUo3C1NnE1hs4i7TdkiVgQQQAABBBAwVYDC3dTMOBQXi7g7lEy6ggACCCCAAAJxE6Bwjxt9cC7MIu7ByTU9RQABBBBAAIHoCbCOe/RsafmoAIu481ZAwEABfXNq7969FZg2DAyPkBBAAAEEygpQuJc1YU8kBfIKinYeOKwW+drUSLLSFgK1FlC93rNnz1o3QwMIIIAAArETYKpM7KyDeaUd+3OLi+ukJiceXz81mAL0GgEEEEAAAQQQiIgAI+4RYaSRCgW8RdxbZqYnJCRUeBAvIIBAzAWKioqysrJ02ebNmycmMogT8wRwQQQQQKD6Avywrr4ZZ1RHgAnu1dHiWARiJ1BQUPD7koc2YndVroQAAgggUAsBCvda4HFqFQRYxL0KSByCAAIIIIAAAggcW4DC/dhGHFEbga17c3U6d6bWxpBzEUAAAQQQQAABCVC48zaIrkDWvkO6gOa4R/cytI4AAggggAACCLguQOHueobj3T9vqkzzzLR4B8L1EUAAAQQQQAABuwUo3O3On+HRFxcXb/3vkRF3psoYninCQwABBBBAAAHzBSjczc+RxRHuzy3IzitUB1pkMFXG4jwSOgIIIIAAAgiYIMA67iZkwdkYvHkyx9VPTU/lO9WdzTIds1RA35x68cUXK3htWNoFwkYAAQSCJkDhHrSMx7S/LOIeU24uhkB1BFSvX3LJJdU5g2MRQAABBOIswFSZOCfA7cuziLvb+aV3CCCAAAIIIBBLAUbcY6kduGuxiHvgUk6H7RHQveO7du1SvE2aNElISLAncCJFAAEEgivAiHtwcx+DnrOIewyQuQQCNRPIz89/uuShjZq1wFkIIIAAAjEWoHA/Av7UU0+1bds2LS2te/fuq1evLjcHjz766Jlnnpment66des777wzN/fIF4KGPu6//36NWo0bNy50Z8C3WcQ94G8Auo8AAggggAACERSgcK+zePHi8ePHT5s2be3atR07duzXr9/OnTvDiF966aWJEyfqmA0bNsyfP1+nTJ48OfSYjz766JlnnunQoUPoTra37T3y6w2LuPNOQAABBBBAAAEEai9A4eHP9oEAACUdSURBVF5nzpw5I0aMuPnmm9u1azdv3rx69eo999xzYbIrVqzo2bPnkCFDNDDft2/fG264IXRg/uDBg0OHDn322WcbN24cdmKQnxYUFm3ff6Rwb5nJIu5BfiPQdwQQQAABBBCIjEDQb07Ny8tbs2bNpEmTPM7ExMTevXuvXLkyTPfCCy9cuHChivVu3bp99dVXb7311o033ugfc/vtt19xxRU68b777vN3hm0cLnl4O/fv368NzSv1ppaG/jfsLKufZu3LLSwqTklKyKyb6PXR6u5UJXhXU1mVvjt2TBBS6X8qteHwzalBSKVjn76KukMqK5Kxbn9YKr2n1vUiXgEHvXDfvXt3YWFh06ZN/QRoe+PGjf5Tb0Nj7TqyV69eWoehoKBg1KhR/lSZRYsWaY6NpsqEnRL2dPbs2dOnTw/duXTpUo3u+3uWLVvmb7ux8dWRX0+SGyUXvfPO2270qIq9cC+VVey4e4e5nUr96PNS9u677zr/HUxup9K9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RoDC3ZlUxq0jpRPcWVImbingwgjUQCAlJWXEiBE1OJFTEEAAAQTiJcBUmXjJu3NdFnF3J5f0BAEEEEAAAQQMFqBwP5Kcp556qm3btmlpad27d1+9enW5+Xr00UfPPPPM9PT01q1b33nnnbm5R27H1OPpp5/u0KFDo5JHjx493n77bW9/cP7LWpDByTU9RQABBBBAAIE4ClC411m8ePH48eOnTZu2du3ajh079uvXb+fOnWEpeemllyZOnKhjNmzYMH/+fJ0yefJk75hWrVrdf//9a9as+fjjj3/2s58NHDjws88+Czvd7acU7m7nl965KpCfn6/xCD204Wof6RcCCCDgmACFe505c+ZooufNN9/crl27efPm1atX77nnngtL84oVK3r27DlkyBANzPft2/eGG27wB+YHDBhw+eWXn3766WecccbMmTMbNGiwatWqsNPdfrpt3yF1sAWLuLudZnrnnEBxcfG+koc2nOscHUIAAQTcFAh64Z6Xl6fB8t69e3vpTUxM1PbKlSvDsn3hhRfqMK9Y/+qrr9566y0V62HHFBYWLlq0KDs7WxNmwl5y+6k3x51F3N3OMr1DAAEEEEAAgbgLBH1Vmd27d6vgbtq0qZ8JbW/cuNF/6m1orF1H9urVS0NTBQUFo0aN8qfK6ID169erWNesdw23v/baaxq5DztdTw+XPLz9+/fv14b+PO39hTr0v2VPNHxPUVFxVsmI+4n1k72OGB5wVMOzOpVRlbGu8SCk0v/AasPhFSGDkErrPl81C5hU1szNwLPCUuk9NTBOM0MKeuFexawsX7581qxZc+fO1d2rmzdvHjt27IwZM6ZMmeKdrptW161bp785v/rqqzfddNP7779ftnafPXv29OnTQy+3dOlSTcvx9yxbtszftmhjX16d/MLkhDrFa/7593UsBl2SOUtTadG7Lmahup1KjVl4ku+++25SUlLMVONyIbdTGRfSeF2UVMZLPuLX9VOZk5MT8cYdbjAh4LMbNVVG1bMK7kGDBnlpVuW9d+/eN954IzTrF1100QUXXPDggw96OxcuXDhy5MiDBw9qak3oYdrWTJtTTz31mWeeCdsfNuKupWk0hK+laHSYftfU27dPnz5aVjnsLPOfrvtu77X/d7W+M/WDCT8xP9poR2h1KqONY1f7QUilfvo99NBDysuECRNSU1PtSlDVow1C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AAEEEIiLACPucWHnoggggECEBTRSrvpbQ+OXXXbZ8uXLVXNrNL3sNTSafu+993oj9Pqvan0Nxufk5HhH9ujRwz9F296I+/Dhw9etW3fmmWeqgl+6dKl/ABsIIIAAAjEWoHCPMTiXQwABBKIloDnoffr00XSXFStWqNqeNm1a2SsdPHhQ89pViHuP9evXb9q0SSeWPdLfo98Bvv766xkzZmg4/7rrrrvmmmv8l9hAAAEEEIilAIV7LLW5FgIIIBAjgXbt2mVnZ+tiKSkphYWF/lVVhWuq+mk/fiQmHv23YNWqVf6R2j777LO9p40aNRo8ePCzzz67ePFizcnZs2ePfxgbCCCAAAIxE2A5yJhRcyEEEEAgWgJa+fHaa6/95S9/qXntDRs2/Pjjjx944IGBAwfqerqXVEtD9uzZU+u7N27ceOrUqT//+c+13LsGzlWva+bMp59+et9993mRvfLKK127du3Vq5duQl29evX8+fO1f86cOVpSpnPnzjpeB2iR+MzMzGj1hHYRQAABBCoWoHCv2IZXEEAAAUsENFu9e/fujzzyyJdffpmfn9+6dWtNXp88ebLCf/jhh8ePH6/B8pYtW37zzTf6YqY333xT09y1dqQG488666xbb73V76Vm0SxatGj06NGq1F9++WUN2+sl/SagXwM0oyYpKen8889/6623/BF6/0Q2EEAAAQRiIMCqMjFA5hIIIICABQJh689YEDEhIoAAAgETYI57wBJOdxFAAAEEEEAAAQTsFKBwtzNvRI0AAggggAACCCAQMAGmygQs4XQXAQQQQAABBBBAwE4BRtztzBtRI4AAAggggAACCARMgMI9YAmnuwgggAACCCCAAAJ2ClC425k3okYAAQQQQAABBBAImACFe8ASTncRQAABBBBAAAEE7BSgcLczb0SNAAIIIIAAAgggEDABCveAJZzuIoAAAggggAACCNgpQOFuZ96IGgEEEEAAAQQQQCBgAhTuAUs43UUAAQQQQAABBBCwU4DC3c68ETUCCCCAAAIIIIBAwAQo3AOWcLqLAAIIIIAAAgggYKcAhbudeSNqBBBAAAEEEEAAgYAJULgHLOF0FwEEEEAAAQQQQMBOAQp3O/NG1AgggAACCCCAAAIBE6BwD1jC6S4CCCCAAAIIIICAnQIU7nbmjagRQAABBBBAAAEEAiZA4R6whNNdBBBAAAEEEEAAATsFKNztzBtRI4AAAggggAACCARMgMI9YAmnuwgggAACCCCAAAJ2ClC425k3okYAAQQQQAABBBAImACFe8ASTncRQAABBBBAAAEE7BSgcLczb0SNAAIIIIAAAgggEDABCveAJZzuIoAAAggggAACCNgp8P8BhKg2I1BqsPcAAAAASUVORK5CYII=)\n" + ], + "metadata": { + "id": "xHF95Kr4CzGq" + } + }, + { + "cell_type": "markdown", + "source": [ + "\n", + "# Installation\n", + "\n", + "1. Use `pip` to install the `adalflow` Python package. We will need `openai`, `groq` from the extra packages.\n", + "\n", + " ```bash\n", + " pip install adalflow[openai,groq]\n", + " ```\n", + "2. Setup `openai` and `groq` API key in the environment variables\n", + "\n", + "You can choose to use different client. You can import the model client you prefer. We support `Anthropic`, `Cohere`, `Google`, `GROQ`, `OpenAI`, `Transformer` and more in development. We will use OpenAI here as an example.Please refer to our [full installation guide](https://adalflow.sylph.ai/get_started/installation.html)" + ], + "metadata": { + "id": "Kof5M6DRaKhh" + } + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "id": "tAp3eDjOCma1" + }, + "outputs": [], + "source": [ + "from IPython.display import clear_output\n", + "\n", + "!pip install -U adalflow[openai] # also install the package for the model client you'll use\n", + "!pip install datasets\n", + "clear_output()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Set Environment Variables\n", + "\n", + "Run the following code and pass your api key.\n", + "\n", + "Note: for normal `.py` projects, follow our [official installation guide](https://lightrag.sylph.ai/get_started/installation.html).\n", + "\n", + "*Go to [OpenAI](https://platform.openai.com/docs/introduction) to get API keys if you don't already have.*" + ], + "metadata": { + "id": "KapUyHMM07pJ" + } + }, + { + "cell_type": "code", + "source": [ + "import os\n", + "\n", + "from getpass import getpass\n", + "\n", + "# Prompt user to enter their API keys securely\n", + "openai_api_key = getpass(\"Please enter your OpenAI API key: \")\n", + "\n", + "\n", + "# Set environment variables\n", + "os.environ['OPENAI_API_KEY'] = openai_api_key\n", + "\n", + "print(\"API keys have been set.\")" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "ONfzF9Puzdd_", + "outputId": "e5c3cfc5-69cb-448a-c248-a8cebda5ba71" + }, + "execution_count": 43, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Please enter your OpenAI API key: ··········\n", + "API keys have been set.\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "from dataclasses import dataclass, field\n", + "from typing import List, Dict, Union, Optional, Tuple, Any, Callable\n", + "from datasets import load_dataset\n", + "from adalflow.components.model_client import OpenAIClient\n", + "import adalflow as adal\n", + "from adalflow.core.component import Component\n", + "from adalflow.datasets.types import TrecData\n", + "from adalflow.eval.answer_match_acc import AnswerMatchAcc\n", + "\n", + "\n", + "_COARSE_LABELS = [\n", + " \"ABBR\",\n", + " \"DESC\",\n", + " \"ENTY\",\n", + " \"HUM\",\n", + " \"LOC\",\n", + " \"NUM\"\n", + "]\n", + "\n", + "_COARSE_LABELS_DESC = [\n", + " \"Abbreviation: Questions about abbreviations and their meanings\",\n", + " \"Description: Questions seeking descriptions of people, things, or concepts\",\n", + " \"Entity: Questions about entities (e.g., animals, colors, inventions)\",\n", + " \"Human: Questions about people or organizations\",\n", + " \"Location: Questions about places, cities, countries\",\n", + " \"Numeric: Questions seeking numeric answers (e.g., dates, amounts, distances)\"\n", + "]\n", + "\n", + "\n", + "template = r\"\"\"\n", + " {{system_prompt}}\n", + " {% if output_format_str is not none %}\n", + " {{output_format_str}}\n", + " {% endif %}\n", + " {% if few_shot_demos is not none %}\n", + " Here are some examples:\n", + " {{few_shot_demos}}\n", + " {% endif %}\n", + " \n", + " \n", + " {{input_str}}\n", + " \n", + " \"\"\"\n", + "\n", + "task_desc_template = r\"\"\"You are a classifier. Given a question, you need to classify it into one of the following classes:\n", + " Format: class_index. class_name, class_description\n", + " {% if classes %}\n", + " {% for class in classes %}\n", + " {{loop.index-1}}. {{class.label}}, {{class.desc}}\n", + " {% endfor %}\n", + " {% endif %}\n", + " - Do not try to answer the question:\n", + " \"\"\"\n", + "\n", + "@dataclass\n", + "class TRECExtendedData(TrecData):\n", + " rationale: str = field(\n", + " metadata={\n", + " \"desc\": \"Your step-by-step reasoning to classify the question to class_name\"\n", + " },\n", + " default=None,\n", + " )\n", + " __input_fields__ = [\"question\"]\n", + " __output_fields__ = [\"rationale\", \"class_name\"] # it is important to have the rationale before the class_name" + ], + "metadata": { + "id": "ZZIEtZYHNVjo" + }, + "execution_count": 49, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "class TRECClassifierStructuredOutput(adal.Component):\n", + "\n", + " def __init__(self, model_client: adal.ModelClient, model_kwargs: Dict):\n", + " super().__init__()\n", + "\n", + " label_desc = [\n", + " {\"label\": label, \"desc\": desc}\n", + " for label, desc in zip(_COARSE_LABELS, _COARSE_LABELS_DESC)\n", + " ]\n", + "\n", + " task_desc_str = adal.Prompt(\n", + " template=task_desc_template, prompt_kwargs={\"classes\": label_desc}\n", + " )()\n", + "\n", + " self.data_class = TRECExtendedData\n", + " self.data_class.set_task_desc(task_desc_str)\n", + "\n", + " self.parser = adal.DataClassParser(\n", + " data_class=self.data_class, return_data_class=True, format_type=\"yaml\"\n", + " )\n", + "\n", + " prompt_kwargs = {\n", + " \"system_prompt\": adal.Parameter(\n", + " data=self.parser.get_task_desc_str(),\n", + " role_desc=\"Task description\",\n", + " requires_opt=True,\n", + " param_type=adal.ParameterType.PROMPT,\n", + " ),\n", + " \"output_format_str\": adal.Parameter(\n", + " data=self.parser.get_output_format_str(),\n", + " role_desc=\"Output format requirements\",\n", + " requires_opt=False,\n", + " param_type=adal.ParameterType.PROMPT,\n", + " ),\n", + " \"few_shot_demos\": adal.Parameter(\n", + " data=None,\n", + " requires_opt=True,\n", + " role_desc=\"Few shot examples to help the model\",\n", + " param_type=adal.ParameterType.DEMOS,\n", + " ),\n", + " }\n", + "\n", + " self.llm = adal.Generator(\n", + " model_client=model_client,\n", + " model_kwargs=model_kwargs,\n", + " prompt_kwargs=prompt_kwargs,\n", + " template=template,\n", + " output_processors=self.parser,\n", + " use_cache=True,\n", + " )\n", + "\n", + " def _prepare_input(self, question: str):\n", + " input_data = self.data_class(question=question)\n", + " input_str = self.parser.get_input_str(input_data)\n", + " prompt_kwargs = {\n", + " \"input_str\": adal.Parameter(\n", + " data=input_str, requires_opt=False, role_desc=\"input to the LLM\"\n", + " )\n", + " }\n", + " return prompt_kwargs\n", + "\n", + " def call(\n", + " self, question: str, id: Optional[str] = None\n", + " ) -> Union[adal.GeneratorOutput, adal.Parameter]:\n", + " prompt_kwargs = self._prepare_input(question)\n", + " output = self.llm(prompt_kwargs=prompt_kwargs, id=id)\n", + " return output" + ], + "metadata": { + "id": "3Q3H9XC4Ncfi" + }, + "execution_count": 50, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "class TrecClassifierAdal(adal.AdalComponent):\n", + " def __init__(\n", + " self,\n", + " model_client: adal.ModelClient,\n", + " model_kwargs: Dict,\n", + " teacher_model_config: Dict,\n", + " backward_engine_model_config: Dict,\n", + " text_optimizer_model_config: Dict,\n", + " ):\n", + " task = TRECClassifierStructuredOutput(model_client, model_kwargs)\n", + " eval_fn = AnswerMatchAcc(type=\"exact_match\").compute_single_item\n", + " loss_fn = adal.EvalFnToTextLoss(\n", + " eval_fn=eval_fn,\n", + " eval_fn_desc=\"exact_match: 1 if str(y) == str(y_gt) else 0\",\n", + " )\n", + " super().__init__(\n", + " task=task,\n", + " eval_fn=eval_fn,\n", + " loss_fn=loss_fn,\n", + " backward_engine_model_config=backward_engine_model_config,\n", + " text_optimizer_model_config=text_optimizer_model_config,\n", + " teacher_model_config=teacher_model_config,\n", + " )\n", + "\n", + " def prepare_task(self, sample: TRECExtendedData):\n", + " return self.task.call, {\"question\": sample.question, \"id\": sample.id}\n", + "\n", + " def prepare_eval(\n", + " self, sample: TRECExtendedData, y_pred: adal.GeneratorOutput\n", + " ) -> float:\n", + " y_label = -1\n", + " if y_pred and y_pred.data is not None and y_pred.data.class_name is not None:\n", + " y_label = y_pred.data.class_name\n", + " return self.eval_fn, {\"y\": y_label, \"y_gt\": sample.class_name}\n", + "\n", + " def prepare_loss(\n", + " self, sample: TRECExtendedData, y_pred: adal.Parameter, *args, **kwargs\n", + " ) -> Tuple[Callable[..., Any], Dict]:\n", + " full_response = y_pred.full_response\n", + " y_label = -1\n", + " if (\n", + " full_response\n", + " and full_response.data is not None\n", + " and full_response.data.class_name is not None\n", + " ):\n", + " y_label = full_response.data.class_name\n", + "\n", + " y_pred.eval_input = y_label\n", + " y_gt = adal.Parameter(\n", + " name=\"y_gt\",\n", + " data=sample.class_name,\n", + " eval_input=sample.class_name,\n", + " requires_opt=False,\n", + " )\n", + " return self.loss_fn, {\"kwargs\": {\"y\": y_pred, \"y_gt\": y_gt}}" + ], + "metadata": { + "id": "HpkQYsh2NevT" + }, + "execution_count": 51, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def train(\n", + " model_client: adal.ModelClient,\n", + " model_kwargs: Dict,\n", + " train_batch_size=4,\n", + " raw_shots: int = 0,\n", + " bootstrap_shots: int = 1,\n", + " max_steps=12,\n", + " num_workers=4,\n", + " strategy=\"constrained\",\n", + " optimization_order=\"sequential\",\n", + " debug=False,\n", + "):\n", + " print(\"Starting training process...\")\n", + "\n", + " # Define the model configuration for all components\n", + " gpt_4o_model = {\n", + " \"model\": \"gpt-4-turbo-preview\",\n", + " \"temperature\": 0,\n", + " \"max_tokens\": 1000,\n", + " \"top_p\": 1,\n", + " \"frequency_penalty\": 0,\n", + " \"presence_penalty\": 0\n", + " }\n", + " print(f\"Component model configuration: {gpt_4o_model}\")\n", + "\n", + " try:\n", + " print(\"Initializing ADAL component...\")\n", + " adal_component = TrecClassifierAdal(\n", + " model_client=model_client,\n", + " model_kwargs=model_kwargs,\n", + " text_optimizer_model_config=gpt_4o_model,\n", + " backward_engine_model_config=gpt_4o_model,\n", + " teacher_model_config=gpt_4o_model,\n", + " )\n", + " print(\"ADAL component initialized successfully\")\n", + "\n", + " print(\"Initializing trainer...\")\n", + " trainer = adal.Trainer(\n", + " train_batch_size=train_batch_size,\n", + " adaltask=adal_component,\n", + " strategy=strategy,\n", + " max_steps=max_steps,\n", + " num_workers=num_workers,\n", + " raw_shots=raw_shots,\n", + " bootstrap_shots=bootstrap_shots,\n", + " debug=debug,\n", + " weighted_sampling=True,\n", + " optimization_order=optimization_order,\n", + " exclude_input_fields_from_bootstrap_demos=True,\n", + " )\n", + " print(\"Trainer initialized successfully\")\n", + "\n", + " print(\"Loading datasets...\")\n", + " train_dataset, val_dataset, test_dataset = load_datasets()\n", + " print(f\"Datasets loaded - Train size: {len(train_dataset)}, Val size: {len(val_dataset)}, Test size: {len(test_dataset)}\")\n", + "\n", + " print(\"Starting model training...\")\n", + " trainer.fit(\n", + " train_dataset=train_dataset,\n", + " val_dataset=test_dataset,\n", + " debug=debug,\n", + " )\n", + " print(\"Training completed successfully\")\n", + "\n", + " except Exception as e:\n", + " print(f\"Error occurred: {str(e)}\")\n", + " raise" + ], + "metadata": { + "id": "PEj6xiZ5dVaj" + }, + "execution_count": 52, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "from adalflow.components.model_client.openai_client import OpenAIClient\n", + "\n", + "\n", + "gpt_4o_model = {\n", + " \"model_client\": OpenAIClient(),\n", + " \"model_kwargs\": {\n", + " \"model\": \"gpt-4o-mini\",\n", + " \"max_tokens\": 2000,\n", + "\n", + " },\n", + "}\n", + "\n", + "\n", + "train(\n", + " model_client=OpenAIClient(),\n", + " model_kwargs=gpt_4o_model,\n", + " )" + ], + "metadata": { + "id": "GnlZBQOMEj6E", + "collapsed": true + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Issues and feedback\n", + "\n", + "If you encounter any issues, please report them here: [GitHub Issues](https://github.com/SylphAI-Inc/LightRAG/issues).\n", + "\n", + "For feedback, you can use either the [GitHub discussions](https://github.com/SylphAI-Inc/LightRAG/discussions) or [Discord](https://discord.gg/ezzszrRZvT)." + ], + "metadata": { + "id": "AmkbyxmuruUu" + } + } + ] +} diff --git a/notebooks/tutorials/adalflow_function_calls.ipynb b/notebooks/tutorials/adalflow_function_calls.ipynb new file mode 100644 index 00000000..622448c9 --- /dev/null +++ b/notebooks/tutorials/adalflow_function_calls.ipynb @@ -0,0 +1,737 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Function calls\n", + "\n", + "Tools are means LLM can use to interact with the world beyond of its internal knowledge. Technically speaking, retrievers are tools to help LLM to get more relevant context, and memory is a tool for LLM to carry out a conversation. Deciding when, which, and how to use a tool, and even to creating a tool is an agentic behavior: Function calls is a process of showing LLM a list of funciton definitions and prompt it to choose one or few of them. Many places use tools and function calls interchangably.\n", + "\n", + "In this notebook we will covert function calls, including:\n", + "\n", + "- Function call walkthrough\n", + "\n", + "- Overall design\n", + "\n", + "- Function call in action\n", + "\n", + "It follows the tutorial here: https://adalflow.sylph.ai/tutorials/tool_helper.html#" + ], + "metadata": { + "id": "lLGpv1fLLIjF" + } + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "sfKEfaYC3Go7" + }, + "outputs": [], + "source": [ + "from IPython.display import clear_output\n", + "\n", + "!pip install -U adalflow[openai,groq,faiss-cpu]\n", + "\n", + "clear_output()\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import os\n", + "from getpass import getpass\n", + "\n", + "# Prompt user to enter their API keys securely\n", + "openai_api_key = getpass(\"Please enter your OpenAI API key: \")\n", + "groq_api_key = getpass(\"Please enter your GROQ API key: \")\n", + "\n", + "# Set environment variables\n", + "os.environ['OPENAI_API_KEY'] = openai_api_key\n", + "os.environ['GROQ_API_KEY'] = groq_api_key\n", + "\n", + "print(\"API keys have been set.\")\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "-4c_AGBt3PlR", + "outputId": "21a26437-9f95-4478-84e9-ba4369956b6f" + }, + "execution_count": 2, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Please enter your OpenAI API key: ··········\n", + "Please enter your GROQ API key: ··········\n", + "API keys have been set.\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "from dataclasses import dataclass\n", + "from typing import List\n", + "import numpy as np\n", + "import time\n", + "import asyncio\n", + "\n", + "\n", + "\n", + "def multiply(a: int, b: int) -> int:\n", + " \"\"\"Multiply two numbers.\"\"\"\n", + " time.sleep(1)\n", + " return a * b\n", + "\n", + "\n", + "def add(a: int, b: int) -> int:\n", + " \"\"\"Add two numbers.\"\"\"\n", + " time.sleep(1)\n", + " return a + b\n", + "\n", + "\n", + "async def divide(a: float, b: float) -> float:\n", + " \"\"\"Divide two numbers.\"\"\"\n", + " await asyncio.sleep(1)\n", + " return float(a) / b\n", + "\n", + "\n", + "async def search(query: str) -> List[str]:\n", + " \"\"\"Search for query and return a list of results.\"\"\"\n", + " await asyncio.sleep(1)\n", + " return [\"result1\" + query, \"result2\" + query]\n", + "\n", + "\n", + "def numpy_sum(arr: np.ndarray) -> float:\n", + " \"\"\"Sum the elements of an array.\"\"\"\n", + " return np.sum(arr)\n", + "\n", + "\n", + "x = 2\n", + "\n", + "@dataclass\n", + "class Point:\n", + " x: int\n", + " y: int\n", + "\n", + "\n", + "def add_points(p1: Point, p2: Point) -> Point:\n", + " return Point(p1.x + p2.x, p1.y + p2.y)" + ], + "metadata": { + "id": "GMKuuP7xR9Nt" + }, + "execution_count": 4, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Function Tool" + ], + "metadata": { + "id": "jCA7HMjtT16P" + } + }, + { + "cell_type": "code", + "source": [ + "from adalflow.core.func_tool import FunctionTool\n", + "\n", + "functions =[multiply, add, divide, search, numpy_sum, add_points]\n", + "tools = [\n", + " FunctionTool(fn=fn) for fn in functions\n", + "]\n", + "for tool in tools:\n", + " print(tool)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "fgOEoLoDSBqh", + "outputId": "7e636e2c-9a5d-44f1-f0fe-fe8a6bea474d" + }, + "execution_count": 5, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='multiply', func_desc='multiply(a: int, b: int) -> int\\nMultiply two numbers.', func_parameters={'type': 'object', 'properties': {'a': {'type': 'int'}, 'b': {'type': 'int'}}, 'required': ['a', 'b']}))\n", + "FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='add', func_desc='add(a: int, b: int) -> int\\nAdd two numbers.', func_parameters={'type': 'object', 'properties': {'a': {'type': 'int'}, 'b': {'type': 'int'}}, 'required': ['a', 'b']}))\n", + "FunctionTool(fn: , async: True, definition: FunctionDefinition(func_name='divide', func_desc='divide(a: float, b: float) -> float\\nDivide two numbers.', func_parameters={'type': 'object', 'properties': {'a': {'type': 'float'}, 'b': {'type': 'float'}}, 'required': ['a', 'b']}))\n", + "FunctionTool(fn: , async: True, definition: FunctionDefinition(func_name='search', func_desc='search(query: str) -> List[str]\\nSearch for query and return a list of results.', func_parameters={'type': 'object', 'properties': {'query': {'type': 'str'}}, 'required': ['query']}))\n", + "FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='numpy_sum', func_desc='numpy_sum(arr: numpy.ndarray) -> float\\nSum the elements of an array.', func_parameters={'type': 'object', 'properties': {'arr': {'type': 'ndarray'}}, 'required': ['arr']}))\n", + "FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='add_points', func_desc='add_points(p1: __main__.Point, p2: __main__.Point) -> __main__.Point\\nNone', func_parameters={'type': 'object', 'properties': {'p1': {'type': \"{'type': 'Point', 'properties': {'x': {'type': 'int'}, 'y': {'type': 'int'}}, 'required': ['x', 'y']}\"}, 'p2': {'type': \"{'type': 'Point', 'properties': {'x': {'type': 'int'}, 'y': {'type': 'int'}}, 'required': ['x', 'y']}\"}}, 'required': ['p1', 'p2']}))\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "print(tools[-2].definition.to_dict())" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "CYJaHFhGSEzH", + "outputId": "9ab36c6c-7509-4e7f-ce85-11dae889c8c2" + }, + "execution_count": 6, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "{'func_name': 'numpy_sum', 'func_desc': 'numpy_sum(arr: numpy.ndarray) -> float\\nSum the elements of an array.', 'func_parameters': {'type': 'object', 'properties': {'arr': {'type': 'ndarray'}}, 'required': ['arr']}}\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "context_map = {tool.definition.func_name: tool for tool in tools}" + ], + "metadata": { + "id": "_O4bQgXrSKb6" + }, + "execution_count": 7, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "function_name = \"add\"\n", + "function_to_call = context_map[function_name]\n", + "function_args = {\"a\": 1, \"b\": 2}\n", + "function_response = function_to_call.call(**function_args)" + ], + "metadata": { + "id": "-RgWWMdISL1u" + }, + "execution_count": 8, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "from adalflow.core.tool_manager import ToolManager\n", + "\n", + "tool_manager = ToolManager(tools=functions)\n", + "print(tool_manager)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "6CT7Tez1SOai", + "outputId": "e486d882-9179-4db3-f077-6adfc9fc6579" + }, + "execution_count": 9, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "ToolManager(Tools: [FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='multiply', func_desc='multiply(a: int, b: int) -> int\\nMultiply two numbers.', func_parameters={'type': 'object', 'properties': {'a': {'type': 'int'}, 'b': {'type': 'int'}}, 'required': ['a', 'b']})), FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='add', func_desc='add(a: int, b: int) -> int\\nAdd two numbers.', func_parameters={'type': 'object', 'properties': {'a': {'type': 'int'}, 'b': {'type': 'int'}}, 'required': ['a', 'b']})), FunctionTool(fn: , async: True, definition: FunctionDefinition(func_name='divide', func_desc='divide(a: float, b: float) -> float\\nDivide two numbers.', func_parameters={'type': 'object', 'properties': {'a': {'type': 'float'}, 'b': {'type': 'float'}}, 'required': ['a', 'b']})), FunctionTool(fn: , async: True, definition: FunctionDefinition(func_name='search', func_desc='search(query: str) -> List[str]\\nSearch for query and return a list of results.', func_parameters={'type': 'object', 'properties': {'query': {'type': 'str'}}, 'required': ['query']})), FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='numpy_sum', func_desc='numpy_sum(arr: numpy.ndarray) -> float\\nSum the elements of an array.', func_parameters={'type': 'object', 'properties': {'arr': {'type': 'ndarray'}}, 'required': ['arr']})), FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='add_points', func_desc='add_points(p1: __main__.Point, p2: __main__.Point) -> __main__.Point\\nNone', func_parameters={'type': 'object', 'properties': {'p1': {'type': \"{'type': 'Point', 'properties': {'x': {'type': 'int'}, 'y': {'type': 'int'}}, 'required': ['x', 'y']}\"}, 'p2': {'type': \"{'type': 'Point', 'properties': {'x': {'type': 'int'}, 'y': {'type': 'int'}}, 'required': ['x', 'y']}\"}}, 'required': ['p1', 'p2']}))], Additional Context: {})\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## ToolManager" + ], + "metadata": { + "id": "jzFqNnN_T-cu" + } + }, + { + "cell_type": "code", + "source": [ + "from adalflow.core.tool_manager import ToolManager\n", + "\n", + "tool_manager = ToolManager(tools=functions)\n", + "print(tool_manager)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "JX7MibWiUF3U", + "outputId": "20707186-5ec3-49a4-d553-c3160c3daa84" + }, + "execution_count": 10, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "ToolManager(Tools: [FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='multiply', func_desc='multiply(a: int, b: int) -> int\\nMultiply two numbers.', func_parameters={'type': 'object', 'properties': {'a': {'type': 'int'}, 'b': {'type': 'int'}}, 'required': ['a', 'b']})), FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='add', func_desc='add(a: int, b: int) -> int\\nAdd two numbers.', func_parameters={'type': 'object', 'properties': {'a': {'type': 'int'}, 'b': {'type': 'int'}}, 'required': ['a', 'b']})), FunctionTool(fn: , async: True, definition: FunctionDefinition(func_name='divide', func_desc='divide(a: float, b: float) -> float\\nDivide two numbers.', func_parameters={'type': 'object', 'properties': {'a': {'type': 'float'}, 'b': {'type': 'float'}}, 'required': ['a', 'b']})), FunctionTool(fn: , async: True, definition: FunctionDefinition(func_name='search', func_desc='search(query: str) -> List[str]\\nSearch for query and return a list of results.', func_parameters={'type': 'object', 'properties': {'query': {'type': 'str'}}, 'required': ['query']})), FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='numpy_sum', func_desc='numpy_sum(arr: numpy.ndarray) -> float\\nSum the elements of an array.', func_parameters={'type': 'object', 'properties': {'arr': {'type': 'ndarray'}}, 'required': ['arr']})), FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='add_points', func_desc='add_points(p1: __main__.Point, p2: __main__.Point) -> __main__.Point\\nNone', func_parameters={'type': 'object', 'properties': {'p1': {'type': \"{'type': 'Point', 'properties': {'x': {'type': 'int'}, 'y': {'type': 'int'}}, 'required': ['x', 'y']}\"}, 'p2': {'type': \"{'type': 'Point', 'properties': {'x': {'type': 'int'}, 'y': {'type': 'int'}}, 'required': ['x', 'y']}\"}}, 'required': ['p1', 'p2']}))], Additional Context: {})\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Function Call end-to-end" + ], + "metadata": { + "id": "9Bw2fs--UKX7" + } + }, + { + "cell_type": "code", + "source": [ + "template = r\"\"\"You have these tools available:\n", + "{% if tools %}\n", + "\n", + "{% for tool in tools %}\n", + "{{ loop.index }}.\n", + "{{tool}}\n", + "------------------------\n", + "{% endfor %}\n", + "\n", + "{% endif %}\n", + "\n", + "{{output_format_str}}\n", + "\n", + "\n", + "User: {{input_str}}\n", + "You:\n", + "\"\"\"" + ], + "metadata": { + "id": "TywPQMIVUOqh" + }, + "execution_count": 11, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "from adalflow.core.prompt_builder import Prompt\n", + "\n", + "prompt = Prompt(template=template)\n", + "small_tool_manager = ToolManager(tools=tools[:2])\n", + "\n", + "renered_prompt = prompt(tools=small_tool_manager.yaml_definitions)\n", + "print(renered_prompt)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "-vMajeXoUQ5A", + "outputId": "ca68601b-e9c8-41c3-a6fa-777f225e68e3" + }, + "execution_count": 12, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "You have these tools available:\n", + "\n", + "1.\n", + "func_name: multiply\n", + "func_desc: 'multiply(a: int, b: int) -> int\n", + "\n", + " Multiply two numbers.'\n", + "func_parameters:\n", + " type: object\n", + " properties:\n", + " a:\n", + " type: int\n", + " b:\n", + " type: int\n", + " required:\n", + " - a\n", + " - b\n", + "------------------------\n", + "2.\n", + "func_name: add\n", + "func_desc: 'add(a: int, b: int) -> int\n", + "\n", + " Add two numbers.'\n", + "func_parameters:\n", + " type: object\n", + " properties:\n", + " a:\n", + " type: int\n", + " b:\n", + " type: int\n", + " required:\n", + " - a\n", + " - b\n", + "------------------------\n", + "\n", + "\n", + "None\n", + "\n", + "\n", + "User: None\n", + "You:\n", + "\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "from adalflow.core.types import Function\n", + "\n", + "output_data_class = Function\n", + "output_format_str = output_data_class.to_json_signature(exclude=[\"thought\", \"args\"])\n", + "\n", + "renered_prompt= prompt(output_format_str=output_format_str)\n", + "print(renered_prompt)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "V9-90IFRUUNT", + "outputId": "ed2f829e-c656-43c6-a454-8a7c32d5dafe" + }, + "execution_count": 13, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "You have these tools available:\n", + "\n", + "{\n", + " \"name\": \"The name of the function (str) (optional)\",\n", + " \"kwargs\": \"The keyword arguments of the function (Optional[Dict[str, object]]) (optional)\"\n", + "}\n", + "\n", + "\n", + "User: None\n", + "You:\n", + "\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "from adalflow.core.types import FunctionExpression\n", + "\n", + "output_data_class = FunctionExpression\n", + "output_format_str = output_data_class.to_json_signature(exclude=[\"thought\"])\n", + "print(prompt(output_format_str=output_format_str))" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "p3kPMhWaUYT1", + "outputId": "a3de7117-c3eb-404e-e2e7-8a5187b32f6b" + }, + "execution_count": 14, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "You have these tools available:\n", + "\n", + "{\n", + " \"action\": \"FuncName() Valid function call expression. Example: \\\"FuncName(a=1, b=2)\\\" Follow the data type specified in the function parameters.e.g. for Type object with x,y properties, use \\\"ObjectType(x=1, y=2) (str) (required)\"\n", + "}\n", + "\n", + "\n", + "User: None\n", + "You:\n", + "\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "from adalflow.components.output_parsers import JsonOutputParser\n", + "\n", + "func_parser = JsonOutputParser(data_class=Function, exclude_fields=[\"thought\", \"args\"])\n", + "instructions = func_parser.format_instructions()\n", + "print(instructions)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "MvGyoUmMUatR", + "outputId": "e819866b-f6e3-4c88-f9f1-22d725a28865" + }, + "execution_count": 17, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Your output should be formatted as a standard JSON instance with the following schema:\n", + "```\n", + "{\n", + " \"name\": \"The name of the function (str) (optional)\",\n", + " \"kwargs\": \"The keyword arguments of the function (Optional[Dict[str, object]]) (optional)\"\n", + "}\n", + "```\n", + "-Make sure to always enclose the JSON output in triple backticks (```). Please do not add anything other than valid JSON output!\n", + "-Use double quotes for the keys and string values.\n", + "-DO NOT mistaken the \"properties\" and \"type\" in the schema as the actual fields in the JSON output.\n", + "-Follow the JSON formatting conventions.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Function Output Format" + ], + "metadata": { + "id": "9W7DiGcpUme5" + } + }, + { + "cell_type": "code", + "source": [ + "from adalflow.core.generator import Generator\n", + "from adalflow.core.types import ModelClientType\n", + "\n", + "model_kwargs = {\"model\": \"gpt-4o-mini\"}\n", + "prompt_kwargs = {\n", + " \"tools\": tool_manager.yaml_definitions,\n", + " \"output_format_str\": func_parser.format_instructions(),\n", + "}\n", + "generator = Generator(\n", + " model_client=ModelClientType.OPENAI(),\n", + " model_kwargs=model_kwargs,\n", + " template=template,\n", + " prompt_kwargs=prompt_kwargs,\n", + " output_processors=func_parser,\n", + ")" + ], + "metadata": { + "id": "z5tNhoruUp6o" + }, + "execution_count": 20, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "queries = [\n", + " \"add 2 and 3\",\n", + " \"search for something\",\n", + " \"add points (1, 2) and (3, 4)\",\n", + " \"sum numpy array with arr = np.array([[1, 2], [3, 4]])\",\n", + " \"multiply 2 with local variable x\",\n", + " \"divide 2 by 3\",\n", + " \"Add 5 to variable y\",\n", + "]\n", + "\n", + "for idx, query in enumerate(queries):\n", + " prompt_kwargs = {\"input_str\": query}\n", + " print(f\"\\n{idx} Query: {query}\")\n", + " print(f\"{'-'*50}\")\n", + " try:\n", + " result = generator(prompt_kwargs=prompt_kwargs)\n", + " # print(f\"LLM raw output: {result.raw_response}\")\n", + " func = Function.from_dict(result.data)\n", + " print(f\"Function: {func}\")\n", + " func_output = tool_manager.execute_func(func)\n", + " print(f\"Function output: {func_output}\")\n", + " except Exception as e:\n", + " print(\n", + " f\"Failed to execute the function for query: {query}, func: {result.data}, error: {e}\"\n", + " )" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "9DCukn1SUs_x", + "outputId": "dcfd952c-0699-4d79-ee6d-a59373e3c75d" + }, + "execution_count": 21, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "0 Query: add 2 and 3\n", + "--------------------------------------------------\n", + "Function: Function(thought=None, name='add', args=[], kwargs={'a': 2, 'b': 3})\n", + "Function output: FunctionOutput(name='add', input=Function(thought=None, name='add', args=(), kwargs={'a': 2, 'b': 3}), parsed_input=None, output=5, error=None)\n", + "\n", + "1 Query: search for something\n", + "--------------------------------------------------\n", + "Function: Function(thought=None, name='search', args=[], kwargs={'query': 'something'})\n", + "Function output: FunctionOutput(name='search', input=Function(thought=None, name='search', args=(), kwargs={'query': 'something'}), parsed_input=None, output=['result1something', 'result2something'], error=None)\n", + "\n", + "2 Query: add points (1, 2) and (3, 4)\n", + "--------------------------------------------------\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "ERROR:adalflow.core.func_tool:Error at calling : 'dict' object has no attribute 'x'\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Function: Function(thought=None, name='add_points', args=[], kwargs={'p1': {'x': 1, 'y': 2}, 'p2': {'x': 3, 'y': 4}})\n", + "Function output: FunctionOutput(name='add_points', input=Function(thought=None, name='add_points', args=(), kwargs={'p1': {'x': 1, 'y': 2}, 'p2': {'x': 3, 'y': 4}}), parsed_input=None, output=None, error=\"'dict' object has no attribute 'x'\")\n", + "\n", + "3 Query: sum numpy array with arr = np.array([[1, 2], [3, 4]])\n", + "--------------------------------------------------\n", + "Function: Function(thought=None, name='numpy_sum', args=[], kwargs={'arr': [[1, 2], [3, 4]]})\n", + "Function output: FunctionOutput(name='numpy_sum', input=Function(thought=None, name='numpy_sum', args=(), kwargs={'arr': [[1, 2], [3, 4]]}), parsed_input=None, output=10, error=None)\n", + "\n", + "4 Query: multiply 2 with local variable x\n", + "--------------------------------------------------\n", + "Function: Function(thought=None, name='multiply', args=[], kwargs={'a': 2, 'b': 'x'})\n", + "Function output: FunctionOutput(name='multiply', input=Function(thought=None, name='multiply', args=(), kwargs={'a': 2, 'b': 'x'}), parsed_input=None, output='xx', error=None)\n", + "\n", + "5 Query: divide 2 by 3\n", + "--------------------------------------------------\n", + "Function: Function(thought=None, name='divide', args=[], kwargs={'a': 2.0, 'b': 3.0})\n", + "Function output: FunctionOutput(name='divide', input=Function(thought=None, name='divide', args=(), kwargs={'a': 2.0, 'b': 3.0}), parsed_input=None, output=0.6666666666666666, error=None)\n", + "\n", + "6 Query: Add 5 to variable y\n", + "--------------------------------------------------\n", + "Function: Function(thought=None, name='add', args=[], kwargs={'a': 5, 'b': 'y'})\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "ERROR:adalflow.core.func_tool:Error at calling : unsupported operand type(s) for +: 'int' and 'str'\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Function output: FunctionOutput(name='add', input=Function(thought=None, name='add', args=(), kwargs={'a': 5, 'b': 'y'}), parsed_input=None, output=None, error=\"unsupported operand type(s) for +: 'int' and 'str'\")\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## FunctionExpression Output Format" + ], + "metadata": { + "id": "O-sBTPATUwsD" + } + }, + { + "cell_type": "code", + "source": [ + "tool_manager = ToolManager(\n", + " tools=functions,\n", + " additional_context={\"x\": x, \"y\": 0, \"np.array\": np.array, \"np\": np},\n", + ")\n", + "func_parser = JsonOutputParser(data_class=FunctionExpression)" + ], + "metadata": { + "id": "TVRZ44N1UyWg" + }, + "execution_count": 22, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "context = r\"\"\"\n", + "Your function expression also have access to these context:\n", + "{{context_str}}\n", + "\n", + "\"\"\"" + ], + "metadata": { + "id": "9h47p4XpU2BC" + }, + "execution_count": 23, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "async def run_async_function_call(self, generator, tool_manager):\n", + " answers = []\n", + " start_time = time.time()\n", + " tasks = []\n", + " for idx, query in enumerate(queries):\n", + " tasks.append(self.process_query(idx, query, generator, tool_manager))\n", + "\n", + " results = await asyncio.gather(*tasks)\n", + " answers.extend(results)\n", + " end_time = time.time()\n", + " print(f\"Total time taken: {end_time - start_time :.2f} seconds\")\n", + " return answers\n", + "\n", + "async def process_query(self, idx, query, generator, tool_manager: ToolManager):\n", + " print(f\"\\n{idx} Query: {query}\")\n", + " print(f\"{'-'*50}\")\n", + " try:\n", + " result = generator(prompt_kwargs={\"input_str\": query})\n", + " func_expr = FunctionExpression.from_dict(result.data)\n", + " print(f\"Function_expr: {func_expr}\")\n", + " func = tool_manager.parse_func_expr(func_expr)\n", + " func_output = await tool_manager.execute_func_async(func)\n", + " print(f\"Function output: {func_output}\")\n", + " return func_output\n", + " except Exception as e:\n", + " print(\n", + " f\"Failed to execute the function for query: {query}, func: {result.data}, error: {e}\"\n", + " )\n", + " return None" + ], + "metadata": { + "id": "n9Qq7wcOU4X9" + }, + "execution_count": 24, + "outputs": [] + } + ] +} diff --git a/notebooks/tutorials/adalflow_logger.ipynb b/notebooks/tutorials/adalflow_logger.ipynb new file mode 100644 index 00000000..135d6450 --- /dev/null +++ b/notebooks/tutorials/adalflow_logger.ipynb @@ -0,0 +1,242 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Adalflow RAG Playbook example\n", + "\n", + "There are different patterns to build a RAG:\n", + "\n", + "- RAG with separate data process pipeline and a RAG task pipeline. This fits into a scenario where there is lots of data in production database, and we preprocess the data to embeddings and then we build a RAG task pipeline that retrieves context in multiple stages.\n", + "\n", + "- RAG with dynamic data access and caching the embedding dynamically in a local storage.\n", + "\n", + "Here we will have have a look at an example with a local DB using FAISS" + ], + "metadata": { + "id": "lLGpv1fLLIjF" + } + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "sfKEfaYC3Go7" + }, + "outputs": [], + "source": [ + "from IPython.display import clear_output\n", + "\n", + "!pip install -U adalflow[openai,groq,faiss-cpu]\n", + "\n", + "clear_output()\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import os\n", + "from getpass import getpass\n", + "\n", + "# Prompt user to enter their API keys securely\n", + "openai_api_key = getpass(\"Please enter your OpenAI API key: \")\n", + "groq_api_key = getpass(\"Please enter your GROQ API key: \")\n", + "\n", + "# Set environment variables\n", + "os.environ['OPENAI_API_KEY'] = openai_api_key\n", + "os.environ['GROQ_API_KEY'] = groq_api_key\n", + "\n", + "print(\"API keys have been set.\")\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "-4c_AGBt3PlR", + "outputId": "275b050a-ce64-4b40-a5f9-4ccc12d92add" + }, + "execution_count": 2, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Please enter your OpenAI API key: ··········\n", + "Please enter your GROQ API key: ··········\n", + "API keys have been set.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Design\n", + "\n", + "Some libraries may use hooks [2] and callbacks [3] [4], or advanced web-based debugging tools [5] [6] [7]. Hooks and callbacks are conceptually similar in that they both allow users to execute custom code at specific points during the execution of a program. Both provide mechanisms to inject additional behavior in response to certain events or conditions, without modifying the core logic. PyTorch defines, registers, and executes hooks mainly in its base classes like nn.Module and Tensor, without polluting the functional and user-facing APIs.\n", + "\n", + "At this point, our objectives are:\n", + "\n", + "1. Maximize debugging capabilities via the simple logging module to keep the source code clean.\n", + "\n", + "2. Additionally, as we can’t always control the outputs of generators, we will provide customized logger and tracers(drop-in decorators) for them, for which we will explain in Tracing. This will not break the first objective.\n", + "\n", + "In the future, when we have more complex requirements from users, we will consider adding hooks/callbacks but we will do it in a way to keep the functional and user-facing APIs clean." + ], + "metadata": { + "id": "4NztjiLR_EQE" + } + }, + { + "cell_type": "code", + "source": [ + "import logging\n", + "\n", + "log = logging.getLogger(__name__)" + ], + "metadata": { + "id": "d2H1vYoC_F-g" + }, + "execution_count": 3, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "from adalflow.utils.logger import get_logger\n", + "\n", + "\n", + "root_logger = get_logger()" + ], + "metadata": { + "id": "e2GxAapG_TJH" + }, + "execution_count": 4, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "from adalflow.utils.logger import printc\n", + "\n", + "printc(\"All logging examples are done. Feeling green!\", color=\"green\")" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Yk4oiBFE_asG", + "outputId": "470e30dc-1b31-40c1-9e48-30754ae54b45" + }, + "execution_count": 5, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\u001b[32m2024-11-28 13:39:41 - [:3:] - All logging examples are done. Feeling green!\u001b[0m\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "Set up all logs in one file\n", + "\n", + "Assume your source code is at src/task.py. You can log simply by:" + ], + "metadata": { + "id": "B8lmlT_9_nVP" + } + }, + { + "cell_type": "code", + "source": [ + "import logging\n", + "\n", + "log = logging.getLogger(__name__)\n", + "\n", + "class Task:\n", + " def __init__(self):\n", + " log.info(\"This is a user program child logger\")" + ], + "metadata": { + "id": "o_Ru1myM_c-J" + }, + "execution_count": 6, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "import logging\n", + "from adalflow.utils.logger import get_logger\n", + "\n", + "root_logger = get_logger(level=\"DEBUG\", save_dir=\"./logs\") # log to ./logs/lib.log\n", + "\n", + "# run code from the library components such as generator\n", + "# ....\n", + "\n", + "root_logger.info(\"This is the log in the main file\")" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "o7YPjEZk_ehg", + "outputId": "ad0f58e9-6f5c-4d00-e737-2fa1ad5ebd85" + }, + "execution_count": 7, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "2024-11-28 13:39:46 - - INFO - [:9:] - This is the log in the main file\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "Separate library and application logs" + ], + "metadata": { + "id": "Db1_Ob3X_gpe" + } + }, + { + "cell_type": "code", + "source": [ + "from adalflow.utils.logger import get_logger\n", + "\n", + "app_logger = get_logger(name=\"my_app\", level=\"DEBUG\", save_dir=\"./logs\") # log to ./logs/my_app.log\n", + "\n", + "class Task:\n", + " def __init__(self):\n", + " app_logger.info(\"This is a user program child logger\")" + ], + "metadata": { + "id": "rQWuFnUc_gNm" + }, + "execution_count": 8, + "outputs": [] + } + ] +} diff --git a/notebooks/tutorials/adalflow_rag_optimization.ipynb b/notebooks/tutorials/adalflow_rag_optimization.ipynb new file mode 100644 index 00000000..7ae0b152 --- /dev/null +++ b/notebooks/tutorials/adalflow_rag_optimization.ipynb @@ -0,0 +1,495 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# 🤗 Welcome to AdalFlow!\n", + "## The PyTorch library to auto-optimize any LLM task pipelines\n", + "\n", + "Thanks for trying us out, we're here to provide you with the best LLM application development experience you can dream of 😊 any questions or concerns you may have, [come talk to us on discord,](https://discord.gg/ezzszrRZvT) we're always here to help! ⭐ Star us on Github ⭐\n", + "\n", + "\n", + "# Quick Links\n", + "\n", + "Github repo: https://github.com/SylphAI-Inc/AdalFlow\n", + "\n", + "Full Tutorials: https://adalflow.sylph.ai/index.html#.\n", + "\n", + "Deep dive on each API: check out the [developer notes](https://adalflow.sylph.ai/tutorials/index.html).\n", + "\n", + "Common use cases along with the auto-optimization: check out [Use cases](https://adalflow.sylph.ai/use_cases/index.html).\n", + "\n", + "## 📖 Outline\n", + "\n", + "In this tutorial, we will cover the auto-optimization of a standard RAG:\n", + "\n", + "- Introducing HotPotQA dataset and HotPotQAData class.\n", + "\n", + "- Convert Dspy’s Retriever to AdalFlow’s Retriever to easy comparison.\n", + "\n", + "- Build the standard RAG with Retriever and Generator components.\n", + "\n", + "- Learn how to connect the output-input between components to enable auto-text-grad optimization." + ], + "metadata": { + "id": "xHF95Kr4CzGq" + } + }, + { + "cell_type": "markdown", + "source": [ + "\n", + "# Installation\n", + "\n", + "1. Use `pip` to install the `adalflow` Python package. We will need `openai`, `groq` from the extra packages.\n", + "\n", + " ```bash\n", + " pip install adalflow[openai,groq]\n", + " ```\n", + "2. Setup `openai` and `groq` API key in the environment variables\n", + "\n", + "You can choose to use different client. You can import the model client you prefer. We support `Anthropic`, `Cohere`, `Google`, `GROQ`, `OpenAI`, `Transformer` and more in development. We will use OpenAI here as an example.Please refer to our [full installation guide](https://adalflow.sylph.ai/get_started/installation.html)" + ], + "metadata": { + "id": "Kof5M6DRaKhh" + } + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "id": "tAp3eDjOCma1" + }, + "outputs": [], + "source": [ + "from IPython.display import clear_output\n", + "\n", + "!pip install -U adalflow[openai] # also install the package for the model client you'll use\n", + "!pip install dspy\n", + "!pip install datasets\n", + "clear_output()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Set Environment Variables\n", + "\n", + "Run the following code and pass your api key.\n", + "\n", + "Note: for normal `.py` projects, follow our [official installation guide](https://lightrag.sylph.ai/get_started/installation.html).\n", + "\n", + "*Go to [OpenAI](https://platform.openai.com/docs/introduction) to get API keys if you don't already have.*" + ], + "metadata": { + "id": "KapUyHMM07pJ" + } + }, + { + "cell_type": "code", + "source": [ + "import os\n", + "\n", + "from getpass import getpass\n", + "\n", + "# Prompt user to enter their API keys securely\n", + "openai_api_key = getpass(\"Please enter your OpenAI API key: \")\n", + "\n", + "\n", + "# Set environment variables\n", + "os.environ['OPENAI_API_KEY'] = openai_api_key\n", + "\n", + "print(\"API keys have been set.\")" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "ONfzF9Puzdd_", + "outputId": "5fc0cd30-9ae7-443a-c06c-31e9edeafd69" + }, + "execution_count": 3, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Please enter your OpenAI API key: ··········\n", + "API keys have been set.\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "import dspy\n", + "import re\n", + "from typing import List, Union, Optional, Dict, Callable, Any, Tuple\n", + "from dataclasses import dataclass, field\n", + "import adalflow as adal\n", + "from adalflow.optim.parameter import Parameter, ParameterType\n", + "from adalflow.datasets.hotpot_qa import HotPotQA, HotPotQAData\n", + "from adalflow.datasets.types import Example\n", + "from adalflow.core.types import RetrieverOutput\n", + "from adalflow.core import Component, Generator\n", + "from adalflow.core.retriever import Retriever\n", + "from adalflow.core.component import fun_to_component\n", + "from adalflow.components.model_client.openai_client import OpenAIClient" + ], + "metadata": { + "id": "aE3I05BqOmd7" + }, + "execution_count": 20, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "\n", + "gpt_4o_model = {\n", + " \"model_client\": OpenAIClient(),\n", + " \"model_kwargs\": {\n", + " \"model\": \"gpt-4o-mini\",\n", + " \"max_tokens\": 2000,\n", + " },\n", + "}\n", + "\n", + "gpt_3_model = {\n", + " \"model_client\": OpenAIClient(),\n", + " \"model_kwargs\": {\n", + " \"model\": \"gpt-3.5-turbo\",\n", + " \"max_tokens\": 2000,\n", + " },\n", + "}" + ], + "metadata": { + "id": "cqUUoua9fUxQ" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def load_datasets():\n", + "\n", + " trainset = HotPotQA(split=\"train\", size=20)\n", + " valset = HotPotQA(split=\"val\", size=50)\n", + " testset = HotPotQA(split=\"test\", size=50)\n", + " print(f\"trainset, valset: {len(trainset)}, {len(valset)}, example: {trainset[0]}\")\n", + " return trainset, valset, testset\n", + "\n", + "\n", + "@dataclass\n", + "class AnswerData(adal.DataClass):\n", + " reasoning: str = field(\n", + " metadata={\"desc\": \"The reasoning to produce the answer\"},\n", + " )\n", + " answer: str = field(\n", + " metadata={\"desc\": \"The answer you produced\"},\n", + " )\n", + "\n", + " __output_fields__ = [\"reasoning\", \"answer\"]\n", + "\n", + "\n", + "dataset = HotPotQA(split=\"train\", size=20)\n", + "print(dataset[0], type(dataset[0]))\n", + "\n", + "HotPotQAData(id='5a8b57f25542995d1e6f1371', question='Were Scott Derrickson and Ed Wood of the same nationality?', answer='yes', gold_titles=\"{'Scott Derrickson', 'Ed Wood'}\")" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "0irHeHUkOmL8", + "outputId": "61f778a2-9ec1-4fda-daa2-bcc7f31baa78" + }, + "execution_count": 22, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "HotPotQAData(id='5a8b57f25542995d1e6f1371', question='Were Scott Derrickson and Ed Wood of the same nationality?', answer='yes', gold_titles=\"{'Scott Derrickson', 'Ed Wood'}\") \n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "HotPotQAData(id='5a8b57f25542995d1e6f1371', question='Were Scott Derrickson and Ed Wood of the same nationality?', answer='yes', gold_titles=\"{'Scott Derrickson', 'Ed Wood'}\")" + ] + }, + "metadata": {}, + "execution_count": 22 + } + ] + }, + { + "cell_type": "code", + "source": [ + "class DspyRetriever(adal.Retriever):\n", + " def __init__(self, top_k: int = 3):\n", + " super().__init__()\n", + " self.top_k = top_k\n", + " self.dspy_retriever = dspy.Retrieve(k=top_k)\n", + "\n", + " def call(self, input: str, top_k: Optional[int] = None) -> List[adal.RetrieverOutput]:\n", + "\n", + " k = top_k or self.top_k\n", + "\n", + " output = self.dspy_retriever(query_or_queries=input, k=k)\n", + " final_output: List[RetrieverOutput] = []\n", + " documents = output.passages\n", + "\n", + " final_output.append(\n", + " RetrieverOutput(\n", + " query=input,\n", + " documents=documents,\n", + " doc_indices=[],\n", + " )\n", + " )\n", + " return final_output\n", + "\n", + "def test_retriever():\n", + " question = \"How many storeys are in the castle that David Gregory inherited?\"\n", + " retriever = DspyRetriever(top_k=3)\n", + " retriever_out = retriever(input=question)\n", + " print(f\"retriever_out: {retriever_out}\")\n", + "\n", + "\n", + "def call(\n", + " self, question: str, id: Optional[str] = None\n", + " ) -> Union[adal.GeneratorOutput, adal.Parameter]:\n", + " prompt_kwargs = self._prepare_input(question)\n", + " output = self.llm(prompt_kwargs=prompt_kwargs, id=id)\n", + " return output\n", + "\n", + "\n", + "def call(self, question: str, id: str = None) -> adal.GeneratorOutput:\n", + " if self.training:\n", + " raise ValueError(\n", + " \"This component is not supposed to be called in training mode\"\n", + " )\n", + "\n", + " retriever_out = self.retriever.call(input=question)\n", + "\n", + " successor_map_fn = lambda x: ( # noqa E731\n", + " \"\\n\\n\".join(x[0].documents) if x and x[0] and x[0].documents else \"\"\n", + " )\n", + " retrieved_context = successor_map_fn(retriever_out)\n", + "\n", + " prompt_kwargs = {\n", + " \"context\": retrieved_context,\n", + " \"question\": question,\n", + " }\n", + "\n", + " output = self.llm.call(\n", + " prompt_kwargs=prompt_kwargs,\n", + " id=id,\n", + " )\n", + " return output\n", + "\n", + "\n", + "def forward(self, question: str, id: str = None) -> adal.Parameter:\n", + " if not self.training:\n", + " raise ValueError(\"This component is not supposed to be called in eval mode\")\n", + " retriever_out = self.retriever.forward(input=question)\n", + " successor_map_fn = lambda x: ( # noqa E731\n", + " \"\\n\\n\".join(x.data[0].documents)\n", + " if x.data and x.data[0] and x.data[0].documents\n", + " else \"\"\n", + " )\n", + " retriever_out.add_successor_map_fn(successor=self.llm, map_fn=successor_map_fn)\n", + " generator_out = self.llm.forward(\n", + " prompt_kwargs={\"question\": question, \"context\": retriever_out}, id=id\n", + " )\n", + " return generator_out\n", + "\n", + "\n", + "def bicall(\n", + " self, question: str, id: str = None\n", + ") -> Union[adal.GeneratorOutput, adal.Parameter]:\n", + " \"\"\"You can also combine both the forward and call in the same function.\n", + " Supports both training and eval mode by using __call__ for GradComponents\n", + " like Retriever and Generator\n", + " \"\"\"\n", + " retriever_out = self.retriever(input=question)\n", + " if isinstance(retriever_out, adal.Parameter):\n", + " successor_map_fn = lambda x: ( # noqa E731\n", + " \"\\n\\n\".join(x.data[0].documents)\n", + " if x.data and x.data[0] and x.data[0].documents\n", + " else \"\"\n", + " )\n", + " retriever_out.add_successor_map_fn(\n", + " successor=self.llm, map_fn=successor_map_fn\n", + " )\n", + " else:\n", + " successor_map_fn = lambda x: ( # noqa E731\n", + " \"\\n\\n\".join(x[0].documents) if x and x[0] and x[0].documents else \"\"\n", + " )\n", + " retrieved_context = successor_map_fn(retriever_out)\n", + " prompt_kwargs = {\n", + " \"context\": retrieved_context,\n", + " \"question\": question,\n", + " }\n", + " output = self.llm(prompt_kwargs=prompt_kwargs, id=id)\n", + " return output\n", + "\n", + "task_desc_str = r\"\"\"Answer questions with short factoid answers.\n", + "\n", + "You will receive context(may contain relevant facts) and a question.\n", + "Think step by step.\"\"\"\n", + "\n", + "\n", + "class VanillaRAG(adal.GradComponent):\n", + " def __init__(self, passages_per_hop=3, model_client=None, model_kwargs=None):\n", + " super().__init__()\n", + "\n", + " self.passages_per_hop = passages_per_hop\n", + "\n", + " self.retriever = DspyRetriever(top_k=passages_per_hop)\n", + " self.llm_parser = adal.DataClassParser(\n", + " data_class=AnswerData, return_data_class=True, format_type=\"json\"\n", + " )\n", + " self.llm = Generator(\n", + " model_client=model_client,\n", + " model_kwargs=model_kwargs,\n", + " prompt_kwargs={\n", + " \"task_desc_str\": adal.Parameter(\n", + " data=task_desc_str,\n", + " role_desc=\"Task description for the language model\",\n", + " param_type=adal.ParameterType.PROMPT,\n", + " ),\n", + " \"few_shot_demos\": adal.Parameter(\n", + " data=None,\n", + " requires_opt=True,\n", + " role_desc=\"To provide few shot demos to the language model\",\n", + " param_type=adal.ParameterType.DEMOS,\n", + " ),\n", + " \"output_format_str\": self.llm_parser.get_output_format_str(),\n", + " },\n", + " template=answer_template,\n", + " output_processors=self.llm_parser,\n", + " use_cache=True,\n", + " )\n", + "\n", + "\n", + "class VallinaRAGAdal(adal.AdalComponent):\n", + " def __init__(\n", + " self,\n", + " model_client: adal.ModelClient,\n", + " model_kwargs: Dict,\n", + " backward_engine_model_config: Dict | None = None,\n", + " teacher_model_config: Dict | None = None,\n", + " text_optimizer_model_config: Dict | None = None,\n", + " ):\n", + " task = VanillaRAG(\n", + " model_client=model_client,\n", + " model_kwargs=model_kwargs,\n", + " passages_per_hop=3,\n", + " )\n", + " eval_fn = AnswerMatchAcc(type=\"fuzzy_match\").compute_single_item\n", + " loss_fn = adal.EvalFnToTextLoss(\n", + " eval_fn=eval_fn, eval_fn_desc=\"fuzzy_match: 1 if str(y) in str(y_gt) else 0\"\n", + " )\n", + " super().__init__(\n", + " task=task,\n", + " eval_fn=eval_fn,\n", + " loss_fn=loss_fn,\n", + " backward_engine_model_config=backward_engine_model_config,\n", + " teacher_model_config=teacher_model_config,\n", + " text_optimizer_model_config=text_optimizer_model_config,\n", + " )\n", + "\n", + " # tell the trainer how to call the task\n", + " def prepare_task(self, sample: HotPotQAData) -> Tuple[Callable[..., Any], Dict]:\n", + " if self.task.training:\n", + " return self.task.forward, {\"question\": sample.question, \"id\": sample.id}\n", + " else:\n", + " return self.task.call, {\"question\": sample.question, \"id\": sample.id}\n", + "\n", + "\n", + " # eval mode: get the generator output, directly engage with the eval_fn\n", + " def prepare_eval(self, sample: HotPotQAData, y_pred: adal.GeneratorOutput) -> float:\n", + " y_label = \"\"\n", + " if y_pred and y_pred.data and y_pred.data.answer:\n", + " y_label = y_pred.data.answer\n", + " return self.eval_fn, {\"y\": y_label, \"y_gt\": sample.answer}\n", + "\n", + "\n", + " # train mode: get the loss and get the data from the full_response\n", + " def prepare_loss(self, sample: HotPotQAData, pred: adal.Parameter):\n", + " # prepare gt parameter\n", + " y_gt = adal.Parameter(\n", + " name=\"y_gt\",\n", + " data=sample.answer,\n", + " eval_input=sample.answer,\n", + " requires_opt=False,\n", + " )\n", + "\n", + " # pred's full_response is the output of the task pipeline which is GeneratorOutput\n", + " pred.eval_input = (\n", + " pred.full_response.data.answer\n", + " if pred.full_response\n", + " and pred.full_response.data\n", + " and pred.full_response.data.answer\n", + " else \"\"\n", + " )\n", + " return self.loss_fn, {\"kwargs\": {\"y\": pred, \"y_gt\": y_gt}}\n", + "\n", + "def train_diagnose(\n", + " model_client: adal.ModelClient,\n", + " model_kwargs: Dict,\n", + ") -> Dict:\n", + "\n", + " trainset, valset, testset = load_datasets()\n", + "\n", + " adal_component = VallinaRAGAdal(\n", + " model_client,\n", + " model_kwargs,\n", + " backward_engine_model_config=gpt_4o_model,\n", + " teacher_model_config=gpt_3_model,\n", + " text_optimizer_model_config=gpt_3_model,\n", + " )\n", + " trainer = adal.Trainer(adaltask=adal_component)\n", + " trainer.diagnose(dataset=trainset, split=\"train\")\n", + " # trainer.diagnose(dataset=valset, split=\"val\")\n", + " # trainer.diagnose(dataset=testset, split=\"test\")\n" + ], + "metadata": { + "id": "ZZIEtZYHNVjo" + }, + "execution_count": 23, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Issues and feedback\n", + "\n", + "If you encounter any issues, please report them here: [GitHub Issues](https://github.com/SylphAI-Inc/LightRAG/issues).\n", + "\n", + "For feedback, you can use either the [GitHub discussions](https://github.com/SylphAI-Inc/LightRAG/discussions) or [Discord](https://discord.gg/ezzszrRZvT)." + ], + "metadata": { + "id": "AmkbyxmuruUu" + } + } + ] +} diff --git a/notebooks/tutorials/adalflow_rag_playbook.ipynb b/notebooks/tutorials/adalflow_rag_playbook.ipynb new file mode 100644 index 00000000..27c6bda0 --- /dev/null +++ b/notebooks/tutorials/adalflow_rag_playbook.ipynb @@ -0,0 +1,522 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Adalflow RAG Playbook example\n", + "\n", + "There are different patterns to build a RAG:\n", + "\n", + "- RAG with separate data process pipeline and a RAG task pipeline. This fits into a scenario where there is lots of data in production database, and we preprocess the data to embeddings and then we build a RAG task pipeline that retrieves context in multiple stages.\n", + "\n", + "- RAG with dynamic data access and caching the embedding dynamically in a local storage.\n", + "\n", + "Here we will have have a look at an example with a local DB using FAISS" + ], + "metadata": { + "id": "lLGpv1fLLIjF" + } + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "sfKEfaYC3Go7" + }, + "outputs": [], + "source": [ + "from IPython.display import clear_output\n", + "\n", + "!pip install -U adalflow[openai,groq,faiss-cpu]\n", + "\n", + "clear_output()\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import os\n", + "from getpass import getpass\n", + "\n", + "# Prompt user to enter their API keys securely\n", + "openai_api_key = getpass(\"Please enter your OpenAI API key: \")\n", + "groq_api_key = getpass(\"Please enter your GROQ API key: \")\n", + "\n", + "# Set environment variables\n", + "os.environ['OPENAI_API_KEY'] = openai_api_key\n", + "os.environ['GROQ_API_KEY'] = groq_api_key\n", + "\n", + "print(\"API keys have been set.\")\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "-4c_AGBt3PlR", + "outputId": "a36f157b-0b18-4f3d-d5a8-09aa94743922" + }, + "execution_count": 2, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Please enter your OpenAI API key: ··········\n", + "Please enter your GROQ API key: ··········\n", + "API keys have been set.\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "from typing import Any, List, Optional\n", + "import os\n", + "from adalflow.core import Component, Generator, Embedder, Sequential\n", + "from adalflow.core.types import Document, ModelClientType\n", + "from adalflow.core.string_parser import JsonParser\n", + "from adalflow.core.db import LocalDB\n", + "from adalflow.utils import setup_env\n", + "from adalflow.components.retriever.faiss_retriever import FAISSRetriever\n", + "from adalflow.components.data_process import (\n", + " RetrieverOutputToContextStr,\n", + " ToEmbeddings,\n", + " TextSplitter,\n", + ")\n", + "from adalflow.utils.global_config import get_adalflow_default_root_path\n" + ], + "metadata": { + "id": "V9LsGDnm3RbV" + }, + "execution_count": 4, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "configs = {\n", + " \"embedder\": {\n", + " \"batch_size\": 100,\n", + " \"model_kwargs\": {\n", + " \"model\": \"text-embedding-3-small\",\n", + " \"dimensions\": 256,\n", + " \"encoding_format\": \"float\",\n", + " },\n", + " },\n", + " \"retriever\": {\n", + " \"top_k\": 5,\n", + " },\n", + " \"generator\": {\n", + " \"model_client\": ModelClientType.OPENAI(),\n", + " \"model_kwargs\": {\n", + " \"model\": \"gpt-3.5-turbo\",\n", + " \"temperature\": 0.3,\n", + " \"stream\": False,\n", + " },\n", + " },\n", + " \"text_splitter\": {\n", + " \"split_by\": \"word\",\n", + " \"chunk_size\": 400,\n", + " \"chunk_overlap\": 200,\n", + " },\n", + "}\n" + ], + "metadata": { + "id": "kWGTZxrw3Tli" + }, + "execution_count": 5, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def prepare_data_pipeline():\n", + " splitter = TextSplitter(**configs[\"text_splitter\"])\n", + " embedder = Embedder(\n", + " model_client=ModelClientType.OPENAI(),\n", + " model_kwargs=configs[\"embedder\"][\"model_kwargs\"],\n", + " )\n", + " embedder_transformer = ToEmbeddings(\n", + " embedder=embedder, batch_size=configs[\"embedder\"][\"batch_size\"]\n", + " )\n", + " data_transformer = Sequential(splitter, embedder_transformer)\n", + " return data_transformer\n", + "\n", + "def prepare_database_with_index(\n", + " docs: List[Document],\n", + " index_file: str = \"index.faiss\",\n", + " index_path: Optional[str] = None,\n", + "):\n", + " index_path = index_path or get_adalflow_default_root_path()\n", + " index_path = os.path.join(index_path, index_file)\n", + " if os.path.exists(index_path):\n", + " return None\n", + " db = LocalDB()\n", + " db.load(docs)\n", + " data_transformer = prepare_data_pipeline()\n", + " db.transform(data_transformer, key=\"data_transformer\")\n", + " db.save_state(index_path)\n" + ], + "metadata": { + "id": "1QE0PCKs4BLz" + }, + "execution_count": 6, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "RAG_PROMPT_TEMPLATE = r\"\"\"\n", + "{{task_desc}}\n", + "\n", + "\n", + "{{input_str}}\n", + "{{context_str}}\n", + "\n", + "\"\"\"\n", + "\n", + "rag_prompt_task_desc = r\"\"\"\n", + "You are a helpful assistant.\n", + "\n", + "Your task is to answer the query that may or may not come with context information.\n", + "When context is provided, you should stick to the context and less on your prior knowledge to answer the query.\n", + "\n", + "Output JSON format:\n", + "{\n", + " \"answer\": \"The answer to the query\",\n", + "}\"\"\"\n", + "\n", + "class RAG(Component):\n", + " def __init__(\n", + " self,\n", + " index_file: str = \"index.faiss\",\n", + " index_path: Optional[str] = None,\n", + " configs: dict = configs,\n", + " ):\n", + " super().__init__()\n", + "\n", + " index_path = index_path or get_adalflow_default_root_path()\n", + " index_path = os.path.join(index_path, index_file)\n", + " self.index_path = index_path\n", + "\n", + " if not os.path.exists(index_path):\n", + " self.db = LocalDB()\n", + " self.register_data_transformer()\n", + " self.transformed_docs = []\n", + " else:\n", + " self.db = LocalDB.load_state(index_path)\n", + " self.transformed_docs = self.db.get_transformed_data(\"data_transformer\")\n", + "\n", + " embedder = Embedder(\n", + " model_client=ModelClientType.OPENAI(),\n", + " model_kwargs=configs[\"embedder\"][\"model_kwargs\"],\n", + " )\n", + "\n", + " self.retriever = FAISSRetriever(\n", + " **configs[\"retriever\"],\n", + " embedder=embedder,\n", + " documents=self.transformed_docs,\n", + " document_map_func=lambda doc: doc.vector,\n", + " )\n", + " self.retriever_output_processors = RetrieverOutputToContextStr(deduplicate=True)\n", + "\n", + " self.generator = Generator(\n", + " **configs[\"generator\"],\n", + " prompt_kwargs={\"task_desc_str\": rag_prompt_task_desc},\n", + " output_processors=JsonParser(),\n", + " )\n", + "\n", + " def register_data_transformer(self):\n", + " if \"data_transformer\" not in self.db.get_transformer_keys():\n", + " data_transformer = prepare_data_pipeline()\n", + " self.db.register_transformer(data_transformer, key=\"data_transformer\")\n", + " print(\"Data transformer registered\")\n", + "\n", + " def add_documents(self, docs: List[Document]):\n", + " self.db.extend(docs, apply_transformer=True)\n", + " self.db.save_state(self.index_path)\n", + "\n", + " def get_transformed_docs(self, filter_func=None):\n", + " return self.db.get_transformed_data(\"data_transformer\", filter_func)\n", + "\n", + " def prepare_retriever(self, filter_func=None):\n", + " self.transformed_docs = self.get_transformed_docs(filter_func)\n", + " self.retriever.build_index_from_documents(\n", + " self.transformed_docs, document_map_func=lambda doc: doc.vector\n", + " )\n", + "\n", + " def generate(self, query: str, context: Optional[str] = None) -> Any:\n", + " if not self.generator:\n", + " raise ValueError(\"Generator is not set\")\n", + " prompt_kwargs = {\"context_str\": context, \"input_str\": query}\n", + " response = self.generator(prompt_kwargs=prompt_kwargs)\n", + " return response, context\n", + "\n", + " def call(self, query: str, verbose: bool = False) -> Any:\n", + " retrieved_documents = self.retriever(query)\n", + " for i, retriever_output in enumerate(retrieved_documents):\n", + " retrieved_documents[i].documents = [\n", + " self.transformed_docs[doc_index]\n", + " for doc_index in retriever_output.doc_indices\n", + " ]\n", + " if verbose:\n", + " print(f\"retrieved_documents: \\n {retrieved_documents}\")\n", + "\n", + " context_str = self.retriever_output_processors(retrieved_documents)\n", + " if verbose:\n", + " print(f\"context_str: \\n {context_str}\")\n", + "\n", + " return self.generate(query, context=context_str)\n" + ], + "metadata": { + "id": "6Mu1HXhy4DIG" + }, + "execution_count": 7, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# Prepare initial documents\n", + "doc1 = Document(\n", + " meta_data={\"title\": \"Li Yin's profile\"},\n", + " text=\"My name is Li Yin, I love rock climbing\" + \"lots of nonsense text\" * 500,\n", + " id=\"doc1\",\n", + ")\n", + "doc2 = Document(\n", + " meta_data={\"title\": \"Interviewing Li Yin\"},\n", + " text=\"lots of more nonsense text\" * 250\n", + " + \"Li Yin is an AI researcher and a software engineer\"\n", + " + \"lots of more nonsense text\" * 250,\n", + " id=\"doc2\",\n", + ")\n", + "\n", + "# Prepare the database (only runs once)\n", + "prepare_database_with_index([doc1, doc2], index_file=\"index.faiss\")\n", + "\n", + "# Initialize RAG\n", + "rag = RAG(index_file=\"index.faiss\")\n", + "print(rag)\n", + "\n", + "# Query the RAG system\n", + "query = \"What is Li Yin's hobby and profession?\"\n", + "response = rag.call(query)\n", + "print(f\"Response: {response}\")\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "sPnx4PY34D1j", + "outputId": "f66d6f1a-70bf-40e9-a160-591fcfdcbed3" + }, + "execution_count": 8, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "Splitting Documents in Batches: 100%|██████████| 1/1 [00:00<00:00, 109.58it/s]\n", + "Batch embedding documents: 100%|██████████| 1/1 [00:01<00:00, 1.33s/it]\n", + "Adding embeddings to documents from batch: 1it [00:00, 6462.72it/s]\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Saved the state of the DB to /root/.adalflow/index.faiss\n", + "RAG(\n", + " (db): LocalDB(name='LocalDB', items=[Document(id=doc1, text='My name is Li Yin, I love rock climbinglots of nonsense textlots of nonsense textlots of nonsense te...', meta_data={'title': \"Li Yin's profile\"}, vector=[], parent_doc_id=None, order=None, score=None), Document(id=doc2, text='lots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense ...', meta_data={'title': 'Interviewing Li Yin'}, vector=[], parent_doc_id=None, order=None, score=None)], transformed_items={'data_transformer': [Document(id=59f7f6ad-eb4c-4fdb-8d04-6dba1ee439bc, text='My name is Li Yin, I love rock climbinglots of nonsense textlots of nonsense textlots of nonsense te...', meta_data={'title': \"Li Yin's profile\"}, vector='len: 256', parent_doc_id=doc1, order=0, score=None), Document(id=2486725e-47ff-4978-84fc-7937778b0e45, text='textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nons...', meta_data={'title': \"Li Yin's profile\"}, vector='len: 256', parent_doc_id=doc1, order=1, score=None), Document(id=96993047-4cff-436d-b8ac-e02da4ae7fec, text='nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlot...', meta_data={'title': \"Li Yin's profile\"}, vector='len: 256', parent_doc_id=doc1, order=2, score=None), Document(id=77742f90-0c0c-4143-802d-3557577d4935, text='of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense text...', meta_data={'title': \"Li Yin's profile\"}, vector='len: 256', parent_doc_id=doc1, order=3, score=None), Document(id=81ba770e-c5f2-4dc5-98fc-349ab9143ef9, text='textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nons...', meta_data={'title': \"Li Yin's profile\"}, vector='len: 256', parent_doc_id=doc1, order=4, score=None), Document(id=dff6f5e3-5929-4e3c-ba5f-79f5116c1fa3, text='nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlot...', meta_data={'title': \"Li Yin's profile\"}, vector='len: 256', parent_doc_id=doc1, order=5, score=None), Document(id=1e7888e2-0783-40b2-ab85-067e3ba71fad, text='of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense text...', meta_data={'title': \"Li Yin's profile\"}, vector='len: 256', parent_doc_id=doc1, order=6, score=None), Document(id=2deb945f-dfb9-46d3-a60b-dae77e2f5fd8, text='lots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense ...', meta_data={'title': 'Interviewing Li Yin'}, vector='len: 256', parent_doc_id=doc2, order=0, score=None), Document(id=3d9c21aa-d583-47fe-b143-710b4bc4a8b2, text='textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonse...', meta_data={'title': 'Interviewing Li Yin'}, vector='len: 256', parent_doc_id=doc2, order=1, score=None), Document(id=a318ffea-2542-4493-ab2d-03d10a94e860, text='textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonse...', meta_data={'title': 'Interviewing Li Yin'}, vector='len: 256', parent_doc_id=doc2, order=2, score=None), Document(id=b5c05820-7545-43a8-a4a3-691c5ccc79d1, text='textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonse...', meta_data={'title': 'Interviewing Li Yin'}, vector='len: 256', parent_doc_id=doc2, order=3, score=None), Document(id=a739cd3e-8826-4e74-afa9-499498115621, text='textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonse...', meta_data={'title': 'Interviewing Li Yin'}, vector='len: 256', parent_doc_id=doc2, order=4, score=None), Document(id=7153cde2-b6ee-4485-91e9-9de2f4bd45ab, text='textLi Yin is an AI researcher and a software engineerlots of more nonsense textlots of more nonsens...', meta_data={'title': 'Interviewing Li Yin'}, vector='len: 256', parent_doc_id=doc2, order=5, score=None), Document(id=c3f3ed48-acc2-41b5-b4ac-a6107b651789, text='nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of m...', meta_data={'title': 'Interviewing Li Yin'}, vector='len: 256', parent_doc_id=doc2, order=6, score=None), Document(id=7bfd84e6-0025-4cfa-8c0a-63c9de9a8d4a, text='nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of m...', meta_data={'title': 'Interviewing Li Yin'}, vector='len: 256', parent_doc_id=doc2, order=7, score=None), Document(id=8bece98d-65f0-4dd1-9407-d1c54413bef4, text='nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of m...', meta_data={'title': 'Interviewing Li Yin'}, vector='len: 256', parent_doc_id=doc2, order=8, score=None), Document(id=cf9ab236-af73-4af6-9302-b3c7ffdd9ca7, text='nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of m...', meta_data={'title': 'Interviewing Li Yin'}, vector='len: 256', parent_doc_id=doc2, order=9, score=None)]}, transformer_setups={'data_transformer': Sequential(\n", + " (0): TextSplitter(split_by=word, chunk_size=400, chunk_overlap=200)\n", + " (1): ToEmbeddings(\n", + " batch_size=100\n", + " (embedder): Embedder(\n", + " model_kwargs={'model': 'text-embedding-3-small', 'dimensions': 256, 'encoding_format': 'float'}, \n", + " (model_client): OpenAIClient()\n", + " )\n", + " (batch_embedder): BatchEmbedder(\n", + " (embedder): Embedder(\n", + " model_kwargs={'model': 'text-embedding-3-small', 'dimensions': 256, 'encoding_format': 'float'}, \n", + " (model_client): OpenAIClient()\n", + " )\n", + " )\n", + " )\n", + " )}, mapper_setups={}, index_path='/root/.adalflow/index.faiss')\n", + " (retriever): FAISSRetriever(\n", + " top_k=5, metric=prob, dimensions=256, total_documents=17\n", + " (embedder): Embedder(\n", + " model_kwargs={'model': 'text-embedding-3-small', 'dimensions': 256, 'encoding_format': 'float'}, \n", + " (model_client): OpenAIClient()\n", + " )\n", + " )\n", + " (retriever_output_processors): RetrieverOutputToContextStr(deduplicate=True)\n", + " (generator): Generator(\n", + " model_kwargs={'model': 'gpt-3.5-turbo', 'temperature': 0.3, 'stream': False}, trainable_prompt_kwargs=[]\n", + " (prompt): Prompt(\n", + " template: \n", + " {# task desc #}\n", + " {% if task_desc_str %}\n", + " {{task_desc_str}}\n", + " {% else %}\n", + " You are a helpful assistant.\n", + " {% endif %}\n", + " {#input format#}\n", + " {% if input_format_str %}\n", + " \n", + " {{input_format_str}}\n", + " \n", + " {% endif %}\n", + " {# output format #}\n", + " {% if output_format_str %}\n", + " \n", + " {{output_format_str}}\n", + " \n", + " {% endif %}\n", + " {# tools #}\n", + " {% if tools_str %}\n", + " \n", + " {{tools_str}}\n", + " \n", + " {% endif %}\n", + " {# example #}\n", + " {% if examples_str %}\n", + " \n", + " {{examples_str}}\n", + " \n", + " {% endif %}\n", + " {# chat history #}\n", + " {% if chat_history_str %}\n", + " \n", + " {{chat_history_str}}\n", + " \n", + " {% endif %}\n", + " {#contex#}\n", + " {% if context_str %}\n", + " \n", + " {{context_str}}\n", + " \n", + " {% endif %}\n", + " \n", + " \n", + " {% if input_str %}\n", + " {{input_str}}\n", + " {% endif %}\n", + " \n", + " {# steps #}\n", + " {% if steps_str %}\n", + " \n", + " {{steps_str}}\n", + " \n", + " {% endif %}\n", + " , prompt_kwargs: {'task_desc_str': '\\nYou are a helpful assistant.\\n\\nYour task is to answer the query that may or may not come with context information.\\nWhen context is provided, you should stick to the context and less on your prior knowledge to answer the query.\\n\\nOutput JSON format:\\n{\\n \"answer\": \"The answer to the query\",\\n}'}, prompt_variables: ['examples_str', 'context_str', 'chat_history_str', 'tools_str', 'task_desc_str', 'input_str', 'input_format_str', 'output_format_str', 'steps_str']\n", + " )\n", + " (model_client): OpenAIClient()\n", + " (output_processors): JsonParser()\n", + " )\n", + ")\n", + "Response: (GeneratorOutput(id=None, data={'answer': \"Li Yin's hobby is rock climbing and profession is an AI researcher and a software engineer.\"}, error=None, usage=CompletionUsage(completion_tokens=25, prompt_tokens=2713, total_tokens=2738), raw_response='{\\n \"answer\": \"Li Yin\\'s hobby is rock climbing and profession is an AI researcher and a software engineer.\"\\n}', metadata=None), ' My name is Li Yin, I love rock climbinglots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textLi Yin is an AI researcher and a software engineerlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more textLi Yin is an AI researcher and a software engineerlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense ')\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "# Add more documents at runtime\n", + "doc3 = Document(\n", + " meta_data={\"title\": \"Apple's profile\"},\n", + " text=\"Apple is a cute dog with black and tan fur\" + \"lots of nonsense text\" * 500,\n", + " id=\"doc3\",\n", + ")\n", + "doc4 = Document(\n", + " meta_data={\"title\": \"Apple's characteristics\"},\n", + " text=\"lots of more nonsense text\" * 250\n", + " + \"Apple is energetic, loves to play with her monkey toy\"\n", + " + \"lots of more nonsense text\" * 250,\n", + " id=\"doc4\",\n", + ")\n", + "\n", + "rag.add_documents([doc3, doc4])\n", + "rag.prepare_retriever()\n", + "\n", + "# Test a new query\n", + "query = \"What is Apple's favorite toy?\"\n", + "response = rag.call(query)\n", + "print(f\"Response: {response}\")\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "bcC1-dCheVEC", + "outputId": "133bab3f-ff2e-40db-99dc-71d64af6283f" + }, + "execution_count": 9, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "Splitting Documents in Batches: 100%|██████████| 1/1 [00:00<00:00, 114.76it/s]\n", + "Batch embedding documents: 100%|██████████| 1/1 [00:00<00:00, 1.35it/s]\n", + "Adding embeddings to documents from batch: 1it [00:00, 1915.21it/s]\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Saved the state of the DB to /root/.adalflow/index.faiss\n", + "Response: (GeneratorOutput(id=None, data={'answer': \"Apple's favorite toy is her monkey toy.\"}, error=None, usage=CompletionUsage(completion_tokens=16, prompt_tokens=2647, total_tokens=2663), raw_response='{\\n \"answer\": \"Apple\\'s favorite toy is her monkey toy.\"\\n}', metadata=None), ' Apple is a cute dog with black and tan furlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots of nonsense textlots textApple is energetic, loves to play with her monkey toylots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textApple is energetic, loves to play with her monkey toylots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textLi Yin is an AI researcher and a software engineerlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more textLi Yin is an AI researcher and a software engineerlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more ')\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "# View all documents in the database\n", + "print(\"All documents in the database:\")\n", + "for item in rag.db.items:\n", + " print(f\"ID: {item.id}, Title: {item.meta_data['title']}, Text: {item.text[:100]}...\")\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "o9TzVv5GeZZ2", + "outputId": "bde56355-186c-4013-d702-b4530f82881b" + }, + "execution_count": 10, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "All documents in the database:\n", + "ID: doc1, Title: Li Yin's profile, Text: My name is Li Yin, I love rock climbinglots of nonsense textlots of nonsense textlots of nonsense te...\n", + "ID: doc2, Title: Interviewing Li Yin, Text: lots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense ...\n", + "ID: doc3, Title: Apple's profile, Text: Apple is a cute dog with black and tan furlots of nonsense textlots of nonsense textlots of nonsense...\n", + "ID: doc4, Title: Apple's characteristics, Text: lots of more nonsense textlots of more nonsense textlots of more nonsense textlots of more nonsense ...\n" + ] + } + ] + } + ] +} diff --git a/notebooks/tutorials/adalflow_tracing.ipynb b/notebooks/tutorials/adalflow_tracing.ipynb new file mode 100644 index 00000000..014c1b5e --- /dev/null +++ b/notebooks/tutorials/adalflow_tracing.ipynb @@ -0,0 +1,183 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Tracing\n", + "\n", + "In particular, we provide two tracing methods to help you develop and improve the Generator:\n", + "\n", + "1. Trace the history change(states) on prompt during your development process. Developers typically go through a long process of prompt optimization and it is frustrating to lose track of the prompt changes when your current change actually makes the performance much worse.\n" + ], + "metadata": { + "id": "lLGpv1fLLIjF" + } + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "sfKEfaYC3Go7" + }, + "outputs": [], + "source": [ + "from IPython.display import clear_output\n", + "\n", + "!pip install -U adalflow[openai,groq,faiss-cpu]\n", + "\n", + "clear_output()\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import os\n", + "from getpass import getpass\n", + "\n", + "# Prompt user to enter their API keys securely\n", + "openai_api_key = getpass(\"Please enter your OpenAI API key: \")\n", + "groq_api_key = getpass(\"Please enter your GROQ API key: \")\n", + "\n", + "# Set environment variables\n", + "os.environ['OPENAI_API_KEY'] = openai_api_key\n", + "os.environ['GROQ_API_KEY'] = groq_api_key\n", + "\n", + "print(\"API keys have been set.\")\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "-4c_AGBt3PlR", + "outputId": "85aba038-ee9c-463d-bdbd-027cbfff0094" + }, + "execution_count": 2, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Please enter your OpenAI API key: ··········\n", + "Please enter your GROQ API key: ··········\n", + "API keys have been set.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "We created a GeneratorStateLogger to handle the logging and saving into json files. To further simplify developers’s process, we provides a class decorator trace_generator_states where a single line of code can be added to any of your task component. It will automatically track any attributes of type Generator." + ], + "metadata": { + "id": "yWi2uEiE6UIf" + } + }, + { + "cell_type": "code", + "source": [ + "from adalflow.tracing import trace_generator_states\n", + "from adalflow.core import Component, Generator\n", + "import adalflow as adal\n", + "from adalflow.components.model_client import OpenAIClient\n", + "\n", + "template_doc = r\"\"\" You are a doctor User: {{input_str}}\"\"\"\n", + "\n", + "@trace_generator_states()\n", + "class DocQA(adal.Component):\n", + " def __init__(self):\n", + " super(DocQA, self).__init__()\n", + " self.generator = Generator(\n", + " template=template_doc,\n", + " model_client=OpenAIClient(),\n", + " model_kwargs={\"model\": \"gpt-4o-mini\"},\n", + " )\n", + "\n", + " def call(self, query: str) -> str:\n", + " return self.doc(prompt_kwargs={\"input_str\": query}).data\n" + ], + "metadata": { + "id": "qk9pkcCVzdek" + }, + "execution_count": 13, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Here is the folder structer of where the trace is generated as a .json file and also an example output below" + ], + "metadata": { + "id": "LAZUSnYn-lnI" + } + }, + { + "cell_type": "markdown", + "source": [ + "![image.png](data:image/png;base64,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zbq6aOkpV/TYnUL89O7d2za1FAEREAEREAERyDiBmhU+uLfSMImfNCiqDBEQAREQARFoGgI16+oqNaan0O1A/Jjrq1C6QsemTZvmJk6c6MaMGeOmTp1aKKmOiYAIiIAIiIAIlEmgZlt8yuTVaNkQYt26dfNxR8QfyURABERABERABNInIOGTPtOKSiTuiPgjmQiIgAiIgAiIQPoEatbVlT7K9EqsJP6oZ8+erl+/fr4yjz7yiBs5alR6FVNJIiACIiACIlDlBCR8MngDK4k/WmKJJdwCCyzgr6rLcstl8OpUJREQAREQARFoPgJydTUfe51ZBERABERABESgiQmoxaeJgTfW6ZZeemk322yzuY4dO0anaN++vVtmmWX89qeffup+++03v77kkku6Nm3auF9++cVNnjzZtWvXzm2wwQZu8cUXd4899ph7//33ozJYIe1qq63mVl55Zffdd9+5Dz74wL355pvu999/r5MuaWPOOed0a621lltqqaV8/d5++233xhtvuJ9++ikpeZ191GudddbxeadMmeJefvllN2nSpDppkjaIk6KuXbp08eekvpzz+++/T0qufSIgAiIgAjVEQMKnhdzsc845p96V8OIfMmSI33/VVVe5+++/36+fe+65fvnzzz+7xx9/3G2++eZR3k6dOkV5FllkEXfKKadErrMoUW5lxowZbsSIEe7uu+8Od9dZP/TQQ92GG27oxUd4YObMmV5gDRs2LK94Ovroo93aa6/tWrVqFWb14o1rffHFF+vsZwOBdsghh3gRhwiM24QJE9yFF14oARQHo20REAERqCECFQsfRi6udAwb4635r4xE0yznmmuuOqInPCtB0oiI1q1bh7ujdUTG3nvv7RAx99xzT7TfVvr37+8oI8kQMxtvvLHrutJK7pBcurgde+yxvpUovp/t2Wef3Q0ePNiLrttvv71OkjPPPDNq4apzYNYGrVbnDh3qDvrHP9wff/yRlET7REAEREAEWjiB1rlg2JMrucYPP/zQ/yo3l0q5ZTW16Mn6VBPwKMVef/1134qCmCHAGWNAxAsuuMCNHz/et5DQSoPttttufmn/cHlxH1977TX31FNPeXfSGWec4UUGab7++msvbq688kpHK9Fiiy3mcGFhq666qrvtttu8API7cv923HFHt+2229qmL/Paa6919957r2+Vodca4gdXFq65559/PkqLmOrVq5ffRpzceeedjvPSwrPssstGk8l27drVtzaZu41Wq8022ywqhzJHjhzp880///yRCxA+Cy64oHvuueeitFoRAREQARGoHQIVt/iAyibqLLflp6lFT0u8vcSwYAgRM0aARswUsi+++MIPmhimIa7GhA0jStN6Yy0kw4cPd/fdd5+7/PLLfRZcSoiQ8Dx9+/aNigtdbOy85JJL3HvvvecOPPBAn4bYoksvvdSvI2xCwXTCCSe4d955xx/76KOP3AsvvOAuvvhit9BCC/mWqP3339/hLsPWXXddv+QfIu7ss8+OthF+++23n9tqq638vlVWWSU6phUREAEREIHaIlA/EKLM60f8lNpKwakkesoEnkI2xMwxxxxTr6RPPvnEt5bQYnL++edHoscSfvvtt+7zzz+3TbdSzmVltuKKK0ZxOQQTW1yRHWc5duxY9+OPP/pdtMBY9/0ddtghSvbqq69GoifamVu54ooros1QwORzyVni0aNHu7feesv/0TomEwEREAERqE0CqbT4GLpSW34keoxc8ywRPj/88EO9kyNqcF+ZEc9DTzB6Zs0777x+dxg83CaIA1pzzTUtm2+hiTZiK7jg6EWGmQgKBRTCh0DruIWCy+pCGlx0JoRwu5544onujjvucK+88oovAhFGC5JMBERABESgtgmkKnxAWaz4kejJ/oPXo0cPPwo0vbuKtVC80FU+nxGzE++ZNffcc0fJ99xzT8dfITN3HGkeyY1SzVQfCDSMec/4IwaIetDKNG7cuLy9yHwm/RMBERABEWjxBFJzdYWkGnJ7SfSEtLK5TlzPkUce6ULRQw8uAqQJhmY9ycIJVr/66qukJHn30bJUioVd3anXEUcc4cWNBXFTFi4wxNABBxzgiE9ab731SjmF0oqACIiACLQwAqW9aUq4+HwtPxI9JUBspqS4jKxnFVWghxSigUEQzXAbhYHUtp8BDhdddFG/SRByKcYAixarM2bMGEdgdSGzHl1hGoKu+aO1aqONNnLEHJlLjLIRR/GA6zC/1kVABERABFo2gUYTPmCLix+Jnup4mHbeeeeoooiesIdUdCDPCuLI3F0Ww5Mnab3diCZrYaI31xNPPFEvTbE7nn76accfRvf+k086yXWYbz6/Tc+xpKBrf1D/REAEREAEWjSBRhU+kDPxE19v0VSr/OIY58aMHl5Jxtg4ScbYQWbdu3e31XpLemfNN0uI0C2d1p3PPvssEj477bRTXuGDS2yOOeaIgqKJDbrmmmv8OQiUprzQEGP/vegid/zxx/vdSUHTYXqti4AIiIAItFwCjRLjE8eF+AkFUPy4ttMjQPyNWbkveFpezDbdZBNbjZaHH354FETMztlzIsSMVj2bEwxBkhSgvEmuTBM9nMtcWrfeeqsV48tnIMS44bZi7B7cVauvvro/zLxfv/76q3eT0TU+aTyp6dOnR0Xli0+KEmhFBERABESgxRJo9BafFksuoxf28ccfRzUj1uawww7zA/q99NJLdWJ0okQJK6QlNgbDPXT11Vf7AQppaWG/jbtjWS2Ghm0Cixn/hxGYMcQLrq8nn3zSj/pM7A2Tlpo9+OCDturH7WGQQjuOaCI9E6cSz0MvLaadsN5c/fr18xOXUgATmFrg8sEHH+y2335798wzz3hRRR7r6k5aBkOUiYAIiIAI1CYBCZ8Wdt8RLYgPRAq9ngjw5Y/pIUaNGlXU1TKGD1N6WHAyQsdEhRVAq4n1qmJm+NCYuJTWGAt+RiyZkArT0Qp40003hbv8gInEFDEtBsZozvzFDfHCnF5mTD5KfZmYlXqRP6nFCDY2Savl1VIEREAERKB2CDSJq6t2cDb/lfJivygXz5LU46mU2g0aNMj35oq7hSiX8XCYOsLMenHZNsvTTjvN965ibq+4MZUGriqbqiI8TvqBAwe6m2++OXKZhceJ4WGwQuoXXiPXjRB64IEHHK6vuHEdDGY4YMCAOqNOx9NpWwREQAREoGUTaJVzJSQPyNKyr9sxCWeW7bjjjquoesxizhxa7du39wP40UJi822VUjBdwJdffnk/sSeBy2GX9mLLwTWFmwrxMWHCBN8iVWxe4oRwU1F3hIvFDzWUnxYvWpyYEmPSpEmOIO1yrr+h8+i4CIiACIhAdRGQq6u67lfRtUUgIDIqNVpVmOOqEiPgOpyBvZSyaL0pZyZ1WoDiI0OXcl6lFQEREAERaJkEatbVZT2Jsnhbs1y3LPJSnURABERABESgWAI1K3zC8WaKhdVU6bJct6ZioPOIgAiIgAiIQGMQqFnhw5QINit4Y4Att0zqRN1kIiACIiACIiAC6RNonRvO/+T0i81+icSdECzLGDRt27aNxoZprprj3mKahhEjRjh6PclEQAREQAREQATSJ1CzvbrSR6kSRUAEREAEREAEsk6gZl1dWb8xqp8IiIAIiIAIiED6BCR80meqEkVABERABERABDJKQMInozdG1RIBERABERABEUifgIRP+kxVogiIgAiIgAiIQEYJSPhk9MaoWiIgAiIgAiIgAukTkPBJn6lKFAEREAEREAERyCgBCZ+M3hhVSwREQAREQAREIH0CEj7pM1WJIiACIiACIiACGSUg4ZPRG6NqiYAIiIAIiIAIpE9Awid9pipRBERABERABEQgowQkfDJ6Y1QtERABERABERCB9AlI+KTPVCWKgAiIgAiIgAhklICET0ZvjKolAiIgAiIgAiKQPgEJn/SZqkQREAEREAEREIGMEpDwyeiNUbVEQAREQAREQATSJyDhkz5TlSgCIiACIiACIpBRAhI+Gb0xqpYIiIAIiIAIiED6BCR80meqEkVABERABERABDJKQMInozdG1RIBERABERABEUifgIRP+kxVogiIgAiIgAiIQEYJSPhk9MaoWiIgAiIgAiIgAukTkPBJn6lKFAEREAEREAERyCgBCZ+M3hhVSwREQAREQAREIH0CEj7pM1WJIiACIiACIiACGSUg4ZPRG6NqiYAIiIAIiIAIpE9Awid9pipRBERABERABEQgowQkfDJ6Y1QtERABERABERCB9AlI+KTPVCWKgAiIgAiIgAhklICET0ZvjKolAiIgAiIgAiKQPgEJn/SZqkQREAEREAEREIGMEpDwyeiNUbVEQAREQAREQATSJ9Am/SJVYloEOnTo4Pr06eM6d+7s5plnnpKKffjhh91DDz1UUh4lFgEREAEREIGWTkDCJ6N3GNEzYMAA17Zt27JquOmmm/p8Ej9l4VMmERABERCBFkpArq6M3lhaesoVPXZJiJ/evXvbppYiIAIiIAIiUPMEJHwy+gjg3krDJH7SoKgyREAEREAEWgoBuboyeidLjekpdBmIH3N9FUpX6Ni0adPcxIkT3ZgxY9zUqVMLJdUxERABERABEcgsAQmfzN6abFUMIdatWzfXpUsXd9FFF5UtftZaay3Xvn37ehdnwuqbb76pdyzrO+aaay7Xs2dPt/TSS7uOHTu6KVOmuE8++cSNHz/e/fTTT2VVf8MNN3StW7d23333nZswYUJZZSiTCIiACIhAfQISPvWZaE8BAsQdEX80atSoAqnyHxo0aJBr0yb/Y/fHH3+49957zw0fPty98cYb+QvKwJE555zTHX744W7NNdd0s81W32u83377uVdffdUNHTq0JAG03nrruYEDB/ornDlzptt3333djz/+mIErVhVEQAREoPoJ1P+2rv5r0hU0MoG04o+SqomAWGGFFdwpp5wSvfyT0jX3vvnnn99dcsklbu21104UPdSPa1lttdXcsGHDXKdOnYqu8g477BClbdWqldt5552jba2IgAiIgAhURiD/T+/KylXuFkwgrfij0aNHu6+//trxcl9yySXdcsst5xBVs88+u6eHu2ehhRZyxx13XOZoDhkyxM0777xRvT788EP37LPPunfffddfBy695Zdf3h9v166dI/0BBxzgaNEqZHPPPbdbZpll6iTp1auXu+GGG+rs04YIiIAIiEB5BCR8yuOmXCkQeOKJJ9ynn35apyTcYAidVVZZxe+n9Wfbbbd1d999d510zbnRd4893HzzzRdV4bacgBsZuP6IyUHUbbfddm6vvfbywo64JlxWV199dZQvaWWnnXby6cNjCCxiq95///1wt9ZFQAREQATKICBXVxnQlKXxCMyYMcO7uZ577rnoJP369cvrTiIRLiVcTrvssosjLe6lQnFEUcGzVojVWX/99d0eOUGz5557OlpraHnJZ1tvs0106IUXXqgjeqIDuZW77rrLIe7MNt98c1vNu9xkk02iY88//3y03rdv32hdKyIgAiIgAuUTUItP+eyUsxEJ0HPsmmuu8aKG3k30mnr00UfrnXHXXXd1tJKEQsdiZAiSPvXUUwsGFh966KEOl1o8OJmg4scee8zH5/z+++/ReWmBQihhpLniiiuiY0kr1157rS8fdx51pCXrtddeS0rqll12WceI3dj06dN97zkYkJd81LEhV1liwdopAiIgAiIQEWjxwoeRiysdw8Zoaf4rI9H4S3ox0Zqyzjrr+JPRWhIXPrjAdtttt7yVIWYIYXLSSScluon69+/vBVVSAYiNjTfe2HVdaSV3SC6dWTgS9meffea7rtuxpOX333/vu7YvtdRS/jBl5hM+tDiZPfXUU+6HH35wkydNcosvsYTv2r7FFlu4+++/35JoKQIiIAIiUAaBFu/qYq4qBEulJtFTKcHS8xMobLbgggvaql/S5XvvvfeO9r3zzju+2/ixxx7rbr/9dmetNLTOsC9uO+64oyNo2Ayh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DTfc0E2dOtX973//y0K1aroOEj41ffur5+JN3JjYsZrbth23/Vpmh0DPnj1dv379fIUefeQRN3LUqOxUrpFqwkjmQ4YM8aV/+umnjpdf2nbQQQe5tdZayxd78cUXu1deeSXtU5RUXlNcc0kVykjiNm3+fM0usMACGalVbVdDrq7avv9VdfWIm3xd3avqQmqssjT184XPX5fllquJq+cXvl3zsssu2yjXvFyOpZ1jkUUWaZRzlFJoU1xzKfXJStprrrnGff755+6FF15wDzzwQFaqVdP1+FOK1jQGXXy1ELCWHWvpqZZ6q54iIAK1SYAfa0k/2GqTRjauWsInG/dBtZhF4IwzzsgMi06dOrl1113XzTvvvO7ll192b731lvvjjz/cQgst5Nq1a+d+++03hxsjn/ErfPXVV3csP/zwQ/fSSy+577//PjE55+IX88yZM93EiRN9Gs679tprO1pM2DdhwgQfI5BYQLCTuq2zzjpuqaWWclOmTPF1nzRpUpDiz1VG3F5wwQX9Dn6V/vTTT26FFVbw1/3DDz+422677c/Es9bIg4uFaUomT57s3n33XX998YRLL720m2222VzHjh2jQ1zjMsss47dhB8O4wRduiy66qPviiy/ciy++6L788st4smi7nGuIMhe5wnV07drV0cqCS+ejjz7yzwPMQrNng7qb4eqwa+Z+5HsGFl54YX/faMXhfr/55pvuq6++smL8cvbZZ/fPAxtzzz13dIy8nGPGjBnuk08+ifaHK6U8F2G+htZLuWZ7zikThq1atfLPOGzffvtt99RTT9U7XTFc6mWatQMmlE0dP/vsM/faa68V/MxaOdxvnvHlunRxM37/3b366qvunXfe8Z9/S1PskuefzzLGNfMdkmR8nlZcccXouedzxZ8sfQKtevToMTP9YlVirRFobsFy3HHHpYYcocL12JeVFYwoGTFihNtqq63c/PPP73fvuuuudjha8mI8+eSToxdUdCC38vXXX7sTTzyx3gvt6quvdjbtB7EbnJ+XRGic/5FcjMywYcPC3XXWjz76aP8i4YUSGgLjnHPO8SIi3P+vf/3LrbHGGn7XmDFj3CabbOLmmmuuKEl4fbnvCte/f/86xy3hd9995/773/9GcSZMWHvdddfZ4cTlVVdd5e6///7oGC982Cy++OLRPlvhhc51P/bYY7YrWpZyDVGm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)" + ], + "metadata": { + "id": "cVofNXVW-EMo" + } + }, + { + "cell_type": "code", + "source": [ + "'''\n", + " {\n", + " \"doc\": [\n", + " {\n", + " \"prompt_states\": {\n", + " \"type\": \"Prompt\",\n", + " \"data\": {\n", + " \"_components\": {\n", + " \"_ordered_dict\": true,\n", + " \"data\": []\n", + " },\n", + " \"_parameters\": {\n", + " \"_ordered_dict\": true,\n", + " \"data\": []\n", + " },\n", + " \"training\": false,\n", + " \"teacher_mode\": false,\n", + " \"tracing\": false,\n", + " \"name\": \"Prompt\",\n", + " \"_init_args\": {\n", + " \"template\": null,\n", + " \"prompt_kwargs\": {}\n", + " },\n", + " \"template\": \" You are a doctor User: {{input_str}}\",\n", + " \"prompt_variables\": [\n", + " \"input_str\"\n", + " ],\n", + " \"prompt_kwargs\": {}\n", + " }\n", + " },\n", + " \"time_stamp\": \"2024-11-29T12:36:33.302956\"\n", + " }\n", + " ]\n", + "}\n", + "'''" + ], + "metadata": { + "id": "dPd9i6_t7ERJ" + }, + "execution_count": null, + "outputs": [] + } + ] +} diff --git a/tutorials/adalflow_function_calls.py b/tutorials/adalflow_function_calls.py new file mode 100644 index 00000000..184e2b88 --- /dev/null +++ b/tutorials/adalflow_function_calls.py @@ -0,0 +1,108 @@ +""" +This script demonstrates the usage of AdalFlow's Tool Helper functionality. +It can be run independently to showcase function calling capabilities. +""" + +from adalflow.components import Generator +from adalflow.components.model_client import OpenAIClient +from adalflow.utils import setup_env +from typing import List, Dict +import json + + +def setup_generator(): + """Initialize and configure the Generator with OpenAI client.""" + setup_env() + generator = Generator( + model_client=OpenAIClient(), + model_kwargs={"model": "gpt-3.5-turbo", "temperature": 0, "max_tokens": 1000}, + ) + return generator + + +def define_tools() -> List[Dict]: + """Define the available tools/functions that can be called.""" + return [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the weather in a location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location", "unit"], + }, + }, + } + ] + + +def get_weather(location: str, unit: str) -> str: + """Mock function to simulate weather data retrieval.""" + # This is a mock implementation + weather_data = { + "San Francisco, CA": {"celsius": 20, "fahrenheit": 68}, + "New York, NY": {"celsius": 22, "fahrenheit": 72}, + } + + if location in weather_data: + temp = weather_data[location][unit] + return f"The temperature in {location} is {temp}°{'C' if unit == 'celsius' else 'F'}" + return f"Weather data not available for {location}" + + +def process_function_calls(generator: Generator, query: str): + """Process user query and handle any function calls.""" + # Get the response from the model + response = generator.generate(prompt_kwargs={"query": query}, tools=define_tools()) + + # Check if the response includes a function call + if hasattr(response, "tool_calls") and response.tool_calls: + for tool_call in response.tool_calls: + if tool_call.function.name == "get_weather": + # Parse the function arguments + args = json.loads(tool_call.function.arguments) + + # Call the function with the provided arguments + weather_result = get_weather(args["location"], args["unit"]) + + # Generate final response incorporating the function result + final_response = generator.generate( + prompt_kwargs={"query": query}, + tools=define_tools(), + tool_results=[ + {"tool_call_id": tool_call.id, "output": weather_result} + ], + ) + return final_response + + return response + + +def main(): + """Main function to demonstrate tool helper functionality.""" + # Initialize generator + generator = setup_generator() + + # Example queries + queries = [ + "What's the weather like in San Francisco?", + "Tell me the temperature in New York in Celsius", + ] + + # Process each query + for query in queries: + print(f"\nQuery: {query}") + response = process_function_calls(generator, query) + print(f"Response: {response}") + + +if __name__ == "__main__": + main() diff --git a/tutorials/adalflow_logger.py b/tutorials/adalflow_logger.py new file mode 100644 index 00000000..e4c4bb7e --- /dev/null +++ b/tutorials/adalflow_logger.py @@ -0,0 +1,143 @@ +""" +This script demonstrates the usage of AdalFlow's Logger functionality. +It can be run independently to showcase logging capabilities. +""" + +from adalflow.components import Generator +from adalflow.components.model_client import OpenAIClient +from adalflow.utils import setup_env +from adalflow.utils.logger import get_logger +import logging +from typing import Dict, Any +import json + + +def setup_logging(log_file: str = "adalflow.log") -> logging.Logger: + """ + Initialize and configure the logger. + + Args: + log_file: Name of the log file + + Returns: + Configured logger instance + """ + logger = get_logger(__name__) + + # Add file handler if not already present + if not any(isinstance(handler, logging.FileHandler) for handler in logger.handlers): + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter( + logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + ) + logger.addHandler(file_handler) + + return logger + + +def setup_generator() -> Generator: + """ + Initialize and configure the Generator with OpenAI client. + + Returns: + Configured Generator instance + """ + setup_env() + return Generator( + model_client=OpenAIClient(), + model_kwargs={"model": "gpt-3.5-turbo", "temperature": 0, "max_tokens": 1000}, + ) + + +def process_query( + generator: Generator, query: str, logger: logging.Logger +) -> Dict[str, Any]: + """ + Process a query using the generator and log the interaction. + + Args: + generator: The configured Generator instance + query: User query to process + logger: Logger instance for recording the interaction + + Returns: + Dictionary containing the query and response + """ + logger.info(f"Processing query: {query}") + + try: + # Generate response + response = generator.generate(prompt_kwargs={"query": query}) + + # Log successful response + logger.info(f"Generated response: {response}") + + return {"query": query, "response": str(response), "status": "success"} + + except Exception as e: + # Log error if generation fails + logger.error(f"Error processing query: {str(e)}") + return {"query": query, "response": None, "status": "error", "error": str(e)} + + +def analyze_logs(log_file: str, logger: logging.Logger) -> Dict[str, int]: + """ + Analyze the log file to gather statistics. + + Args: + log_file: Path to the log file + logger: Logger instance for recording the analysis + + Returns: + Dictionary containing log statistics + """ + stats = {"total_queries": 0, "successful_queries": 0, "failed_queries": 0} + + try: + with open(log_file, "r") as f: + for line in f: + if "Processing query:" in line: + stats["total_queries"] += 1 + if "Generated response:" in line: + stats["successful_queries"] += 1 + if "Error processing query:" in line: + stats["failed_queries"] += 1 + + logger.info(f"Log analysis complete: {json.dumps(stats, indent=2)}") + return stats + + except Exception as e: + logger.error(f"Error analyzing logs: {str(e)}") + return stats + + +def main(): + """Main function to demonstrate logger functionality.""" + # Setup + log_file = "adalflow.log" + logger = setup_logging(log_file) + generator = setup_generator() + + # Example queries + queries = [ + "What is artificial intelligence?", + "Explain the concept of machine learning.", + "Tell me about neural networks.", + ] + + # Process queries + results = [] + for query in queries: + result = process_query(generator, query, logger) + results.append(result) + print(f"\nQuery: {query}") + print(f"Response: {result['response']}") + + # Analyze logs + stats = analyze_logs(log_file, logger) + print("\nLog Analysis:") + print(json.dumps(stats, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tutorials/adalflow_tracing.py b/tutorials/adalflow_tracing.py new file mode 100644 index 00000000..d49008e6 --- /dev/null +++ b/tutorials/adalflow_tracing.py @@ -0,0 +1,88 @@ +""" +This script demonstrates the usage of AdalFlow's tracing functionality. +It shows how to track Generator states and changes during development. +""" + +import os +from getpass import getpass +from adalflow.tracing import trace_generator_states +from adalflow.core import Generator +import adalflow as adal +from adalflow.components.model_client import OpenAIClient + + +def setup_environment(): + """Setup API keys and environment variables.""" + # In a production environment, you might want to use environment variables + # or a configuration file instead of getpass + if "OPENAI_API_KEY" not in os.environ: + openai_api_key = getpass("Please enter your OpenAI API key: ") + os.environ["OPENAI_API_KEY"] = openai_api_key + + if "GROQ_API_KEY" not in os.environ: + groq_api_key = getpass("Please enter your GROQ API key: ") + os.environ["GROQ_API_KEY"] = groq_api_key + + print("API keys have been set.") + + +# Define the template for the doctor QA system +template_doc = r""" You are a doctor User: {{input_str}}""" + + +@trace_generator_states() +class DocQA(adal.Component): + """ + A component that uses a Generator to answer medical questions. + The @trace_generator_states decorator automatically tracks changes + to any Generator attributes in this class. + """ + + def __init__(self): + super(DocQA, self).__init__() + self.generator = Generator( + template=template_doc, + model_client=OpenAIClient(), + model_kwargs={"model": "gpt-4-turbo-preview"}, + ) + + def call(self, query: str) -> str: + """ + Process a medical query and return the response. + + Args: + query: The medical question to be answered + + Returns: + The generated response from the doctor AI + """ + return self.generator(prompt_kwargs={"input_str": query}).data + + +def main(): + """Main function to demonstrate tracing functionality.""" + # Setup environment + setup_environment() + + # Initialize the DocQA component + doc_qa = DocQA() + + # Example queries + queries = [ + "What are the common symptoms of the flu?", + "How can I manage my allergies?", + "What should I do for a minor burn?", + ] + + # Process each query + for query in queries: + print(f"\nQuery: {query}") + response = doc_qa.call(query) + print(f"Response: {response}") + + print("\nNote: Generator states have been logged to the traces directory.") + print("You can find the logs in: ./traces/DocQA/generator_state_trace.json") + + +if __name__ == "__main__": + main()