diff --git a/samples/multi-agent-planner-researcher-coder-distributed/.agent/SDK_REFERENCE.md b/samples/multi-agent-planner-researcher-coder-distributed/.agent/SDK_REFERENCE.md index e3b7ae83..499faab4 100644 --- a/samples/multi-agent-planner-researcher-coder-distributed/.agent/SDK_REFERENCE.md +++ b/samples/multi-agent-planner-researcher-coder-distributed/.agent/SDK_REFERENCE.md @@ -221,6 +221,12 @@ sdk.context_grounding.delete_index(index: uipath.platform.context_grounding.cont # Asynchronously delete a context grounding index. sdk.context_grounding.delete_index_async(index: uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> None +# Downloads the Batch Transform result file to the specified path. +sdk.context_grounding.download_batch_transform_result(id: str, destination_path: str, validate_status: bool=True, index_name: str | None=None) -> None + +# Asynchronously downloads the Batch Transform result file to the specified path. +sdk.context_grounding.download_batch_transform_result_async(id: str, destination_path: str, validate_status: bool=True, index_name: str | None=None) -> None + # Ingest data into the context grounding index. sdk.context_grounding.ingest_data(index: uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> None @@ -233,6 +239,12 @@ sdk.context_grounding.retrieve(name: str, folder_key: Optional[str]=None, folder # Asynchronously retrieve context grounding index information by its name. sdk.context_grounding.retrieve_async(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex +# Retrieves a Batch Transform task status. +sdk.context_grounding.retrieve_batch_transform(id: str, index_name: str | None=None) -> uipath.platform.context_grounding.context_grounding.BatchTransformResponse + +# Asynchronously retrieves a Batch Transform task status. +sdk.context_grounding.retrieve_batch_transform_async(id: str, index_name: str | None=None) -> uipath.platform.context_grounding.context_grounding.BatchTransformResponse + # Retrieve context grounding index information by its ID. sdk.context_grounding.retrieve_by_id(id: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> typing.Any @@ -251,11 +263,17 @@ sdk.context_grounding.search(name: str, query: str, number_of_results: int=10, f # Search asynchronously for contextual information within a specific index. sdk.context_grounding.search_async(name: str, query: str, number_of_results: int=10, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> typing.List[uipath.platform.context_grounding.context_grounding.ContextGroundingQueryResponse] +# Starts a Batch Transform, task on the targeted index. +sdk.context_grounding.start_batch_transform(name: str, index_name: str, prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], output_columns: list[uipath.platform.context_grounding.context_grounding.BatchTransformOutputColumn], storage_bucket_folder_path_prefix: Annotated[str | None, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=512)])]=None, enable_web_search_grounding: bool=False, folder_key: str | None=None, folder_path: str | None=None) -> uipath.platform.context_grounding.context_grounding.BatchTransformCreationResponse + +# Asynchronously starts a Batch Transform, task on the targeted index. +sdk.context_grounding.start_batch_transform_async(name: str, index_name: str, prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], output_columns: list[uipath.platform.context_grounding.context_grounding.BatchTransformOutputColumn], storage_bucket_folder_path_prefix: Annotated[str | None, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=512)])]=None, enable_web_search_grounding: bool=False, folder_key: str | None=None, folder_path: str | None=None) -> uipath.platform.context_grounding.context_grounding.BatchTransformCreationResponse + # Starts a Deep RAG task on the targeted index. -sdk.context_grounding.start_deep_rag(name: str, index_name: str, prompt: str, glob_pattern: str="*", citation_mode: uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse +sdk.context_grounding.start_deep_rag(name: str, index_name: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=512)])], prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], glob_pattern: Annotated[str, FieldInfo(annotation=NoneType, required=False, default='*', metadata=[MaxLen(max_length=512)])]="**", citation_mode: uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse # Asynchronously starts a Deep RAG task on the targeted index. -sdk.context_grounding.start_deep_rag_async(name: str, index_name: str, prompt: str, glob_pattern: str="*", citation_mode: uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse +sdk.context_grounding.start_deep_rag_async(name: str, index_name: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=512)])], prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], glob_pattern: Annotated[str, FieldInfo(annotation=NoneType, required=False, default='*', metadata=[MaxLen(max_length=512)])]="**", citation_mode: uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse ``` @@ -380,7 +398,7 @@ Guardrails service ```python # Validate input text using the provided guardrail. -sdk.guardrails.evaluate_guardrail(input_data: str | dict[str, Any], guardrail: Annotated[Union[uipath.platform.guardrails.guardrails.DeterministicGuardrail, uipath.platform.guardrails.guardrails.BuiltInValidatorGuardrail], FieldInfo(annotation=NoneType, required=True, discriminator='guardrail_type')]) -> uipath.platform.guardrails.guardrails.GuardrailValidationResult +sdk.guardrails.evaluate_guardrail(input_data: str | dict[str, Any], guardrail: uipath.platform.guardrails.guardrails.BuiltInValidatorGuardrail) -> uipath.core.guardrails.guardrails.GuardrailValidationResult ``` diff --git a/samples/multi-agent-planner-researcher-coder-distributed/entry-points.json b/samples/multi-agent-planner-researcher-coder-distributed/entry-points.json index 9f14031e..0139fce8 100644 --- a/samples/multi-agent-planner-researcher-coder-distributed/entry-points.json +++ b/samples/multi-agent-planner-researcher-coder-distributed/entry-points.json @@ -4,7 +4,7 @@ "entryPoints": [ { "filePath": "planner", - "uniqueId": "7ab9bf62-e4a4-4261-8b47-0223be930f45", + "uniqueId": "612002c9-74ee-4b66-b262-0dd0a524f3c8", "type": "agent", "input": { "type": "object", @@ -46,15 +46,18 @@ "metadata": {} }, { - "id": "create_plan", - "name": "create_plan", - "type": "node", + "id": "planner_agent", + "name": "planner_agent", + "type": "model", "subgraph": null, - "metadata": {} + "metadata": { + "model_name": "claude-3-7-sonnet-latest", + "max_tokens": 64000 + } }, { - "id": "supervisor", - "name": "supervisor", + "id": "router", + "name": "router", "type": "node", "subgraph": null, "metadata": {} @@ -66,6 +69,13 @@ "subgraph": null, "metadata": {} }, + { + "id": "output", + "name": "output", + "type": "node", + "subgraph": null, + "metadata": {} + }, { "id": "__end__", "name": "__end__", @@ -82,22 +92,37 @@ }, { "source": "input", - "target": "supervisor", + "target": "router", "label": null }, { - "source": "supervisor", - "target": "__end__", + "source": "invoke_agent", + "target": "router", "label": null }, { - "source": "create_plan", - "target": "supervisor", + "source": "planner_agent", + "target": "router", "label": null }, { - "source": "invoke_agent", - "target": "supervisor", + "source": "router", + "target": "invoke_agent", + "label": null + }, + { + "source": "router", + "target": "output", + "label": "__end__" + }, + { + "source": "router", + "target": "planner_agent", + "label": null + }, + { + "source": "output", + "target": "__end__", "label": null } ] @@ -105,7 +130,7 @@ }, { "filePath": "researcher", - "uniqueId": "834546c0-6741-4a84-9159-dcc40f5942e8", + "uniqueId": "84adaea3-48d6-4e92-a095-2e4f9d4f60eb", "type": "agent", "input": { "type": "object", @@ -1283,7 +1308,7 @@ }, { "filePath": "coder", - "uniqueId": "49f05858-56b9-412e-affb-c63048d689bc", + "uniqueId": "ecd1a7d1-3348-4d86-9cfe-69408e575d87", "type": "agent", "input": { "type": "object", diff --git a/samples/multi-agent-planner-researcher-coder-distributed/planner.mermaid b/samples/multi-agent-planner-researcher-coder-distributed/planner.mermaid index 470ff34b..3f42e18b 100644 --- a/samples/multi-agent-planner-researcher-coder-distributed/planner.mermaid +++ b/samples/multi-agent-planner-researcher-coder-distributed/planner.mermaid @@ -1,12 +1,16 @@ flowchart TB __start__(__start__) input(input) - create_plan(create_plan) - supervisor(supervisor) + planner_agent(planner_agent) + router(router) invoke_agent(invoke_agent) + output(output) __end__(__end__) __start__ --> input - input --> supervisor - supervisor --> __end__ - create_plan --> supervisor - invoke_agent --> supervisor + input --> router + invoke_agent --> router + planner_agent --> router + router --> invoke_agent + router --> |__end__|output + router --> planner_agent + output --> __end__ diff --git a/samples/multi-agent-planner-researcher-coder-distributed/pyproject.toml b/samples/multi-agent-planner-researcher-coder-distributed/pyproject.toml index 3b1785c9..942b45a5 100644 --- a/samples/multi-agent-planner-researcher-coder-distributed/pyproject.toml +++ b/samples/multi-agent-planner-researcher-coder-distributed/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "langchain-anthropic>=1.2.0", "langchain-experimental>=0.4.0", "langchain-tavily>=0.2.13", - "uipath-langchain>=0.1.22, <0.2.0", + "uipath-langchain>=0.1.28, <0.2.0", "uipath>=2.2.26, <2.3.0", ] diff --git a/samples/multi-agent-planner-researcher-coder-distributed/src/multi-agent-distributed/planner.py b/samples/multi-agent-planner-researcher-coder-distributed/src/multi-agent-distributed/planner.py index 9b936cce..46f33c2a 100644 --- a/samples/multi-agent-planner-researcher-coder-distributed/src/multi-agent-distributed/planner.py +++ b/samples/multi-agent-planner-researcher-coder-distributed/src/multi-agent-distributed/planner.py @@ -1,6 +1,6 @@ from typing import List, Literal - from langchain_anthropic import ChatAnthropic +from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import HumanMessage from langchain_core.output_parsers import PydanticOutputParser from langchain_core.prompts import ChatPromptTemplate @@ -17,7 +17,6 @@ class Router(TypedDict): """Worker to route to next. If no workers needed, route to FINISH.""" - next: Literal[*options] @@ -31,7 +30,6 @@ class GraphOutput(BaseModel): class PlanStep(BaseModel): """A single step in the execution plan""" - agent: str = Field( description="The agent to execute this step (researcher-agent or coder-agent)" ) @@ -40,7 +38,6 @@ class PlanStep(BaseModel): class ExecutionPlan(BaseModel): """A plan for executing a complex task using specialized agents""" - steps: List[PlanStep] = Field( description="The ordered sequence of steps to execute" ) @@ -48,7 +45,6 @@ class ExecutionPlan(BaseModel): class State(MessagesState): """State for the graph""" - next: str next_task: str execution_plan: ExecutionPlan = None @@ -67,149 +63,173 @@ def input(state: GraphInput): } +def output(state: State) -> GraphOutput: + """Extract the final answer from the last agent message.""" + agent_messages = [ + msg for msg in state["messages"] + if isinstance(msg, HumanMessage) and hasattr(msg, "name") and msg.name + ] + + if agent_messages: + final_answer = "\n\n".join([msg.content for msg in agent_messages]) + return GraphOutput(answer=final_answer) + + # Fallback if no agent messages found + return GraphOutput(answer="No answer generated.") + + llm = ChatAnthropic(model="claude-3-7-sonnet-latest") -async def create_plan(state: State) -> Command: - """Create an execution plan based on the user's question.""" - parser = PydanticOutputParser(pydantic_object=ExecutionPlan) - - planning_prompt = ChatPromptTemplate.from_messages( - [ - ( - "system", - """You are a planning agent that creates execution plans for tasks. - Break down complex tasks into steps that can be performed by specialized agents.""", - ), - ("human", "{question}"), - ( - "system", - """ - Based on the user's request, create a structured execution plan. - - {format_instructions} - - Available agents: - - researcher-agent: Finds information, formulas, and reference material - - coder-agent: Performs calculations and evaluates formulas with specific values - - Create a plan with the minimum necessary steps to complete the task. - """, - ), - ] - ) - # Format the prompt with parser instructions and the user question - formatted_prompt = planning_prompt.format( - question=state["messages"][0].content, - format_instructions=parser.get_format_instructions(), - ) +def make_planner_node(model: BaseChatModel): + # Wrap the planner node to capture the model in the schema + async def planner_node(state: State) -> dict: + """Create an execution plan based on the user's question.""" + parser = PydanticOutputParser(pydantic_object=ExecutionPlan) + planning_prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + """You are a planning agent that creates execution plans for tasks. +Break down complex tasks into steps that can be performed by specialized agents.""", + ), + ("human", "{question}"), + ( + "system", + """ +Based on the user's request, create a structured execution plan. + +{format_instructions} + +Available agents: +- researcher-agent: Finds information, formulas, and reference material +- coder-agent: Performs calculations and evaluates formulas with specific values + +Create a plan with the minimum necessary steps to complete the task. +""", + ), + ] + ) - plan_response = await llm.ainvoke(formatted_prompt) - - try: - plan_output = parser.parse(plan_response.content) - - steps = [] - for step in plan_output.steps: - agent_key = "researcher" if "researcher" in step.agent else "coder" - steps.append( - PlanStep(agent=worker_agents[agent_key], task=step.task) - ) - - execution_plan = ExecutionPlan(steps=steps) - except Exception as e: - print(f"Failed to parse plan: {e}") - return Command(goto="supervisor") - - # Create a plan summary for the messages - plan_summary = "Execution Plan:\n" + "\n".join( - [ - f"{i + 1}. {step.agent}: {step.task}" - for i, step in enumerate(execution_plan.steps) - ] - ) + formatted_prompt = planning_prompt.format( + question=state["messages"][0].content, + format_instructions=parser.get_format_instructions(), + ) + plan_response = await model.ainvoke(formatted_prompt) + + try: + plan_output = parser.parse(plan_response.content) + steps = [] + for step in plan_output.steps: + agent_key = "researcher" if "researcher" in step.agent else "coder" + steps.append( + PlanStep(agent=worker_agents[agent_key], task=step.task) + ) + execution_plan = ExecutionPlan(steps=steps) + except Exception as e: + print(f"Failed to parse plan: {e}") + return {} + + plan_summary = "Execution Plan:\n" + "\n".join( + [ + f"{i + 1}. {step.agent}: {step.task}" + for i, step in enumerate(execution_plan.steps) + ] + ) - return Command( - update={ + return { "messages": [ HumanMessage( content=f"I've created an execution plan for this task:\n{plan_summary}" ) ], "execution_plan": execution_plan, - }, - goto="supervisor", - ) + } + + return planner_node -def supervisor_node(state: State) -> dict | GraphOutput: - """Execute the next step in the plan or finish if complete.""" +def router(state: State) -> dict: + """Node that prepares routing information. The actual routing happens via conditional edge.""" + plan = state["execution_plan"] + + # If we have a plan and steps remaining, prepare the next step + if plan and state["current_step"] < len(plan.steps): + next_step = plan.steps[state["current_step"]] + return { + "next": next_step.agent, + "next_task": next_step.task, + } + + return {} + + +def route_agent(state: State) -> str: + """Routing function to determine next node.""" plan = state["execution_plan"] # If no plan exists, create one if plan is None: - return {} # Will route to create_plan + return "planner_agent" # If we've completed all steps, finish if state["current_step"] >= len(plan.steps): - return GraphOutput(answer=state["messages"][-1].content) - - # Get the next step from the plan and UPDATE STATE - next_step = plan.steps[state["current_step"]] - return { - "next": next_step.agent, - "next_task": next_step.task - } - - -def route_supervisor(state: State): - if state.get("execution_plan") is None: - return "create_plan" - if state["current_step"] >= len(state["execution_plan"].steps): return END + + # Otherwise, invoke the next agent return "invoke_agent" -def invoke_agent(state: State) -> Command: - """Invoke the agent specified in the current step of the execution plan.""" +def invoke_agent(state: State) -> dict: + """Invoke the agent specified in the current step of the execution plan.""" agent_name = state["next"] task = state["next_task"] # Create a list of messages to send to the agent - # Keep previous agent messages + append the current task input_messages = [ - msg for msg in state["messages"] - if isinstance(msg, HumanMessage) and hasattr(msg, "name") and msg.name + msg + for msg in state["messages"] + if isinstance(msg, HumanMessage) + and hasattr(msg, "name") + and msg.name ] input_messages.append(HumanMessage(content=task)) + agent_response = interrupt( - InvokeProcess( - name=state["next"], input_arguments={"messages": input_messages} - ) + InvokeProcess(name=state["next"], input_arguments={"messages": input_messages}) ) response_content = agent_response["answer"] - agent_message = HumanMessage(content=response_content, name=agent_name) - return Command( - update={ - "messages": [agent_message], - "current_step": state["current_step"] + 1, - }, - goto="supervisor", - ) + return { + "messages": [agent_message], + "current_step": state["current_step"] + 1, + } + +# Build the graph builder = StateGraph(State, input=GraphInput, output=GraphOutput) + builder.add_node("input", input) -builder.add_node("create_plan", create_plan) -builder.add_node("supervisor", supervisor_node) +builder.add_node("planner_agent", make_planner_node(llm)) +builder.add_node("router", router) builder.add_node("invoke_agent", invoke_agent) +builder.add_node("output", output) builder.add_edge(START, "input") -builder.add_edge("input", "supervisor") -builder.add_conditional_edges("supervisor", route_supervisor) -builder.add_edge("create_plan", "supervisor") -builder.add_edge("invoke_agent", "supervisor") +builder.add_edge("input", "router") +builder.add_conditional_edges( + "router", + route_agent, + { + "planner_agent": "planner_agent", + "invoke_agent": "invoke_agent", + END: "output" + } +) +builder.add_edge("planner_agent", "router") +builder.add_edge("invoke_agent", "router") +builder.add_edge("output", END) graph = builder.compile() diff --git a/samples/multi-agent-planner-researcher-coder-distributed/uv.lock b/samples/multi-agent-planner-researcher-coder-distributed/uv.lock index 3341a932..8fcc90a5 100644 --- a/samples/multi-agent-planner-researcher-coder-distributed/uv.lock +++ b/samples/multi-agent-planner-researcher-coder-distributed/uv.lock @@ -1170,7 +1170,7 @@ requires-dist = [ { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11.1" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.1" }, { name = "uipath", specifier = ">=2.2.26,<2.3.0" }, - { name = "uipath-langchain", specifier = ">=0.1.22,<0.2.0" }, + { name = "uipath-langchain", specifier = ">=0.1.28,<0.2.0" }, ] provides-extras = ["dev"] @@ -2439,7 +2439,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.2.26" +version = "2.2.30" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -2459,23 +2459,23 @@ dependencies = [ { name = "uipath-core" }, { name = "uipath-runtime" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/10/c1bb4b0695844fb212b937fe2a4f741d0f1850bfbd000a4c78fe4c3ddac8/uipath-2.2.26.tar.gz", hash = "sha256:848d639c1844a8d15cec89890e066b324e4a951a1fc8d5716bc5aca847e0fefd", size = 3416492, upload-time = "2025-12-06T10:45:30.634Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/b5/0b2320ac4f978eb665380a1244adbe58986dd9b7809ea17204b41b296a52/uipath-2.2.30.tar.gz", hash = "sha256:a035edb8c4245738840e0d3953904c84255d18e86ab5058d9387b30125aaab1c", size = 3420128, upload-time = "2025-12-12T08:56:59.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/c7/bd4c0060e989b34e8ccae317e04d60de1cddba718649c25fdc5369780b6b/uipath-2.2.26-py3-none-any.whl", hash = "sha256:3ad74e997d4b9193aab6e621f3eb8b014cc0bc447db51e256637385fcf768d8a", size = 390046, upload-time = "2025-12-06T10:45:28.511Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ee/db944c6fd4ad1f3c41c724e6be969494fae23e86b73f4e4cdb4ccd06e869/uipath-2.2.30-py3-none-any.whl", hash = "sha256:5727f967752a567569ef62ae96680b18bf9eb79c0b21820c647cd8e7ed38152e", size = 392043, upload-time = "2025-12-12T08:56:58.115Z" }, ] [[package]] name = "uipath-core" -version = "0.1.0" +version = "0.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/79/0fa81ec1439eec09460a8df0f4bc88eb0f9e036020196e1977727aa029a5/uipath_core-0.1.0.tar.gz", hash = "sha256:97dac457279d8d44784833a7d62a931f4f5e0bf06b17d101e198c63938001cfb", size = 87645, upload-time = "2025-12-04T11:01:23.544Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/94/b548c087afe9853f979b7827815cd4cc34d7a1584fd82fb05881b13415d4/uipath_core-0.1.3.tar.gz", hash = "sha256:8d25e5e372137fc8b59b0a904fd412ec249a6d30b49512cb9e7ece04aca3bca8", size = 94942, upload-time = "2025-12-12T07:43:35.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/31/b9e3a89e47762ced4a417fb7eb865acaeb840d5ab2ee1fc7404b433fc332/uipath_core-0.1.0-py3-none-any.whl", hash = "sha256:b5ef76b02b7720d48e97c645217698a9e1499c0f854ca3e5571fdd9987195c36", size = 22224, upload-time = "2025-12-04T11:01:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/e63602e2160e0fc7092a2cd55fc62beaefcdc79e313ef67913ef1032394c/uipath_core-0.1.3-py3-none-any.whl", hash = "sha256:6596b4a10d8236b5ddf6102f2a772eb09b0ef2457bd1a0da878fb7f718b86f45", size = 29483, upload-time = "2025-12-12T07:43:34.077Z" }, ] [[package]] @@ -2494,9 +2494,10 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.1.22" +version = "0.1.28" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "aiosqlite" }, { name = "httpx" }, { name = "jsonpath-ng" }, { name = "jsonschema-pydantic-converter" }, @@ -2510,21 +2511,21 @@ dependencies = [ { name = "python-dotenv" }, { name = "uipath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/e0/a75e91faf5abf243ff36a1d43831eb87bb01059e41089c90be9298780ee9/uipath_langchain-0.1.22.tar.gz", hash = "sha256:3620664841f3cfc2371393f37fa8fd3ec1967394e75ad9d5a11694204d936102", size = 7292309, upload-time = "2025-12-08T17:36:46.931Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/2b/6a75cee64a2013d53110ba12b511be0e73a336d4cde6724a27d9f1b6a483/uipath_langchain-0.1.28.tar.gz", hash = "sha256:78e4123cc7032252f42d5210633971486ebe6b2a01e1102cf04a4d2d734c1607", size = 7280233, upload-time = "2025-12-15T09:49:26.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/b7/51827947435c822d8420f7778ddd93326f1c26be12017911f8e5fb9b7328/uipath_langchain-0.1.22-py3-none-any.whl", hash = "sha256:66189ce6a1c7ce33e89b338a05f7dc1a4b244d036c4d08695789cd4fbf9445e0", size = 81083, upload-time = "2025-12-08T17:36:45.3Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6f/aac9522c1d00893a4f62ca842d3ce977ee550734e2fcbf660c0ab8dd2442/uipath_langchain-0.1.28-py3-none-any.whl", hash = "sha256:b3bfbc1b26b4b8fca7ab363a2ac7bbfa1b7b76804931e409e869de89f7989417", size = 86159, upload-time = "2025-12-15T09:49:25.136Z" }, ] [[package]] name = "uipath-runtime" -version = "0.2.4" +version = "0.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/46/c035cf6464d5512f7a8e7ce85b6deab413c93f045c63c4df5eae8eb6449b/uipath_runtime-0.2.4.tar.gz", hash = "sha256:e402e7fd08de9c06a9584d56509187c2b0940da75dccdc55efc0726f7892b7f0", size = 95864, upload-time = "2025-12-07T12:47:10.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/61/52c1dca619f6903a5487265d890088f77e4af2b5d6bb13f5ddeec18fff63/uipath_runtime-0.2.9.tar.gz", hash = "sha256:551314539392e866aacffd9ee063324cd6a976d75fa1c7bfdc8dad756fbbb937", size = 96159, upload-time = "2025-12-15T11:04:15.645Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/47/7610adecf30a44eedd96ebedcf400f5ec0dab8a055397ac8d4fb6f55eed4/uipath_runtime-0.2.4-py3-none-any.whl", hash = "sha256:01bcb68ec186521451a9bdca0736192cc62202b46b5df1226454fa282090b846", size = 36883, upload-time = "2025-12-07T12:47:09.137Z" }, + { url = "https://files.pythonhosted.org/packages/97/b1/238ee2c81205bef4bc694151bc1c9863393fdb107d328240bdcb3ed51586/uipath_runtime-0.2.9-py3-none-any.whl", hash = "sha256:30555d0a1184eb8926d5e54bafcbf03e1355fca879aa2eeede6583a608f7b460", size = 37002, upload-time = "2025-12-15T11:04:14.111Z" }, ] [[package]] diff --git a/samples/multi-agent-supervisor-researcher-coder/.agent/SDK_REFERENCE.md b/samples/multi-agent-supervisor-researcher-coder/.agent/SDK_REFERENCE.md index e3b7ae83..499faab4 100644 --- a/samples/multi-agent-supervisor-researcher-coder/.agent/SDK_REFERENCE.md +++ b/samples/multi-agent-supervisor-researcher-coder/.agent/SDK_REFERENCE.md @@ -221,6 +221,12 @@ sdk.context_grounding.delete_index(index: uipath.platform.context_grounding.cont # Asynchronously delete a context grounding index. sdk.context_grounding.delete_index_async(index: uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> None +# Downloads the Batch Transform result file to the specified path. +sdk.context_grounding.download_batch_transform_result(id: str, destination_path: str, validate_status: bool=True, index_name: str | None=None) -> None + +# Asynchronously downloads the Batch Transform result file to the specified path. +sdk.context_grounding.download_batch_transform_result_async(id: str, destination_path: str, validate_status: bool=True, index_name: str | None=None) -> None + # Ingest data into the context grounding index. sdk.context_grounding.ingest_data(index: uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> None @@ -233,6 +239,12 @@ sdk.context_grounding.retrieve(name: str, folder_key: Optional[str]=None, folder # Asynchronously retrieve context grounding index information by its name. sdk.context_grounding.retrieve_async(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex +# Retrieves a Batch Transform task status. +sdk.context_grounding.retrieve_batch_transform(id: str, index_name: str | None=None) -> uipath.platform.context_grounding.context_grounding.BatchTransformResponse + +# Asynchronously retrieves a Batch Transform task status. +sdk.context_grounding.retrieve_batch_transform_async(id: str, index_name: str | None=None) -> uipath.platform.context_grounding.context_grounding.BatchTransformResponse + # Retrieve context grounding index information by its ID. sdk.context_grounding.retrieve_by_id(id: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> typing.Any @@ -251,11 +263,17 @@ sdk.context_grounding.search(name: str, query: str, number_of_results: int=10, f # Search asynchronously for contextual information within a specific index. sdk.context_grounding.search_async(name: str, query: str, number_of_results: int=10, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> typing.List[uipath.platform.context_grounding.context_grounding.ContextGroundingQueryResponse] +# Starts a Batch Transform, task on the targeted index. +sdk.context_grounding.start_batch_transform(name: str, index_name: str, prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], output_columns: list[uipath.platform.context_grounding.context_grounding.BatchTransformOutputColumn], storage_bucket_folder_path_prefix: Annotated[str | None, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=512)])]=None, enable_web_search_grounding: bool=False, folder_key: str | None=None, folder_path: str | None=None) -> uipath.platform.context_grounding.context_grounding.BatchTransformCreationResponse + +# Asynchronously starts a Batch Transform, task on the targeted index. +sdk.context_grounding.start_batch_transform_async(name: str, index_name: str, prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], output_columns: list[uipath.platform.context_grounding.context_grounding.BatchTransformOutputColumn], storage_bucket_folder_path_prefix: Annotated[str | None, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=512)])]=None, enable_web_search_grounding: bool=False, folder_key: str | None=None, folder_path: str | None=None) -> uipath.platform.context_grounding.context_grounding.BatchTransformCreationResponse + # Starts a Deep RAG task on the targeted index. -sdk.context_grounding.start_deep_rag(name: str, index_name: str, prompt: str, glob_pattern: str="*", citation_mode: uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse +sdk.context_grounding.start_deep_rag(name: str, index_name: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=512)])], prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], glob_pattern: Annotated[str, FieldInfo(annotation=NoneType, required=False, default='*', metadata=[MaxLen(max_length=512)])]="**", citation_mode: uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse # Asynchronously starts a Deep RAG task on the targeted index. -sdk.context_grounding.start_deep_rag_async(name: str, index_name: str, prompt: str, glob_pattern: str="*", citation_mode: uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse +sdk.context_grounding.start_deep_rag_async(name: str, index_name: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=512)])], prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], glob_pattern: Annotated[str, FieldInfo(annotation=NoneType, required=False, default='*', metadata=[MaxLen(max_length=512)])]="**", citation_mode: uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse ``` @@ -380,7 +398,7 @@ Guardrails service ```python # Validate input text using the provided guardrail. -sdk.guardrails.evaluate_guardrail(input_data: str | dict[str, Any], guardrail: Annotated[Union[uipath.platform.guardrails.guardrails.DeterministicGuardrail, uipath.platform.guardrails.guardrails.BuiltInValidatorGuardrail], FieldInfo(annotation=NoneType, required=True, discriminator='guardrail_type')]) -> uipath.platform.guardrails.guardrails.GuardrailValidationResult +sdk.guardrails.evaluate_guardrail(input_data: str | dict[str, Any], guardrail: uipath.platform.guardrails.guardrails.BuiltInValidatorGuardrail) -> uipath.core.guardrails.guardrails.GuardrailValidationResult ``` diff --git a/samples/multi-agent-supervisor-researcher-coder/agent.mermaid b/samples/multi-agent-supervisor-researcher-coder/agent.mermaid index 5a8ffdef..e0a89bb8 100644 --- a/samples/multi-agent-supervisor-researcher-coder/agent.mermaid +++ b/samples/multi-agent-supervisor-researcher-coder/agent.mermaid @@ -2,14 +2,16 @@ flowchart TB __start__(__start__) input(input) supervisor(supervisor) + output(output) __end__(__end__) __start__ --> input coder --> supervisor input --> supervisor researcher --> supervisor - supervisor --> __end__ supervisor --> coder + supervisor --> |__end__|output supervisor --> researcher + output --> __end__ subgraph researcher [researcher] direction LR __start__(__start__) diff --git a/samples/multi-agent-supervisor-researcher-coder/entry-points.json b/samples/multi-agent-supervisor-researcher-coder/entry-points.json index 58da540f..14fc7817 100644 --- a/samples/multi-agent-supervisor-researcher-coder/entry-points.json +++ b/samples/multi-agent-supervisor-researcher-coder/entry-points.json @@ -4,7 +4,7 @@ "entryPoints": [ { "filePath": "agent", - "uniqueId": "ee84ff72-76ce-40f4-adb0-f76bd220f634", + "uniqueId": "3252b086-675a-4d9d-a549-e99fa6f7cd2d", "type": "agent", "input": { "type": "object", @@ -48,9 +48,12 @@ { "id": "supervisor", "name": "supervisor", - "type": "node", + "type": "model", "subgraph": null, - "metadata": {} + "metadata": { + "model_name": "claude-3-7-sonnet-latest", + "max_tokens": 64000 + } }, { "id": "researcher", @@ -188,6 +191,13 @@ }, "metadata": {} }, + { + "id": "output", + "name": "output", + "type": "node", + "subgraph": null, + "metadata": {} + }, { "id": "__end__", "name": "__end__", @@ -219,18 +229,23 @@ }, { "source": "supervisor", - "target": "__end__", + "target": "coder", "label": null }, { "source": "supervisor", - "target": "coder", - "label": null + "target": "output", + "label": "__end__" }, { "source": "supervisor", "target": "researcher", "label": null + }, + { + "source": "output", + "target": "__end__", + "label": null } ] } diff --git a/samples/multi-agent-supervisor-researcher-coder/graph.py b/samples/multi-agent-supervisor-researcher-coder/graph.py index 9cce1d95..a0eca77d 100644 --- a/samples/multi-agent-supervisor-researcher-coder/graph.py +++ b/samples/multi-agent-supervisor-researcher-coder/graph.py @@ -2,6 +2,7 @@ from langchain_anthropic import ChatAnthropic from langchain_tavily import TavilySearch +from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import HumanMessage, SystemMessage, BaseMessage from langchain_core.tools import tool from langchain_experimental.utilities import PythonREPL @@ -32,16 +33,21 @@ def python_repl_tool( return result_str members = ["researcher", "coder"] -# Our team supervisor is an LLM node. It just picks the next agent to process -# and decides when the work is completed options = members + ["FINISH"] system_prompt = ( "You are a supervisor tasked with managing a conversation between the" f" following workers: {members}. Given the following user request," " respond with the worker to act next. Each worker will perform a" - " task and respond with their results and status. When finished," - " respond with FINISH." + " task and respond with their results and status.\n\n" + "When to choose FINISH:\n" + "- If the user's question has been fully answered\n" + "- If a worker has provided a complete solution\n" + "- If no additional work is needed\n\n" + "When to choose a worker:\n" + "- researcher: For searching information, finding facts, or research tasks\n" + "- coder: For mathematical calculations, data analysis, or code execution\n\n" + "Avoid sending workers back and forth unnecessarily. Once a worker completes the task, choose FINISH." ) @@ -61,6 +67,7 @@ class GraphOutput(BaseModel): class State(MessagesState): next: str + answer: str def get_message_text(msg: BaseMessage) -> str: """LangChain-style safe message text extractor.""" @@ -79,27 +86,59 @@ def input(state: GraphInput): HumanMessage(content=state.question), ], "next": "", + "answer": "", } -async def supervisor_node(state: State) -> dict | GraphOutput: - response = await llm.with_structured_output(Router).ainvoke(state["messages"]) - goto = response["next"] - if goto == "FINISH": - return GraphOutput(answer=get_message_text(state["messages"][-1])) - else: - return {"next": goto} - -def route_supervisor(state: State) -> Literal["researcher", "coder"] | Literal["__end__"]: +def make_supervisor_node(model: BaseChatModel): + # Wrapper to identify the node as model-based + supervisor_llm = model.with_structured_output(Router) + async def supervisor_node(state: State) -> dict: + response = await supervisor_llm.ainvoke(state["messages"]) + goto = response["next"] + + # When finishing, extract the answer and store it in state + if goto == "FINISH": + # Get the last message from a worker (not system message) + last_worker_message = None + for msg in reversed(state["messages"]): + if msg.type == "human" and hasattr(msg, "name") and msg.name in members: + last_worker_message = msg + break + + if last_worker_message: + answer = get_message_text(last_worker_message) + else: + # Fallback: get last non-system message + answer = get_message_text(state["messages"][-1]) + + return {"next": goto, "answer": answer} + else: + return {"next": goto} + + return supervisor_node + +def route_supervisor(state: State) -> Literal["researcher", "coder", "__end__"]: next_node = state.get("next", "") if next_node == "researcher": return "researcher" elif next_node == "coder": return "coder" + elif next_node == "FINISH": + return "__end__" else: - return END + return "__end__" + +def output_node(state: State) -> GraphOutput: + return GraphOutput(answer=state.get("answer", "")) research_agent = create_agent( - llm, tools=[tavily_tool], system_prompt="You are a researcher. DO NOT do any math." + llm, + tools=[tavily_tool], + system_prompt=( + "You are a researcher. DO NOT do any math. " + "Search for information and provide findings. " + "When you've completed your research, clearly state your findings." + ) ) @@ -113,7 +152,15 @@ async def research_node(state: State): # NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION, WHICH CAN BE UNSAFE WHEN NOT SANDBOXED -code_agent = create_agent(llm, tools=[python_repl_tool]) +code_agent = create_agent( + llm, + tools=[python_repl_tool], + system_prompt=( + "You are a coder. Execute Python code to solve problems. " + "When you've successfully completed the calculation or task, " + "provide the final answer clearly." + ) +) async def code_node(state: State): @@ -127,19 +174,20 @@ async def code_node(state: State): builder = StateGraph(State, input=GraphInput, output=GraphOutput) builder.add_node("input", input) -builder.add_node("supervisor", supervisor_node) +builder.add_node("supervisor", make_supervisor_node(llm)) builder.add_node("researcher", research_node) builder.add_node("coder", code_node) +builder.add_node("output", output_node) builder.add_edge(START, "input") builder.add_edge("input", "supervisor") builder.add_conditional_edges("supervisor", route_supervisor, { "researcher": "researcher", "coder": "coder", - END: END + "__end__": "output" }) builder.add_edge("researcher", "supervisor") builder.add_edge("coder", "supervisor") -builder.add_edge("supervisor", END) +builder.add_edge("output", END) graph = builder.compile() diff --git a/samples/multi-agent-supervisor-researcher-coder/pyproject.toml b/samples/multi-agent-supervisor-researcher-coder/pyproject.toml index f5060b11..87f2be70 100644 --- a/samples/multi-agent-supervisor-researcher-coder/pyproject.toml +++ b/samples/multi-agent-supervisor-researcher-coder/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "langchain-anthropic>=1.2.0", "langchain-experimental>=0.4.0", "tavily-python>=0.7.13", - "uipath-langchain>=0.1.22, <0.2.0", + "uipath-langchain>=0.1.28, <0.2.0", "langchain-tavily>=0.2.13", "uipath>=2.2.0, <2.3.0", ] diff --git a/samples/multi-agent-supervisor-researcher-coder/uv.lock b/samples/multi-agent-supervisor-researcher-coder/uv.lock index 8c1e303a..047a5503 100644 --- a/samples/multi-agent-supervisor-researcher-coder/uv.lock +++ b/samples/multi-agent-supervisor-researcher-coder/uv.lock @@ -1172,7 +1172,7 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.1" }, { name = "tavily-python", specifier = ">=0.7.13" }, { name = "uipath", specifier = ">=2.2.0,<2.3.0" }, - { name = "uipath-langchain", specifier = ">=0.1.22,<0.2.0" }, + { name = "uipath-langchain", specifier = ">=0.1.28,<0.2.0" }, ] provides-extras = ["dev"] @@ -2455,7 +2455,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.2.26" +version = "2.2.30" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -2475,23 +2475,23 @@ dependencies = [ { name = "uipath-core" }, { name = "uipath-runtime" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/10/c1bb4b0695844fb212b937fe2a4f741d0f1850bfbd000a4c78fe4c3ddac8/uipath-2.2.26.tar.gz", hash = "sha256:848d639c1844a8d15cec89890e066b324e4a951a1fc8d5716bc5aca847e0fefd", size = 3416492, upload-time = "2025-12-06T10:45:30.634Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/b5/0b2320ac4f978eb665380a1244adbe58986dd9b7809ea17204b41b296a52/uipath-2.2.30.tar.gz", hash = "sha256:a035edb8c4245738840e0d3953904c84255d18e86ab5058d9387b30125aaab1c", size = 3420128, upload-time = "2025-12-12T08:56:59.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/c7/bd4c0060e989b34e8ccae317e04d60de1cddba718649c25fdc5369780b6b/uipath-2.2.26-py3-none-any.whl", hash = "sha256:3ad74e997d4b9193aab6e621f3eb8b014cc0bc447db51e256637385fcf768d8a", size = 390046, upload-time = "2025-12-06T10:45:28.511Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ee/db944c6fd4ad1f3c41c724e6be969494fae23e86b73f4e4cdb4ccd06e869/uipath-2.2.30-py3-none-any.whl", hash = "sha256:5727f967752a567569ef62ae96680b18bf9eb79c0b21820c647cd8e7ed38152e", size = 392043, upload-time = "2025-12-12T08:56:58.115Z" }, ] [[package]] name = "uipath-core" -version = "0.1.0" +version = "0.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/79/0fa81ec1439eec09460a8df0f4bc88eb0f9e036020196e1977727aa029a5/uipath_core-0.1.0.tar.gz", hash = "sha256:97dac457279d8d44784833a7d62a931f4f5e0bf06b17d101e198c63938001cfb", size = 87645, upload-time = "2025-12-04T11:01:23.544Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/94/b548c087afe9853f979b7827815cd4cc34d7a1584fd82fb05881b13415d4/uipath_core-0.1.3.tar.gz", hash = "sha256:8d25e5e372137fc8b59b0a904fd412ec249a6d30b49512cb9e7ece04aca3bca8", size = 94942, upload-time = "2025-12-12T07:43:35.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/31/b9e3a89e47762ced4a417fb7eb865acaeb840d5ab2ee1fc7404b433fc332/uipath_core-0.1.0-py3-none-any.whl", hash = "sha256:b5ef76b02b7720d48e97c645217698a9e1499c0f854ca3e5571fdd9987195c36", size = 22224, upload-time = "2025-12-04T11:01:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/e63602e2160e0fc7092a2cd55fc62beaefcdc79e313ef67913ef1032394c/uipath_core-0.1.3-py3-none-any.whl", hash = "sha256:6596b4a10d8236b5ddf6102f2a772eb09b0ef2457bd1a0da878fb7f718b86f45", size = 29483, upload-time = "2025-12-12T07:43:34.077Z" }, ] [[package]] @@ -2510,9 +2510,10 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.1.23" +version = "0.1.28" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "aiosqlite" }, { name = "httpx" }, { name = "jsonpath-ng" }, { name = "jsonschema-pydantic-converter" }, @@ -2526,21 +2527,21 @@ dependencies = [ { name = "python-dotenv" }, { name = "uipath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/d1/8a2f33facf51cb5e9dba37b9b69e2f369cefc6bce88e2d38b3d7928a93dc/uipath_langchain-0.1.23.tar.gz", hash = "sha256:7605209b68bd731c93c87a840235f0d3f8fd20ad8bab089423dccfdba3360616", size = 7292305, upload-time = "2025-12-09T11:11:12.015Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/2b/6a75cee64a2013d53110ba12b511be0e73a336d4cde6724a27d9f1b6a483/uipath_langchain-0.1.28.tar.gz", hash = "sha256:78e4123cc7032252f42d5210633971486ebe6b2a01e1102cf04a4d2d734c1607", size = 7280233, upload-time = "2025-12-15T09:49:26.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/ec/34d72f71aba55f4e9d4d131767156644fa842950bb367cf2f4d17e56ddb3/uipath_langchain-0.1.23-py3-none-any.whl", hash = "sha256:3312d828fe04ddeb36f72b76054a5d7f42ece62cf51d71a3759e791fbc03036b", size = 81038, upload-time = "2025-12-09T11:11:10.396Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6f/aac9522c1d00893a4f62ca842d3ce977ee550734e2fcbf660c0ab8dd2442/uipath_langchain-0.1.28-py3-none-any.whl", hash = "sha256:b3bfbc1b26b4b8fca7ab363a2ac7bbfa1b7b76804931e409e869de89f7989417", size = 86159, upload-time = "2025-12-15T09:49:25.136Z" }, ] [[package]] name = "uipath-runtime" -version = "0.2.4" +version = "0.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/46/c035cf6464d5512f7a8e7ce85b6deab413c93f045c63c4df5eae8eb6449b/uipath_runtime-0.2.4.tar.gz", hash = "sha256:e402e7fd08de9c06a9584d56509187c2b0940da75dccdc55efc0726f7892b7f0", size = 95864, upload-time = "2025-12-07T12:47:10.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/61/52c1dca619f6903a5487265d890088f77e4af2b5d6bb13f5ddeec18fff63/uipath_runtime-0.2.9.tar.gz", hash = "sha256:551314539392e866aacffd9ee063324cd6a976d75fa1c7bfdc8dad756fbbb937", size = 96159, upload-time = "2025-12-15T11:04:15.645Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/47/7610adecf30a44eedd96ebedcf400f5ec0dab8a055397ac8d4fb6f55eed4/uipath_runtime-0.2.4-py3-none-any.whl", hash = "sha256:01bcb68ec186521451a9bdca0736192cc62202b46b5df1226454fa282090b846", size = 36883, upload-time = "2025-12-07T12:47:09.137Z" }, + { url = "https://files.pythonhosted.org/packages/97/b1/238ee2c81205bef4bc694151bc1c9863393fdb107d328240bdcb3ed51586/uipath_runtime-0.2.9-py3-none-any.whl", hash = "sha256:30555d0a1184eb8926d5e54bafcbf03e1355fca879aa2eeede6583a608f7b460", size = 37002, upload-time = "2025-12-15T11:04:14.111Z" }, ] [[package]] diff --git a/src/uipath_langchain/runtime/schema.py b/src/uipath_langchain/runtime/schema.py index 29086886..4e425aff 100644 --- a/src/uipath_langchain/runtime/schema.py +++ b/src/uipath_langchain/runtime/schema.py @@ -1,3 +1,4 @@ +from collections.abc import Iterable from dataclasses import dataclass from typing import Any, Callable, TypeVar @@ -31,28 +32,83 @@ class SchemaDetails: def _unwrap_runnable_callable( - runnable: Runnable[Any, Any], target_type: type[T] + runnable: Runnable[Any, Any], + target_type: type[T], + _seen: set[int] | None = None, ) -> T | None: - """Unwrap a RunnableCallable to find an instance of the target type. - - Args: - runnable: The runnable to unwrap - target_type: The type to search for (e.g., BaseChatModel) - - Returns: - Instance of target_type if found in the closure, None otherwise + """Try to find an instance of target_type (e.g., BaseChatModel) + inside a Runnable. + + Handles: + - Direct model runnables + - LangGraph RunnableCallable + - LangChain function runnables (RunnableLambda, etc.) + - RunnableBinding / RunnableSequence with nested steps """ if isinstance(runnable, target_type): return runnable + if _seen is None: + _seen = set() + obj_id = id(runnable) + if obj_id in _seen: + return None + _seen.add(obj_id) + + func: Callable[..., Any] | None = None + + # 1) LangGraph internal RunnableCallable if RunnableCallable is not None and isinstance(runnable, RunnableCallable): - func: Callable[..., Any] | None = getattr(runnable, "func", None) - if func is not None and hasattr(func, "__closure__") and func.__closure__: - for cell in func.__closure__: - if hasattr(cell, "cell_contents"): - content = cell.cell_contents - if isinstance(content, target_type): - return content + func = getattr(runnable, "func", None) + + # 2) Generic LangChain function-wrapping runnables + if func is None: + for attr_name in ("func", "_func", "afunc", "_afunc"): + maybe = getattr(runnable, attr_name, None) + if callable(maybe): + func = maybe + break + + # 3) Look into the function closure for a model + if func is not None: + closure = getattr(func, "__closure__", None) or () + for cell in closure: + content = getattr(cell, "cell_contents", None) + if isinstance(content, target_type): + return content + if isinstance(content, Runnable): + found = _unwrap_runnable_callable(content, target_type, _seen) + if found is not None: + return found + + # 4) Deep-scan attributes, including nested runnables / containers + def _scan_value(value: Any) -> T | None: + if isinstance(value, target_type): + return value + if isinstance(value, Runnable): + return _unwrap_runnable_callable(value, target_type, _seen) + if isinstance(value, dict): + for v in value.values(): + found = _scan_value(v) + if found is not None: + return found + # Handle lists, tuples, sets, etc. but avoid strings/bytes + if isinstance(value, Iterable) and not isinstance(value, (str, bytes)): + for item in value: + found = _scan_value(item) + if found is not None: + return found + return None + + try: + attrs = vars(runnable) + except TypeError: + attrs = {} + + for value in attrs.values(): + found = _scan_value(value) + if found is not None: + return found return None