Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/uipath_langchain/agent/react/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""UiPath ReAct Agent implementation"""

from .agent import create_agent
from .types import AgentGraphConfig, AgentGraphNode, AgentGraphState
from .types import AgentGraphConfig, AgentGraphNode, AgentGraphState, AgentResources
from .utils import resolve_input_model, resolve_output_model

__all__ = [
Expand All @@ -11,4 +11,5 @@
"AgentGraphNode",
"AgentGraphState",
"AgentGraphConfig",
"AgentResources",
]
10 changes: 8 additions & 2 deletions src/uipath_langchain/agent/react/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
AgentGraphConfig,
AgentGraphNode,
AgentGraphState,
AgentResources,
)
from .utils import create_state_with_input

Expand All @@ -53,6 +54,7 @@ def create_agent(
output_schema: Type[OutputT] | None = None,
config: AgentGraphConfig | None = None,
guardrails: Sequence[tuple[BaseGuardrail, GuardrailAction]] | None = None,
resources: AgentResources | None = None,
) -> StateGraph[AgentGraphState, None, InputT, OutputT]:
"""Build agent graph with INIT -> AGENT (subgraph) <-> TOOLS loop, terminated by control flow tools.

Expand All @@ -74,8 +76,12 @@ def create_agent(
)
llm_tools: list[BaseTool] = [*agent_tools, *flow_control_tools]

init_node = create_init_node(messages, input_schema, config.is_conversational)

init_node = create_init_node(
messages,
input_schema,
config.is_conversational,
resources_for_init=resources,
)
tool_nodes = create_tool_node(
agent_tools, handle_tool_errors=config.is_conversational
)
Expand Down
69 changes: 69 additions & 0 deletions src/uipath_langchain/agent/react/init_context_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Registry for resource types that contribute init-time context.

Resource modules self-register by calling ``register_init_context_provider``
at module level. The INIT node calls ``gather_init_context`` to collect
additional context from all registered providers, without needing to know
which resource types participate.
"""

import logging
from typing import Protocol, Sequence

from uipath.agent.models.agent import BaseAgentResourceConfig

logger = logging.getLogger(__name__)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant whitespaces. Can you run ruff with whitespace check if there is a rule for that. Dont like these spaces Agents generate


class InitContextProvider(Protocol):
"""Contract for a resource type's init-time context builder."""

async def __call__(
self,
resources: Sequence[BaseAgentResourceConfig],
) -> str | None: ...


_registry: dict[str, InitContextProvider] = {}


def register_init_context_provider(
name: str,
provider: InitContextProvider,
) -> None:
"""Register a provider that contributes init-time context.

Args:
name: Identifier for logging and deduplication.
provider: Async callable matching ``InitContextProvider``.
"""
if name in _registry:
raise ValueError(f"Init context provider '{name}' is already registered")
_registry[name] = provider
logger.debug("Registered init context provider: %s", name)


async def gather_init_context(
resources: Sequence[BaseAgentResourceConfig],
) -> str | None:
"""Call all registered providers and merge their context contributions.

Args:
resources: The agent's resource configs.

Returns:
Merged context string, or None if no provider contributed.
"""
parts: list[str] = []
for name, provider in _registry.items():
try:
result = await provider(resources)
if result:
parts.append(result)
logger.info(
"Init context provider '%s' contributed %d chars",
name,
len(result),
)
except Exception:
logger.exception("Init context provider '%s' failed; skipping", name)
return "\n\n".join(parts) if parts else None

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it structured rather than free form text. Expose it as a pydantic model to be consumed by caller

