Skip to content
Open
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-langchain"
version = "0.14.7"
version = "0.14.8"
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"
Expand Down
13 changes: 13 additions & 0 deletions src/uipath_langchain/_client_side_tool_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Shared client-side tool types."""

from typing import Any, TypedDict


class ClientSideToolInfo(TypedDict):
"""Schemas exposed for a client-side tool."""

input_schema: dict[str, Any] | None
output_schema: dict[str, Any] | None


__all__ = ["ClientSideToolInfo"]
29 changes: 16 additions & 13 deletions src/uipath_langchain/agent/advanced/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
from langchain_core.tools import BaseTool
from langgraph.graph import END, START
from langgraph.graph.state import CompiledStateGraph, StateGraph
from pydantic import BaseModel
from pydantic import BaseModel, Field
from uipath.core.chat import UiPathConversationMessageData
from uipath.runtime.errors import UiPathErrorCategory

from uipath_langchain.agent.exceptions import AgentRuntimeError, AgentRuntimeErrorCode
from uipath_langchain.agent.react.job_attachments import get_job_attachment_paths
from uipath_langchain.runtime.messages import UiPathChatMessagesMapper

from .types import AdvancedAgentGraphState, ConversationalAdvancedAgentGraphState
from .utils import (
Expand Down Expand Up @@ -129,16 +132,7 @@ def create_conversational_advanced_agent_graph(
system_prompt: str,
backend: BackendProtocol | BackendFactory | None,
) -> StateGraph[Any, Any, Any, Any]:
"""Wrap the advanced agent in a parent graph that speaks the conversational contract.

