diff --git a/pyproject.toml b/pyproject.toml index 32096e02..e79964c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.1.18" +version = "0.1.19" description = "UiPath Langchain" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" @@ -113,4 +113,3 @@ name = "testpypi" url = "https://test.pypi.org/simple/" publish-url = "https://test.pypi.org/legacy/" explicit = true - diff --git a/src/uipath_langchain/chat/mapper.py b/src/uipath_langchain/chat/mapper.py index 1656dbd0..56dbffa1 100644 --- a/src/uipath_langchain/chat/mapper.py +++ b/src/uipath_langchain/chat/mapper.py @@ -7,12 +7,17 @@ from langchain_core.messages import ( AIMessageChunk, BaseMessage, + Citation, HumanMessage, TextContentBlock, ToolCallChunk, ToolMessage, ) from uipath.core.chat import ( + UiPathConversationCitationEndEvent, + UiPathConversationCitationEvent, + UiPathConversationCitationSource, + UiPathConversationCitationStartEvent, UiPathConversationContentPartChunkEvent, UiPathConversationContentPartEndEvent, UiPathConversationContentPartEvent, @@ -184,13 +189,50 @@ def map_event( text_block = cast(TextContentBlock, block) text = text_block["text"] - msg_event.content_part = UiPathConversationContentPartEvent( - content_part_id=f"chunk-{message.id}-0", - chunk=UiPathConversationContentPartChunkEvent( - data=text, - content_part_sequence=0, - ), - ) + # Map citations if present + annotations = text_block.get("annotations", []) + citation_annotations = [ + cast(Citation, annotation) + for annotation in annotations + if annotation.get("type") == "citation" + ] + + if citation_annotations: + for citation_annotation in citation_annotations: + # Build citation source, only include url if present + block_index = text_block.get("index", 0) + block_index = block_index // 2 + 1 + source_args = { + "title": citation_annotation.get("title"), + "number": block_index, + } + if citation_annotation.get("url") is not None: + source_args["url"] = citation_annotation.get("url") + + citation_source = UiPathConversationCitationSource( + **source_args + ) + + msg_event.content_part = UiPathConversationContentPartEvent( + content_part_id=f"chunk-{message.id}-0", + chunk=UiPathConversationContentPartChunkEvent( + data=citation_annotation["cited_text"], + citation=UiPathConversationCitationEvent( + citation_id=str(uuid4()), + start=UiPathConversationCitationStartEvent(), + end=UiPathConversationCitationEndEvent( + sources=[citation_source] + ), + ), + ), + ) + else: + msg_event.content_part = UiPathConversationContentPartEvent( + content_part_id=f"chunk-{message.id}-0", + chunk=UiPathConversationContentPartChunkEvent( + data=text + ), + ) elif block_type == "tool_call_chunk": tool_chunk_block = cast(ToolCallChunk, block) @@ -204,10 +246,7 @@ def map_event( msg_event.content_part = UiPathConversationContentPartEvent( content_part_id=f"chunk-{message.id}-0", - chunk=UiPathConversationContentPartChunkEvent( - data=args, - content_part_sequence=0, - ), + chunk=UiPathConversationContentPartChunkEvent(data=args), ) # Continue so that multiple tool_call_chunks in the same block list # are handled correctly @@ -217,10 +256,7 @@ def map_event( elif isinstance(message.content, str) and message.content: msg_event.content_part = UiPathConversationContentPartEvent( content_part_id=f"content-{message.id}", - chunk=UiPathConversationContentPartChunkEvent( - data=message.content, - content_part_sequence=0, - ), + chunk=UiPathConversationContentPartChunkEvent(data=message.content), ) if ( @@ -269,12 +305,12 @@ def map_event( tool_call_id=message.tool_call_id, start=UiPathConversationToolCallStartEvent( tool_name=message.name, - arguments=None, + input=None, timestamp=timestamp, ), end=UiPathConversationToolCallEndEvent( timestamp=timestamp, - result=UiPathInlineValue(inline=content_value), + output=UiPathInlineValue(inline=content_value), ), ), ) diff --git a/src/uipath_langchain/runtime/runtime.py b/src/uipath_langchain/runtime/runtime.py index bd28764c..634e640f 100644 --- a/src/uipath_langchain/runtime/runtime.py +++ b/src/uipath_langchain/runtime/runtime.py @@ -7,6 +7,7 @@ from langgraph.errors import EmptyInputError, GraphRecursionError, InvalidUpdateError from langgraph.graph.state import CompiledStateGraph from langgraph.types import Command, Interrupt, StateSnapshot +from pydantic import ValidationError from uipath.runtime import ( UiPathBreakpointResult, UiPathExecuteOptions, @@ -135,10 +136,17 @@ async def stream( if chunk_type == "messages": if isinstance(data, tuple): message, _ = data - event = UiPathRuntimeMessageEvent( - payload=self.chat.map_event(message), - ) - yield event + + try: + mapped_event = self.chat.map_event(message) + event = UiPathRuntimeMessageEvent( + payload=mapped_event, + ) + yield event + except ValidationError as e: + logger.warning( + f"Failed to map event due to validation error, skipping: {e}" + ) # Emit UiPathRuntimeStateEvent for state updates elif chunk_type == "updates": diff --git a/uv.lock b/uv.lock index ebc33c36..ba2d5817 100644 --- a/uv.lock +++ b/uv.lock @@ -3265,7 +3265,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.1.18" +version = "0.1.19" source = { editable = "." } dependencies = [ { name = "boto3-stubs" },