Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[project]
name = "uipath-langchain"
version = "0.5.21"
version = "0.5.22"
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"
dependencies = [
"uipath>=2.6.22,<2.7.0",
"uipath>=2.7.0,<2.8.0",
"uipath-runtime>=0.6.0, <0.7.0",
"langgraph>=1.0.0, <2.0.0",
"langchain-core>=1.2.5, <2.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Batch Transform tool for creating and retrieving batch transformations."""

import uuid
from typing import Any

from langchain_core.language_models import BaseChatModel
from langchain_core.messages.tool import ToolCall
from langchain_core.tools import BaseTool, StructuredTool
from langgraph.types import interrupt
from uipath.agent.models.agent import (
AgentInternalBatchTransformToolProperties,
AgentInternalToolResourceConfig,
)
from uipath.eval.mocks import mockable
from uipath.platform import UiPath
from uipath.platform.common import CreateBatchTransform
from uipath.platform.common.interrupt_models import WaitEphemeralIndex
from uipath.platform.context_grounding import (
BatchTransformOutputColumn,
EphemeralIndexUsage,
)
from uipath.platform.context_grounding.context_grounding_index import (
ContextGroundingIndex,
)

from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
from uipath_langchain.agent.react.types import AgentGraphState
from uipath_langchain.agent.tools.internal_tools.schema_utils import (
add_query_field_to_schema,
)
from uipath_langchain.agent.tools.static_args import handle_static_args
from uipath_langchain.agent.tools.structured_tool_with_argument_properties import (
StructuredToolWithArgumentProperties,
)
from uipath_langchain.agent.tools.tool_node import ToolWrapperReturnType
from uipath_langchain.agent.tools.utils import sanitize_tool_name


def create_batch_transform_tool(
resource: AgentInternalToolResourceConfig, llm: BaseChatModel
) -> StructuredTool:
"""Create a Batch Transform internal tool from resource configuration."""
if not isinstance(resource.properties, AgentInternalBatchTransformToolProperties):
raise ValueError(
f"Expected AgentInternalBatchTransformToolProperties, got {type(resource.properties)}"
)

tool_name = sanitize_tool_name(resource.name)
properties = resource.properties
settings = properties.settings

# Extract settings
query_setting = settings.query
folder_path_prefix_setting = settings.folder_path_prefix
output_columns_setting = settings.output_columns
web_search_grounding_setting = settings.web_search_grounding

is_query_static = query_setting and query_setting.variant == "static"
static_query = query_setting.value if is_query_static else None

static_folder_path_prefix = None
if folder_path_prefix_setting:
static_folder_path_prefix = getattr(folder_path_prefix_setting, "value", None)

static_web_search = False
if web_search_grounding_setting:
value = getattr(web_search_grounding_setting, "value", None)
static_web_search = value == "Enabled" if value else False

batch_transform_output_columns = [
BatchTransformOutputColumn(name=col.name, description=col.description or "")
for col in output_columns_setting
]

# Use resource input schema and add query field if dynamic
input_schema = dict(resource.input_schema)
if not is_query_static:
add_query_field_to_schema(
input_schema,
query_description=query_setting.description if query_setting else None,
default_description="Describe the task: what to research, what to synthesize.",
)

# Create input model from modified schema
input_model = create_model(input_schema)
output_model = create_model(resource.output_schema)

@mockable(
name=resource.name,
description=resource.description,
input_schema=input_model.model_json_schema() if input_model else None,
output_schema=output_model.model_json_schema(),
example_calls=[], # Examples cannot be provided for internal tools
)
async def batch_transform_tool_fn(**kwargs: Any) -> dict[str, Any]:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can the args not be typed?

Copy link
Contributor Author

@CalebMartinUiPath CalebMartinUiPath Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the problem with typing the args is that then I would have to duplicate the function. 1 for static which doesn't expect a query and 1 for dynamic which needs a query. which I could do idc

query = kwargs.get("query") if not is_query_static else static_query
if not query:
raise ValueError("Query is required for Batch Transform tool")

if "attachment" not in kwargs:
raise ValueError("Argument 'attachment' is not available")

attachment = kwargs.get("attachment")
if not attachment:
raise ValueError("Attachment is required for Batch Transform tool")

attachment_id = getattr(attachment, "ID", None)
if not attachment_id:
raise ValueError("Attachment ID is required")

destination_path = kwargs.get("destination_path", "output.csv")

uipath = UiPath()
ephemeral_index = await uipath.context_grounding.create_ephemeral_index_async(
usage=EphemeralIndexUsage.BATCH_RAG,
attachments=[attachment_id],
)

if ephemeral_index.in_progress_ingestion():
ephemeral_index_dict = interrupt(WaitEphemeralIndex(index=ephemeral_index))
ephemeral_index = ContextGroundingIndex(**ephemeral_index_dict)

return interrupt(
CreateBatchTransform(
name=f"task-{uuid.uuid4()}",
index_name=ephemeral_index.name,
index_id=ephemeral_index.id,
prompt=query,
output_columns=batch_transform_output_columns,
storage_bucket_folder_path_prefix=static_folder_path_prefix,
enable_web_search_grounding=static_web_search,
destination_path=destination_path,
is_ephemeral_index=True,
)
)

# Import here to avoid circular dependency
from uipath_langchain.agent.wrappers import get_job_attachment_wrapper

job_attachment_wrapper = get_job_attachment_wrapper(output_type=output_model)

async def batch_transform_tool_wrapper(
tool: BaseTool,
call: ToolCall,
state: AgentGraphState,
) -> ToolWrapperReturnType:
call["args"] = handle_static_args(resource, state, call["args"])
return await job_attachment_wrapper(tool, call, state)

tool = StructuredToolWithArgumentProperties(
name=tool_name,
description=resource.description,
args_schema=input_model,
coroutine=batch_transform_tool_fn,
output_type=output_model,
argument_properties=resource.argument_properties,
)
tool.set_tool_wrappers(awrapper=batch_transform_tool_wrapper)
return tool
142 changes: 142 additions & 0 deletions src/uipath_langchain/agent/tools/internal_tools/deeprag_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Deeprag tool for creation and retrieval of deeprags."""

import uuid
from typing import Any

from langchain_core.language_models import BaseChatModel
from langchain_core.messages.tool import ToolCall
from langchain_core.tools import BaseTool, StructuredTool
from langgraph.types import interrupt
from uipath.agent.models.agent import (
AgentInternalDeepRagToolProperties,
AgentInternalToolResourceConfig,
)
from uipath.eval.mocks import mockable
from uipath.platform import UiPath
from uipath.platform.common import CreateDeepRag
from uipath.platform.common.interrupt_models import WaitEphemeralIndex
from uipath.platform.context_grounding import (
CitationMode,
EphemeralIndexUsage,
)
from uipath.platform.context_grounding.context_grounding_index import (
ContextGroundingIndex,
)

from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
from uipath_langchain.agent.react.types import AgentGraphState
from uipath_langchain.agent.tools.internal_tools.schema_utils import (
add_query_field_to_schema,
)
from uipath_langchain.agent.tools.static_args import handle_static_args
from uipath_langchain.agent.tools.structured_tool_with_argument_properties import (
StructuredToolWithArgumentProperties,
)
from uipath_langchain.agent.tools.tool_node import ToolWrapperReturnType
from uipath_langchain.agent.tools.utils import sanitize_tool_name


def create_deeprag_tool(
resource: AgentInternalToolResourceConfig, llm: BaseChatModel
) -> StructuredTool:
"""Create a DeepRAG internal tool from resource configuration."""
if not isinstance(resource.properties, AgentInternalDeepRagToolProperties):
raise ValueError(
f"Expected AgentInternalDeepRagToolProperties, got {type(resource.properties)}"
)

tool_name = sanitize_tool_name(resource.name)
properties = resource.properties
settings = properties.settings

# Extract settings
query_setting = settings.query
citation_mode_setting = settings.citation_mode

citation_mode = (
CitationMode(citation_mode_setting.value)
if citation_mode_setting
else CitationMode.INLINE
)

is_query_static = query_setting and query_setting.variant == "static"
static_query = query_setting.value if is_query_static else None

input_schema = dict(resource.input_schema)
if not is_query_static:
add_query_field_to_schema(
input_schema,
query_description=query_setting.description if query_setting else None,
default_description="Describe the task: what to research across documents, what to synthesize and how to cite sources.",
)

input_model = create_model(input_schema)
output_model = create_model(resource.output_schema)

@mockable(
name=resource.name,
description=resource.description,
input_schema=input_model.model_json_schema() if input_model else None,
output_schema=output_model.model_json_schema(),
example_calls=[], # Examples cannot be provided for internal tools
)
async def deeprag_tool_fn(**kwargs: Any) -> dict[str, Any]:
query = kwargs.get("query") if not is_query_static else static_query
if not query:
raise ValueError("Query is required for DeepRAG tool")

if "attachment" not in kwargs:
raise ValueError("Argument 'attachment' is not available")

attachment = kwargs.get("attachment")
if not attachment:
raise ValueError("Attachment is required for DeepRAG tool")

attachment_id = getattr(attachment, "ID", None)
if not attachment_id:
raise ValueError("Attachment ID is required")

uipath = UiPath()
ephemeral_index = await uipath.context_grounding.create_ephemeral_index_async(
usage=EphemeralIndexUsage.DEEP_RAG,
attachments=[attachment_id],
)

if ephemeral_index.in_progress_ingestion():
ephemeral_index_dict = interrupt(WaitEphemeralIndex(index=ephemeral_index))
ephemeral_index = ContextGroundingIndex(**ephemeral_index_dict)

return interrupt(
CreateDeepRag(
name=f"task-{uuid.uuid4()}",
index_name=ephemeral_index.name,
index_id=ephemeral_index.id,
prompt=query,
citation_mode=citation_mode,
is_ephemeral_index=True,
)
)

# Import here to avoid circular dependency
from uipath_langchain.agent.wrappers import get_job_attachment_wrapper

job_attachment_wrapper = get_job_attachment_wrapper(output_type=output_model)

async def deeprag_tool_wrapper(
tool: BaseTool,
call: ToolCall,
state: AgentGraphState,
) -> ToolWrapperReturnType:
call["args"] = handle_static_args(resource, state, call["args"])
return await job_attachment_wrapper(tool, call, state)

tool = StructuredToolWithArgumentProperties(
name=tool_name,
description=resource.description,
args_schema=input_model,
coroutine=deeprag_tool_fn,
output_type=output_model,
argument_properties=resource.argument_properties,
)
tool.set_tool_wrappers(awrapper=deeprag_tool_wrapper)
return tool
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@
)

from .analyze_files_tool import create_analyze_file_tool
from .batch_transform_tool import create_batch_transform_tool
from .deeprag_tool import create_deeprag_tool

_INTERNAL_TOOL_HANDLERS: dict[
AgentInternalToolType,
Callable[[AgentInternalToolResourceConfig, BaseChatModel], StructuredTool],
] = {
AgentInternalToolType.ANALYZE_FILES: create_analyze_file_tool,
AgentInternalToolType.DEEP_RAG: create_deeprag_tool,
AgentInternalToolType.BATCH_TRANSFORM: create_batch_transform_tool,
}


Expand Down
33 changes: 33 additions & 0 deletions src/uipath_langchain/agent/tools/internal_tools/schema_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Utility functions for internal tool schema manipulation."""

from typing import Any


def add_query_field_to_schema(
input_schema: dict[str, Any],
query_description: str | None = None,
default_description: str = "Query or prompt for the operation.",
) -> None:
"""Add a dynamic query field to an input schema.

This modifies the input schema in-place by adding a 'query' property
and marking it as required.

Args:
input_schema: The JSON schema dict to modify
query_description: Custom description for the query field
default_description: Default description if query_description is not provided
"""
if "properties" not in input_schema:
input_schema["properties"] = {}

input_schema["properties"]["query"] = {
"type": "string",
"description": query_description if query_description else default_description,
}

if "required" not in input_schema:
input_schema["required"] = []

if "query" not in input_schema["required"]:
input_schema["required"].append("query")
4 changes: 2 additions & 2 deletions tests/agent/tools/internal_tools/test_analyze_files_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from langchain_core.messages import AIMessage, HumanMessage
from pydantic import BaseModel, ConfigDict, Field
from uipath.agent.models.agent import (
AgentInternalToolProperties,
AgentInternalAnalyzeFilesToolProperties,
AgentInternalToolResourceConfig,
AgentInternalToolType,
)
Expand Down Expand Up @@ -60,7 +60,7 @@ def resource_config(self):
}
output_schema = {"type": "object", "properties": {"result": {"type": "string"}}}

properties = AgentInternalToolProperties(
properties = AgentInternalAnalyzeFilesToolProperties(
tool_type=AgentInternalToolType.ANALYZE_FILES
)

Expand Down
Loading