Skip to content

Commit 29057a3

Browse files
feat: add citation and reasoning mappings
1 parent 9e94bda commit 29057a3

File tree

4 files changed

+75
-19
lines changed

4 files changed

+75
-19
lines changed

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.1.18"
3+
version = "0.1.19"
44
description = "UiPath Langchain"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
@@ -113,4 +113,3 @@ name = "testpypi"
113113
url = "https://test.pypi.org/simple/"
114114
publish-url = "https://test.pypi.org/legacy/"
115115
explicit = true
116-

src/uipath_langchain/chat/mapper.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@
77
from langchain_core.messages import (
88
AIMessageChunk,
99
BaseMessage,
10+
Citation,
1011
HumanMessage,
12+
ReasoningContentBlock,
1113
TextContentBlock,
1214
ToolCallChunk,
1315
ToolMessage,
1416
)
1517
from uipath.core.chat import (
18+
UiPathConversationCitationEndEvent,
19+
UiPathConversationCitationEvent,
20+
UiPathConversationCitationSource,
21+
UiPathConversationCitationStartEvent,
1622
UiPathConversationContentPartChunkEvent,
1723
UiPathConversationContentPartEndEvent,
1824
UiPathConversationContentPartEvent,
@@ -184,11 +190,60 @@ def map_event(
184190
text_block = cast(TextContentBlock, block)
185191
text = text_block["text"]
186192

193+
# Map citations if present
194+
annotations = text_block.get("annotations", [])
195+
citation_annotations = [
196+
cast(Citation, annotation)
197+
for annotation in annotations
198+
if annotation.get("type") == "citation"
199+
]
200+
201+
if citation_annotations:
202+
for citation_annotation in citation_annotations:
203+
# Build citation source, only include url if present
204+
block_index = text_block.get("index", 0)
205+
block_index = block_index // 2 + 1
206+
source_args = {
207+
"title": citation_annotation.get("title"),
208+
"number": block_index,
209+
}
210+
if citation_annotation.get("url") is not None:
211+
source_args["url"] = citation_annotation.get("url")
212+
213+
citation_source = UiPathConversationCitationSource(
214+
**source_args
215+
)
216+
217+
msg_event.content_part = UiPathConversationContentPartEvent(
218+
content_part_id=f"chunk-{message.id}-0",
219+
chunk=UiPathConversationContentPartChunkEvent(
220+
data=citation_annotation["cited_text"],
221+
citation=UiPathConversationCitationEvent(
222+
citation_id=str(uuid4()),
223+
start=UiPathConversationCitationStartEvent(),
224+
end=UiPathConversationCitationEndEvent(
225+
sources=[citation_source]
226+
),
227+
),
228+
),
229+
)
230+
else:
231+
msg_event.content_part = UiPathConversationContentPartEvent(
232+
content_part_id=f"chunk-{message.id}-0",
233+
chunk=UiPathConversationContentPartChunkEvent(
234+
data=text
235+
),
236+
)
237+
238+
# Mapping to data temporarily until proper support is added
239+
elif block_type == "reasoning":
240+
reasoning_block = cast(ReasoningContentBlock, block)
241+
reasoning = reasoning_block["reasoning"]
242+
187243
msg_event.content_part = UiPathConversationContentPartEvent(
188244
content_part_id=f"chunk-{message.id}-0",
189245
chunk=UiPathConversationContentPartChunkEvent(
190-
data=text,
191-
content_part_sequence=0,
246+
data=reasoning
192247
),
193248
)
194249

@@ -204,10 +259,7 @@ def map_event(
204259

205260
msg_event.content_part = UiPathConversationContentPartEvent(
206261
content_part_id=f"chunk-{message.id}-0",
207-
chunk=UiPathConversationContentPartChunkEvent(
208-
data=args,
209-
content_part_sequence=0,
210-
),
262+
chunk=UiPathConversationContentPartChunkEvent(data=args),
211263
)
212264
# Continue so that multiple tool_call_chunks in the same block list
213265
# are handled correctly
@@ -217,10 +269,7 @@ def map_event(
217269
elif isinstance(message.content, str) and message.content:
218270
msg_event.content_part = UiPathConversationContentPartEvent(
219271
content_part_id=f"content-{message.id}",
220-
chunk=UiPathConversationContentPartChunkEvent(
221-
data=message.content,
222-
content_part_sequence=0,
223-
),
272+
chunk=UiPathConversationContentPartChunkEvent(data=message.content),
224273
)
225274

226275
if (
@@ -269,12 +318,12 @@ def map_event(
269318
tool_call_id=message.tool_call_id,
270319
start=UiPathConversationToolCallStartEvent(
271320
tool_name=message.name,
272-
arguments=None,
321+
input=None,
273322
timestamp=timestamp,
274323
),
275324
end=UiPathConversationToolCallEndEvent(
276325
timestamp=timestamp,
277-
result=UiPathInlineValue(inline=content_value),
326+
output=UiPathInlineValue(inline=content_value),
278327
),
279328
),
280329
)

src/uipath_langchain/runtime/runtime.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from langgraph.errors import EmptyInputError, GraphRecursionError, InvalidUpdateError
88
from langgraph.graph.state import CompiledStateGraph
99
from langgraph.types import Command, Interrupt, StateSnapshot
10+
from pydantic import ValidationError
1011
from uipath.runtime import (
1112
UiPathBreakpointResult,
1213
UiPathExecuteOptions,
@@ -135,10 +136,17 @@ async def stream(
135136
if chunk_type == "messages":
136137
if isinstance(data, tuple):
137138
message, _ = data
138-
event = UiPathRuntimeMessageEvent(
139-
payload=self.chat.map_event(message),
140-
)
141-
yield event
139+
140+
try:
141+
mapped_event = self.chat.map_event(message)
142+
event = UiPathRuntimeMessageEvent(
143+
payload=mapped_event,
144+
)
145+
yield event
146+
except ValidationError as e:
147+
logger.warning(
148+
f"Failed to map event due to validation error, skipping: {e}"
149+
)
142150

143151
# Emit UiPathRuntimeStateEvent for state updates
144152
elif chunk_type == "updates":

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)