-
Notifications
You must be signed in to change notification settings - Fork 33
feat: add attachments support for coded conversational agents #612
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
GabrielVasilescu04
wants to merge
1
commit into
main
Choose a base branch
from
feature/coded-conversational-attachments
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| UIPATH_ACCESS_TOKEN=YOUR TOKEN HERE | ||
| UIPATH_URL=https://alpha.uipath.com/<organization>/<tenant> | ||
| OPENAI_API_KEY=your_openai_api_key |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # File Attachments Chat Agent | ||
|
|
||
| An AI assistant that reads and analyzes file attachments shared in the conversation. | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Python 3.11+ | ||
| - OpenAI API key | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| uv venv -p 3.11 .venv | ||
| source .venv/bin/activate # On Windows: .venv\Scripts\activate | ||
| uv sync | ||
| ``` | ||
|
|
||
| Set your API key as an environment variable in .env | ||
|
|
||
| ```bash | ||
| OPENAI_API_KEY=your_openai_api_key | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| **1.** Upload the file to Orchestrator using the [attachments API](https://uipath.github.io/uipath-python/core/attachments/). | ||
|
|
||
| **2.** Run the agent, passing the attachment ID and file metadata returned by the upload: | ||
|
|
||
| ```bash | ||
| uipath run agent '{ | ||
| "messages": [ | ||
| { | ||
| "type": "human", | ||
| "content": [ | ||
| { "type": "text", "text": "Summarize this document." }, | ||
| { "type": "text", "text": "<uip:attachments>[{\"id\": \"{orchestrator_attachment_id}\", \"full_name\": \"{file_name}\", \"mime_type\": \"{file_mime_type}\"}]</uip:attachments>" } | ||
| ] | ||
| } | ||
| ] | ||
| }' | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| flowchart TB | ||
| __start__(__start__) | ||
| model(model) | ||
| tools(tools) | ||
| __end__(__end__) | ||
| __start__ --> model | ||
| model --> __end__ | ||
| model --> tools | ||
| tools --> model |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "graphs": { | ||
| "agent": "./main.py:graph" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| from langchain.agents import create_agent | ||
| from langchain_openai import ChatOpenAI | ||
|
|
||
| from uipath_langchain.chat.tools import AnalyzeAttachmentsTool | ||
|
|
||
| system_prompt = """ | ||
| You are an AI assistant specialized in analyzing user-provided files using the available file analysis tool. | ||
| Always use the provided tool to read and analyze any uploaded or referenced file. Never guess or fabricate file contents. If a file is missing or inaccessible, ask the user to upload it again. | ||
|
|
||
| When a file is received: | ||
| 1.Identify the file type. | ||
| 2.Provide a clear, concise summary. | ||
| 3.Extract key information relevant to the user’s request. | ||
| 4.Highlight important patterns, issues, or insights when applicable. | ||
| 5.If the user’s request is unclear, ask a focused clarification question before proceeding. | ||
|
|
||
| For follow-up questions: | ||
| 1.Base all answers strictly on the file contents. | ||
| 2.Maintain context across the conversation. | ||
| 3.Perform deeper analysis, comparisons, transformations, or extractions as requested. | ||
| 4.Clearly distinguish between observed facts and inferred insights. If something cannot be determined from the file, state that explicitly. | ||
|
|
||
| Keep responses structured, concise, and professional. Treat all file data as sensitive and do not retain or reuse it outside the current conversation. | ||
| """ | ||
|
|
||
| llm = ChatOpenAI(model="gpt-4.1") | ||
|
|
||
| graph = create_agent( | ||
| llm, | ||
| tools=[AnalyzeAttachmentsTool(llm=llm)], | ||
| system_prompt=system_prompt, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| [project] | ||
| name = "file-attachments-chat-agent" | ||
| version = "0.0.1" | ||
| description = "file-attachments-chat-agent" | ||
| authors = [{ name = "John Doe", email = "john.doe@myemail.com" }] | ||
| dependencies = [ | ||
| "langchain-openai>=1.1.9", | ||
| "uipath-langchain", | ||
| ] | ||
| requires-python = ">=3.11" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "$schema": "https://cloud.uipath.com/draft/2024-12/uipath", | ||
| "runtimeOptions": { | ||
| "isConversational": true | ||
| }, | ||
| "packOptions": { | ||
| "fileExtensionsIncluded": [], | ||
| "filesIncluded": [], | ||
| "filesExcluded": [], | ||
| "directoriesExcluded": [], | ||
| "includeUvLock": true | ||
| }, | ||
| "functions": {} | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| def __getattr__(name): | ||
| if name == "AnalyzeAttachmentsTool": | ||
| from .attachments import AnalyzeAttachmentsTool | ||
|
|
||
| return AnalyzeAttachmentsTool | ||
| if name == "requires_approval": | ||
| from .hitl import requires_approval | ||
|
|
||
| return requires_approval | ||
| if name == "resolve_attachments": | ||
| from .attachments import resolve_attachments | ||
|
|
||
| return resolve_attachments | ||
| raise AttributeError(f"module {__name__!r} has no attribute {name!r}") | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "AnalyzeAttachmentsTool", | ||
| "requires_approval", | ||
| "resolve_attachments", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| """Attachment resolution for conversational agents.""" | ||
|
|
||
| import asyncio | ||
| import json | ||
| import re | ||
| import uuid | ||
| from typing import Any, Type, cast | ||
|
|
||
| from langchain_core.language_models import BaseChatModel | ||
| from langchain_core.messages import ( | ||
| AnyMessage, | ||
| ContentBlock, | ||
| HumanMessage, | ||
| SystemMessage, | ||
| ) | ||
| from langchain_core.runnables.config import var_child_runnable_config | ||
| from langchain_core.tools import BaseTool | ||
| from pydantic import BaseModel, ConfigDict, Field, PrivateAttr | ||
| from uipath.platform import UiPath | ||
| from uipath.platform.attachments import Attachment | ||
|
|
||
| from uipath_langchain.agent.multimodal import FileInfo, build_file_content_block | ||
| from uipath_langchain.chat.helpers import ( | ||
| append_content_blocks_to_message, | ||
| extract_text_content, | ||
| ) | ||
|
|
||
| _ATTACHMENTS_PATTERN = re.compile( | ||
| r"<uip:attachments>(.*?)</uip:attachments>", re.DOTALL | ||
| ) | ||
|
|
||
| _READ_ATTACHMENTS_SYSTEM_MESSAGE = ( | ||
| "Process the provided files to complete the given task. " | ||
| "Analyze the files contents thoroughly to deliver an accurate response " | ||
| "based on the extracted information." | ||
| ) | ||
|
|
||
|
|
||
| async def resolve_attachments(messages: list[AnyMessage]) -> list[AnyMessage]: | ||
| """Resolve file attachments embedded in conversational messages. | ||
|
|
||
| UiPath's runtime injects attachment metadata as a ``<uip:attachments>`` | ||
| text block into HumanMessages. This function resolves those references to | ||
| actual file content blocks so the LLM can read the files. | ||
|
|
||
| Messages that contain no ``<uip:attachments>`` tag are returned unchanged, | ||
| so it is safe to call this on the full message history on every turn. | ||
|
|
||
| To persist resolved content to the checkpointer (avoiding re-downloads on | ||
| subsequent turns), include the resolved messages in the node's return value | ||
| alongside the LLM response. LangGraph's ``add_messages`` reducer replaces | ||
| messages with matching IDs rather than appending them:: | ||
|
|
||
| async def call_model(state: MessagesState): | ||
| resolved = await resolve_attachments(state["messages"]) | ||
| response = await llm.ainvoke([SystemMessage(...)] + resolved) | ||
| return {"messages": resolved + [response]} | ||
|
|
||
| Args: | ||
| messages: List of LangChain messages from the agent state. | ||
|
|
||
| Returns: | ||
| New list where HumanMessages with attachments have their | ||
| ``<uip:attachments>`` text replaced with DataContentBlock items | ||
| containing the actual file content. | ||
| """ | ||
| return list(await asyncio.gather(*[_resolve_message(m) for m in messages])) | ||
|
|
||
|
|
||
| async def _resolve_message(message: AnyMessage) -> AnyMessage: | ||
| if not isinstance(message, HumanMessage): | ||
| return message | ||
|
|
||
| content = message.content | ||
| if not isinstance(content, list): | ||
| return message | ||
|
|
||
| clean_blocks: list[Any] = [] | ||
| file_infos: list[FileInfo] = [] | ||
|
|
||
| for block in content: | ||
| if not isinstance(block, dict) or block.get("type") != "text": | ||
| clean_blocks.append(block) | ||
| continue | ||
|
|
||
| text = block.get("text", "") | ||
| match = _ATTACHMENTS_PATTERN.search(text) | ||
| if not match: | ||
| clean_blocks.append(block) | ||
| continue | ||
|
|
||
| # Parse attachment metadata | ||
| attachments: list[dict[str, Any]] = json.loads(match.group(1)) | ||
| file_infos.extend(await _resolve_file_infos(attachments)) | ||
|
|
||
| # Preserve any text outside the tag | ||
| remaining = _ATTACHMENTS_PATTERN.sub("", text).strip() | ||
| if remaining: | ||
| clean_blocks.append({"type": "text", "text": remaining}) | ||
|
|
||
| if not file_infos: | ||
| return message | ||
|
|
||
| file_blocks = list( | ||
| await asyncio.gather(*[build_file_content_block(fi) for fi in file_infos]) | ||
| ) | ||
|
|
||
| return HumanMessage( | ||
| id=message.id, | ||
| content=clean_blocks + file_blocks, | ||
| additional_kwargs=message.additional_kwargs, | ||
| ) | ||
|
|
||
|
|
||
| async def _resolve_file_infos( | ||
| attachments: list[dict[str, Any]], | ||
| ) -> list[FileInfo]: | ||
| client = UiPath() | ||
| file_infos: list[FileInfo] = [] | ||
|
|
||
| for att in attachments: | ||
| att_id = att.get("id") | ||
| if not att_id: | ||
| continue | ||
|
|
||
| blob_info = await client.attachments.get_blob_file_access_uri_async( | ||
| key=uuid.UUID(att_id) | ||
| ) | ||
| file_infos.append( | ||
| FileInfo( | ||
| url=blob_info.uri, | ||
| name=blob_info.name, | ||
| mime_type=att.get("mime_type", ""), | ||
| ) | ||
| ) | ||
|
|
||
| return file_infos | ||
|
|
||
|
|
||
| class AnalyzeAttachmentsInput(BaseModel): | ||
| """Input schema for the AnalyzeAttachmentsTool.""" | ||
|
|
||
| model_config = ConfigDict(arbitrary_types_allowed=True) | ||
|
|
||
| query: str = Field(description="What you want to know about or do with the files.") | ||
| attachments: list[Attachment] = Field( | ||
| description="The attachment objects from inside the <uip:attachments> tag." | ||
| ) | ||
|
|
||
|
|
||
| class AnalyzeAttachmentsTool(BaseTool): | ||
| """Tool that reads and interprets file attachments using the provided LLM. | ||
|
|
||
| The tool downloads each attachment, passes the file content to a non-streaming | ||
| copy of the provided LLM for interpretation, and returns the result as text. | ||
| This keeps multimodal content out of the agent's message state — the original | ||
| ``<uip:attachments>`` metadata in HumanMessages is never modified. | ||
|
|
||
| Example:: | ||
|
|
||
| from langchain_openai import ChatOpenAI | ||
| from uipath_langchain.chat import AnalyzeAttachmentsTool | ||
|
|
||
| llm = ChatOpenAI(model="gpt-4.1") | ||
| tool = AnalyzeAttachmentsTool(llm=llm) | ||
| """ | ||
|
|
||
| name: str = "analyze_attachments" | ||
| description: str = ( | ||
| "Read and interpret the content of file attachments provided by the user. " | ||
| "Call this when you see a <uip:attachments> tag in a user message, passing " | ||
| "the attachment objects from inside the tag and a query describing what you " | ||
| "want to know about or do with the files." | ||
| ) | ||
| args_schema: Type[BaseModel] = AnalyzeAttachmentsInput | ||
|
|
||
| llm: BaseChatModel | ||
|
|
||
| _non_streaming_llm: BaseChatModel = PrivateAttr() | ||
|
|
||
| def model_post_init(self, __context: Any) -> None: | ||
| self._non_streaming_llm = self.llm.model_copy( | ||
| update={"disable_streaming": True} | ||
| ) | ||
|
|
||
| def _run(self, query: str, attachments: list[Attachment]) -> dict[str, str]: | ||
| raise NotImplementedError("Use async version via arun()") | ||
|
|
||
| async def _arun(self, query: str, attachments: list[Attachment]) -> dict[str, str]: | ||
| file_infos = await _resolve_file_infos( | ||
| [a.model_dump(mode="json") for a in attachments] | ||
| ) | ||
| if not file_infos: | ||
| return {"analysisResult": "No attachments provided to analyze."} | ||
|
|
||
| file_blocks = list( | ||
| await asyncio.gather(*[build_file_content_block(fi) for fi in file_infos]) | ||
| ) | ||
|
|
||
| human_message_with_files = append_content_blocks_to_message( | ||
| HumanMessage(content=query), cast(list[ContentBlock], file_blocks) | ||
| ) | ||
|
|
||
| messages = [ | ||
| SystemMessage(content=_READ_ATTACHMENTS_SYSTEM_MESSAGE), | ||
| human_message_with_files, | ||
| ] | ||
|
|
||
| config = var_child_runnable_config.get(None) | ||
| result = await self._non_streaming_llm.ainvoke(messages, config=config) | ||
| return {"analysisResult": extract_text_content(result)} |
File renamed without changes.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.