25 changes: 22 additions & 3 deletions src/uipath_langchain/agent/react/init_node.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""State initialization node for the ReAct Agent graph."""

import logging
from typing import Any, Callable, Sequence

from langchain_core.messages import HumanMessage, SystemMessage
Expand All @@ -10,20 +11,38 @@
get_job_attachments,
parse_attachments_from_conversation_messages,
)
from .types import AgentResources

logger = logging.getLogger(__name__)


def create_init_node(
messages: Sequence[SystemMessage | HumanMessage]
| Callable[[Any], Sequence[SystemMessage | HumanMessage]],
| Callable[..., Sequence[SystemMessage | HumanMessage]],
input_schema: type[BaseModel] | None,
is_conversational: bool = False,
resources_for_init: AgentResources | None = None,
):
def graph_state_init(state: Any) -> Any:
async def graph_state_init(state: Any) -> Any:
# --- Gather init-time context from registered providers ---
additional_context: str | None = None
if resources_for_init:
from .init_context_registry import gather_init_context

additional_context = await gather_init_context(resources_for_init)

# --- Resolve messages ---
resolved_messages: Sequence[SystemMessage | HumanMessage] | Overwrite
if callable(messages):
resolved_messages = list(messages(state))
if additional_context:
resolved_messages = list(
messages(state, additional_context=additional_context)
)
else:
resolved_messages = list(messages(state))
Comment on lines 36 to +42
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When additional_context is present, the init node calls messages(state, additional_context=...). This will raise TypeError for existing message generator callables that only accept a single positional state argument (no **kwargs), making Data Fabric/resources injection a breaking change for those users. Consider detecting whether the callable accepts additional_context (or **kwargs) and falling back to messages(state) if not.

Copilot uses AI. Check for mistakes.
else:
resolved_messages = list(messages)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Inject schema context for static message definitions

If messages is provided as a static sequence (which create_agent supports), the INIT node ignores schema_context and returns list(messages) unchanged. In that path, Data Fabric schemas are fetched but never reach the system prompt, so query_datafabric runs without table/column guidance and the feature silently underperforms or fails to generate valid SQL. The static-message branch should also append/update a system message with the generated schema context.

Useful? React with 👍 / 👎.

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

additional_context is computed from resources_for_init, but it is only passed into a callable messages generator. When messages is a static list/sequence, the additional context is silently ignored, so Data Fabric schemas (and any other provider output) won’t be injected into the system prompt in that common usage. Consider appending additional_context to the SystemMessage content (or inserting a new SystemMessage) when messages is a sequence, so init-time context is applied consistently.

Suggested change
resolved_messages = list(messages)
resolved_messages = list(messages)
# When using a static sequence of messages, inject any init-time context
# into the system prompt so provider output (e.g., Data Fabric schemas)
# is not silently ignored.
if additional_context:
# Try to append the additional context to the first SystemMessage.
system_msg_index = next(
(i for i, m in enumerate(resolved_messages) if isinstance(m, SystemMessage)),
None,
)
if system_msg_index is not None:
system_msg = resolved_messages[system_msg_index]
# Safely append to existing content, assuming string content.
existing_content = str(system_msg.content)
updated_content = f"{existing_content}\n\n{additional_context}"
resolved_messages[system_msg_index] = SystemMessage(
content=updated_content, additional_kwargs=system_msg.additional_kwargs
)
else:
# No SystemMessage present; prepend a new one with the additional context.
resolved_messages.insert(0, SystemMessage(content=additional_context))

Copilot uses AI. Check for mistakes.

if is_conversational:
# For conversational agents we need to reorder the messages so that the system message is first, followed by
# the initial user message. When resuming the conversation, the state will have the entire message history,
Expand Down
5 changes: 4 additions & 1 deletion src/uipath_langchain/agent/react/types.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from enum import StrEnum
from typing import Annotated, Any, Hashable, Literal, Optional
from typing import Annotated, Any, Hashable, Literal, Optional, Sequence

from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
from pydantic import BaseModel, Field
from uipath.agent.models.agent import BaseAgentResourceConfig
from uipath.agent.react import END_EXECUTION_TOOL, RAISE_ERROR_TOOL
from uipath.platform.attachments import Attachment

Expand All @@ -14,6 +15,8 @@

FLOW_CONTROL_TOOLS = [END_EXECUTION_TOOL.name, RAISE_ERROR_TOOL.name]

AgentResources = Sequence[BaseAgentResourceConfig]


class InnerAgentGraphState(BaseModel):
job_attachments: Annotated[dict[str, Attachment], merge_dicts] = {}
Expand Down
10 changes: 10 additions & 0 deletions src/uipath_langchain/agent/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"""Tool creation and management for LowCode agents."""

from .context_tool import create_context_tool
from .datafabric_tool import (
fetch_entity_schemas,
format_schemas_for_context,
get_datafabric_contexts,
get_datafabric_entity_identifiers_from_resources,
)
from .escalation_tool import create_escalation_tool
from .extraction_tool import create_ixp_extraction_tool
from .integration_tool import create_integration_tool
Expand All @@ -22,6 +28,10 @@
"create_escalation_tool",
"create_ixp_extraction_tool",
"create_ixp_escalation_tool",
"fetch_entity_schemas",
"format_schemas_for_context",
"get_datafabric_contexts",
"get_datafabric_entity_identifiers_from_resources",
"UiPathToolNode",
"ToolWrapperMixin",
]
22 changes: 17 additions & 5 deletions src/uipath_langchain/agent/tools/context_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from uipath.agent.models.agent import (
AgentContextResourceConfig,
AgentContextRetrievalMode,
AgentContextType,
AgentToolArgumentArgumentProperties,
AgentToolArgumentProperties,
)
Expand Down Expand Up @@ -134,16 +135,27 @@ def is_static_query(resource: AgentContextResourceConfig) -> bool:
return resource.settings.query.variant.lower() == "static"


def create_context_tool(resource: AgentContextResourceConfig) -> StructuredTool:
tool_name = sanitize_tool_name(resource.name)
def create_context_tool(
resource: AgentContextResourceConfig,
) -> StructuredTool | BaseTool:
assert resource.context_type is not None

if resource.context_type == AgentContextType.DATA_FABRIC_ENTITY_SET:
from .datafabric_tool import create_datafabric_query_tool

return create_datafabric_query_tool()

assert resource.settings is not None

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this behind isIndexTool flag.

tool_name = sanitize_tool_name(resource.name)
retrieval_mode = resource.settings.retrieval_mode.lower()

if retrieval_mode == AgentContextRetrievalMode.DEEP_RAG.value.lower():
return handle_deep_rag(tool_name, resource)
elif retrieval_mode == AgentContextRetrievalMode.BATCH_TRANSFORM.value.lower():

if retrieval_mode == AgentContextRetrievalMode.BATCH_TRANSFORM.value.lower():
return handle_batch_transform(tool_name, resource)
else:
return handle_semantic_search(tool_name, resource)

return handle_semantic_search(tool_name, resource)


def handle_semantic_search(
Expand Down
50 changes: 50 additions & 0 deletions src/uipath_langchain/agent/tools/datafabric_tool/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Data Fabric tool module for entity-based SQL queries."""