Conversational agents receive the full conversation history in the
``messages`` input each exchange and must output the newly produced
messages as ``uipath__agent_response_messages``. The deepagent already
operates on ``messages``, so the wrapper only records the incoming history
size and maps the new messages to the conversational output field.
"""
# deferred: avoids a circular import (runtime.messages imports agent modules)
from uipath_langchain.runtime.messages import UiPathChatMessagesMapper
"""Map conversation history and new deepagent messages to the CAS contract."""

memory_sources = (
[MEMORY_INDEX_VIRTUAL_PATH] if isinstance(backend, FilesystemBackend) else []
Expand All @@ -153,7 +147,9 @@ def create_conversational_advanced_agent_graph(
)

class ConversationalAdvancedAgentOutput(BaseModel):
uipath__agent_response_messages: list[UiPathConversationMessageData] = []
uipath__agent_response_messages: list[UiPathConversationMessageData] = Field(
default_factory=list
)

def capture_exchange_start(
state: ConversationalAdvancedAgentGraphState,
Expand All @@ -163,7 +159,14 @@ def capture_exchange_start(
def transform_output(
state: ConversationalAdvancedAgentGraphState,
) -> dict[str, Any]:
initial_count = state.initial_message_count or 0
if state.initial_message_count is None:
raise AgentRuntimeError(
code=AgentRuntimeErrorCode.STATE_ERROR,
title="Conversation state is incomplete",
detail="The initial message count was not captured before execution.",
category=UiPathErrorCategory.SYSTEM,
)
initial_count = state.initial_message_count
new_messages = state.messages[initial_count:]
converted = (
UiPathChatMessagesMapper.map_langchain_messages_to_uipath_message_data_list(
Expand Down
8 changes: 4 additions & 4 deletions src/uipath_langchain/agent/advanced/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@

from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
from pydantic import BaseModel
from pydantic import BaseModel, Field


class AdvancedAgentGraphState(BaseModel):
"""Graph state for the advanced agent wrapper."""

messages: Annotated[list[AnyMessage], add_messages] = []
structured_response: dict[str, Any] = {}
messages: Annotated[list[AnyMessage], add_messages] = Field(default_factory=list)
structured_response: dict[str, Any] = Field(default_factory=dict)


class ConversationalAdvancedAgentGraphState(BaseModel):
"""Graph state for the conversational advanced agent wrapper."""

messages: Annotated[list[AnyMessage], add_messages] = []
messages: Annotated[list[AnyMessage], add_messages] = Field(default_factory=list)
initial_message_count: int | None = None
10 changes: 4 additions & 6 deletions src/uipath_langchain/agent/tools/client_side_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

import json
from contextvars import ContextVar
from typing import Annotated, Any, TypedDict
from typing import Annotated, Any

from langchain_core.messages import ToolMessage
from langchain_core.tools import InjectedToolCallId, StructuredTool
from uipath.agent.models.agent import AgentClientSideToolResourceConfig
from uipath.eval.mocks import mockable

from uipath_langchain._client_side_tool_types import (
ClientSideToolInfo as ClientSideToolInfo,
)
from uipath_langchain._utils.durable_interrupt import durable_interrupt
from uipath_langchain.agent.react.jsonschema_pydantic_converter import (
create_model as create_model_from_schema,
Expand All @@ -26,11 +29,6 @@
UIPATH_CLIENT_SIDE_TOOLS_INPUT_KEY = "uipath__client_side_tools"


class ClientSideToolInfo(TypedDict):
input_schema: dict[str, Any] | None
output_schema: dict[str, Any] | None


def apply_tool_filter(
declared_tools: list[str | dict[str, Any]],
agent_tools: dict[str, ClientSideToolInfo],
Expand Down
16 changes: 11 additions & 5 deletions src/uipath_langchain/runtime/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
)
from uipath.runtime import UiPathRuntimeStorageProtocol

from uipath_langchain.agent.tools.client_side_tool import ClientSideToolInfo
from uipath_langchain._client_side_tool_types import ClientSideToolInfo
from uipath_langchain.chat.hitl import IS_CONVERSATIONAL_CLIENT_SIDE_TOOL

from ._citations import (
Expand Down Expand Up @@ -188,10 +188,16 @@ def _map_messages_internal(
)
)
elif isinstance(data, UiPathExternalValue):
if uipath_message.role == "assistant":
# Workspace files persisted by the advanced runtime
# (hydrated into the file backend before the graph
# runs); they are not attachments for the LLM.
part_metadata = getattr(uipath_content_part, "metadata", None)
has_workspace_metadata = (
isinstance(part_metadata, dict)
and part_metadata.get("fileKind") == "workspace"
)
is_workspace_file = (
has_workspace_metadata
or uipath_content_part.content_part_id.startswith("ws-")
)
if uipath_message.role == "assistant" and is_workspace_file:
continue
attachment_id = self.parse_attachment_id_from_content_part_uri(
data.uri
Expand Down
2 changes: 1 addition & 1 deletion src/uipath_langchain/runtime/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
)
from uipath.runtime.schema import UiPathRuntimeSchema

from uipath_langchain.agent.tools.client_side_tool import ClientSideToolInfo
from uipath_langchain._client_side_tool_types import ClientSideToolInfo
from uipath_langchain.chat.hitl import (
IS_CONVERSATIONAL_CLIENT_SIDE_TOOL,
get_confirmation_schema,
Expand Down
44 changes: 42 additions & 2 deletions tests/runtime/test_chat_message_mapper_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@
CAS_URI = "urn:uipath:cas:file:orchestrator:a940a416-b97b-4146-3089-08de5f4d0a87"


def _file_part(part_id: str, name: str) -> UiPathConversationContentPart:
def _file_part(
part_id: str,
name: str,
*,
metadata: dict[str, str] | None = None,
) -> UiPathConversationContentPart:
return UiPathConversationContentPart(
content_part_id=part_id,
mime_type="text/markdown",
data=UiPathExternalValue(uri=CAS_URI),
name=name,
metadata=metadata,
citations=[],
)

Expand All @@ -35,7 +41,7 @@ def test_assistant_file_parts_are_skipped() -> None:
data=UiPathInlineValue(inline="done, see the plan"),
citations=[],
),
_file_part("p2", "plan/todo.md"),
_file_part("ws-a1-0", "plan/todo.md", metadata={"fileKind": "workspace"}),
],
tool_calls=[],
)
Expand Down Expand Up @@ -65,3 +71,37 @@ def test_user_file_parts_still_produce_attachments() -> None:
user = result[0]
assert isinstance(user, HumanMessage)
assert user.additional_kwargs["attachments"][0]["full_name"] == "report.pdf"


def test_ordinary_assistant_file_parts_still_produce_attachments() -> None:
mapper = UiPathChatMessagesMapper("test-runtime", None)
message = UiPathConversationMessage(
message_id="a1",
role="assistant",
content_parts=[_file_part("p1", "report.pdf")],
tool_calls=[],
)

result = mapper.map_messages([message])

assert len(result) == 1
assistant = result[0]
assert isinstance(assistant, AIMessage)
assert assistant.additional_kwargs["attachments"][0]["full_name"] == "report.pdf"


def test_workspace_part_id_is_hidden_without_metadata() -> None:
mapper = UiPathChatMessagesMapper("test-runtime", None)
message = UiPathConversationMessage(
message_id="a1",
role="assistant",
content_parts=[_file_part("ws-a1-0", "plan.md")],
tool_calls=[],
)

result = mapper.map_messages([message])

assert len(result) == 1
assistant = result[0]
assert isinstance(assistant, AIMessage)
assert "attachments" not in assistant.additional_kwargs
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading