Skip to content

Commit 5e7b175

Browse files
committed
feat: support conversational runtime prompts
1 parent be0ae4a commit 5e7b175

25 files changed

Lines changed: 1362 additions & 947 deletions

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.16.1"
3+
version = "0.16.2"
44
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
@@ -163,6 +163,7 @@ uipath = false
163163
uipath-core = false
164164
uipath-platform = false
165165
uipath-runtime = false
166+
uipath-dev = false
166167
uipath-langchain-client = false
167168
uipath-llm-client = false
168169
jsonschema-pydantic-converter = false
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Shared conversational message components."""
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Shared conversational contracts."""
2+
3+
from typing import Any, TypedDict
4+
5+
6+
class ClientSideToolInfo(TypedDict):
7+
input_schema: dict[str, Any] | None
8+
output_schema: dict[str, Any] | None

src/uipath_langchain/agent/advanced/agent.py

Lines changed: 112 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@
2020
from langchain_core.tools import BaseTool
2121
from langgraph.graph import END, START
2222
from langgraph.graph.state import CompiledStateGraph, StateGraph
23-
from pydantic import BaseModel, create_model
23+
from pydantic import BaseModel, ConfigDict, create_model
2424
from uipath.core.chat import UiPathConversationMessageData
2525

2626
from uipath_langchain._utils import get_unique_model_field_name
2727
from uipath_langchain.agent.react.job_attachments import get_job_attachment_paths
28+
from uipath_langchain.runtime.messages import UiPathChatMessagesMapper
2829

29-
from .types import AdvancedAgentGraphState, ConversationalAdvancedAgentGraphState
30+
from .types import (
31+
AdvancedAgentGraphState,
32+
ConversationalAdvancedAgentGraphState,
33+
_ConversationalAdvancedAgentGraphInput,
34+
)
3035
from .utils import (
3136
MEMORY_INDEX_VIRTUAL_PATH,
3237
create_state_with_input,
@@ -213,45 +218,132 @@ def transform_output(state: BaseModel) -> dict[str, Any]:
213218
def create_conversational_advanced_agent_graph(
214219
model: BaseChatModel,
215220
tools: Sequence[BaseTool],
216-
system_prompt: str,
221+
system_prompt: str | Callable[[dict[str, Any]], str],
217222
backend: BackendProtocol | BackendFactory | None,
223+
input_schema: type[BaseModel] | None = None,
218224
) -> StateGraph[Any, Any, Any, Any]:
219225
"""Wrap the advanced agent in a parent graph that speaks the conversational contract.
220226
221227
Conversational agents receive the full conversation history in the
222228
``messages`` input each exchange and must output the newly produced
223229
messages as ``uipath__agent_response_messages``. The deepagent already
224-
operates on ``messages``, so the wrapper only records the incoming history
225-
size and maps the new messages to the conversational output field.
230+
operates on ``messages``. Callable system prompts are resolved once from
231+
the exchange input and applied to every main-agent model request in that
232+
invocation.
226233
"""
227-
# deferred: avoids a circular import (runtime.messages imports agent modules)
228-
from uipath_langchain.runtime.messages import UiPathChatMessagesMapper
229-
230234
memory_sources = (
231235
[MEMORY_INDEX_VIRTUAL_PATH] if isinstance(backend, FilesystemBackend) else []
232236
)
237+
if callable(system_prompt):
238+
build_system_prompt = system_prompt
239+
static_system_prompt = None
240+
else:
241+
build_system_prompt = None
242+
static_system_prompt = system_prompt
243+
initial_message_count_key = get_unique_model_field_name(
244+
"initial_message_count",
245+
_ConversationalAdvancedAgentGraphInput,
246+
input_schema,
247+
)
248+
runtime_system_prompt_key = (
249+
get_unique_model_field_name(
250+
"uipath__system_prompt",
251+
_ConversationalAdvancedAgentGraphInput,
252+
input_schema,
253+
)
254+
if build_system_prompt is not None
255+
else None
256+
)
233257

234258
inner_graph = create_advanced_agent(
235259
model=model,
236260
tools=tools,
237-
system_prompt=system_prompt,
261+
system_prompt=static_system_prompt,
238262
backend=backend,
239263
memory=memory_sources,
264+
middleware=(
265+
[_RuntimeSystemPromptMiddleware(runtime_system_prompt_key)]
266+
if runtime_system_prompt_key is not None
267+
else []
268+
),
240269
)
241270

242271
class ConversationalAdvancedAgentOutput(BaseModel):
243272
uipath__agent_response_messages: list[UiPathConversationMessageData] = []
244273

245-
def capture_exchange_start(
246-
state: ConversationalAdvancedAgentGraphState,
247-
) -> dict[str, Any]:
248-
return {"initial_message_count": len(state.messages)}
274+
graph_input: type[BaseModel] = _ConversationalAdvancedAgentGraphInput
275+
wrapper_input = graph_input
276+
if input_schema is not None:
277+
wrapper_input = type(
278+
"CompleteConversationalAdvancedAgentInput",
279+
(_ConversationalAdvancedAgentGraphInput, input_schema),
280+
{
281+
"model_config": ConfigDict(
282+
validate_by_alias=True,
283+
validate_by_name=True,
284+
)
285+
},
286+
)
287+
wrapper_input.model_rebuild()
288+
graph_input = (
289+
input_schema if "messages" in input_schema.model_fields else wrapper_input
290+
)
291+
initial_count_field: dict[str, Any] = {
292+
initial_message_count_key: (int | None, None)
293+
}
294+
base_wrapper_state = cast(
295+
type[BaseModel],
296+
create_model(
297+
"ConversationalAdvancedAgentGraphState",
298+
__base__=wrapper_input,
299+
**initial_count_field,
300+
),
301+
)
302+
if runtime_system_prompt_key is not None:
303+
runtime_state_field: dict[str, Any] = {
304+
runtime_system_prompt_key: (str | None, None)
305+
}
306+
wrapper_state = cast(
307+
type[BaseModel],
308+
create_model(
309+
"RuntimeConversationalAdvancedAgentGraphState",
310+
__base__=base_wrapper_state,
311+
**runtime_state_field,
312+
),
313+
)
314+
else:
315+
wrapper_state = base_wrapper_state
316+
317+
internal_fields = set(_ConversationalAdvancedAgentGraphInput.model_fields)
318+
internal_fields.add(initial_message_count_key)
319+
if runtime_system_prompt_key is not None:
320+
internal_fields.add(runtime_system_prompt_key)
249321

250-
def transform_output(
251-
state: ConversationalAdvancedAgentGraphState,
252-
) -> dict[str, Any]:
253-
initial_count = state.initial_message_count or 0
254-
new_messages = state.messages[initial_count:]
322+
def capture_exchange_start(state: BaseModel) -> dict[str, Any]:
323+
conversation_state = cast(ConversationalAdvancedAgentGraphState, state)
324+
update: dict[str, Any] = {
325+
initial_message_count_key: len(conversation_state.messages)
326+
}
327+
if build_system_prompt is not None and runtime_system_prompt_key is not None:
328+
input_args = (
329+
input_schema.model_construct(
330+
**{
331+
field_name: getattr(state, field_name)
332+
for field_name in input_schema.model_fields
333+
if field_name not in internal_fields
334+
}
335+
).model_dump(by_alias=True, exclude_unset=True)
336+
if input_schema is not None
337+
else {}
338+
)
339+
update[runtime_system_prompt_key] = build_system_prompt(input_args)
340+
return update
341+
342+
def transform_output(state: BaseModel) -> dict[str, Any]:
343+
initial_count = getattr(state, initial_message_count_key) or 0
344+
new_messages = cast(ConversationalAdvancedAgentGraphState, state).messages[
345+
initial_count:
346+
]
255347
converted = (
256348
UiPathChatMessagesMapper.map_langchain_messages_to_uipath_message_data_list(
257349
messages=new_messages, include_tool_results=False
@@ -262,7 +354,8 @@ def transform_output(
262354
return {"uipath__agent_response_messages": converted}
263355

264356
wrapper: StateGraph[Any, Any, Any, Any] = StateGraph(
265-
ConversationalAdvancedAgentGraphState,
357+
wrapper_state,
358+
input_schema=graph_input,
266359
output_schema=ConversationalAdvancedAgentOutput,
267360
)
268361
wrapper.add_node("capture_exchange_start", capture_exchange_start)

src/uipath_langchain/agent/advanced/types.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from langchain_core.messages import AnyMessage
66
from langgraph.graph.message import add_messages
7-
from pydantic import BaseModel
7+
from pydantic import BaseModel, ConfigDict
88

99

1010
class AdvancedAgentGraphState(BaseModel):
@@ -14,8 +14,13 @@ class AdvancedAgentGraphState(BaseModel):
1414
structured_response: dict[str, Any] = {}
1515

1616

17-
class ConversationalAdvancedAgentGraphState(BaseModel):
18-
"""Graph state for the conversational advanced agent wrapper."""
17+
class _ConversationalAdvancedAgentGraphInput(BaseModel):
18+
model_config = ConfigDict(validate_by_alias=True, validate_by_name=True)
1919

2020
messages: Annotated[list[AnyMessage], add_messages] = []
21+
22+
23+
class ConversationalAdvancedAgentGraphState(_ConversationalAdvancedAgentGraphInput):
24+
"""Graph state for the conversational advanced agent wrapper."""
25+
2126
initial_message_count: int | None = None

0 commit comments

Comments
 (0)