From 7f759cbaf5c6ab42c5ad9e503008bf5e1a4db433 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:24:08 +0100 Subject: [PATCH 01/21] fix: use argumentProperties in context_tool [ECS-1658] --- .../agent/tools/context_tool.py | 73 ++-- tests/agent/tools/test_context_tool.py | 371 ++++++++++++------ 2 files changed, 294 insertions(+), 150 deletions(-) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 472bafe4..865efdbc 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -1,16 +1,15 @@ """Context tool creation for semantic index retrieval.""" import uuid -from typing import Any, Dict, Optional +from typing import Any, Optional from langchain_core.documents import Document from langchain_core.messages import ToolCall from langchain_core.tools import BaseTool, StructuredTool -from pydantic import BaseModel, Field, TypeAdapter, create_model +from pydantic import BaseModel, Field, create_model from uipath.agent.models.agent import ( AgentContextResourceConfig, AgentContextRetrievalMode, - AgentToolArgumentProperties, ) from uipath.eval.mocks import mockable from uipath.platform import UiPath @@ -42,24 +41,6 @@ from .tool_node import ToolWrapperReturnType from .utils import sanitize_tool_name -_ARG_PROPS_ADAPTER = TypeAdapter(Dict[str, AgentToolArgumentProperties]) - - -def _get_argument_properties( - resource: AgentContextResourceConfig, -) -> dict[str, AgentToolArgumentProperties]: - """Extract argumentProperties from the resource's extra fields. - - AgentContextResourceConfig doesn't declare argument_properties yet, - but BaseCfg(extra="allow") preserves the raw JSON value. - """ - raw = ( - resource.model_extra.get("argumentProperties") if resource.model_extra else None - ) - if not raw: - return {} - return _ARG_PROPS_ADAPTER.validate_python(raw) - def _build_folder_path_prefix_arg_props( resource: AgentContextResourceConfig, @@ -209,7 +190,7 @@ def handle_deep_rag( deep_rag_id=(str, Field(alias="deepRagId")), ) - arg_props = _get_argument_properties(resource) + arg_props = dict(resource.argument_properties) has_folder_path_prefix_arg = "folder_path_prefix" in arg_props or ( resource.settings.folder_path_prefix @@ -272,7 +253,15 @@ async def create_deep_rag(): return await create_deep_rag() - return StructuredToolWithArgumentProperties( + async def context_deep_rag_wrapper( + tool: BaseTool, + call: ToolCall, + state: AgentGraphState, + ) -> ToolWrapperReturnType: + call["args"] = handle_static_args(tool, state, call["args"]) + return await tool.ainvoke(call) + + tool = StructuredToolWithArgumentProperties( name=tool_name, description=resource.description, args_schema=input_model, @@ -286,6 +275,8 @@ async def create_deep_rag(): "context_retrieval_mode": resource.settings.retrieval_mode, }, ) + tool.set_tool_wrappers(awrapper=context_deep_rag_wrapper) + return tool def handle_batch_transform( @@ -341,7 +332,7 @@ def handle_batch_transform( ): static_folder_path_prefix = resource.settings.folder_path_prefix.value - arg_props = _get_argument_properties(resource) + arg_props = dict(resource.argument_properties) has_folder_path_prefix_arg = "folder_path_prefix" in arg_props or ( resource.settings.folder_path_prefix @@ -435,7 +426,7 @@ async def context_batch_transform_wrapper( call: ToolCall, state: AgentGraphState, ) -> ToolWrapperReturnType: - call["args"] = handle_static_args(resource, state, call["args"]) + call["args"] = handle_static_args(tool, state, call["args"]) return await job_attachment_wrapper(tool, call, state) tool = StructuredToolWithArgumentProperties( @@ -475,29 +466,35 @@ def ensure_valid_fields(resource_config: AgentContextResourceConfig): ) +def _normalize_folder_prefix(folder_path_prefix: str | None) -> str: + """Normalize a folder path prefix to a clean directory-only pattern. + + Strips leading/trailing slashes and trailing file-matching globs + (e.g. /*, /**, /**/*) since the caller appends the file extension part. + """ + if not folder_path_prefix: + return "**" + + prefix = folder_path_prefix.strip("/").rstrip("/*") + if not prefix: + return "**" + + return prefix + + def build_glob_pattern( folder_path_prefix: str | None, file_extension: str | None ) -> str: - # Handle prefix - prefix = "**" - if folder_path_prefix: - prefix = folder_path_prefix.rstrip("/") - - if not prefix.startswith("**"): - if prefix.startswith("/"): - prefix = prefix[1:] + prefix = _normalize_folder_prefix(folder_path_prefix) # Handle extension extension = "*" if file_extension: ext = file_extension.lower() - if ext in {"pdf", "txt", "docx", "csv"}: - extension = f"*.{ext}" - else: - extension = f"*.{ext}" + extension = f"*.{ext}" # Final pattern logic - if not prefix or prefix == "**": + if prefix == "**": return "**/*" if extension == "*" else f"**/{extension}" return f"{prefix}/{extension}" diff --git a/tests/agent/tools/test_context_tool.py b/tests/agent/tools/test_context_tool.py index 47f207f5..2db40ce0 100644 --- a/tests/agent/tools/test_context_tool.py +++ b/tests/agent/tools/test_context_tool.py @@ -20,54 +20,65 @@ from uipath_langchain.agent.exceptions import AgentStartupError, AgentStartupErrorCode from uipath_langchain.agent.tools.context_tool import ( + _normalize_folder_prefix, build_glob_pattern, create_context_tool, handle_batch_transform, handle_deep_rag, handle_semantic_search, ) +from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( + StructuredToolWithArgumentProperties, +) from uipath_langchain.agent.tools.structured_tool_with_output_type import ( StructuredToolWithOutputType, ) +def _make_context_resource( + name="test_tool", + description="Test tool", + index_name="test-index", + folder_path="/test/folder", + query_value=None, + query_variant="static", + citation_mode_value=None, + retrieval_mode=AgentContextRetrievalMode.SEMANTIC, + folder_path_prefix=None, + argument_properties=None, + **kwargs, +): + """Helper to create an AgentContextResourceConfig.""" + return AgentContextResourceConfig( + name=name, + description=description, + resource_type="context", + index_name=index_name, + folder_path=folder_path, + settings=AgentContextSettings( + result_count=1, + retrieval_mode=retrieval_mode, + query=AgentContextQuerySetting( + value=query_value, + description="some description", + variant=query_variant, + ), + citation_mode=citation_mode_value, + folder_path_prefix=folder_path_prefix, + ), + is_enabled=True, + argument_properties=argument_properties or {}, + **kwargs, + ) + + class TestHandleDeepRag: """Test cases for handle_deep_rag function.""" @pytest.fixture def base_resource_config(self): """Fixture for base resource configuration.""" - - def _create_config( - name="test_deep_rag", - description="Test Deep RAG tool", - index_name="test-index", - folder_path="/test/folder", - query_value=None, - query_variant="static", - citation_mode_value=None, - retrieval_mode=AgentContextRetrievalMode.SEMANTIC, - ): - return AgentContextResourceConfig( - name=name, - description=description, - resource_type="context", - index_name=index_name, - folder_path=folder_path, - settings=AgentContextSettings( - result_count=1, - retrieval_mode=retrieval_mode, - query=AgentContextQuerySetting( - value=query_value, - description="some description", - variant=query_variant, - ), - citation_mode=citation_mode_value, - ), - is_enabled=True, - ) - - return _create_config + return _make_context_resource def test_successful_deep_rag_creation(self, base_resource_config): """Test successful creation of Deep RAG tool with all required fields.""" @@ -78,9 +89,9 @@ def test_successful_deep_rag_creation(self, base_resource_config): result = handle_deep_rag("test_deep_rag", resource) - assert isinstance(result, StructuredToolWithOutputType) + assert isinstance(result, StructuredToolWithArgumentProperties) assert result.name == "test_deep_rag" - assert result.description == "Test Deep RAG tool" + assert result.description == "Test tool" assert hasattr(result.args_schema, "model_json_schema") assert result.args_schema.model_json_schema()["properties"] == {} assert issubclass(result.output_type, DeepRagContent) @@ -88,6 +99,58 @@ def test_successful_deep_rag_creation(self, base_resource_config): assert "deepRagId" in schema["properties"] assert schema["properties"]["deepRagId"]["type"] == "string" + def test_deep_rag_has_tool_wrapper(self, base_resource_config): + """Test that Deep RAG tool has a tool wrapper for static args resolution.""" + resource = base_resource_config( + citation_mode_value=AgentContextValueSetting(value="Inline"), + query_value="some query", + ) + + result = handle_deep_rag("test_deep_rag", resource) + + assert result.awrapper is not None + + def test_deep_rag_with_argument_properties(self, base_resource_config): + """Test that Deep RAG tool correctly receives argument_properties from resource.""" + resource = base_resource_config( + citation_mode_value=AgentContextValueSetting(value="Inline"), + query_value="some query", + argument_properties={ + "folder_path_prefix": { + "variant": "argument", + "argumentPath": "deepRagFolderPrefix", + "isSensitive": False, + }, + }, + ) + + result = handle_deep_rag("test_deep_rag", resource) + + assert isinstance(result, StructuredToolWithArgumentProperties) + assert "folder_path_prefix" in result.argument_properties + # Schema should include folder_path_prefix field + schema = result.args_schema.model_json_schema() + assert "folder_path_prefix" in schema["properties"] + + def test_deep_rag_folder_path_prefix_from_settings_fallback( + self, base_resource_config + ): + """Test that folder_path_prefix argument is detected from settings when not in argument_properties.""" + resource = base_resource_config( + citation_mode_value=AgentContextValueSetting(value="Inline"), + query_value="some query", + folder_path_prefix=AgentContextQuerySetting( + value="{myArgPath}", variant="argument" + ), + ) + + result = handle_deep_rag("test_deep_rag", resource) + + assert isinstance(result, StructuredToolWithArgumentProperties) + assert "folder_path_prefix" in result.argument_properties + schema = result.args_schema.model_json_schema() + assert "folder_path_prefix" in schema["properties"] + def test_missing_static_query_value_raises_error(self, base_resource_config): """Test that missing query.value for static variant raises AgentStartupError.""" resource = base_resource_config(query_variant="static", query_value=None) @@ -139,7 +202,7 @@ def test_citation_mode_conversion( result = handle_deep_rag("test_deep_rag", resource) - assert isinstance(result, StructuredToolWithOutputType) + assert isinstance(result, StructuredToolWithArgumentProperties) def test_tool_name_preserved(self, base_resource_config): """Test that the sanitized tool name is correctly applied.""" @@ -228,9 +291,9 @@ def test_dynamic_query_deep_rag_creation(self, base_resource_config): result = handle_deep_rag("test_deep_rag", resource) - assert isinstance(result, StructuredToolWithOutputType) + assert isinstance(result, StructuredToolWithArgumentProperties) assert result.name == "test_deep_rag" - assert result.description == "Test Deep RAG tool" + assert result.description == "Test tool" assert result.args_schema is not None # Dynamic has input schema assert issubclass(result.output_type, DeepRagContent) @@ -300,44 +363,23 @@ class TestCreateContextTool: @pytest.fixture def semantic_search_config(self): """Fixture for semantic search configuration.""" - return AgentContextResourceConfig( + return _make_context_resource( name="test_semantic_search", description="Test semantic search", - resource_type="context", - index_name="test-index", - folder_path="/test/folder", - settings=AgentContextSettings( - result_count=10, - retrieval_mode=AgentContextRetrievalMode.SEMANTIC, - query=AgentContextQuerySetting( - value=None, - description="Query for semantic search", - variant="dynamic", - ), - ), - is_enabled=True, + retrieval_mode=AgentContextRetrievalMode.SEMANTIC, + query_variant="dynamic", ) @pytest.fixture def deep_rag_config(self): """Fixture for deep RAG configuration.""" - return AgentContextResourceConfig( + return _make_context_resource( name="test_deep_rag", description="Test Deep RAG", - resource_type="context", - index_name="test-index", - folder_path="/test/folder", - settings=AgentContextSettings( - result_count=5, - retrieval_mode=AgentContextRetrievalMode.DEEP_RAG, - query=AgentContextQuerySetting( - value="test query", - description="Test query description", - variant="static", - ), - citation_mode=AgentContextValueSetting(value="Inline"), - ), - is_enabled=True, + retrieval_mode=AgentContextRetrievalMode.DEEP_RAG, + query_value="test query", + query_variant="static", + citation_mode_value=AgentContextValueSetting(value="Inline"), ) def test_create_semantic_search_tool(self, semantic_search_config): @@ -352,7 +394,7 @@ def test_create_deep_rag_tool(self, deep_rag_config): """Test that deep_rag retrieval mode creates Deep RAG tool.""" result = create_context_tool(deep_rag_config) - assert isinstance(result, StructuredToolWithOutputType) + assert isinstance(result, StructuredToolWithArgumentProperties) assert result.name == "test_deep_rag" assert hasattr(result.args_schema, "model_json_schema") assert result.args_schema.model_json_schema()["properties"] == {} @@ -363,12 +405,12 @@ def test_case_insensitive_retrieval_mode(self, deep_rag_config): # Test with uppercase deep_rag_config.settings.retrieval_mode = "DEEP_RAG" result = create_context_tool(deep_rag_config) - assert isinstance(result, StructuredToolWithOutputType) + assert isinstance(result, StructuredToolWithArgumentProperties) # Test with mixed case deep_rag_config.settings.retrieval_mode = "Deep_Rag" result = create_context_tool(deep_rag_config) - assert isinstance(result, StructuredToolWithOutputType) + assert isinstance(result, StructuredToolWithArgumentProperties) class TestHandleSemanticSearch: @@ -377,22 +419,11 @@ class TestHandleSemanticSearch: @pytest.fixture def semantic_config(self): """Fixture for semantic search configuration.""" - return AgentContextResourceConfig( + return _make_context_resource( name="semantic_tool", description="Semantic search tool", - resource_type="context", - index_name="test-index", - folder_path="/test/folder", - settings=AgentContextSettings( - result_count=5, - retrieval_mode=AgentContextRetrievalMode.SEMANTIC, - query=AgentContextQuerySetting( - value=None, - description="Query for semantic search", - variant="dynamic", - ), - ), - is_enabled=True, + retrieval_mode=AgentContextRetrievalMode.SEMANTIC, + query_variant="dynamic", ) def test_semantic_search_tool_creation(self, semantic_config): @@ -445,22 +476,12 @@ async def test_semantic_search_returns_documents(self, semantic_config): def test_static_query_semantic_search_creation(self): """Test successful creation of semantic search tool with static query.""" - resource = AgentContextResourceConfig( + resource = _make_context_resource( name="semantic_tool", description="Semantic search tool", - resource_type="context", - index_name="test-index", - folder_path="/test/folder", - settings=AgentContextSettings( - result_count=5, - retrieval_mode=AgentContextRetrievalMode.SEMANTIC, - query=AgentContextQuerySetting( - value="predefined static query", - description="Static query for semantic search", - variant="static", - ), - ), - is_enabled=True, + retrieval_mode=AgentContextRetrievalMode.SEMANTIC, + query_value="predefined static query", + query_variant="static", ) result = handle_semantic_search("semantic_tool", resource) @@ -474,22 +495,12 @@ def test_static_query_semantic_search_creation(self): @pytest.mark.asyncio async def test_static_query_uses_predefined_query(self): """Test that static query variant uses the predefined query value.""" - resource = AgentContextResourceConfig( + resource = _make_context_resource( name="semantic_tool", description="Semantic search tool", - resource_type="context", - index_name="test-index", - folder_path="/test/folder", - settings=AgentContextSettings( - result_count=5, - retrieval_mode=AgentContextRetrievalMode.SEMANTIC, - query=AgentContextQuerySetting( - value="predefined static query", - description="Static query for semantic search", - variant="static", - ), - ), - is_enabled=True, + retrieval_mode=AgentContextRetrievalMode.SEMANTIC, + query_value="predefined static query", + query_variant="static", ) mock_documents = [ @@ -561,7 +572,7 @@ def test_static_query_batch_transform_creation(self, batch_transform_config): """Test successful creation of batch transform tool with static query.""" result = handle_batch_transform("batch_transform_tool", batch_transform_config) - assert isinstance(result, StructuredToolWithOutputType) + assert isinstance(result, StructuredToolWithArgumentProperties) assert result.name == "batch_transform_tool" assert result.description == "Batch transform tool" assert result.args_schema is not None # Has destination_path parameter @@ -611,7 +622,7 @@ def test_dynamic_query_batch_transform_creation(self): result = handle_batch_transform("batch_transform_tool", resource) - assert isinstance(result, StructuredToolWithOutputType) + assert isinstance(result, StructuredToolWithArgumentProperties) assert result.name == "batch_transform_tool" assert result.args_schema is not None output_schema = result.output_type.model_json_schema() @@ -652,6 +663,46 @@ def test_dynamic_query_batch_transform_has_both_parameters(self): assert "query" in schema["properties"] assert "destination_path" in schema["properties"] + def test_batch_transform_with_argument_properties(self): + """Test that batch transform tool correctly receives argument_properties from resource.""" + resource = AgentContextResourceConfig( + name="batch_transform_tool", + description="Batch transform tool", + resource_type="context", + index_name="test-index", + folder_path="/test/folder", + settings=AgentContextSettings( + result_count=5, + retrieval_mode=AgentContextRetrievalMode.BATCH_TRANSFORM, + query=AgentContextQuerySetting( + value="transform query", + description="Static query", + variant="static", + ), + web_search_grounding=AgentContextValueSetting(value="enabled"), + output_columns=[ + AgentContextOutputColumn( + name="output_col1", description="First output column" + ), + ], + ), + is_enabled=True, + argument_properties={ + "folder_path_prefix": { + "variant": "argument", + "argumentPath": "batchFolderPrefix", + "isSensitive": False, + }, + }, + ) + + result = handle_batch_transform("batch_transform_tool", resource) + + assert isinstance(result, StructuredToolWithArgumentProperties) + assert "folder_path_prefix" in result.argument_properties + schema = result.args_schema.model_json_schema() + assert "folder_path_prefix" in schema["properties"] + @pytest.mark.asyncio async def test_static_query_batch_transform_uses_predefined_query( self, batch_transform_config @@ -907,3 +958,99 @@ def test_supported_extensions(self, ext): def test_unsupported_extension_still_works(self): """Extensions outside the named set are handled identically.""" assert build_glob_pattern("data", "xlsx") == "data/*.xlsx" + + # --- Trailing file-matching globs stripped --- + + def test_prefix_with_trailing_star(self): + """Trailing /* is stripped since extension is appended separately.""" + assert build_glob_pattern("documents/*", "pdf") == "documents/*.pdf" + + def test_prefix_with_trailing_double_star(self): + """Trailing /** is stripped.""" + assert build_glob_pattern("documents/**", "pdf") == "documents/*.pdf" + + def test_prefix_with_trailing_double_star_star(self): + """Trailing /**/* is stripped.""" + assert build_glob_pattern("documents/**/*", "pdf") == "documents/*.pdf" + + def test_match_all_glob_treated_as_no_prefix(self): + """/**/* is a match-all pattern and should be treated as no prefix.""" + assert build_glob_pattern("/**/*", "pdf") == "**/*.pdf" + + def test_star_slash_star_treated_as_no_prefix(self): + """*/* is a match-all pattern and should be treated as no prefix.""" + assert build_glob_pattern("*/*", "pdf") == "**/*.pdf" + + def test_double_star_slash_star_treated_as_no_prefix(self): + """**/* is a match-all pattern and should be treated as no prefix.""" + assert build_glob_pattern("**/*", "pdf") == "**/*.pdf" + + +class TestNormalizeFolderPrefix: + """Test cases for _normalize_folder_prefix function.""" + + # --- None / empty --- + + def test_none_returns_double_star(self): + assert _normalize_folder_prefix(None) == "**" + + def test_empty_string_returns_double_star(self): + assert _normalize_folder_prefix("") == "**" + + def test_only_slashes_returns_double_star(self): + assert _normalize_folder_prefix("///") == "**" + + # --- Leading/trailing slash stripping --- + + def test_strips_leading_slash(self): + assert _normalize_folder_prefix("/documents") == "documents" + + def test_strips_trailing_slash(self): + assert _normalize_folder_prefix("documents/") == "documents" + + def test_strips_both_slashes(self): + assert _normalize_folder_prefix("/documents/") == "documents" + + # --- Trailing glob stripping --- + + def test_strips_trailing_star(self): + assert _normalize_folder_prefix("documents/*") == "documents" + + def test_strips_trailing_double_star(self): + assert _normalize_folder_prefix("documents/**") == "documents" + + def test_strips_trailing_double_star_star(self): + assert _normalize_folder_prefix("documents/**/*") == "documents" + + def test_nested_prefix_strips_trailing_glob(self): + assert _normalize_folder_prefix("folder/subfolder/*") == "folder/subfolder" + + def test_nested_prefix_strips_trailing_double_star_star(self): + assert ( + _normalize_folder_prefix("folder/subfolder/**/*") == "folder/subfolder" + ) + + # --- Match-all patterns become ** --- + + def test_star_slash_star_returns_double_star(self): + assert _normalize_folder_prefix("*/*") == "**" + + def test_double_star_slash_star_returns_double_star(self): + assert _normalize_folder_prefix("**/*") == "**" + + def test_slash_double_star_slash_star_returns_double_star(self): + assert _normalize_folder_prefix("/**/*") == "**" + + # --- Preserves valid prefixes --- + + def test_simple_prefix(self): + assert _normalize_folder_prefix("folder") == "folder" + + def test_nested_prefix(self): + assert _normalize_folder_prefix("folder/subfolder") == "folder/subfolder" + + def test_double_star_prefix_preserved(self): + assert _normalize_folder_prefix("**/documents") == "**/documents" + + def test_double_star_nested_prefix_preserved(self): + assert _normalize_folder_prefix("**/docs/reports") == "**/docs/reports" From f1ba0dec751db53f04fa2a5bc373ca6ab35349ba Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:25:00 +0100 Subject: [PATCH 02/21] chore: bump version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2550fd64..ab5e324a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.8.10" +version = "0.8.11" description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/uv.lock b/uv.lock index 21706692..09e4aa12 100644 --- a/uv.lock +++ b/uv.lock @@ -3324,7 +3324,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.8.10" +version = "0.8.11" source = { editable = "." } dependencies = [ { name = "httpx" }, From f926e84e42c35f1a64fddcc93d48fb5c58c04f16 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:55:33 +0100 Subject: [PATCH 03/21] chore: lint --- .../agent/tools/context_tool.py | 19 +++++++++++++------ tests/agent/tools/test_context_tool.py | 7 ++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 865efdbc..73681e30 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -1,7 +1,7 @@ """Context tool creation for semantic index retrieval.""" import uuid -from typing import Any, Optional +from typing import Any, Optional, cast from langchain_core.documents import Document from langchain_core.messages import ToolCall @@ -30,7 +30,10 @@ from uipath_langchain.agent.tools.internal_tools.schema_utils import ( BATCH_TRANSFORM_OUTPUT_SCHEMA, ) -from uipath_langchain.agent.tools.static_args import handle_static_args +from uipath_langchain.agent.tools.static_args import ( + ArgumentPropertiesMixin, + handle_static_args, +) from uipath_langchain.retrievers import ContextGroundingRetriever from .durable_interrupt import durable_interrupt @@ -152,7 +155,7 @@ async def context_tool_fn(query: Optional[str] = None) -> dict[str, Any]: def handle_deep_rag( tool_name: str, resource: AgentContextResourceConfig -) -> StructuredTool: +) -> StructuredToolWithArgumentProperties: ensure_valid_fields(resource) assert resource.settings.query.variant is not None @@ -258,7 +261,9 @@ async def context_deep_rag_wrapper( call: ToolCall, state: AgentGraphState, ) -> ToolWrapperReturnType: - call["args"] = handle_static_args(tool, state, call["args"]) + call["args"] = handle_static_args( + cast(ArgumentPropertiesMixin, tool), state, call["args"] + ) return await tool.ainvoke(call) tool = StructuredToolWithArgumentProperties( @@ -281,7 +286,7 @@ async def context_deep_rag_wrapper( def handle_batch_transform( tool_name: str, resource: AgentContextResourceConfig -) -> StructuredTool: +) -> StructuredToolWithArgumentProperties: ensure_valid_fields(resource) assert resource.settings.query is not None @@ -426,7 +431,9 @@ async def context_batch_transform_wrapper( call: ToolCall, state: AgentGraphState, ) -> ToolWrapperReturnType: - call["args"] = handle_static_args(tool, state, call["args"]) + call["args"] = handle_static_args( + cast(ArgumentPropertiesMixin, tool), state, call["args"] + ) return await job_attachment_wrapper(tool, call, state) tool = StructuredToolWithArgumentProperties( diff --git a/tests/agent/tools/test_context_tool.py b/tests/agent/tools/test_context_tool.py index 2db40ce0..da24f9cc 100644 --- a/tests/agent/tools/test_context_tool.py +++ b/tests/agent/tools/test_context_tool.py @@ -129,6 +129,7 @@ def test_deep_rag_with_argument_properties(self, base_resource_config): assert isinstance(result, StructuredToolWithArgumentProperties) assert "folder_path_prefix" in result.argument_properties # Schema should include folder_path_prefix field + assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() assert "folder_path_prefix" in schema["properties"] @@ -148,6 +149,7 @@ def test_deep_rag_folder_path_prefix_from_settings_fallback( assert isinstance(result, StructuredToolWithArgumentProperties) assert "folder_path_prefix" in result.argument_properties + assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() assert "folder_path_prefix" in schema["properties"] @@ -700,6 +702,7 @@ def test_batch_transform_with_argument_properties(self): assert isinstance(result, StructuredToolWithArgumentProperties) assert "folder_path_prefix" in result.argument_properties + assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() assert "folder_path_prefix" in schema["properties"] @@ -1026,9 +1029,7 @@ def test_nested_prefix_strips_trailing_glob(self): assert _normalize_folder_prefix("folder/subfolder/*") == "folder/subfolder" def test_nested_prefix_strips_trailing_double_star_star(self): - assert ( - _normalize_folder_prefix("folder/subfolder/**/*") == "folder/subfolder" - ) + assert _normalize_folder_prefix("folder/subfolder/**/*") == "folder/subfolder" # --- Match-all patterns become ** --- From 5c7aa20d79c86822e9949e9ca9ce9319bb0fde27 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:44:22 +0100 Subject: [PATCH 04/21] chore: bump uipath sdk --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 09e4aa12..300f2544 100644 --- a/uv.lock +++ b/uv.lock @@ -3280,7 +3280,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.7" +version = "2.10.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "applicationinsights" }, @@ -3303,9 +3303,9 @@ dependencies = [ { name = "uipath-platform" }, { name = "uipath-runtime" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/ae/d63b4a9210b10d165c8867b9dda4195ddb3729063cc5ae4f514d9a4a186e/uipath-2.10.7.tar.gz", hash = "sha256:34115b39e52049b3814163701f5294492ae14821241f742d6f36c7313baa1684", size = 2454157, upload-time = "2026-03-05T12:25:15.547Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/28/7eac19f5a20a5658690ad5d30d237e414a288444b30cf65333b26dda9731/uipath-2.10.11.tar.gz", hash = "sha256:28c72ead96f5bff8d4dc721e97f0b546eebc76184f6ab4b4a2c95f6f0e9d4db1", size = 2455334, upload-time = "2026-03-09T14:29:15.275Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/aa/c13b14a085f8793e2157a81c4249390de1c56014c38ca45e6cb593e80fc3/uipath-2.10.7-py3-none-any.whl", hash = "sha256:c960a318a432e6d23dedbc2463c441832ac670169145fcffc5ad24ad8d85b851", size = 356892, upload-time = "2026-03-05T12:25:13.318Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0a/49d0904346ce70440b6aee6a98df1252b58a2d72f2f54db41fd213dcf980/uipath-2.10.11-py3-none-any.whl", hash = "sha256:7187df5536084c2097d7b2340bb99f545a4d914ca5533abf0b21bd2c3822c5b6", size = 357297, upload-time = "2026-03-09T14:29:13.213Z" }, ] [[package]] From bd7c5de192a872aaa8830a925efdb1da0088b23c Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:24:18 +0100 Subject: [PATCH 05/21] fix: old broken tests --- tests/agent/tools/test_context_tool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/agent/tools/test_context_tool.py b/tests/agent/tools/test_context_tool.py index da24f9cc..8af58426 100644 --- a/tests/agent/tools/test_context_tool.py +++ b/tests/agent/tools/test_context_tool.py @@ -405,12 +405,12 @@ def test_create_deep_rag_tool(self, deep_rag_config): def test_case_insensitive_retrieval_mode(self, deep_rag_config): """Test that retrieval mode matching is case-insensitive.""" # Test with uppercase - deep_rag_config.settings.retrieval_mode = "DEEP_RAG" + deep_rag_config.settings.retrieval_mode = "DEEPRAG" result = create_context_tool(deep_rag_config) assert isinstance(result, StructuredToolWithArgumentProperties) # Test with mixed case - deep_rag_config.settings.retrieval_mode = "Deep_Rag" + deep_rag_config.settings.retrieval_mode = "deeprag" result = create_context_tool(deep_rag_config) assert isinstance(result, StructuredToolWithArgumentProperties) From 2bdcf3423cb410b30874c6c036518ce9af231f8f Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:48:08 +0100 Subject: [PATCH 06/21] feat: switch to unified search and add support for folder prefix [ECS-1662] --- .../agent/tools/context_tool.py | 191 ++++++++++++------ .../retrievers/context_grounding_retriever.py | 37 +++- tests/agent/tools/test_context_tool.py | 11 +- 3 files changed, 168 insertions(+), 71 deletions(-) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 73681e30..adc2b30b 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -1,5 +1,6 @@ """Context tool creation for semantic index retrieval.""" +import logging import uuid from typing import Any, Optional, cast @@ -44,25 +45,67 @@ from .tool_node import ToolWrapperReturnType from .utils import sanitize_tool_name +logger = logging.getLogger(__name__) -def _build_folder_path_prefix_arg_props( + +def _build_arg_props_from_settings( resource: AgentContextResourceConfig, ) -> dict[str, Any]: - """Build argument_properties for folder_path_prefix from settings. + """Build argument_properties from resource settings for query and folder_path_prefix. - Fallback for when settings bag doesn't include argumentProperties - at the resource level but does set settings.folder_path_prefix - with variant="argument". + For each setting with variant="argument", creates an argument property entry + that maps the tool parameter to the agent input argument path. """ - assert resource.settings.folder_path_prefix is not None - argument_path = (resource.settings.folder_path_prefix.value or "").strip("{}") - return { - "folder_path_prefix": { + arg_props = dict(resource.argument_properties) + + if ( + "query" not in arg_props + and resource.settings.query + and resource.settings.query.variant == "argument" + ): + argument_path = (resource.settings.query.value or "").strip("{}") + arg_props["query"] = { + "variant": "argument", + "argumentPath": argument_path, + "isSensitive": False, + } + + if ( + "folder_path_prefix" not in arg_props + and resource.settings.folder_path_prefix + and resource.settings.folder_path_prefix.variant == "argument" + ): + argument_path = (resource.settings.folder_path_prefix.value or "").strip("{}") + arg_props["folder_path_prefix"] = { "variant": "argument", "argumentPath": argument_path, "isSensitive": False, } - } + + return arg_props + + +def _resolve_file_extension(resource: AgentContextResourceConfig) -> str | None: + """Resolve file extension from settings, returning None for 'All' or empty.""" + if resource.settings.file_extension and resource.settings.file_extension.value: + ext = resource.settings.file_extension.value + if ext.lower() == "all": + return None + return ext + return None + + +def _resolve_static_folder_path_prefix( + resource: AgentContextResourceConfig, +) -> str | None: + """Resolve static folder_path_prefix from settings.""" + if ( + resource.settings.folder_path_prefix + and resource.settings.folder_path_prefix.value + and resource.settings.folder_path_prefix.variant == "static" + ): + return resource.settings.folder_path_prefix.value + return None def is_static_query(resource: AgentContextResourceConfig) -> bool: @@ -90,17 +133,16 @@ def handle_semantic_search( assert resource.settings.query.variant is not None - retriever = ContextGroundingRetriever( - index_name=resource.index_name, - folder_path=get_execution_folder_path(), - number_of_results=resource.settings.result_count, - ) + file_extension = _resolve_file_extension(resource) + static_folder_path_prefix = _resolve_static_folder_path_prefix(resource) static = is_static_query(resource) prompt = resource.settings.query.value if static else None if static: assert prompt is not None + arg_props = _build_arg_props_from_settings(resource) + class ContextOutputSchemaModel(BaseModel): documents: list[Document] = Field( ..., description="List of retrieved documents." @@ -108,16 +150,26 @@ class ContextOutputSchemaModel(BaseModel): output_model = ContextOutputSchemaModel - schema_fields: dict[str, Any] = ( - {} - if static - else { - "query": ( - str, - Field(..., description="The query to search for in the knowledge base"), + schema_fields: dict[str, Any] = {} + + if not static: + schema_fields["query"] = ( + str, + Field( + default=None, + description="The query to search for in the knowledge base", ), - } - ) + ) + + if "folder_path_prefix" in arg_props: + schema_fields["folder_path_prefix"] = ( + str, + Field( + default=None, + description="The folder path prefix within the index to filter on", + ), + ) + input_model = create_model("SemanticSearchInput", **schema_fields) @mockable( @@ -127,7 +179,24 @@ class ContextOutputSchemaModel(BaseModel): output_schema=output_model.model_json_schema(), example_calls=[], # Examples cannot be provided for context. ) - async def context_tool_fn(query: Optional[str] = None) -> dict[str, Any]: + async def context_tool_fn( + query: Optional[str] = None, folder_path_prefix: Optional[str] = None + ) -> dict[str, Any]: + resolved_folder_path_prefix = static_folder_path_prefix or folder_path_prefix + if resolved_folder_path_prefix: + logger.info( + "Semantic search folder_path_prefix resolved: %s", + resolved_folder_path_prefix, + ) + + retriever = ContextGroundingRetriever( + index_name=resource.index_name, + folder_path=get_execution_folder_path(), + number_of_results=resource.settings.result_count, + scope_folder=resolved_folder_path_prefix, + scope_extension=file_extension, + ) + actual_query = prompt or query assert actual_query is not None docs = await retriever.ainvoke(actual_query) @@ -138,6 +207,35 @@ async def context_tool_fn(query: Optional[str] = None) -> dict[str, Any]: ] } + if arg_props: + + async def context_semantic_search_wrapper( + tool: BaseTool, + call: ToolCall, + state: AgentGraphState, + ) -> ToolWrapperReturnType: + call["args"] = handle_static_args( + cast(ArgumentPropertiesMixin, tool), state, call["args"] + ) + return await tool.ainvoke(call) + + tool = StructuredToolWithArgumentProperties( + name=tool_name, + description=resource.description, + args_schema=input_model, + coroutine=context_tool_fn, + output_type=output_model, + argument_properties=arg_props, + metadata={ + "tool_type": "context", + "display_name": resource.name, + "index_name": resource.index_name, + "context_retrieval_mode": resource.settings.retrieval_mode, + }, + ) + tool.set_tool_wrappers(awrapper=context_semantic_search_wrapper) + return tool + return StructuredToolWithOutputType( name=tool_name, description=resource.description, @@ -175,17 +273,8 @@ def handle_deep_rag( if static: assert prompt is not None - static_folder_path_prefix = None - if ( - resource.settings.folder_path_prefix - and resource.settings.folder_path_prefix.value - and resource.settings.folder_path_prefix.variant == "static" - ): - static_folder_path_prefix = resource.settings.folder_path_prefix.value - - file_extension = None - if resource.settings.file_extension and resource.settings.file_extension.value: - file_extension = resource.settings.file_extension.value + static_folder_path_prefix = _resolve_static_folder_path_prefix(resource) + file_extension = _resolve_file_extension(resource) output_model = create_model( "DeepRagOutputModel", @@ -193,12 +282,7 @@ def handle_deep_rag( deep_rag_id=(str, Field(alias="deepRagId")), ) - arg_props = dict(resource.argument_properties) - - has_folder_path_prefix_arg = "folder_path_prefix" in arg_props or ( - resource.settings.folder_path_prefix - and resource.settings.folder_path_prefix.variant == "argument" - ) + arg_props = _build_arg_props_from_settings(resource) schema_fields: dict[str, Any] = ( {} @@ -214,7 +298,7 @@ def handle_deep_rag( } ) - if has_folder_path_prefix_arg: + if "folder_path_prefix" in arg_props: schema_fields["folder_path_prefix"] = ( str, Field( @@ -222,8 +306,6 @@ def handle_deep_rag( description="The folder path prefix within the index to filter on", ), ) - if "folder_path_prefix" not in arg_props: - arg_props = _build_folder_path_prefix_arg_props(resource) input_model = create_model("DeepRagInput", **schema_fields) @@ -329,20 +411,9 @@ def handle_batch_transform( if static: assert prompt is not None - static_folder_path_prefix = None - if ( - resource.settings.folder_path_prefix - and resource.settings.folder_path_prefix.value - and resource.settings.folder_path_prefix.variant == "static" - ): - static_folder_path_prefix = resource.settings.folder_path_prefix.value + static_folder_path_prefix = _resolve_static_folder_path_prefix(resource) - arg_props = dict(resource.argument_properties) - - has_folder_path_prefix_arg = "folder_path_prefix" in arg_props or ( - resource.settings.folder_path_prefix - and resource.settings.folder_path_prefix.variant == "argument" - ) + arg_props = _build_arg_props_from_settings(resource) output_model = create_model_from_schema(BATCH_TRANSFORM_OUTPUT_SCHEMA) @@ -362,7 +433,7 @@ def handle_batch_transform( description="The relative file path destination for the modified csv file", ), ) - if has_folder_path_prefix_arg: + if "folder_path_prefix" in arg_props: schema_fields["folder_path_prefix"] = ( str, Field( @@ -370,8 +441,6 @@ def handle_batch_transform( description="The folder path prefix within the index to filter on", ), ) - if "folder_path_prefix" not in arg_props: - arg_props = _build_folder_path_prefix_arg_props(resource) input_model = create_model("BatchTransformInput", **schema_fields) @mockable( diff --git a/src/uipath_langchain/retrievers/context_grounding_retriever.py b/src/uipath_langchain/retrievers/context_grounding_retriever.py index 0a7f02c9..6f8c2a09 100644 --- a/src/uipath_langchain/retrievers/context_grounding_retriever.py +++ b/src/uipath_langchain/retrievers/context_grounding_retriever.py @@ -5,6 +5,7 @@ from langchain_core.documents import Document from langchain_core.retrievers import BaseRetriever from uipath.platform import UiPath +from uipath.platform.context_grounding import UnifiedSearchScope class ContextGroundingRetriever(BaseRetriever): @@ -13,21 +14,36 @@ class ContextGroundingRetriever(BaseRetriever): folder_key: str | None = None uipath_sdk: UiPath | None = None number_of_results: int | None = 10 + scope_folder: str | None = None + scope_extension: str | None = None + + def _build_scope(self) -> UnifiedSearchScope | None: + if self.scope_folder or self.scope_extension: + return UnifiedSearchScope( + folder=self.scope_folder, + extension=self.scope_extension, + ) + return None def _get_relevant_documents( self, query: str, *, run_manager: CallbackManagerForRetrieverRun ) -> list[Document]: - """Sync implementations for retriever calls context_grounding API to search the requested index.""" + """Sync implementation calls context_grounding unified_search API.""" sdk = self.uipath_sdk if self.uipath_sdk is not None else UiPath() - results = sdk.context_grounding.search( + result = sdk.context_grounding.unified_search( self.index_name, query, - self.number_of_results if self.number_of_results is not None else 10, + number_of_results=self.number_of_results + if self.number_of_results is not None + else 10, + scope=self._build_scope(), folder_path=self.folder_path, folder_key=self.folder_key, ) + values = result.semantic_results.values if result.semantic_results else [] + return [ Document( page_content=x.content, @@ -38,23 +54,28 @@ def _get_relevant_documents( "score": x.score, }, ) - for x in results + for x in values ] async def _aget_relevant_documents( self, query: str, *, run_manager: AsyncCallbackManagerForRetrieverRun ) -> list[Document]: - """Async implementations for retriever calls context_grounding API to search the requested index.""" + """Async implementation calls context_grounding unified_search_async API.""" sdk = self.uipath_sdk if self.uipath_sdk is not None else UiPath() - results = await sdk.context_grounding.search_async( + result = await sdk.context_grounding.unified_search_async( self.index_name, query, - self.number_of_results if self.number_of_results is not None else 10, + number_of_results=self.number_of_results + if self.number_of_results is not None + else 10, + scope=self._build_scope(), folder_path=self.folder_path, folder_key=self.folder_key, ) + values = result.semantic_results.values if result.semantic_results else [] + return [ Document( page_content=x.content, @@ -65,5 +86,5 @@ async def _aget_relevant_documents( "score": x.score, }, ) - for x in results + for x in values ] diff --git a/tests/agent/tools/test_context_tool.py b/tests/agent/tools/test_context_tool.py index 8af58426..d19b6299 100644 --- a/tests/agent/tools/test_context_tool.py +++ b/tests/agent/tools/test_context_tool.py @@ -525,13 +525,20 @@ async def test_static_query_uses_predefined_query(self): assert "documents" in result assert len(result["documents"]) == 1 + @pytest.mark.asyncio @patch.dict(os.environ, {"UIPATH_FOLDER_PATH": "/Shared/TestFolder"}) - def test_semantic_search_uses_execution_folder_path(self, semantic_config): + async def test_semantic_search_uses_execution_folder_path(self, semantic_config): """Test that ContextGroundingRetriever receives folder_path from the execution environment.""" with patch( "uipath_langchain.agent.tools.context_tool.ContextGroundingRetriever" ) as mock_retriever_class: - handle_semantic_search("semantic_tool", semantic_config) + mock_retriever = AsyncMock() + mock_retriever.ainvoke.return_value = [] + mock_retriever_class.return_value = mock_retriever + + tool = handle_semantic_search("semantic_tool", semantic_config) + assert tool.coroutine is not None + await tool.coroutine(query="test query") call_kwargs = mock_retriever_class.call_args[1] assert call_kwargs["folder_path"] == "/Shared/TestFolder" From b75e76ff4066672ea8dfbf1714ee1ee3741c4c56 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:58:26 +0100 Subject: [PATCH 07/21] chore: expose search_id --- .../agent/tools/context_tool.py | 22 +++----- .../retrievers/context_grounding_retriever.py | 1 + tests/agent/tools/test_context_tool.py | 53 ++++++------------- 3 files changed, 23 insertions(+), 53 deletions(-) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index adc2b30b..2fe0eb29 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -51,18 +51,14 @@ def _build_arg_props_from_settings( resource: AgentContextResourceConfig, ) -> dict[str, Any]: - """Build argument_properties from resource settings for query and folder_path_prefix. + """Build argument_properties from context resource settings. - For each setting with variant="argument", creates an argument property entry - that maps the tool parameter to the agent input argument path. + Context resources don't receive argumentProperties from the frontend. + Instead, we derive them from the settings when variant="argument". """ - arg_props = dict(resource.argument_properties) + arg_props: dict[str, Any] = {} - if ( - "query" not in arg_props - and resource.settings.query - and resource.settings.query.variant == "argument" - ): + if resource.settings.query and resource.settings.query.variant == "argument": argument_path = (resource.settings.query.value or "").strip("{}") arg_props["query"] = { "variant": "argument", @@ -71,8 +67,7 @@ def _build_arg_props_from_settings( } if ( - "folder_path_prefix" not in arg_props - and resource.settings.folder_path_prefix + resource.settings.folder_path_prefix and resource.settings.folder_path_prefix.variant == "argument" ): argument_path = (resource.settings.folder_path_prefix.value or "").strip("{}") @@ -183,11 +178,6 @@ async def context_tool_fn( query: Optional[str] = None, folder_path_prefix: Optional[str] = None ) -> dict[str, Any]: resolved_folder_path_prefix = static_folder_path_prefix or folder_path_prefix - if resolved_folder_path_prefix: - logger.info( - "Semantic search folder_path_prefix resolved: %s", - resolved_folder_path_prefix, - ) retriever = ContextGroundingRetriever( index_name=resource.index_name, diff --git a/src/uipath_langchain/retrievers/context_grounding_retriever.py b/src/uipath_langchain/retrievers/context_grounding_retriever.py index 6f8c2a09..21678afb 100644 --- a/src/uipath_langchain/retrievers/context_grounding_retriever.py +++ b/src/uipath_langchain/retrievers/context_grounding_retriever.py @@ -49,6 +49,7 @@ def _get_relevant_documents( page_content=x.content, metadata={ "source": x.source, + "search_id": result.metadata.operation_id if result.metadata else None, "reference": x.reference, "page_number": x.page_number, "score": x.score, diff --git a/tests/agent/tools/test_context_tool.py b/tests/agent/tools/test_context_tool.py index d19b6299..a8422bb2 100644 --- a/tests/agent/tools/test_context_tool.py +++ b/tests/agent/tools/test_context_tool.py @@ -45,7 +45,6 @@ def _make_context_resource( citation_mode_value=None, retrieval_mode=AgentContextRetrievalMode.SEMANTIC, folder_path_prefix=None, - argument_properties=None, **kwargs, ): """Helper to create an AgentContextResourceConfig.""" @@ -67,7 +66,6 @@ def _make_context_resource( folder_path_prefix=folder_path_prefix, ), is_enabled=True, - argument_properties=argument_properties or {}, **kwargs, ) @@ -110,38 +108,15 @@ def test_deep_rag_has_tool_wrapper(self, base_resource_config): assert result.awrapper is not None - def test_deep_rag_with_argument_properties(self, base_resource_config): - """Test that Deep RAG tool correctly receives argument_properties from resource.""" - resource = base_resource_config( - citation_mode_value=AgentContextValueSetting(value="Inline"), - query_value="some query", - argument_properties={ - "folder_path_prefix": { - "variant": "argument", - "argumentPath": "deepRagFolderPrefix", - "isSensitive": False, - }, - }, - ) - - result = handle_deep_rag("test_deep_rag", resource) - - assert isinstance(result, StructuredToolWithArgumentProperties) - assert "folder_path_prefix" in result.argument_properties - # Schema should include folder_path_prefix field - assert isinstance(result.args_schema, type) - schema = result.args_schema.model_json_schema() - assert "folder_path_prefix" in schema["properties"] - - def test_deep_rag_folder_path_prefix_from_settings_fallback( + def test_deep_rag_with_folder_path_prefix_from_settings( self, base_resource_config ): - """Test that folder_path_prefix argument is detected from settings when not in argument_properties.""" + """Test that folder_path_prefix argument_properties are built from settings.""" resource = base_resource_config( citation_mode_value=AgentContextValueSetting(value="Inline"), query_value="some query", folder_path_prefix=AgentContextQuerySetting( - value="{myArgPath}", variant="argument" + value="{deepRagFolderPrefix}", variant="argument" ), ) @@ -149,6 +124,10 @@ def test_deep_rag_folder_path_prefix_from_settings_fallback( assert isinstance(result, StructuredToolWithArgumentProperties) assert "folder_path_prefix" in result.argument_properties + assert ( + result.argument_properties["folder_path_prefix"]["argumentPath"] + == "deepRagFolderPrefix" + ) assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() assert "folder_path_prefix" in schema["properties"] @@ -672,8 +651,8 @@ def test_dynamic_query_batch_transform_has_both_parameters(self): assert "query" in schema["properties"] assert "destination_path" in schema["properties"] - def test_batch_transform_with_argument_properties(self): - """Test that batch transform tool correctly receives argument_properties from resource.""" + def test_batch_transform_with_folder_path_prefix_from_settings(self): + """Test that batch transform builds argument_properties from settings.""" resource = AgentContextResourceConfig( name="batch_transform_tool", description="Batch transform tool", @@ -694,21 +673,21 @@ def test_batch_transform_with_argument_properties(self): name="output_col1", description="First output column" ), ], + folder_path_prefix=AgentContextQuerySetting( + value="{batchFolderPrefix}", variant="argument" + ), ), is_enabled=True, - argument_properties={ - "folder_path_prefix": { - "variant": "argument", - "argumentPath": "batchFolderPrefix", - "isSensitive": False, - }, - }, ) result = handle_batch_transform("batch_transform_tool", resource) assert isinstance(result, StructuredToolWithArgumentProperties) assert "folder_path_prefix" in result.argument_properties + assert ( + result.argument_properties["folder_path_prefix"]["argumentPath"] + == "batchFolderPrefix" + ) assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() assert "folder_path_prefix" in schema["properties"] From 647725c4fff51266f9d5fa09397375b155dd19d3 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:02:45 +0100 Subject: [PATCH 08/21] fix: support query of type prompt argument --- src/uipath_langchain/agent/tools/context_tool.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 2fe0eb29..76b6d0da 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -147,7 +147,7 @@ class ContextOutputSchemaModel(BaseModel): schema_fields: dict[str, Any] = {} - if not static: + if "query" in arg_props: schema_fields["query"] = ( str, Field( @@ -155,6 +155,14 @@ class ContextOutputSchemaModel(BaseModel): description="The query to search for in the knowledge base", ), ) + elif not static: + schema_fields["query"] = ( + str, + Field( + ..., + description="The query to search for in the knowledge base", + ), + ) if "folder_path_prefix" in arg_props: schema_fields["folder_path_prefix"] = ( From ec55c08646bad2d3d93112032017e66b107d0b76 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:12:21 +0100 Subject: [PATCH 09/21] feat: add support for threshold param --- src/uipath_langchain/agent/tools/context_tool.py | 1 + src/uipath_langchain/retrievers/context_grounding_retriever.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 76b6d0da..2ff4e5c1 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -191,6 +191,7 @@ async def context_tool_fn( index_name=resource.index_name, folder_path=get_execution_folder_path(), number_of_results=resource.settings.result_count, + threshold=resource.settings.threshold, scope_folder=resolved_folder_path_prefix, scope_extension=file_extension, ) diff --git a/src/uipath_langchain/retrievers/context_grounding_retriever.py b/src/uipath_langchain/retrievers/context_grounding_retriever.py index 21678afb..0c379467 100644 --- a/src/uipath_langchain/retrievers/context_grounding_retriever.py +++ b/src/uipath_langchain/retrievers/context_grounding_retriever.py @@ -14,6 +14,7 @@ class ContextGroundingRetriever(BaseRetriever): folder_key: str | None = None uipath_sdk: UiPath | None = None number_of_results: int | None = 10 + threshold: float = 0.0 scope_folder: str | None = None scope_extension: str | None = None @@ -37,6 +38,7 @@ def _get_relevant_documents( number_of_results=self.number_of_results if self.number_of_results is not None else 10, + threshold=self.threshold, scope=self._build_scope(), folder_path=self.folder_path, folder_key=self.folder_key, @@ -70,6 +72,7 @@ async def _aget_relevant_documents( number_of_results=self.number_of_results if self.number_of_results is not None else 10, + threshold=self.threshold, scope=self._build_scope(), folder_path=self.folder_path, folder_key=self.folder_key, From 2c6e66ee9f91c2d8309a7ab6ecc6f4c562b5ac4b Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:54:21 +0100 Subject: [PATCH 10/21] chore: lint --- .../retrievers/context_grounding_retriever.py | 4 +++- tests/agent/tools/test_context_tool.py | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/uipath_langchain/retrievers/context_grounding_retriever.py b/src/uipath_langchain/retrievers/context_grounding_retriever.py index 0c379467..dc448c6a 100644 --- a/src/uipath_langchain/retrievers/context_grounding_retriever.py +++ b/src/uipath_langchain/retrievers/context_grounding_retriever.py @@ -51,7 +51,9 @@ def _get_relevant_documents( page_content=x.content, metadata={ "source": x.source, - "search_id": result.metadata.operation_id if result.metadata else None, + "search_id": result.metadata.operation_id + if result.metadata + else None, "reference": x.reference, "page_number": x.page_number, "score": x.score, diff --git a/tests/agent/tools/test_context_tool.py b/tests/agent/tools/test_context_tool.py index a8422bb2..a0b6aa26 100644 --- a/tests/agent/tools/test_context_tool.py +++ b/tests/agent/tools/test_context_tool.py @@ -108,9 +108,7 @@ def test_deep_rag_has_tool_wrapper(self, base_resource_config): assert result.awrapper is not None - def test_deep_rag_with_folder_path_prefix_from_settings( - self, base_resource_config - ): + def test_deep_rag_with_folder_path_prefix_from_settings(self, base_resource_config): """Test that folder_path_prefix argument_properties are built from settings.""" resource = base_resource_config( citation_mode_value=AgentContextValueSetting(value="Inline"), From e152ed6a8ceee4a4b7a86c3802d190bf20d25b00 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:46:17 +0100 Subject: [PATCH 11/21] chore: bump uipath-platform --- pyproject.toml | 2 +- uv.lock | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c6549966..721b885e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath>=2.10.0, <2.11.0", "uipath-core>=0.5.2, <0.6.0", - "uipath-platform>=0.0.18, <0.1.0", + "uipath-platform>=0.0.21, <0.1.0", "uipath-runtime>=0.9.1, <0.10.0", "langgraph>=1.0.0, <2.0.0", "langchain-core>=1.2.11, <2.0.0", diff --git a/uv.lock b/uv.lock index bb06cbcb..003d4442 100644 --- a/uv.lock +++ b/uv.lock @@ -3075,6 +3075,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/98/e8bc58b178266eae2fcf4c9c7a8303a8d41164d781b32d71097924a6bebe/sqlite_vec-0.1.6-py3-none-win_amd64.whl", hash = "sha256:c65bcfd90fa2f41f9000052bcb8bb75d38240b2dae49225389eca6c3136d3f0c", size = 281540, upload-time = "2024-11-20T16:40:37.296Z" }, ] +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + [[package]] name = "sse-starlette" version = "3.2.0" @@ -3393,7 +3402,7 @@ requires-dist = [ { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "uipath", specifier = ">=2.10.0,<2.11.0" }, { name = "uipath-core", specifier = ">=0.5.2,<0.6.0" }, - { name = "uipath-platform", specifier = ">=0.0.18,<0.1.0" }, + { name = "uipath-platform", specifier = ">=0.0.21,<0.1.0" }, { name = "uipath-runtime", specifier = ">=0.9.1,<0.10.0" }, ] provides-extras = ["vertex", "bedrock"] @@ -3416,18 +3425,19 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.0.18" +version = "0.0.21" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "pydantic-function-models" }, + { name = "sqlparse" }, { name = "tenacity" }, { name = "truststore" }, { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/eb/848292baafcb7273b189004161b754e46431bb28a711b7f359a2b7642b27/uipath_platform-0.0.18.tar.gz", hash = "sha256:0d0cf196ffc06de90c3ec12a7b52d88b81f38a34e361eb81690ff88cd2a9a0bd", size = 264141, upload-time = "2026-03-09T16:24:11.158Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/e6/c27edc721367a339d664efad0f8957c62b21c9b5eab0db7d9cc0b4730a31/uipath_platform-0.0.21.tar.gz", hash = "sha256:5862f946c59635e75832ac1f17114154aeb8795fec1f7468eba267498224403d", size = 268906, upload-time = "2026-03-12T09:16:31.946Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/69/a96b8c66d4a0ed5d5ac1a3e44a5836d1b9e4151519a884b67d8db18fb3d8/uipath_platform-0.0.18-py3-none-any.whl", hash = "sha256:9f46b0b01254b95b18cc753bd91f5a5802d0a15e7284224d6f9a1309bb71b6bf", size = 159073, upload-time = "2026-03-09T16:24:09.717Z" }, + { url = "https://files.pythonhosted.org/packages/68/87/30148d1f718974f2e6d9213a3b19abf553ed14e646cd6e89b9071a50545f/uipath_platform-0.0.21-py3-none-any.whl", hash = "sha256:ebc74a62d4f56f9e91b17c3923330a9c3956a5718511e1a97a341330c8bf273c", size = 161917, upload-time = "2026-03-12T09:16:30.268Z" }, ] [[package]] From 96531b40e396bfa6737e4a2c8696a3305592cc7a Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:58:11 +0100 Subject: [PATCH 12/21] chore: lint tests --- tests/agent/tools/test_context_tool.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/agent/tools/test_context_tool.py b/tests/agent/tools/test_context_tool.py index a0b6aa26..dae836e2 100644 --- a/tests/agent/tools/test_context_tool.py +++ b/tests/agent/tools/test_context_tool.py @@ -122,10 +122,8 @@ def test_deep_rag_with_folder_path_prefix_from_settings(self, base_resource_conf assert isinstance(result, StructuredToolWithArgumentProperties) assert "folder_path_prefix" in result.argument_properties - assert ( - result.argument_properties["folder_path_prefix"]["argumentPath"] - == "deepRagFolderPrefix" - ) + folder_prop = dict(result.argument_properties["folder_path_prefix"]) + assert folder_prop["argumentPath"] == "deepRagFolderPrefix" assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() assert "folder_path_prefix" in schema["properties"] @@ -682,10 +680,8 @@ def test_batch_transform_with_folder_path_prefix_from_settings(self): assert isinstance(result, StructuredToolWithArgumentProperties) assert "folder_path_prefix" in result.argument_properties - assert ( - result.argument_properties["folder_path_prefix"]["argumentPath"] - == "batchFolderPrefix" - ) + folder_prop = dict(result.argument_properties["folder_path_prefix"]) + assert folder_prop["argumentPath"] == "batchFolderPrefix" assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() assert "folder_path_prefix" in schema["properties"] From ffe0f2a69424b08fdc1ce4f227ade8a40fd15374 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:06:53 +0100 Subject: [PATCH 13/21] fix: tests --- tests/agent/tools/test_context_tool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/agent/tools/test_context_tool.py b/tests/agent/tools/test_context_tool.py index dae836e2..d635a903 100644 --- a/tests/agent/tools/test_context_tool.py +++ b/tests/agent/tools/test_context_tool.py @@ -123,7 +123,7 @@ def test_deep_rag_with_folder_path_prefix_from_settings(self, base_resource_conf assert isinstance(result, StructuredToolWithArgumentProperties) assert "folder_path_prefix" in result.argument_properties folder_prop = dict(result.argument_properties["folder_path_prefix"]) - assert folder_prop["argumentPath"] == "deepRagFolderPrefix" + assert folder_prop["argument_path"] == "deepRagFolderPrefix" assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() assert "folder_path_prefix" in schema["properties"] @@ -681,7 +681,7 @@ def test_batch_transform_with_folder_path_prefix_from_settings(self): assert isinstance(result, StructuredToolWithArgumentProperties) assert "folder_path_prefix" in result.argument_properties folder_prop = dict(result.argument_properties["folder_path_prefix"]) - assert folder_prop["argumentPath"] == "batchFolderPrefix" + assert folder_prop["argument_path"] == "batchFolderPrefix" assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() assert "folder_path_prefix" in schema["properties"] From 68f89e093f46555e8a78c14cd6377667b164b6f5 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:40:16 +0100 Subject: [PATCH 14/21] chore: use AgentToolArgumentProperties; resolve folder_path_prefix from agent state --- .../agent/tools/context_tool.py | 106 +++++++++--------- tests/agent/tools/test_context_tool.py | 18 +-- 2 files changed, 65 insertions(+), 59 deletions(-) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 2ff4e5c1..871a0ea1 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -4,6 +4,7 @@ import uuid from typing import Any, Optional, cast +from jsonpath_ng import parse # type: ignore[import-untyped] from langchain_core.documents import Document from langchain_core.messages import ToolCall from langchain_core.tools import BaseTool, StructuredTool @@ -11,6 +12,8 @@ from uipath.agent.models.agent import ( AgentContextResourceConfig, AgentContextRetrievalMode, + AgentToolArgumentArgumentProperties, + AgentToolArgumentProperties, ) from uipath.eval.mocks import mockable from uipath.platform import UiPath @@ -50,36 +53,38 @@ def _build_arg_props_from_settings( resource: AgentContextResourceConfig, -) -> dict[str, Any]: +) -> dict[str, AgentToolArgumentProperties]: """Build argument_properties from context resource settings. Context resources don't receive argumentProperties from the frontend. Instead, we derive them from the settings when variant="argument". + Only includes fields that belong in the tool's args_schema (i.e. query). """ - arg_props: dict[str, Any] = {} + arg_props: dict[str, AgentToolArgumentProperties] = {} if resource.settings.query and resource.settings.query.variant == "argument": argument_path = (resource.settings.query.value or "").strip("{}") - arg_props["query"] = { - "variant": "argument", - "argumentPath": argument_path, - "isSensitive": False, - } - - if ( - resource.settings.folder_path_prefix - and resource.settings.folder_path_prefix.variant == "argument" - ): - argument_path = (resource.settings.folder_path_prefix.value or "").strip("{}") - arg_props["folder_path_prefix"] = { - "variant": "argument", - "argumentPath": argument_path, - "isSensitive": False, - } + arg_props["query"] = AgentToolArgumentArgumentProperties( + argumentPath=argument_path, + isSensitive=False, + ) return arg_props +def _resolve_folder_path_prefix_from_state( + resource: AgentContextResourceConfig, + state: dict[str, Any], +) -> str | None: + """Resolve folder_path_prefix from agent state using jsonpath from settings.""" + setting = resource.settings.folder_path_prefix + if not setting or setting.variant != "argument" or not setting.value: + return None + argument_path = "$." + setting.value.strip("{}") + matches = parse(argument_path).find(state) + return matches[0].value if matches else None + + def _resolve_file_extension(resource: AgentContextResourceConfig) -> str | None: """Resolve file extension from settings, returning None for 'All' or empty.""" if resource.settings.file_extension and resource.settings.file_extension.value: @@ -164,14 +169,14 @@ class ContextOutputSchemaModel(BaseModel): ), ) - if "folder_path_prefix" in arg_props: - schema_fields["folder_path_prefix"] = ( - str, - Field( - default=None, - description="The folder path prefix within the index to filter on", - ), - ) + has_arg_folder = ( + resource.settings.folder_path_prefix + and resource.settings.folder_path_prefix.variant == "argument" + and resource.settings.folder_path_prefix.value + ) + + # store folder_path_prefix from agent state before tool.ainvoke validates the args. + _resolved_arg_folder_prefix: list[str | None] = [None] input_model = create_model("SemanticSearchInput", **schema_fields) @@ -183,9 +188,11 @@ class ContextOutputSchemaModel(BaseModel): example_calls=[], # Examples cannot be provided for context. ) async def context_tool_fn( - query: Optional[str] = None, folder_path_prefix: Optional[str] = None + query: Optional[str] = None, ) -> dict[str, Any]: - resolved_folder_path_prefix = static_folder_path_prefix or folder_path_prefix + resolved_folder_path_prefix = ( + static_folder_path_prefix or _resolved_arg_folder_prefix[0] + ) retriever = ContextGroundingRetriever( index_name=resource.index_name, @@ -206,7 +213,7 @@ async def context_tool_fn( ] } - if arg_props: + if arg_props or has_arg_folder: async def context_semantic_search_wrapper( tool: BaseTool, @@ -216,6 +223,9 @@ async def context_semantic_search_wrapper( call["args"] = handle_static_args( cast(ArgumentPropertiesMixin, tool), state, call["args"] ) + _resolved_arg_folder_prefix[0] = _resolve_folder_path_prefix_from_state( + resource, dict(state) + ) return await tool.ainvoke(call) tool = StructuredToolWithArgumentProperties( @@ -297,17 +307,11 @@ def handle_deep_rag( } ) - if "folder_path_prefix" in arg_props: - schema_fields["folder_path_prefix"] = ( - str, - Field( - default=None, - description="The folder path prefix within the index to filter on", - ), - ) - input_model = create_model("DeepRagInput", **schema_fields) + # store folder_path_prefix from agent state before tool.ainvoke validates the args. + _resolved_arg_folder_prefix: list[str | None] = [None] + @mockable( name=resource.name, description=resource.description, @@ -316,11 +320,12 @@ def handle_deep_rag( example_calls=[], # Examples cannot be provided for context. ) async def context_tool_fn( - query: Optional[str] = None, folder_path_prefix: Optional[str] = None + query: Optional[str] = None, ) -> dict[str, Any]: actual_prompt = prompt or query glob_pattern = build_glob_pattern( - folder_path_prefix=static_folder_path_prefix or folder_path_prefix, + folder_path_prefix=static_folder_path_prefix + or _resolved_arg_folder_prefix[0], file_extension=file_extension, ) @@ -345,6 +350,9 @@ async def context_deep_rag_wrapper( call["args"] = handle_static_args( cast(ArgumentPropertiesMixin, tool), state, call["args"] ) + _resolved_arg_folder_prefix[0] = _resolve_folder_path_prefix_from_state( + resource, dict(state) + ) return await tool.ainvoke(call) tool = StructuredToolWithArgumentProperties( @@ -432,16 +440,11 @@ def handle_batch_transform( description="The relative file path destination for the modified csv file", ), ) - if "folder_path_prefix" in arg_props: - schema_fields["folder_path_prefix"] = ( - str, - Field( - default=None, - description="The folder path prefix within the index to filter on", - ), - ) input_model = create_model("BatchTransformInput", **schema_fields) + # store folder_path_prefix from agent state before tool.ainvoke validates the args. + _resolved_arg_folder_prefix: list[str | None] = [None] + @mockable( name=resource.name, description=resource.description, @@ -452,11 +455,11 @@ def handle_batch_transform( async def context_tool_fn( query: Optional[str] = None, destination_path: str = "output.csv", - folder_path_prefix: Optional[str] = None, ) -> dict[str, Any]: actual_prompt = prompt or query glob_pattern = build_glob_pattern( - folder_path_prefix=static_folder_path_prefix or folder_path_prefix, + folder_path_prefix=static_folder_path_prefix + or _resolved_arg_folder_prefix[0], file_extension=None, ) @@ -502,6 +505,9 @@ async def context_batch_transform_wrapper( call["args"] = handle_static_args( cast(ArgumentPropertiesMixin, tool), state, call["args"] ) + _resolved_arg_folder_prefix[0] = _resolve_folder_path_prefix_from_state( + resource, dict(state) + ) return await job_attachment_wrapper(tool, call, state) tool = StructuredToolWithArgumentProperties( diff --git a/tests/agent/tools/test_context_tool.py b/tests/agent/tools/test_context_tool.py index d635a903..29a67c52 100644 --- a/tests/agent/tools/test_context_tool.py +++ b/tests/agent/tools/test_context_tool.py @@ -109,7 +109,7 @@ def test_deep_rag_has_tool_wrapper(self, base_resource_config): assert result.awrapper is not None def test_deep_rag_with_folder_path_prefix_from_settings(self, base_resource_config): - """Test that folder_path_prefix argument_properties are built from settings.""" + """Test that folder_path_prefix with argument variant is resolved in wrapper, not via argument_properties.""" resource = base_resource_config( citation_mode_value=AgentContextValueSetting(value="Inline"), query_value="some query", @@ -121,12 +121,12 @@ def test_deep_rag_with_folder_path_prefix_from_settings(self, base_resource_conf result = handle_deep_rag("test_deep_rag", resource) assert isinstance(result, StructuredToolWithArgumentProperties) - assert "folder_path_prefix" in result.argument_properties - folder_prop = dict(result.argument_properties["folder_path_prefix"]) - assert folder_prop["argument_path"] == "deepRagFolderPrefix" + # folder_path_prefix is resolved directly in the wrapper from state, + # not via argument_properties or args_schema + assert "folder_path_prefix" not in result.argument_properties assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() - assert "folder_path_prefix" in schema["properties"] + assert "folder_path_prefix" not in schema.get("properties", {}) def test_missing_static_query_value_raises_error(self, base_resource_config): """Test that missing query.value for static variant raises AgentStartupError.""" @@ -679,12 +679,12 @@ def test_batch_transform_with_folder_path_prefix_from_settings(self): result = handle_batch_transform("batch_transform_tool", resource) assert isinstance(result, StructuredToolWithArgumentProperties) - assert "folder_path_prefix" in result.argument_properties - folder_prop = dict(result.argument_properties["folder_path_prefix"]) - assert folder_prop["argument_path"] == "batchFolderPrefix" + # folder_path_prefix is resolved directly in the wrapper from state, + # not via argument_properties or args_schema + assert "folder_path_prefix" not in result.argument_properties assert isinstance(result.args_schema, type) schema = result.args_schema.model_json_schema() - assert "folder_path_prefix" in schema["properties"] + assert "folder_path_prefix" not in schema.get("properties", {}) @pytest.mark.asyncio async def test_static_query_batch_transform_uses_predefined_query( From 25b9e2fc21f8e9fdfee7e7559ccf318a01bb1e2c Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:48:33 +0100 Subject: [PATCH 15/21] chore: mergefix - add various asserts --- pyproject.toml | 2 +- src/uipath_langchain/agent/tools/context_tool.py | 4 ++++ uv.lock | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6dbd3c56..1c406064 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.8.19" +version = "0.8.20" description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 871a0ea1..38f5af6a 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -60,6 +60,7 @@ def _build_arg_props_from_settings( Instead, we derive them from the settings when variant="argument". Only includes fields that belong in the tool's args_schema (i.e. query). """ + assert resource.settings is not None arg_props: dict[str, AgentToolArgumentProperties] = {} if resource.settings.query and resource.settings.query.variant == "argument": @@ -77,6 +78,7 @@ def _resolve_folder_path_prefix_from_state( state: dict[str, Any], ) -> str | None: """Resolve folder_path_prefix from agent state using jsonpath from settings.""" + assert resource.settings is not None setting = resource.settings.folder_path_prefix if not setting or setting.variant != "argument" or not setting.value: return None @@ -99,6 +101,7 @@ def _resolve_static_folder_path_prefix( resource: AgentContextResourceConfig, ) -> str | None: """Resolve static folder_path_prefix from settings.""" + assert resource.settings is not None if ( resource.settings.folder_path_prefix and resource.settings.folder_path_prefix.value @@ -110,6 +113,7 @@ def _resolve_static_folder_path_prefix( def is_static_query(resource: AgentContextResourceConfig) -> bool: """Check if the resource configuration uses a static query variant.""" + assert resource.settings is not None if resource.settings.query is None or resource.settings.query.variant is None: return False return resource.settings.query.variant.lower() == "static" diff --git a/uv.lock b/uv.lock index 122f72ad..abecd218 100644 --- a/uv.lock +++ b/uv.lock @@ -3333,7 +3333,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.8.17" +version = "0.8.20" source = { editable = "." } dependencies = [ { name = "httpx" }, From 91fa8b64144ead91a5ba03f600dcca7c08c417f4 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:37:51 +0100 Subject: [PATCH 16/21] fix: lint --- src/uipath_langchain/agent/tools/context_tool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 38f5af6a..49e87aaf 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -66,8 +66,8 @@ def _build_arg_props_from_settings( if resource.settings.query and resource.settings.query.variant == "argument": argument_path = (resource.settings.query.value or "").strip("{}") arg_props["query"] = AgentToolArgumentArgumentProperties( - argumentPath=argument_path, - isSensitive=False, + is_sensitive=False, + argument_path=argument_path, ) return arg_props From bbcbf3c7a32e750c4c509e3242f6c561e3566b0e Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:25:36 +0100 Subject: [PATCH 17/21] chore: update deps --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1c406064..7ae3b17b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath>=2.10.0, <2.11.0", "uipath-core>=0.5.2, <0.6.0", - "uipath-platform>=0.0.21, <0.1.0", + "uipath-platform>=0.0.23, <0.1.0", "uipath-runtime>=0.9.1, <0.10.0", "langgraph>=1.0.0, <2.0.0", "langchain-core>=1.2.11, <2.0.0", diff --git a/uv.lock b/uv.lock index abecd218..0a17ff87 100644 --- a/uv.lock +++ b/uv.lock @@ -3402,7 +3402,7 @@ requires-dist = [ { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "uipath", specifier = ">=2.10.0,<2.11.0" }, { name = "uipath-core", specifier = ">=0.5.2,<0.6.0" }, - { name = "uipath-platform", specifier = ">=0.0.21,<0.1.0" }, + { name = "uipath-platform", specifier = ">=0.0.23,<0.1.0" }, { name = "uipath-runtime", specifier = ">=0.9.1,<0.10.0" }, ] provides-extras = ["vertex", "bedrock"] @@ -3425,7 +3425,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.0.21" +version = "0.0.23" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -3435,9 +3435,9 @@ dependencies = [ { name = "truststore" }, { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/e6/c27edc721367a339d664efad0f8957c62b21c9b5eab0db7d9cc0b4730a31/uipath_platform-0.0.21.tar.gz", hash = "sha256:5862f946c59635e75832ac1f17114154aeb8795fec1f7468eba267498224403d", size = 268906, upload-time = "2026-03-12T09:16:31.946Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/c1/f1cfd23d977fc2fec2e880dac42b1fdb78c28fef80008ebf4bd43ca9b12f/uipath_platform-0.0.23.tar.gz", hash = "sha256:a84d9da29865155080efcc837f6c2b2186d480e34fc877dec06f3ec315c1e52c", size = 269669, upload-time = "2026-03-13T07:50:06.953Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/87/30148d1f718974f2e6d9213a3b19abf553ed14e646cd6e89b9071a50545f/uipath_platform-0.0.21-py3-none-any.whl", hash = "sha256:ebc74a62d4f56f9e91b17c3923330a9c3956a5718511e1a97a341330c8bf273c", size = 161917, upload-time = "2026-03-12T09:16:30.268Z" }, + { url = "https://files.pythonhosted.org/packages/cf/32/167abe3730ab8c0dd89abee6ebea6e1a35e0a1548be620a491a60bc7cc0d/uipath_platform-0.0.23-py3-none-any.whl", hash = "sha256:e2ea4d9341540a5a02baca8e07ba05dfb8947f2d525f8677834f81c735826772", size = 161946, upload-time = "2026-03-13T07:50:04.956Z" }, ] [[package]] From 6ae1cc31748497083390de34ba8fcc9050225d8a Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:39:49 +0100 Subject: [PATCH 18/21] chore: _resolved_arg_folder_prefix as str | None --- .../agent/tools/context_tool.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 49e87aaf..ed86e81f 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -179,8 +179,7 @@ class ContextOutputSchemaModel(BaseModel): and resource.settings.folder_path_prefix.value ) - # store folder_path_prefix from agent state before tool.ainvoke validates the args. - _resolved_arg_folder_prefix: list[str | None] = [None] + _resolved_arg_folder_prefix: str | None = None input_model = create_model("SemanticSearchInput", **schema_fields) @@ -195,7 +194,7 @@ async def context_tool_fn( query: Optional[str] = None, ) -> dict[str, Any]: resolved_folder_path_prefix = ( - static_folder_path_prefix or _resolved_arg_folder_prefix[0] + static_folder_path_prefix or _resolved_arg_folder_prefix ) retriever = ContextGroundingRetriever( @@ -227,7 +226,8 @@ async def context_semantic_search_wrapper( call["args"] = handle_static_args( cast(ArgumentPropertiesMixin, tool), state, call["args"] ) - _resolved_arg_folder_prefix[0] = _resolve_folder_path_prefix_from_state( + nonlocal _resolved_arg_folder_prefix + _resolved_arg_folder_prefix = _resolve_folder_path_prefix_from_state( resource, dict(state) ) return await tool.ainvoke(call) @@ -313,8 +313,7 @@ def handle_deep_rag( input_model = create_model("DeepRagInput", **schema_fields) - # store folder_path_prefix from agent state before tool.ainvoke validates the args. - _resolved_arg_folder_prefix: list[str | None] = [None] + _resolved_arg_folder_prefix: str | None = None @mockable( name=resource.name, @@ -329,7 +328,7 @@ async def context_tool_fn( actual_prompt = prompt or query glob_pattern = build_glob_pattern( folder_path_prefix=static_folder_path_prefix - or _resolved_arg_folder_prefix[0], + or _resolved_arg_folder_prefix, file_extension=file_extension, ) @@ -351,10 +350,11 @@ async def context_deep_rag_wrapper( call: ToolCall, state: AgentGraphState, ) -> ToolWrapperReturnType: + nonlocal _resolved_arg_folder_prefix call["args"] = handle_static_args( cast(ArgumentPropertiesMixin, tool), state, call["args"] ) - _resolved_arg_folder_prefix[0] = _resolve_folder_path_prefix_from_state( + _resolved_arg_folder_prefix = _resolve_folder_path_prefix_from_state( resource, dict(state) ) return await tool.ainvoke(call) @@ -446,8 +446,7 @@ def handle_batch_transform( ) input_model = create_model("BatchTransformInput", **schema_fields) - # store folder_path_prefix from agent state before tool.ainvoke validates the args. - _resolved_arg_folder_prefix: list[str | None] = [None] + _resolved_arg_folder_prefix: str | None = None @mockable( name=resource.name, @@ -463,7 +462,7 @@ async def context_tool_fn( actual_prompt = prompt or query glob_pattern = build_glob_pattern( folder_path_prefix=static_folder_path_prefix - or _resolved_arg_folder_prefix[0], + or _resolved_arg_folder_prefix, file_extension=None, ) @@ -509,7 +508,8 @@ async def context_batch_transform_wrapper( call["args"] = handle_static_args( cast(ArgumentPropertiesMixin, tool), state, call["args"] ) - _resolved_arg_folder_prefix[0] = _resolve_folder_path_prefix_from_state( + nonlocal _resolved_arg_folder_prefix + _resolved_arg_folder_prefix = _resolve_folder_path_prefix_from_state( resource, dict(state) ) return await job_attachment_wrapper(tool, call, state) From af53c0ca121eeefbc8135e615ef9496ad722e1b1 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:53:33 +0100 Subject: [PATCH 19/21] chore: tests cleanup --- tests/agent/tools/test_context_tool.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/agent/tools/test_context_tool.py b/tests/agent/tools/test_context_tool.py index 29a67c52..eaaabf00 100644 --- a/tests/agent/tools/test_context_tool.py +++ b/tests/agent/tools/test_context_tool.py @@ -125,8 +125,6 @@ def test_deep_rag_with_folder_path_prefix_from_settings(self, base_resource_conf # not via argument_properties or args_schema assert "folder_path_prefix" not in result.argument_properties assert isinstance(result.args_schema, type) - schema = result.args_schema.model_json_schema() - assert "folder_path_prefix" not in schema.get("properties", {}) def test_missing_static_query_value_raises_error(self, base_resource_config): """Test that missing query.value for static variant raises AgentStartupError.""" @@ -683,8 +681,6 @@ def test_batch_transform_with_folder_path_prefix_from_settings(self): # not via argument_properties or args_schema assert "folder_path_prefix" not in result.argument_properties assert isinstance(result.args_schema, type) - schema = result.args_schema.model_json_schema() - assert "folder_path_prefix" not in schema.get("properties", {}) @pytest.mark.asyncio async def test_static_query_batch_transform_uses_predefined_query( From fb5d653bf569755f878730a613a4eeab998be4f6 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:55:52 +0100 Subject: [PATCH 20/21] chore: lint --- src/uipath_langchain/agent/tools/context_tool.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index ed86e81f..6eced4d9 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -327,8 +327,7 @@ async def context_tool_fn( ) -> dict[str, Any]: actual_prompt = prompt or query glob_pattern = build_glob_pattern( - folder_path_prefix=static_folder_path_prefix - or _resolved_arg_folder_prefix, + folder_path_prefix=static_folder_path_prefix or _resolved_arg_folder_prefix, file_extension=file_extension, ) @@ -461,8 +460,7 @@ async def context_tool_fn( ) -> dict[str, Any]: actual_prompt = prompt or query glob_pattern = build_glob_pattern( - folder_path_prefix=static_folder_path_prefix - or _resolved_arg_folder_prefix, + folder_path_prefix=static_folder_path_prefix or _resolved_arg_folder_prefix, file_extension=None, ) From e2bddbbf60fac7d8bcd7503c18367defe1e95b1f Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:45:58 +0100 Subject: [PATCH 21/21] chore: bump version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7ae3b17b..47e483e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.8.20" +version = "0.8.21" description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/uv.lock b/uv.lock index 0a17ff87..701cc06c 100644 --- a/uv.lock +++ b/uv.lock @@ -3333,7 +3333,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.8.20" +version = "0.8.21" source = { editable = "." } dependencies = [ { name = "httpx" },