import logging
from typing import Sequence

from uipath.agent.models.agent import BaseAgentResourceConfig

from uipath_langchain.agent.react.init_context_registry import (
register_init_context_provider,
)

from .datafabric_tool import (
create_datafabric_query_tool,
fetch_entity_schemas,
get_datafabric_contexts,
get_datafabric_entity_identifiers_from_resources,
)
from .schema_context import format_schemas_for_context

__all__ = [
"create_datafabric_query_tool",
"fetch_entity_schemas",
"format_schemas_for_context",
"get_datafabric_contexts",
"get_datafabric_entity_identifiers_from_resources",
]

_logger = logging.getLogger(__name__)


# --- Init-time context self-registration ---


async def _datafabric_init_context_provider(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to datafabric_tool rather than exposing it as a module method.

resources: Sequence[BaseAgentResourceConfig],
) -> str | None:
"""Fetch and format DataFabric entity schemas for system prompt injection."""
entity_identifiers = get_datafabric_entity_identifiers_from_resources(resources)
if not entity_identifiers:
return None

_logger.info(
"Fetching Data Fabric schemas for %d identifier(s)",
len(entity_identifiers),
)
entities = await fetch_entity_schemas(entity_identifiers)
return format_schemas_for_context(entities)


register_init_context_provider("datafabric", _datafabric_init_context_provider)
Comment on lines +34 to +50
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New Data Fabric init-time context registration and schema formatting logic is introduced here, but there are no unit tests covering (a) provider registration + gather_init_context integration, and (b) that fetched entity schemas are formatted and injected as expected. Given the repo’s existing comprehensive tool/init tests, adding focused tests for the provider behavior would help prevent regressions.

Copilot uses AI. Check for mistakes.
Loading
Loading