diff --git a/docs/context_grounding.md b/docs/context_grounding.md new file mode 100644 index 0000000..8f547fd --- /dev/null +++ b/docs/context_grounding.md @@ -0,0 +1,73 @@ +# Context Grounding + +Context Grounding Service allows you to: + +- Search through indexed documents using natural language queries +- Ground LLM responses in your organization's specific information +- Retrieve context-relevant documents for various applications + + +You will need to create an index in `Context Grounding` to use this feature. To create an index go to organization `Orchestrator` -> the folder where you'd like to create an index -> `Indexes`. There you can create a new index from a storage bucket which you've added documents to. See the full documentation [here](https://docs.uipath.com/automation-cloud/automation-cloud/latest/admin-guide/about-context-grounding) for more details. + + +## ContextGroundingRetriever + +The `ContextGroundingRetriever` is a document retrieval system that uses vector search to efficiently find and retrieve relevant information from your document store. + +### Basic Usage + +Create a simple retriever by specifying an index name: + +```python +from uipath_llamaindex.retrievers import ContextGroundingRetriever + +retriever = ContextGroundingRetriever(index_name = "Company Policy Context") +print(retriever.retrieve("What is the company policy on remote work?")) +``` + +## ContextGroundingQueryEngine + +Query engines are interfaces that allows you to ask question over your data. The `ContextGroundingQueryEngine` is a query engine system that leverages the `ContextGroundingRetriever`. + +### Basic Usage + +Create a simple query engine by specifying an index name and a synthesizer strategy: + +```python +from uipath_llamaindex.query_engines import ContextGroundingQueryEngine +from llama_index.core.response_synthesizers.type import ResponseMode +from llama_index.core import get_response_synthesizer + +synthesizer = get_response_synthesizer(ResponseMode.SIMPLE_SUMMARIZE) +query_engine = ContextGroundingQueryEngine(index_name = "Company Policy Context", response_synthesizer=synthesizer) +print(query_engine.query("What is the company policy on remote work?")) +``` + +### Integration with LlamaIndex Tools + +You can easily integrate the query engine with LlamaIndex's tool system: + +```python +from uipath_llamaindex.query_engines import ContextGroundingQueryEngine +from llama_index.core.response_synthesizers.type import ResponseMode +from llama_index.core import get_response_synthesizer + +query_engine = ContextGroundingQueryEngine( + index_name="Company Policy Context", + response_synthesizer=get_response_synthesizer(ResponseMode.REFINE), +) +query_engine_tools = [QueryEngineTool( + query_engine=query_engine, + metadata=ToolMetadata( + name="Company policy", + description="Information about general company policy", + ) + )] +# You can use the tool in your agents +react_agent = ReActAgent.from_tools(query_engine_tools) +response = react_agent.chat("Answer user questions as best as you can using the query engine tool.") +``` + + + +> **HINT:** Check our [travel-helper-RAG-agent sample](https://github.com/UiPath/uipath-llamaindex-python/tree/main/samples/travel-helper-RAG-agent) to see context grounding query engines in action. diff --git a/docs/sample_images/RAG/create-index-1.png b/docs/sample_images/RAG/create-index-1.png new file mode 100644 index 0000000..00e5ea7 Binary files /dev/null and b/docs/sample_images/RAG/create-index-1.png differ diff --git a/docs/sample_images/RAG/create-index-2.png b/docs/sample_images/RAG/create-index-2.png new file mode 100644 index 0000000..a237c9f Binary files /dev/null and b/docs/sample_images/RAG/create-index-2.png differ diff --git a/docs/sample_images/RAG/create-storage-bucket-1.png b/docs/sample_images/RAG/create-storage-bucket-1.png new file mode 100644 index 0000000..f594cc7 Binary files /dev/null and b/docs/sample_images/RAG/create-storage-bucket-1.png differ diff --git a/docs/sample_images/RAG/create-storage-bucket-2.png b/docs/sample_images/RAG/create-storage-bucket-2.png new file mode 100644 index 0000000..e0ae86e Binary files /dev/null and b/docs/sample_images/RAG/create-storage-bucket-2.png differ diff --git a/docs/sample_images/RAG/indexes.png b/docs/sample_images/RAG/indexes.png new file mode 100644 index 0000000..bb91eba Binary files /dev/null and b/docs/sample_images/RAG/indexes.png differ diff --git a/docs/sample_images/RAG/job-info.png b/docs/sample_images/RAG/job-info.png new file mode 100644 index 0000000..3e2e16d Binary files /dev/null and b/docs/sample_images/RAG/job-info.png differ diff --git a/docs/sample_images/RAG/run-process-1.png b/docs/sample_images/RAG/run-process-1.png new file mode 100644 index 0000000..9897bba Binary files /dev/null and b/docs/sample_images/RAG/run-process-1.png differ diff --git a/docs/sample_images/RAG/run-process-2.png b/docs/sample_images/RAG/run-process-2.png new file mode 100644 index 0000000..3a51458 Binary files /dev/null and b/docs/sample_images/RAG/run-process-2.png differ diff --git a/docs/sample_images/RAG/storage_buckets.png b/docs/sample_images/RAG/storage_buckets.png new file mode 100644 index 0000000..5efc9d0 Binary files /dev/null and b/docs/sample_images/RAG/storage_buckets.png differ diff --git a/docs/sample_images/RAG/traces.png b/docs/sample_images/RAG/traces.png new file mode 100644 index 0000000..6d88cd0 Binary files /dev/null and b/docs/sample_images/RAG/traces.png differ diff --git a/pyproject.toml b/pyproject.toml index 0f92b85..c8adb67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-llamaindex" -version = "0.0.25" +version = "0.0.26" description = "UiPath LlamaIndex SDK" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10" @@ -90,3 +90,4 @@ name = "testpypi" url = "https://test.pypi.org/simple/" publish-url = "https://test.pypi.org/legacy/" explicit = true + diff --git a/samples/simple-hitl-agent/uv.lock b/samples/simple-hitl-agent/uv.lock index e223a57..c3b4098 100644 --- a/samples/simple-hitl-agent/uv.lock +++ b/samples/simple-hitl-agent/uv.lock @@ -914,9 +914,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/43/d1e53b856bc9a96548dea06ee96a6b61260d30ae1a52e885e63c13e47e84/llama_cloud_services-0.6.23-py3-none-any.whl", hash = "sha256:f02dc6531a314c179064c28e296c961ad92ad82ca254704eba1728af084598a6", size = 37116 }, ] +[[package]] +name = "llama-default-escalation" +version = "0.0.4" +source = { virtual = "." } +dependencies = [ + { name = "llama-index-llms-openai" }, + { name = "uipath-llamaindex" }, +] + +[package.metadata] +requires-dist = [ + { name = "llama-index-llms-openai", specifier = ">=0.2.2" }, + { name = "uipath-llamaindex", specifier = "==0.0.26.dev1000320092", index = "https://test.pypi.org/simple/" }, +] + [[package]] name = "llama-index" -version = "0.12.37" +version = "0.12.38" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llama-index-agent-openai" }, @@ -932,9 +947,9 @@ dependencies = [ { name = "llama-index-readers-llama-parse" }, { name = "nltk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/05/b48365c8819d19df365d11516cdb6651963ffc1c21427ccdb004ece6a9d5/llama_index-0.12.37.tar.gz", hash = "sha256:f0644b859ab0d659e00f1179540dd14101b97fdebc813a28ae23b89a0ffd27b1", size = 8018 } +sdist = { url = "https://files.pythonhosted.org/packages/81/27/4f25f8bb095941d84d8f59e30983142e342a9b369be06a885f32c6ff260e/llama_index-0.12.38.tar.gz", hash = "sha256:97a19b92aaae54f559d4252f609c74dce4463d1ee312e3bcbc6a7d70d98a1bf8", size = 8063 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/22/524860c87ef77aa6cf65df6a674ad6b47cdabc9d95ef77b02c0785ba25ef/llama_index-0.12.37-py3-none-any.whl", hash = "sha256:fe3da52f3f23cb2acc9eec6de7c8f85f9e45a4c2f773067c548c74497a13ed81", size = 7052 }, + { url = "https://files.pythonhosted.org/packages/a0/cb/b396f5dd6d4eb06b6af936b6fed41c3229872ffd3b4a9e182e3928bb38e2/llama_index-0.12.38-py3-none-any.whl", hash = "sha256:b8662c770298856f1c76712b0eb527724e11ee27c0d34120679d50844db9c392", size = 7083 }, ] [[package]] @@ -967,7 +982,7 @@ wheels = [ [[package]] name = "llama-index-core" -version = "0.12.37" +version = "0.12.42" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -995,9 +1010,23 @@ dependencies = [ { name = "typing-inspect" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/96/cb236b066184c5154b391d5028a56d6ca1f9aec69edf6cbef6c9a6ff1eff/llama_index_core-0.12.37.tar.gz", hash = "sha256:c2c13c36229bc9cfffc7bf1daca888e382d5ae484af96273c53f001ed84dc253", size = 7289254 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/67/ef1fc8c3b0676b524fd6b67a0e3e082996d768d227ed2b4b39f78119253b/llama_index_core-0.12.42.tar.gz", hash = "sha256:cff21fe15610826997c876a6b1d28d52727932c5f9c2af04b23e041a10f40a24", size = 7292833 } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/e8/1a2c5ca5d2af0f1741964259f09d1c8b7ce27b0e913f39c894038088d430/llama_index_core-0.12.37-py3-none-any.whl", hash = "sha256:1a841db0dcdace161d2fcecfee0ab94d28e50196973974c5142f0d18d3f7663f", size = 7661867 }, + { url = "https://files.pythonhosted.org/packages/1f/92/dc47b702da141d0f617ee86d26b5bdb62ed9a52518d1a5f271e91337e4a7/llama_index_core-0.12.42-py3-none-any.whl", hash = "sha256:0534cd9a4f6113175aa406a47ae9a683b5a43fd55532e9dbbffa96838ff18e07", size = 7665930 }, +] + +[[package]] +name = "llama-index-embeddings-azure-openai" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-embeddings-openai" }, + { name = "llama-index-llms-azure-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/f9/7027f5984e690d6b171d44e944aba0d6fedcedd49c08962ea67dce8a463f/llama_index_embeddings_azure_openai-0.3.8.tar.gz", hash = "sha256:24cff674364cffef4798f5faf220b557115ff9ea9a4f5e643785bd89c595928c", size = 4760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/d0/c13abece3456e7de7d359e70bff8b9d8a53f4908b9f3783c8177f1be9a78/llama_index_embeddings_azure_openai-0.3.8-py3-none-any.whl", hash = "sha256:e5dc9c9103b914c4435a59ed0d7965b3a3eeeafb9868a5876c46315bb49f1e54", size = 4400 }, ] [[package]] @@ -1026,6 +1055,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f4/5decd79fd7f2f0e44c5689af62497447e86832e876b7dad11903259de5f9/llama_index_indices_managed_llama_cloud-0.6.11-py3-none-any.whl", hash = "sha256:64e82e2ac178cd3721b76c0817edd57e05a3bd877c412b4148d3abbdeea62d59", size = 14272 }, ] +[[package]] +name = "llama-index-llms-azure-openai" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-identity" }, + { name = "httpx" }, + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/cf/23c516c5a61c9b7a481c383862ebd99cc5e6a35f820dab871bb12b453b71/llama_index_llms_azure_openai-0.3.2.tar.gz", hash = "sha256:c6ae4e6d896abc784a1d60e02a537c91e019317de69d02256424eab80c988646", size = 6287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/2f/efab9bd63f7f3dd9ec83c945c9516beffcff14ffd4a8187150b46e69124b/llama_index_llms_azure_openai-0.3.2-py3-none-any.whl", hash = "sha256:1a831035129042327f50d243a17918c481dfae39fd5a7ddaaaa0a712fb18ab8e", size = 7283 }, +] + [[package]] name = "llama-index-llms-openai" version = "0.3.42" @@ -1109,21 +1153,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/4f/e30d4257fe9e4224f5612b77fe99aaceddae411b2e74ca30534491de3e6f/llama_index_readers_llama_parse-0.4.0-py3-none-any.whl", hash = "sha256:574e48386f28d2c86c3f961ca4a4906910312f3400dd0c53014465bfbc6b32bf", size = 2472 }, ] -[[package]] -name = "llama-new" -version = "0.0.2" -source = { virtual = "." } -dependencies = [ - { name = "llama-index-llms-openai" }, - { name = "uipath-llamaindex" }, -] - -[package.metadata] -requires-dist = [ - { name = "llama-index-llms-openai", specifier = ">=0.2.2" }, - { name = "uipath-llamaindex", specifier = "==0.0.20.dev1000220026", index = "https://test.pypi.org/simple/" }, -] - [[package]] name = "llama-parse" version = "0.6.23" @@ -1514,7 +1543,7 @@ wheels = [ [[package]] name = "openinference-instrumentation-llama-index" -version = "4.2.1" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openinference-instrumentation" }, @@ -1525,9 +1554,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/ef/ec75993016edb1bd7c7b9de50c3a6db039da0ab5da1d3aaa1216a92cb360/openinference_instrumentation_llama_index-4.2.1.tar.gz", hash = "sha256:615603bd9bcc6bbf24e5ea2d2cfcd11e484102b8d8d131f7777543eca1f4d2b7", size = 58578 } +sdist = { url = "https://files.pythonhosted.org/packages/d5/10/57179fb827705b957921c9a1060143b89dc26025db7b7918ae53711f6833/openinference_instrumentation_llama_index-4.3.0.tar.gz", hash = "sha256:92107bcd61131fd41abf9407a0f6d25d6be692e04987b54de2aa4ecbd098b86c", size = 59253 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/06/aaca472113b499924cbc0b7ea2dd18d61d1f4ad7120f9fb0fd30bb4d8eec/openinference_instrumentation_llama_index-4.2.1-py3-none-any.whl", hash = "sha256:74102a2f1532dfac069ff8e241d498695f481bc3fa6c017d4df6c285fbc7d6cd", size = 27663 }, + { url = "https://files.pythonhosted.org/packages/56/a9/1c4d10ef2a9cf12b0e1446eb21d6ca04c4dfaba417999a895928afcdd5d2/openinference_instrumentation_llama_index-4.3.0-py3-none-any.whl", hash = "sha256:c1559d397f8ed98f420ce0ebe0f5246d33aadd67f144d59c6f66f3258f73e854", size = 28311 }, ] [[package]] @@ -2584,7 +2613,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.0.60" +version = "2.0.66" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-monitor-opentelemetry" }, @@ -2598,23 +2627,25 @@ dependencies = [ { name = "tenacity" }, { name = "tomli" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/ea/b817429c94cae5a169e7135f8fb0e9e1574d1e9840b10fea23e427279c24/uipath-2.0.60.tar.gz", hash = "sha256:d18066ab93107b1f1b5d6d41f2745d0973bf39c39d8d4b2914467f28e64d0904", size = 1819400 } +sdist = { url = "https://files.pythonhosted.org/packages/16/51/dd3c07e194d9899ada9abb4bdfd8198bbe18f6db3e1451c670c6ec3484f5/uipath-2.0.66.tar.gz", hash = "sha256:81592b8ac4891220b16dce94eb1f3b5b4ad59fe05102f77a3fd07263e824e1b5", size = 1829440 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/53/e0577f2e8f0bc01392fe2d63f08d8392dd380a0407a2d702e4a3dcef8ce4/uipath-2.0.60-py3-none-any.whl", hash = "sha256:ce4f24f0241c98ed4711ddc32da9778b1e38159028b950ca5793a7dcff60a280", size = 117115 }, + { url = "https://files.pythonhosted.org/packages/e2/82/8c13b337aabf49eddc1d3a8ce315a16f7bab83f339746c8569c61b2ceddc/uipath-2.0.66-py3-none-any.whl", hash = "sha256:4f75b1edcb543ba495c96b2647aed0d64bc707119a3ef7c03948576cb27083b7", size = 125292 }, ] [[package]] name = "uipath-llamaindex" -version = "0.0.20.dev1000220026" +version = "0.0.26.dev1000320092" source = { registry = "https://test.pypi.org/simple/" } dependencies = [ { name = "llama-index" }, + { name = "llama-index-embeddings-azure-openai" }, + { name = "llama-index-llms-azure-openai" }, { name = "openinference-instrumentation-llama-index" }, { name = "uipath" }, ] -sdist = { url = "https://test-files.pythonhosted.org/packages/98/ce/6d3c35c57148a6cd9495c9311a7958cf0ab3a7c8e3a658d394d12c76fca7/uipath_llamaindex-0.0.20.dev1000220026.tar.gz", hash = "sha256:60644f798277b4008d4b00c9c8c885f6b8cc2d55c2d296ee5f82d763e60e7455", size = 590673 } +sdist = { url = "https://test-files.pythonhosted.org/packages/61/e5/32152c6c4f8e99a07b8dec3b1e76f97c0d1fa9689415cd0b12c4e09d5bd0/uipath_llamaindex-0.0.26.dev1000320092.tar.gz", hash = "sha256:2aff44975a6ab03565269603a8861e5a0e33f6b8788c2ed57d38afe42e9eadf5", size = 868740 } wheels = [ - { url = "https://test-files.pythonhosted.org/packages/80/c7/47c37cc4162d90e7be67123af7d101680629003f27284ba8a6190aa87334/uipath_llamaindex-0.0.20.dev1000220026-py3-none-any.whl", hash = "sha256:9a3ba88bf8003a8aeb2a0440e34b7cc7c1a571b957c599927149c605e3aa58e6", size = 19396 }, + { url = "https://test-files.pythonhosted.org/packages/cd/48/9b76d165dcac2de09a267da87602993c34b9fe9894b75bd2e8b73591c8b7/uipath_llamaindex-0.0.26.dev1000320092-py3-none-any.whl", hash = "sha256:9243d9c6b003b49ee3278cf4016fe3edfa412a1c1ec66eb6a5c3c61a395ab9ed", size = 20862 }, ] [[package]] diff --git a/samples/travel-helper-RAG-agent/README.md b/samples/travel-helper-RAG-agent/README.md new file mode 100644 index 0000000..1c4e2de --- /dev/null +++ b/samples/travel-helper-RAG-agent/README.md @@ -0,0 +1,200 @@ +# Travel helper RAG agent + +## Overview + +The **Travel helper RAG agent** project demonstrates the implementation of a Retrieval-Augmented Generation (RAG) system using [UiPath Context Grounding](https://docs.uipath.com/automation-cloud/automation-cloud/latest/admin-guide/about-context-grounding). + +It consists of a Llama Index agent that utilizes two `ContextGroundingQueryEngines`: one to retrieve relevant data about the company's travel policy and another to assess an individual's personal travel preferences. + +## System Architecture + +### Agent Graph + +```mermaid +flowchart TD + step__done["_done"]:::stepStyle + step_add_data_to_index["add_data_to_index"]:::stepStyle + step_combine_and_interpret_answers["combine_and_interpret_answers"]:::stepStyle + step_create_sub_questions_plan["create_sub_questions_plan"]:::stepStyle + step_handle_sub_question["handle_sub_question"]:::stepStyle + step_wait_for_index_ingestion["wait_for_index_ingestion"]:::stepStyle + step_workflow_entrypoint["workflow_entrypoint"]:::stepStyle + event_AddDataToIndexEvent([
AddDataToIndexEvent
]):::defaultEventStyle + event_WaitForIndexIngestion([WaitForIndexIngestion
]):::defaultEventStyle + event_AnswerEvent([AnswerEvent
]):::defaultEventStyle + event_OutputEvent([OutputEvent
]):::stopEventStyle + event_QueryEvent([QueryEvent
]):::defaultEventStyle + event_SubQuestionEvent([SubQuestionEvent
]):::defaultEventStyle + event_CustomStartEvent([CustomStartEvent
]):::defaultEventStyle + event_OutputEvent --> step__done + step_add_data_to_index --> event_WaitForIndexIngestion + event_AddDataToIndexEvent --> step_add_data_to_index + step_combine_and_interpret_answers --> event_OutputEvent + event_AnswerEvent --> step_combine_and_interpret_answers + step_create_sub_questions_plan --> event_SubQuestionEvent + event_QueryEvent --> step_create_sub_questions_plan + step_handle_sub_question --> event_AnswerEvent + event_SubQuestionEvent --> step_handle_sub_question + step_wait_for_index_ingestion --> event_QueryEvent + step_wait_for_index_ingestion --> event_OutputEvent + event_WaitForIndexIngestion --> step_wait_for_index_ingestion + step_workflow_entrypoint --> event_QueryEvent + step_workflow_entrypoint --> event_AddDataToIndexEvent + event_CustomStartEvent --> step_workflow_entrypoint + classDef stepStyle fill:#f2f0ff,line-height:1.2 + classDef externalStyle fill:#f2f0ff,line-height:1.2 + classDef defaultEventStyle fill-opacity:0 + classDef stopEventStyle fill:#bfb6fc + classDef inputRequiredStyle fill:#f2f0ff,line-height:1.2 +``` + +## Agent Responsibilities + +- Accepts a user query with details about an upcoming business trip. +- Verifies if new data should be added to RAG indexes. +- Initializes two `QueryEngineTools`, each based on distinct context-grounding indexes. +- Breaks down the initial query into multiple `sub-questions`. +- Assigns ReAct agents to address each `sub-question`. +- Combines the responses to generate a travel summary, including the permitted budget, employee preferences, and recommendations. + +## Steps to Execute Project on UiPath Cloud Platform +### Prerequisites +1. **Create two Orchestrator Storage Buckets** + +For this demo we'll create them in the _Shared_ folder. + + + + + + + + +2. **Create two Context Grounding Indexes** + +Next, we'll create two Context Grounding Indexes. + + + +When configuring _Index General Details_, we'll select the previously created storage buckets as data sources. + +Example for `company_policy` index: + + + + + +That's it! Next, we'll deploy the agent. + +### Deploy the agent +1. **Clone the Repository** + ```bash + git clone https://github.com/UiPath/uipath-llamaindex-python.git + ``` + +2. **Navigate to the Sample Directory** + - **Windows:** + ```bash + cd .\uipath-llamaindex-python\samples\travel-helper-RAG-agent + ``` + + - **Unix-like Systems (Linux, macOS):** + ```bash + cd ./uipath-llamaindex-python/samples/travel-helper-RAG-agent + ``` + +3. **Create and Activate a Virtual Python Environment** + ```bash + pip install uv + uv venv -p 3.11 .venv + .venv\Scripts\activate # Windows + source .venv/bin/activate # Unix-like Systems + uv sync + ``` + +4. **Authenticate with UiPath Cloud Platform** + ```bash + uipath auth + ``` + > **Note:** After successful authentication in the browser, select the tenant for publishing the agent package. +``` +👇 Select tenant: + 0: DefaultTenant + 1: Tenant2 + 2: Tenant3 +... +Select tenant: 2 +``` + +5. **Package and Publish Agents** + ```bash + uipath pack + uipath publish --my-workspace + ``` +``` +⠋ Publishing most recent package: travel-helper-RAG-agent.1.0.0.nupkg ... +✓ Package published successfully! +⠇ Getting process information ... +🔗 Process configuration link: [LINK] +💡 Use the link above to configure any environment variables +``` +> Note: when publishing to _my-workspace_ a process will be auto-provisioned for you. + +6. **Run the agent on UiPath Cloud Platform** + + There are two ways we can run the agent: + 1. **Invoke it from command line**: + ```bash + > uipath invoke agent --file .\input.json + ⠴ Loading configuration ... + ⠴ Starting job ... + ✨ Job started successfully! + 🔗 Monitor your job here: [LINK] + ``` + 2. Run it from Orchestrator _Processes_ page: + +  + +  + +8. **Conclusions** + +We can monitor our agent execution from the job info panel. + + + +On traces tab, we can see the steps that our agent took to complete the task, along with the flow diagram. + + + +> **IMPORTANT**❗: The `add_data_to_index` should be set to **true** +> every time new data is added to the sample_data directory. If one wishes to add other file types such as _.csv, .json, .xml etc_ to be ingested by the context grounding index +> the settings section from `uipath.json` file should be updated. See example below: +```json + "settings": { + "fileExtensionsIncluded": [ + ".txt", + ".xlsx", + ".csv", + ".xml", + ... + ] + } +``` + + +## Local Debugging +You can run and debug the agent locally: + +```bash +uipath run agent --file input.json +# optional: use the debug mode +uipath run agent --file input.json --debug +``` +> Check [here](https://uipath.github.io/uipath-python/cli/#run) our CLI documentation. + + +> **_NOTE:_** This assumes that the indexes are created in the folder identified by the _index_folder_path_ parameter. + + + diff --git a/samples/travel-helper-RAG-agent/agent.mermaid b/samples/travel-helper-RAG-agent/agent.mermaid new file mode 100644 index 0000000..ce4de16 --- /dev/null +++ b/samples/travel-helper-RAG-agent/agent.mermaid @@ -0,0 +1,35 @@ +flowchart TD + step__done["_done"]:::stepStyle + step_add_data_to_index["add_data_to_index"]:::stepStyle + step_combine_and_interpret_answers["combine_and_interpret_answers"]:::stepStyle + step_create_sub_questions_plan["create_sub_questions_plan"]:::stepStyle + step_handle_sub_question["handle_sub_question"]:::stepStyle + step_wait_for_index_ingestion["wait_for_index_ingestion"]:::stepStyle + step_workflow_entrypoint["workflow_entrypoint"]:::stepStyle + event_AddDataToIndexEvent([AddDataToIndexEvent
]):::defaultEventStyle + event_WaitForIndexIngestion([WaitForIndexIngestion
]):::defaultEventStyle + event_AnswerEvent([AnswerEvent
]):::defaultEventStyle + event_OutputEvent([OutputEvent
]):::stopEventStyle + event_QueryEvent([QueryEvent
]):::defaultEventStyle + event_SubQuestionEvent([SubQuestionEvent
]):::defaultEventStyle + event_CustomStartEvent([CustomStartEvent
]):::defaultEventStyle + event_OutputEvent --> step__done + step_add_data_to_index --> event_WaitForIndexIngestion + event_AddDataToIndexEvent --> step_add_data_to_index + step_combine_and_interpret_answers --> event_OutputEvent + event_AnswerEvent --> step_combine_and_interpret_answers + step_create_sub_questions_plan --> event_SubQuestionEvent + event_QueryEvent --> step_create_sub_questions_plan + step_handle_sub_question --> event_AnswerEvent + event_SubQuestionEvent --> step_handle_sub_question + step_wait_for_index_ingestion --> event_QueryEvent + step_wait_for_index_ingestion --> event_OutputEvent + event_WaitForIndexIngestion --> step_wait_for_index_ingestion + step_workflow_entrypoint --> event_QueryEvent + step_workflow_entrypoint --> event_AddDataToIndexEvent + event_CustomStartEvent --> step_workflow_entrypoint + classDef stepStyle fill:#f2f0ff,line-height:1.2 + classDef externalStyle fill:#f2f0ff,line-height:1.2 + classDef defaultEventStyle fill-opacity:0 + classDef stopEventStyle fill:#bfb6fc + classDef inputRequiredStyle fill:#f2f0ff,line-height:1.2 diff --git a/samples/travel-helper-RAG-agent/input.json b/samples/travel-helper-RAG-agent/input.json new file mode 100644 index 0000000..de9e171 --- /dev/null +++ b/samples/travel-helper-RAG-agent/input.json @@ -0,0 +1,4 @@ +{ + "query": "I want to travel to Amsterdam from 10 to 20 March", + "add_data_to_index": true +} diff --git a/samples/travel-helper-RAG-agent/llama_index.json b/samples/travel-helper-RAG-agent/llama_index.json new file mode 100644 index 0000000..50581ef --- /dev/null +++ b/samples/travel-helper-RAG-agent/llama_index.json @@ -0,0 +1,7 @@ +{ + "dependencies": ["."], + "workflows": { + "agent": "main.py:agent" + }, + "env": ".env" +} diff --git a/samples/travel-helper-RAG-agent/main.py b/samples/travel-helper-RAG-agent/main.py new file mode 100644 index 0000000..6f34ac9 --- /dev/null +++ b/samples/travel-helper-RAG-agent/main.py @@ -0,0 +1,314 @@ +import json +import time + +from llama_index.core import get_response_synthesizer +from llama_index.core.agent.react.base import ReActAgent +from llama_index.core.response_synthesizers.type import ResponseMode +from llama_index.core.tools import QueryEngineTool, ToolMetadata +from llama_index.core.workflow import ( + Context, + Event, + StartEvent, + StopEvent, + Workflow, + step, +) +from uipath import UiPath + +from uipath_llamaindex.llms import UiPathOpenAI +from uipath_llamaindex.query_engines import ContextGroundingQueryEngine + +index_folder_path = "Shared" +company_policy_index_name = "company_policy" +personal_preferences_index_name = "personal_preferences" +company_policy_files_directory = "sample_data/company_policies" +personal_preferences_files_directory = "sample_data/personal_preferences" + +llm = UiPathOpenAI() +uipath = UiPath() + + +class CustomStartEvent(StartEvent): + query: str + add_data_to_index: bool + + +class QueryEvent(Event): + pass + + +class AddDataToIndexEvent(Event): + pass + + +class SubQuestionEvent(Event): + question: str + + +class WaitForIndexIngestion(Event): + pass + + +class OutputEvent(StopEvent): + """Event representing the final output.""" + + output: str + + +class AnswerEvent(Event): + question: str + answer: str + + +def generate_context_grounding_query_engine_tools( + response_mode: ResponseMode, +) -> list[QueryEngineTool]: + response_synthesizer = get_response_synthesizer( + response_mode=response_mode, llm=llm + ) + query_engine_policies = ContextGroundingQueryEngine( + index_name=company_policy_index_name, + folder_path=index_folder_path, + response_synthesizer=response_synthesizer, + ) + + query_engine_personal_preferences = ContextGroundingQueryEngine( + index_name=personal_preferences_index_name, + folder_path=index_folder_path, + response_synthesizer=response_synthesizer, + ) + + return [ + QueryEngineTool( + query_engine=query_engine_policies, + metadata=ToolMetadata( + name="travel_rates_and_company_policy", + description="Information about company travel rates per states/cities and general company policy", + ), + ), + QueryEngineTool( + query_engine=query_engine_personal_preferences, + metadata=ToolMetadata( + name="personal_preferences", + description="Information about user's personal preferences", + ), + ), + ] + + +async def in_progress_ingestion(index_name: str) -> bool: + """ + returns True if ingestion finished and was successful, False otherwise + """ + index = await uipath.context_grounding.retrieve_async( + index_name, folder_path=index_folder_path + ) + return index.in_progress_ingestion() + + +class SubQuestionQueryEngine(Workflow): + @step + async def workflow_entrypoint( + self, ctx: Context, ev: CustomStartEvent + ) -> QueryEvent | AddDataToIndexEvent: + await ctx.set("original_query", ev.query) + + if ev.add_data_to_index: + return AddDataToIndexEvent() + return QueryEvent() + + @step + async def add_data_to_index(self, ev: AddDataToIndexEvent) -> WaitForIndexIngestion: + async def add_file_to_index(file_path, index_name, ingest_data): + await uipath.context_grounding.add_to_index_async( + name=index_name, + folder_path=index_folder_path, + source_path=file_path, + blob_file_path=os.path.basename(file_path), + ingest_data=ingest_data, + ) + + try: + import os + + company_policy_files = os.listdir(company_policy_files_directory) + for i in range(len(company_policy_files)): + ingest_data = i == len(company_policy_files) - 1 + await add_file_to_index( + os.path.join( + company_policy_files_directory, company_policy_files[i] + ), + company_policy_index_name, + ingest_data, + ) + + personal_preferences_files = os.listdir( + personal_preferences_files_directory + ) + for i in range(len(personal_preferences_files)): + ingest_data = i == len(personal_preferences_files) - 1 + await add_file_to_index( + os.path.join( + personal_preferences_files_directory, + personal_preferences_files[i], + ), + personal_preferences_index_name, + ingest_data, + ) + return WaitForIndexIngestion() + + except Exception as e: + print(e) + raise + + @step + async def wait_for_index_ingestion( + self, ev: WaitForIndexIngestion + ) -> QueryEvent | OutputEvent: + """ + Since ReAct agents can't handle well 'uipath.models.IngestionInProgressException', we use this node to make sure the data added to indexes was successfully ingested, + before moving to 'create_sub_questions_plan' step + """ + no_of_tries = 10 + wait_seconds = 10 + ingested_company_policy_index = ingested_personal_preferences_index = False + while no_of_tries: + should_continue = False + if not ingested_company_policy_index: + should_continue = True + ingested_company_policy_index = not await in_progress_ingestion( + company_policy_index_name + ) + if not ingested_personal_preferences_index: + should_continue = True + ingested_personal_preferences_index = not await in_progress_ingestion( + personal_preferences_index_name + ) + if not should_continue: + break + # wait and retry + no_of_tries -= 1 + print( + "Waiting for index ingestion... Retrying " + + str(no_of_tries) + + " more time(s)" + ) + time.sleep(wait_seconds) + if ingested_company_policy_index and ingested_personal_preferences_index: + return QueryEvent() + return OutputEvent( + output="Cannot evaluate query. Index ingestion is taking too long" + ) + + @step + async def create_sub_questions_plan( + self, ctx: Context, ev: QueryEvent + ) -> SubQuestionEvent: + query_engine_tools = generate_context_grounding_query_engine_tools( + response_mode=ResponseMode.SIMPLE_SUMMARIZE + ) + + response = llm.complete( + f""" + You are a specialized AI travel recommendation agent working exclusively for corporate travel purposes. + You have access to the company's allowed travel budget, and optionally, individual employee preference data for their trips. + Your goal is to provide professional, efficient, and optimized travel recommendations while ensuring compliance with company policies. + + For each request, perform the following: + 1. Summarize the travel information you gathered from the input (destination, dates, employee preferences, company budget, etc.). + 2. Propose an actionable recommendation, such as booking tickets, reservations, or scheduling itineraries, ensuring alignment with the budget and preferences. + + Output relevant sub-questions, such that the answers to all the + sub-questions put together will answer the question. Respond + in pure JSON without any markdown, like this: + {{ + "sub_questions": [ + "What is the allowed expense budget for Amsterdam?", + "What are the user's preferences?", + ] + }} + Here is the user query: {await ctx.get("original_query")} + + And here is the list of tools: {query_engine_tools} + """ + ) + + print(f"Sub-questions are {response}") + + response_obj = json.loads(str(response)) + sub_questions = response_obj["sub_questions"] + + await ctx.set("sub_question_count", len(sub_questions)) + + for question in sub_questions: + ctx.send_event(SubQuestionEvent(question=question)) + + return None + + @step + async def handle_sub_question(self, ev: SubQuestionEvent) -> AnswerEvent: + print(f"Sub-question is {ev.question}") + + # Recreate tools here instead of retrieving from context + query_engine_tools = generate_context_grounding_query_engine_tools( + response_mode=ResponseMode.SIMPLE_SUMMARIZE + ) + + react_agent = ReActAgent.from_tools(query_engine_tools, llm=llm, verbose=True) + response = react_agent.chat(ev.question) + + return AnswerEvent(question=ev.question, answer=str(response)) + + @step + async def combine_and_interpret_answers( + self, ctx: Context, ev: AnswerEvent + ) -> OutputEvent | None: + ready = ctx.collect_events( + ev, [AnswerEvent] * await ctx.get("sub_question_count") + ) + if ready is None: + return None + + answers = "\n\n".join( + [ + f"Question: {event.question}: \n Answer: {event.answer}" + for event in ready + ] + ) + + prompt = f""" + You are given an overall question that has been split into sub-questions, + each of which has been answered. Combine the answers to all the sub-questions + into a single answer to the original question. + Your response should include the following sections: + --- + **Travel Summary:** + - Destination(s): + - Travel Dates: + - Allowed Budget: + - Employee Preferences: + + **Recommendations:** + - Suggested actions (e.g., purchase tickets for X flights, book accommodations, etc.) + - Any important notes regarding budget or policy constraints. + --- + Be concise yet comprehensive in your response. + + Original query: {await ctx.get("original_query")} + + Sub-questions and answers: + {answers} + """ + + print(f"Final prompt is {prompt}") + + response = llm.complete(prompt) + + print("Final response is", response) + + return OutputEvent( + output=response.text, + ) + + +agent = SubQuestionQueryEngine(timeout=120, verbose=True) diff --git a/samples/travel-helper-RAG-agent/pyproject.toml b/samples/travel-helper-RAG-agent/pyproject.toml new file mode 100644 index 0000000..0ce64a8 --- /dev/null +++ b/samples/travel-helper-RAG-agent/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "travel-helper-RAG-agent" +version = "0.0.1" +description = "An agent that facilitates business travel by leveraging multiple RAG sources" +authors = [{ name = "John Doe", email = "john.doe@myemail.com" }] +readme = { file = "README.md", content-type = "text/markdown" } +requires-python = ">=3.10" +dependencies = [ + "uipath-llamaindex>=0.0.26", + "llama-index-llms-openai>=0.2.2", + "uipath>=2.0.70" +] diff --git a/samples/travel-helper-RAG-agent/sample_data/company_policies/company_travel_rates.xlsx b/samples/travel-helper-RAG-agent/sample_data/company_policies/company_travel_rates.xlsx new file mode 100644 index 0000000..09a1531 Binary files /dev/null and b/samples/travel-helper-RAG-agent/sample_data/company_policies/company_travel_rates.xlsx differ diff --git a/samples/travel-helper-RAG-agent/sample_data/company_policies/general_policy.txt b/samples/travel-helper-RAG-agent/sample_data/company_policies/general_policy.txt new file mode 100644 index 0000000..db1d767 --- /dev/null +++ b/samples/travel-helper-RAG-agent/sample_data/company_policies/general_policy.txt @@ -0,0 +1,92 @@ +TechCorp International +Global Travel and Expense Policy 2025 +Version 2.1 +Last Updated: March 19, 2025 + +1. GENERAL GUIDELINES +-------------------- +- All travel must be booked through the company travel portal +- Bookings should be made at least 14 days in advance +- Use company-preferred airlines and hotels when available +- Economy class is standard for all flights under duration threshold +- Rental cars should be economy or midsize only +- All expenses over $25 require itemized receipts +- Expense reports must be submitted within 30 days of travel +- Personal travel combined with business travel must be clearly separated +- Travel insurance is automatically provided for all business trips +- Loyalty program points may be retained by the employee +- Remote work during travel requires manager pre-approval +- Visa and passport fees are reimbursable for business travel + +2. APPROVAL REQUIREMENTS +----------------------- +- International Travel: Department head approval required +- Business Class Travel: VP level approval required +- Extended Stay: Approval required for trips over 14 days +- Emergency Travel: CEO or CFO approval required +- Group Travel (5+ employees): Department head approval +- Conference Attendance: Direct manager approval +- Client Entertainment: Budget owner approval required +- Training Events: Learning & Development approval + +3. PREFERRED VENDORS +------------------- +Airlines: +- United Airlines (Corporate Code: TC2024UA) +- American Airlines (Corporate Code: TC2024AA) +- Delta (Corporate Code: TC2024DL) +- Lufthansa (Corporate Code: TC2024LH) +- Singapore Airlines (Corporate Code: TC2024SQ) + +Hotels: +- Marriott (Corporate Rate Code: TC24MAR) +- Hilton (Corporate Rate Code: TC24HIL) +- Hyatt (Corporate Rate Code: TC24HYT) +- InterContinental (Corporate Rate Code: TC24IHG) + +Car Rental: +- Hertz (CDP: 2024567) +- Enterprise (Corporate Code: TC24ENT) +- Avis (AWD: T24567) + +4. EXPENSE CATEGORIES +-------------------- +Reimbursable: +- Flight tickets and airline fees +- Hotel accommodations +- Ground transportation +- Meals within per diem limits +- Internet access during travel +- Business calls and communication +- Visa and passport fees +- Vaccinations required for travel +- Currency exchange fees +- Business entertainment (with approval) + +Non-Reimbursable: +- Personal entertainment +- Spouse/companion travel +- Room service or mini-bar +- Flight upgrades (unless approved) +- Personal grooming +- Lost baggage +- Traffic violations +- Hotel movie rentals +- Gym or spa fees +- Travel insurance (already provided) + +5. PAYMENT METHODS +----------------- +Preferred Methods: +- Corporate credit card (primary method) +- Virtual cards for hotel bookings +- Corporate travel account for flights +- Personal card (only if corporate card unavailable) + +Documentation Required: +- Original itemized receipts +- Credit card statements +- Boarding passes +- Hotel folios +- Conference/event agendas +- Business purpose justification diff --git a/samples/travel-helper-RAG-agent/sample_data/company_policies/travel_guidelines.txt b/samples/travel-helper-RAG-agent/sample_data/company_policies/travel_guidelines.txt new file mode 100644 index 0000000..ceb3820 --- /dev/null +++ b/samples/travel-helper-RAG-agent/sample_data/company_policies/travel_guidelines.txt @@ -0,0 +1,150 @@ +TRAVEL BOOKING GUIDELINES +======================= + +FLIGHT BOOKING +------------- +1. Advance Booking + - Book at least 14 days before travel + - Use company travel portal + - Select preferred airlines when available + - Compare multiple flight options + - Consider alternate airports + - Book refundable fares for uncertain trips + +2. Class of Service + - Domestic Flights: + * Economy for flights under 6 hours + * Business class allowed for flights over 6 hours + * Premium Economy when Business unavailable + - International Flights: + * Economy for flights under 8 hours + * Business class allowed for flights over 8 hours + * Premium Economy when Business unavailable + - First Class: + * Not permitted for any travel + * Exceptions require CEO approval + +3. Baggage + - Company covers cost of one checked bag + - Additional baggage requires approval + - Excess baggage for equipment pre-approved + - Personal items at traveler's expense + - Lost baggage is traveler's responsibility + +4. Frequent Flyer Programs + - Points belong to the employee + - Cannot influence booking decisions + - Status benefits can be used + - Upgrades using points permitted + - Membership fees not reimbursed + +5. Flight Changes + - Business reasons: Company covers cost + - Personal reasons: Employee covers cost + - Must be documented and approved + - Use airline credits when available + - Cancel unused tickets promptly + +HOTEL BOOKING +------------ +1. Room Selection + - Standard room type only + - Special requirements need approval + - Must be within per diem rates + - Single occupancy only + - Suites require VP approval + +2. Location + - Within 5 miles of primary meeting location + - Use preferred hotel chains when available + - Safe and accessible areas + - Consider transportation costs + - Airport hotels when practical + +3. Duration + - Book only for approved travel dates + - Early arrival/late departure needs justification + - Maximum 1 day before/after event + - Extended stays require approval + - Weekend stays must be cost-effective + +4. Amenities + - Internet access included when possible + - Breakfast included preferred + - Gym access not reimbursed + - Room service not covered + - Laundry after 5 nights allowed + +5. Hotel Loyalty Programs + - Points belong to the employee + - Cannot influence booking decisions + - Status benefits can be used + - Room upgrades using points allowed + - Membership fees not reimbursed + +GROUND TRANSPORTATION +-------------------- +1. Airport Transfers + - Use hotel shuttle when available + - Shared ride services preferred + - Taxi/rideshare when necessary + - Private car service needs approval + - Keep all receipts + +2. Rental Cars + - Economy or midsize only + - Luxury vehicles not permitted + - Insurance included in corporate rate + - Fuel up before returning + - Document any damages + +3. Public Transportation + - Preferred in major cities + - Reimbursed with receipts + - Monthly passes for extended stays + - Safety considerations paramount + - First class rail needs approval + +4. Personal Vehicle + - Standard mileage rate applies + - Parking fees reimbursed + - Tolls reimbursed with receipts + - Insurance is owner's responsibility + - Most economic option required + +SPECIAL CONSIDERATIONS +--------------------- +1. International Travel + - Check visa requirements early + - Verify passport validity + - Register with embassy + - Get required vaccinations + - Review local customs + +2. Group Travel + - Coordinate bookings through Travel Team + - Share ground transportation + - Consider meeting package rates + - Document all attendees + - Joint billing when possible + +3. Extended Stays + - Consider apartment/corporate housing + - Weekly/monthly rates + - Kitchen facilities preferred + - Local transportation options + - Cost-benefit analysis required + +4. Emergency Travel + - 24/7 travel assistance available + - Document all expenses + - Higher rates may be approved + - Safety is priority + - Keep all communication + +5. Combined Business/Personal + - Personal portions clearly separated + - No reimbursement for personal days + - Document cost comparisons + - Use vacation time appropriately + - Get manager's approval diff --git a/samples/travel-helper-RAG-agent/sample_data/personal_preferences/personal_preferences.txt b/samples/travel-helper-RAG-agent/sample_data/personal_preferences/personal_preferences.txt new file mode 100644 index 0000000..0202e11 --- /dev/null +++ b/samples/travel-helper-RAG-agent/sample_data/personal_preferences/personal_preferences.txt @@ -0,0 +1,76 @@ +Travel Preferences +General Preferences: +I am open to flying with low-cost airlines, provided the flights adhere to the following criteria: +They must be direct flights without lengthy layovers, as I value efficiency during travel. +The flights should be scheduled at convenient hours — early mornings should be avoided unless absolutely necessary. Mid-morning and early afternoon departures are preferable. +I prefer window seats, as I find them more comfortable for resting or enjoying the view during flights. +While complimentary in-flight meals and beverages are appreciated, this is not required for shorter flights. +Airlines should have transparent luggage policies, allowing me to easily manage both carry-on and checked baggage without unexpected fees. +In-flight entertainment and USB/power charging ports would be an added bonus. +Accommodation Preferences: +Hotel Quality: +I prefer staying in hotels that are rated 4 stars or higher, ensuring that I receive high-quality amenities, a clean environment, and professional customer service. +Boutique hotels or luxury chains are often preferred for their reliability and attention to detail. +In-Room Amenities: +The room should have WiFi that is fast and reliable, as maintaining online connectivity for work purposes is critical. Ethernet options are also welcome for faster speeds. +A comfortable workspace within the room (e.g., desk, ergonomic chair) is ideal for catching up on emails or preparing for meetings. +Temp-controlled rooms (air conditioning/heating) should be functional and easy to adjust. +The bed should be king or queen-sized, and the noise levels should be minimal to ensure good rest. I appreciate soundproof or quieter room environments. +Complimentary toiletries and storage space are essential — I often require an iron, hairdryer, or extra hangers during trips. +Hotel Features: +Breakfast options are a priority. Ideally, the hotel should provide complimentary breakfast, including continental, healthy, and local options. +A fitness center or gym with basic workout equipment (e.g., treadmills, free weights) is desirable to maintain my exercise routine. +Hotels with restaurants or cafes onsite would be beneficial, especially for quick access to meals or drinks after business meetings. +I appreciate 24-hour front desk services, allowing flexibility for check-in/check-out or addressing any issues. +A hotel bar or lounge could be useful for informal meetings or relaxation after work sessions. +Location and Accessibility: +Hotels should be centrally located, preferably within walking distance to meeting venues, restaurants, public transport stops, or major city landmarks. +Safety is a top priority — accommodations should be in areas with low crime rates, well-lit surroundings, and easy transportation access. +Parking facilities at or nearby the hotel are welcome if transportation involves car rentals. +Ideally, nearby conveniences like pharmacies, grocery stores, or coffee shops should be within walking distance. +Transportation Preferences: +Flights: +I prefer direct flights over connecting ones unless necessary. Flexibility in layover options is appreciated (under 2 hours for domestic layovers, under 4 hours for international layovers). +I am willing to upgrade to business class or premium economy for long-haul flights where extra comfort is needed (e.g., reclinable seats, extra legroom, priority boarding). +Car Rentals: +Automatic vehicles are preferred over manual transmissions for ease of use, especially in unfamiliar destinations. +Car rentals should include essential features such as GPS navigation systems, roadside assistance, and unlimited mileage options for convenience. +I value pickup/drop-off locations near airports, meeting venues, or hotels. +Public Transportation: +If public transportation is the primary option, the routes should include clear directions and easy navigation tools. +Ride-sharing services such as Uber/Lyft or private car transfers are acceptable alternatives if public transit is unavailable or inconvenient. +Taxis/Ride-Share: +Drivers should be vetted and reputable, and clear pricing should be offered upfront to avoid surprises. +Dining Preferences: +Meals: +I prioritize breakfast as part of the accommodation package — options should include a mix of healthy items such as fresh fruit, yogurt, eggs, and coffee/tea. +During the day, I prefer quick grab-and-go meals or cafes for convenience. +Dinner should ideally be at a sit-down restaurant, with options for casual or upscale dining depending on the mood. +Cuisine: +I enjoy trying local cuisine or specialties, provided the restaurant has good reviews and adheres to hygiene standards. +Vegetarian and pescatarian meal options are highly desirable, as my diet leans heavily towards these. Gluten-free options are appreciated but not mandatory. +Restaurants that offer nutritional information or allergy labeling are preferred, as they help ensure the meals are suitable for me. +Dining Environment: +I prefer dining options with a moderate ambiance, avoiding excessively noisy or crowded venues. +Outdoor seating is appreciated when the weather allows for it. +I am open to dining alone or with colleagues, and private or semi-private spaces are desirable for informal corporate discussions. +Meeting Preferences: +Venues should provide high-speed WiFi, power outlets, and equipment such as projectors, whiteboards, and conference phones for presentations. +Seating should be ergonomic and comfortable for extended meeting durations. +Refreshments like water, coffee, tea, and light snacks should be provided, especially before or after long sessions. +Meeting venues should ideally be located within or near the city center, reducing commuting time. +Backup meeting spaces or informal lounges within the hotel are appreciated for urgent discussions or last-minute schedule changes. +Leisure and Activities Preferences: +If the schedule permits, I like visiting local cultural landmarks, museums, or scenic parks during my free time. These activities are relaxing and enriching. +Activities such as walking tours, exploring unique architectural sites, or visiting shopping districts are my favorites. +If possible, suggestions for local events (e.g., concerts, gallery openings, performances) during the trip would add extra joy to my stay. +I prefer scenic outdoor destinations over crowded indoor attractions unless required by the trip schedule. +Budget Preferences: +I am flexible with budgets provided that spending optimizes comfort and convenience. +While I am open to low-cost transportation options, flights and accommodations should balance price and quality effectively. +Meals should fit the corporate budget guidelines but allow flexibility for business dinners or higher-quality options when necessary. +Safety and convenience are prioritized over aggressive cost-cutting measures, especially for critical aspects like accommodations or transportation. +Special Requests: +I appreciate proactive communication from travel managers or agents to ensure changes or issues (e.g., cancellations, delays) are promptly addressed. +My itinerary should include backup options — alternative flights, hotels, or transport arrangements in case of emergencies. +It would be helpful to receive a detailed travel package, including confirmations, receipts, maps, and local tips, to ensure a smooth experience. diff --git a/samples/travel-helper-RAG-agent/uipath.json b/samples/travel-helper-RAG-agent/uipath.json new file mode 100644 index 0000000..c40f0e7 --- /dev/null +++ b/samples/travel-helper-RAG-agent/uipath.json @@ -0,0 +1,48 @@ +{ + "settings": { + "fileExtensionsIncluded": [ + ".txt", + ".xlsx" + ] + }, + "entryPoints": [ + { + "filePath": "agent", + "uniqueId": "53271079-41d3-4f2b-a682-6c57ee3334c8", + "type": "agent", + "input": { + "type": "object", + "properties": { + "query": { + "title": "Query", + "type": "string" + }, + "add_data_to_index": { + "title": "Add Data To Index", + "type": "boolean" + } + }, + "required": [ + "query", + "add_data_to_index" + ] + }, + "output": { + "type": "object", + "properties": { + "output": { + "title": "Output", + "type": "string" + } + }, + "required": [ + "output" + ] + } + } + ], + "bindings": { + "version": "2.0", + "resources": [] + } +} diff --git a/src/uipath_llamaindex/_cli/_runtime/_runtime.py b/src/uipath_llamaindex/_cli/_runtime/_runtime.py index 11a99bb..acf89c5 100644 --- a/src/uipath_llamaindex/_cli/_runtime/_runtime.py +++ b/src/uipath_llamaindex/_cli/_runtime/_runtime.py @@ -11,6 +11,7 @@ InputRequiredEvent, JsonPickleSerializer, ) +from llama_index.core.workflow.errors import WorkflowTimeoutError from llama_index.core.workflow.handler import WorkflowHandler from openinference.instrumentation.llama_index import ( LlamaIndexInstrumentor, @@ -114,15 +115,33 @@ async def execute(self) -> Optional[UiPathRuntimeResult]: if resume_trigger is None: try: output = await handler + # catch any script exceptions + except Exception as e: + raise UiPathLlamaIndexRuntimeError( + "AGENT_EXECUTION_FAILURE", + "There was an exception while executing the agent ", + str(e), + UiPathErrorCategory.USER, + ) from e + try: serialized_output = self._serialize_object(output) # create simple kvp from string if type(serialized_output) is str: serialized_output = {"result": serialized_output} + print(serialized_output) self.context.result = UiPathRuntimeResult( output=serialized_output, status=UiPathRuntimeStatus.SUCCESSFUL, ) + # check if workflow failed because of timeout constraints + except WorkflowTimeoutError as e: + raise UiPathLlamaIndexRuntimeError( + "TIMEOUT_ERROR", + "Workflow timed out", + str(e), + UiPathErrorCategory.USER, + ) from e except Exception as e: raise UiPathLlamaIndexRuntimeError( "SERIALIZE_OUTPUT_ERROR", diff --git a/src/uipath_llamaindex/query_engines/__init__.py b/src/uipath_llamaindex/query_engines/__init__.py new file mode 100644 index 0000000..a780c29 --- /dev/null +++ b/src/uipath_llamaindex/query_engines/__init__.py @@ -0,0 +1,3 @@ +from .context_grounding_query_engine import ContextGroundingQueryEngine + +__all__ = ["ContextGroundingQueryEngine"] diff --git a/src/uipath_llamaindex/query_engines/context_grounding_query_engine.py b/src/uipath_llamaindex/query_engines/context_grounding_query_engine.py new file mode 100644 index 0000000..6e7628b --- /dev/null +++ b/src/uipath_llamaindex/query_engines/context_grounding_query_engine.py @@ -0,0 +1,42 @@ +from typing import Optional + +from llama_index.core.query_engine import CustomQueryEngine +from llama_index.core.response_synthesizers import BaseSynthesizer +from uipath import UiPath + +from uipath_llamaindex.retrievers import ContextGroundingRetriever + + +class ContextGroundingQueryEngine(CustomQueryEngine): + """RAG Query Engine.""" + + def __init__( + self, + response_synthesizer: BaseSynthesizer, + index_name: str, + folder_path: Optional[str] = None, + folder_key: Optional[str] = None, + uipath: Optional[UiPath] = None, + number_of_results: Optional[int] = 10, + **kwargs, + ): + super().__init__() + self._retriever = ContextGroundingRetriever( + index_name=index_name, + folder_path=folder_path, + folder_key=folder_key, + number_of_results=number_of_results, + uipath=uipath, + **kwargs, + ) + self._response_synthesizer = response_synthesizer + + def custom_query(self, query_str: str): + nodes = self._retriever.retrieve(query_str) + response_obj = self._response_synthesizer.synthesize(query_str, nodes) + return response_obj + + async def acustom_query(self, query_str: str): + nodes = await self._retriever.aretrieve(query_str) + response_obj = self._response_synthesizer.synthesize(query_str, nodes) + return response_obj diff --git a/src/uipath_llamaindex/retrievers/__init__.py b/src/uipath_llamaindex/retrievers/__init__.py new file mode 100644 index 0000000..2b8050e --- /dev/null +++ b/src/uipath_llamaindex/retrievers/__init__.py @@ -0,0 +1,3 @@ +from .context_grounding_retriever import ContextGroundingRetriever + +__all__ = ["ContextGroundingRetriever"] diff --git a/src/uipath_llamaindex/retrievers/context_grounding_retriever.py b/src/uipath_llamaindex/retrievers/context_grounding_retriever.py new file mode 100644 index 0000000..8b0b940 --- /dev/null +++ b/src/uipath_llamaindex/retrievers/context_grounding_retriever.py @@ -0,0 +1,63 @@ +from typing import List, Optional + +from llama_index.core.retrievers import ( + BaseRetriever, +) +from llama_index.core.schema import NodeWithScore, QueryBundle, TextNode +from uipath import UiPath +from uipath.models import ContextGroundingQueryResponse + + +class ContextGroundingRetriever(BaseRetriever): + def __init__( + self, + index_name: str, + folder_path: Optional[str] = None, + folder_key: Optional[str] = None, + uipath: Optional[UiPath] = None, + number_of_results: Optional[int] = 10, + **kwargs, + ): + super().__init__() + self._index_name = index_name + self._folder_path = folder_path + self._folder_key = folder_key + self._uipath = uipath or UiPath() + self._number_of_results = number_of_results + self._results: list[ContextGroundingQueryResponse] = [] + + def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: + self._results = self._uipath.context_grounding.search( + self._index_name, + query_bundle.query_str, + self._number_of_results, + folder_path=self._folder_path, + folder_key=self._folder_key, + ) + + return self._to_nodes_with_scores() + + async def _aretrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: + self._results = await self._uipath.context_grounding.search_async( + self._index_name, + query_bundle.query_str, + self._number_of_results, + folder_path=self._folder_path, + folder_key=self._folder_key, + ) + + return self._to_nodes_with_scores() + + def _to_nodes_with_scores(self) -> List[NodeWithScore]: + nodes_with_scores = [] + for chunk in self._results: + node = TextNode( + text=chunk.content, + metadata={ + "source_document_id": chunk.source_document_id, + "source": chunk.source, + "page_number": chunk.page_number, + }, + ) + nodes_with_scores.append(NodeWithScore(node=node, score=chunk.score)) + return nodes_with_scores diff --git a/uv.lock b/uv.lock index 167a7f8..d5a76be 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12'", @@ -2847,7 +2848,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.0.66" +version = "2.0.68" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-monitor-opentelemetry" }, @@ -2861,9 +2862,9 @@ dependencies = [ { name = "tenacity" }, { name = "tomli" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/51/dd3c07e194d9899ada9abb4bdfd8198bbe18f6db3e1451c670c6ec3484f5/uipath-2.0.66.tar.gz", hash = "sha256:81592b8ac4891220b16dce94eb1f3b5b4ad59fe05102f77a3fd07263e824e1b5", size = 1829440 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/19/d6b7281b9e773708671ead073abea1ae645a135f4e6eae0f17bd57786944/uipath-2.0.68.tar.gz", hash = "sha256:17f6a8fe066e50ec1afe1b51069dc86b143ba7f4bf5166bf33a681de8c36b6a5", size = 1830506 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/82/8c13b337aabf49eddc1d3a8ce315a16f7bab83f339746c8569c61b2ceddc/uipath-2.0.66-py3-none-any.whl", hash = "sha256:4f75b1edcb543ba495c96b2647aed0d64bc707119a3ef7c03948576cb27083b7", size = 125292 }, + { url = "https://files.pythonhosted.org/packages/0d/b9/3cf988d3dcb888873af7665384bda5bfb4f1c253bd9ed92f10437ab69598/uipath-2.0.68-py3-none-any.whl", hash = "sha256:a645720fead5b2acdea037968bedc126ee181be7967c6fbac4a5524fe2422fd5", size = 125557 }, ] [[package]]