Skip to content
Merged
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: 3 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ This sample shows how to build an AI assistant using LangGraph and Tavily search
## [Company research agent](company-research-agent)
This sample demonstrates how to create an agent that researches companies and develops outreach strategies using web search capabilities.

## [Data Fabric coded agent](datafabric-coded-agent)
This sample demonstrates a coded LangGraph agent using the same Data Fabric query-tool core as low-code entity-set contexts, including runtime entity and folder overrides.

## [Email organizer agent](email-organizer-agent)
This sample shows how to automate Outlook inbox organization with AI-powered rule suggestions and human-in-the-loop approval.

Expand Down
17 changes: 17 additions & 0 deletions samples/datafabric-coded-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Data Fabric agent

A minimal LangGraph agent that queries a Data Fabric entity with
`create_datafabric_tool` with direct entity references.

Replace the entity ID, name, and folder key in `graph.py` with your entity values.

Pass the coded agent's outer system prompt to both `create_agent` and
`create_datafabric_tool`. The latter forwards it to the tool's inner
SQL-generation graph so the same agent instructions apply at both levels.

## Usage

```bash
uv sync
uip codedagent run agent '{"messages":[{"role":"user","content":"List the names."}]}'
```
24 changes: 24 additions & 0 deletions samples/datafabric-coded-agent/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from langchain.agents import create_agent
from uipath.platform.entities import DataFabricEntityItem

from uipath_langchain.agent.tools import create_datafabric_tool
from uipath_langchain.chat import UiPathChat

llm = UiPathChat(model="gpt-4.1-mini-2025-04-14")
system_prompt = "Answer questions using only the configured Data Fabric entities."

datafabric_tool = create_datafabric_tool(
llm=llm,
name="query_agent_test",
description="Query the agentTest Data Fabric entity.",
base_system_prompt=system_prompt,
entities=[
DataFabricEntityItem(
id="1312e893-8295-f111-9b33-0022482a9eea",
name="agentTest",
folder_key="379fec63-62b1-41ec-b2fc-718f8f7dda3c",
)
],
)

graph = create_agent(llm, tools=[datafabric_tool], system_prompt=system_prompt)
7 changes: 7 additions & 0 deletions samples/datafabric-coded-agent/langgraph.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"dependencies": ["."],
"graphs": {
"agent": "./graph.py:graph"
},
"env": ".env"
}
14 changes: 14 additions & 0 deletions samples/datafabric-coded-agent/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[project]
name = "datafabric-coded-agent"
version = "0.0.1"
description = "Coded agent that queries a UiPath Data Fabric entity"
authors = [{ name = "UiPath" }]
dependencies = [
"uipath-langchain",
]
requires-python = ">=3.11"

[dependency-groups]
dev = [
"uipath-dev",
]
1 change: 1 addition & 0 deletions samples/datafabric-coded-agent/uipath.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
4 changes: 3 additions & 1 deletion src/uipath_langchain/agent/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Tool creation and management for LowCode agents."""
"""Tool creation and management for low-code and coded agents."""

from .a2a import A2aClient, create_a2a_tools_and_clients, open_a2a_tools
from .context_tool import create_context_tool
from .datafabric_tool import create_datafabric_tool
from .escalation_tool import create_escalation_tool
from .extraction_tool import create_ixp_extraction_tool
from .integration_tool import create_integration_tool
Expand All @@ -26,6 +27,7 @@
"create_tools_from_resources",
"create_tool_node",
"create_context_tool",
"create_datafabric_tool",
"open_mcp_tools",
"create_process_tool",
"create_integration_tool",
Expand Down
8 changes: 2 additions & 6 deletions src/uipath_langchain/agent/tools/datafabric_tool/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
"""Data Fabric tool module for entity-based SQL queries."""

from .datafabric_tool import (
create_datafabric_query_tool,
)
from .datafabric_tool import create_datafabric_query_tool, create_datafabric_tool

__all__ = [
"create_datafabric_query_tool",
]
__all__ = ["create_datafabric_query_tool", "create_datafabric_tool"]
Original file line number Diff line number Diff line change
Expand Up @@ -349,4 +349,4 @@
8. **Explicit GROUP BY** - All non-aggregated columns in SELECT must be in GROUP BY
9. **Simple aggregations only** - No DISTINCT in aggregates
10. **ORDER BY only selected columns** - Cannot ORDER BY columns not in SELECT list
11. **ALWAYS include LIMIT** - Queries without WHERE must include a LIMIT clause (e.g., LIMIT 100). This applies to aggregates too (e.g., SELECT COUNT(col) FROM table LIMIT 1)"""
11. **Limit unbounded row queries** - Queries without WHERE that could return many rows must include a LIMIT clause (e.g., LIMIT 100). Scalar aggregate queries do not require LIMIT"""
151 changes: 119 additions & 32 deletions src/uipath_langchain/agent/tools/datafabric_tool/datafabric_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

import asyncio
import logging
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any

from langchain_core.language_models import BaseChatModel
Expand All @@ -30,6 +32,21 @@
BASE_SYSTEM_PROMPT = "base_system_prompt"


@dataclass(frozen=True, slots=True)
class _DataFabricToolConfig:
"""Framework-neutral configuration for a Data Fabric query tool.

Low-code contexts and coded-agent calls are normalized into this model before
the LangChain tool and its lazy query handler are created.
"""

name: str
description: str
entities: tuple[DataFabricEntityItem, ...]
resource_description: str = ""
base_system_prompt: str = ""


class DataFabricTextQueryHandler:
"""Manages lazy initialization and invocation of the Data Fabric sub-graph.

Expand Down Expand Up @@ -139,49 +156,119 @@ def _format_terminal_tool_messages(tool_messages: list[ToolMessage]) -> str:
)


def _normalize_entities(
entities: Sequence[DataFabricEntityItem],
) -> tuple[DataFabricEntityItem, ...]:
"""Copy entity references so caller mutations cannot change a built tool."""
return tuple(
DataFabricEntityItem.model_validate(entity.model_dump(by_alias=True))
for entity in entities
)


def _default_tool_description(entities: Sequence[DataFabricEntityItem]) -> str:
entity_lines = []
for entity in entities:
line = f"- {entity.name}"
if entity.description:
line += f": {entity.description}"
entity_lines.append(line)
entity_summary = "\n".join(entity_lines)
return (
"Query the following Data Fabric entities using natural language:\n"
f"{entity_summary}\n"
"Describe what data you need and the tool will translate it to SQL, "
"execute the query, and return a natural language answer."
)


def _build_datafabric_tool(
config: _DataFabricToolConfig,
llm: BaseChatModel,
) -> BaseTool:
"""Build the shared LangChain tool used by coded and low-code agents."""
handler = DataFabricTextQueryHandler(
entity_set=list(config.entities),
llm=llm,
resource_description=config.resource_description,
base_system_prompt=config.base_system_prompt,
)
return BaseUiPathStructuredTool(
name=config.name,
description=config.description,
args_schema=DataFabricQueryInput,
coroutine=handler.__call__,
metadata={"tool_type": "datafabric_sql"},
)


def create_datafabric_query_tool(
resource: AgentContextResourceConfig,
llm: BaseChatModel,
tool_name: str = "query_datafabric",
agent_config: dict[str, str] | None = None,
) -> BaseTool:
"""Create the ``query_datafabric`` agentic tool.
"""Create the low-code Data Fabric query tool from a context resource.

Entity schemas and runtime binding overrides are resolved lazily on the first
invocation. Keep the resulting tool scoped to one agent execution so its
cached schema and routing cannot cross execution contexts.

Args:
resource: The Data Fabric context resource configuration.
llm: The language model for the inner SQL generation loop.
tool_name: Sanitized tool name from the resource.
agent_config: Optional dict with agent-level config.
Key ``base_system_prompt`` carries the outer agent's system prompt.
resource: Low-code Data Fabric context resource.
llm: Language model for the inner SQL generation loop.
tool_name: LangChain tool name exposed to the agent.
agent_config: Optional agent-level configuration. Key
``base_system_prompt`` carries the outer agent's system prompt.
"""
config = agent_config or {}
entity_set = [
DataFabricEntityItem.model_validate(item.model_dump(by_alias=True))
for item in (resource.entity_set or [])
]
handler = DataFabricTextQueryHandler(
entity_set=entity_set,
llm=llm,
resource_description=resource.description or "",
base_system_prompt=config.get(BASE_SYSTEM_PROMPT, ""),
entity_set = _normalize_entities(resource.entity_set or [])

return _build_datafabric_tool(
_DataFabricToolConfig(
name=tool_name,
description=_default_tool_description(entity_set),
entities=entity_set,
resource_description=resource.description or "",
base_system_prompt=config.get(BASE_SYSTEM_PROMPT, ""),
),
llm,
)
entity_lines = []
for e in entity_set:
line = f"- {e.name}"
if e.description:
line += f": {e.description}"
entity_lines.append(line)
entity_summary = "\n".join(entity_lines)

return BaseUiPathStructuredTool(
name=tool_name,
description=(
"Query the following Data Fabric entities using natural language:\n"
f"{entity_summary}\n"
"Describe what data you need and the tool will translate it to SQL, "
"execute the query, and return a natural language answer."

def create_datafabric_tool(
*,
llm: BaseChatModel,
name: str,
description: str,
entities: Sequence[DataFabricEntityItem],
base_system_prompt: str,
) -> BaseTool:
"""Create a Data Fabric query tool for a coded agent.

Entity schemas are resolved lazily on the first invocation. Keep the tool
scoped to one agent execution so its cached schema and routing cannot cross
execution contexts.

Pass the same outer-agent system prompt used to construct the coded agent so
its instructions are also available to the inner SQL-generation graph.

Args:
llm: Language model for the inner SQL generation loop.
name: LangChain tool name exposed to the coded agent.
description: Description used by the outer agent to select the tool.
entities: Data Fabric entity references available to the tool.
base_system_prompt: Outer coded-agent system prompt forwarded to the
inner SQL-generation graph.
"""
entity_set = _normalize_entities(entities)
return _build_datafabric_tool(
_DataFabricToolConfig(
name=name,
description=description,
entities=entity_set,
resource_description=description,
base_system_prompt=base_system_prompt,
),
args_schema=DataFabricQueryInput,
coroutine=handler,
metadata={"tool_type": "datafabric_sql"},
llm,
)
8 changes: 8 additions & 0 deletions tests/agent/tools/test_datafabric_prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ def test_v1_prompt_documents_left_vs_inner_join_intent():
assert "INNER JOIN" in prompt


def test_v1_prompt_does_not_require_limit_for_scalar_aggregates():
prompt = build([_fake_entity(_fake_field())])

assert "Scalar aggregate queries do not require LIMIT" in prompt
assert "This applies to aggregates too" not in prompt
assert "SELECT COUNT(col) FROM table LIMIT 1" not in prompt


def test_relationship_subsection_absent_when_no_foreign_keys():
prompt = build([_fake_entity(_fake_field())])

Expand Down
Loading
Loading