Skip to content

Commit 1021af7

Browse files
feat: add Data Fabric tool support (#726)
1 parent 27d7988 commit 1021af7

File tree

14 files changed

+1142
-15
lines changed

14 files changed

+1142
-15
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requires-python = ">=3.11"
77
dependencies = [
88
"uipath>=2.10.29, <2.11.0",
99
"uipath-core>=0.5.2, <0.6.0",
10-
"uipath-platform>=0.1.10, <0.2.0",
10+
"uipath-platform>=0.1.12, <0.2.0",
1111
"uipath-runtime>=0.10.0, <0.11.0",
1212
"langgraph>=1.0.0, <2.0.0",
1313
"langchain-core>=1.2.11, <2.0.0",

src/uipath_langchain/agent/tools/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""Tool creation and management for LowCode agents."""
22

33
from .context_tool import create_context_tool
4+
from .datafabric_tool import (
5+
fetch_entity_schemas,
6+
)
47
from .escalation_tool import create_escalation_tool
58
from .extraction_tool import create_ixp_extraction_tool
69
from .integration_tool import create_integration_tool
@@ -22,6 +25,7 @@
2225
"create_escalation_tool",
2326
"create_ixp_extraction_tool",
2427
"create_ixp_escalation_tool",
28+
"fetch_entity_schemas",
2529
"UiPathToolNode",
2630
"ToolWrapperMixin",
2731
]

src/uipath_langchain/agent/tools/context_tool.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66

77
from jsonpath_ng import parse # type: ignore[import-untyped]
88
from langchain_core.documents import Document
9+
from langchain_core.language_models import BaseChatModel
910
from langchain_core.messages import ToolCall
1011
from langchain_core.tools import BaseTool, StructuredTool
1112
from pydantic import BaseModel, Field, create_model
1213
from uipath.agent.models.agent import (
1314
AgentContextResourceConfig,
1415
AgentContextRetrievalMode,
16+
AgentContextType,
17+
AgentMessageRole,
1518
AgentToolArgumentArgumentProperties,
1619
AgentToolArgumentProperties,
20+
LowCodeAgentDefinition,
1721
)
1822
from uipath.eval.mocks import mockable
1923
from uipath.platform import UiPath
@@ -130,16 +134,48 @@ def is_static_query(resource: AgentContextResourceConfig) -> bool:
130134
return resource.settings.query.variant.lower() == "static"
131135

132136

133-
def create_context_tool(resource: AgentContextResourceConfig) -> StructuredTool:
137+
def _extract_system_prompt(agent: LowCodeAgentDefinition | None) -> str:
138+
"""Extract system prompt from agent definition messages."""
139+
if agent is None:
140+
return ""
141+
return "\n\n".join(
142+
msg.content
143+
for msg in agent.messages
144+
if msg.role == AgentMessageRole.SYSTEM and msg.content
145+
)
146+
147+
148+
def create_context_tool(
149+
resource: AgentContextResourceConfig,
150+
llm: BaseChatModel | None = None,
151+
agent: LowCodeAgentDefinition | None = None,
152+
) -> StructuredTool | BaseTool | None:
153+
assert resource.context_type is not None
134154
tool_name = sanitize_tool_name(resource.name)
155+
156+
if resource.context_type == AgentContextType.DATA_FABRIC_ENTITY_SET:
157+
if llm is None:
158+
raise ValueError("Data Fabric entity set tools require an LLM instance")
159+
from .datafabric_tool import create_datafabric_query_tool
160+
from .datafabric_tool.datafabric_tool import BASE_SYSTEM_PROMPT
161+
162+
return create_datafabric_query_tool(
163+
resource,
164+
llm,
165+
tool_name=tool_name,
166+
agent_config={BASE_SYSTEM_PROMPT: _extract_system_prompt(agent)},
167+
)
168+
135169
assert resource.settings is not None
136170
retrieval_mode = resource.settings.retrieval_mode.lower()
171+
137172
if retrieval_mode == AgentContextRetrievalMode.DEEP_RAG.value.lower():
138173
return handle_deep_rag(tool_name, resource)
139-
elif retrieval_mode == AgentContextRetrievalMode.BATCH_TRANSFORM.value.lower():
174+
175+
if retrieval_mode == AgentContextRetrievalMode.BATCH_TRANSFORM.value.lower():
140176
return handle_batch_transform(tool_name, resource)
141-
else:
142-
return handle_semantic_search(tool_name, resource)
177+
178+
return handle_semantic_search(tool_name, resource)
143179

144180

145181
def handle_semantic_search(
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Data Fabric query tool with SQL syntax validation via sqlparse."""
2+
3+
from typing import Any
4+
5+
import sqlparse
6+
from langchain_core.callbacks import AsyncCallbackManagerForToolRun
7+
from langchain_core.runnables import RunnableConfig
8+
9+
from .base_uipath_structured_tool import BaseUiPathStructuredTool
10+
11+
12+
def _validate_sql(sql: str) -> str | None:
13+
"""Validate SQL syntax using sqlparse.
14+
15+
Returns:
16+
Error string if invalid, None if valid.
17+
"""
18+
parsed = sqlparse.parse(sql)
19+
if not parsed or not parsed[0].tokens:
20+
return "Empty or unparseable SQL query"
21+
return None
22+
23+
24+
class DataFabricQueryTool(BaseUiPathStructuredTool):
25+
"""Data Fabric query tool with SQL syntax validation.
26+
27+
Validates that the input SQL is parseable before delegating
28+
to the underlying coroutine. On validation failure, raises
29+
a ValueError so the caller can handle it as needed.
30+
"""
31+
32+
async def _arun(
33+
__obj_internal_self__,
34+
*args: Any,
35+
config: RunnableConfig,
36+
run_manager: AsyncCallbackManagerForToolRun | None = None,
37+
**kwargs: Any,
38+
) -> Any:
39+
sql_query = kwargs.get("sql_query") or (args[0] if args else "")
40+
error = _validate_sql(sql_query)
41+
if error:
42+
raise ValueError(error)
43+
return await super()._arun(
44+
*args, config=config, run_manager=run_manager, **kwargs
45+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Data Fabric tool module for entity-based SQL queries."""
2+
3+
from .datafabric_tool import (
4+
create_datafabric_query_tool,
5+
fetch_entity_schemas,
6+
)
7+
8+
__all__ = [
9+
"create_datafabric_query_tool",
10+
"fetch_entity_schemas",
11+
]
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"""Schema context building and formatting for Data Fabric entities.
2+
3+
Converts raw Entity SDK objects into structured Pydantic models (SQLContext),
4+
then formats them as text for system prompt injection.
5+
6+
Note: This module will go through refinements as we better understand
7+
the tool's performance characteristics and scoring in production.
8+
"""
9+
10+
import logging
11+
12+
from uipath.platform.entities import Entity
13+
14+
from .datafabric_prompts import SQL_CONSTRAINTS, SQL_EXPERT_SYSTEM_PROMPT
15+
from .models import (
16+
EntitySchema,
17+
EntitySQLContext,
18+
FieldSchema,
19+
QueryPattern,
20+
SQLContext,
21+
)
22+
23+
logger = logging.getLogger(__name__)
24+
25+
26+
def build_entity_context(entity: Entity) -> EntitySQLContext:
27+
"""Convert an Entity SDK object to schema + derived query patterns."""
28+
field_schemas: list[FieldSchema] = []
29+
numeric_field: str | None = None
30+
text_field: str | None = None
31+
32+
for field in entity.fields or []:
33+
if field.is_hidden_field or field.is_system_field:
34+
continue
35+
type_name = field.sql_type.name if field.sql_type else "unknown"
36+
fs = FieldSchema(
37+
name=field.name,
38+
display_name=field.display_name,
39+
type=type_name,
40+
description=field.description,
41+
is_foreign_key=field.is_foreign_key,
42+
is_required=field.is_required,
43+
is_unique=field.is_unique,
44+
nullable=not field.is_required,
45+
)
46+
field_schemas.append(fs)
47+
48+
if not numeric_field and fs.is_numeric:
49+
numeric_field = fs.name
50+
if not text_field and fs.is_text:
51+
text_field = fs.name
52+
53+
field_names = [f.name for f in field_schemas]
54+
table = entity.name
55+
56+
group_field = text_field or (field_names[0] if field_names else "Category")
57+
agg_field = numeric_field or (field_names[1] if len(field_names) > 1 else "Amount")
58+
filter_field = text_field or (field_names[0] if field_names else "Name")
59+
fields_sample = ", ".join(field_names[:5]) if field_names else "*"
60+
count_col = field_names[0] if field_names else "id"
61+
62+
query_patterns = [
63+
QueryPattern(
64+
intent="Show all",
65+
sql=f"SELECT {fields_sample} FROM {table} LIMIT 100",
66+
),
67+
QueryPattern(
68+
intent="Find by X",
69+
sql=f"SELECT {fields_sample} FROM {table} WHERE {filter_field} = 'value' LIMIT 100",
70+
),
71+
QueryPattern(
72+
intent="Top N by Y",
73+
sql=f"SELECT {fields_sample} FROM {table} ORDER BY {agg_field} DESC LIMIT N",
74+
),
75+
QueryPattern(
76+
intent="Count by X",
77+
sql=f"SELECT {group_field}, COUNT({count_col}) as count FROM {table} GROUP BY {group_field}",
78+
),
79+
QueryPattern(
80+
intent="Top N segments",
81+
sql=f"SELECT {group_field}, COUNT({count_col}) as count FROM {table} GROUP BY {group_field} ORDER BY count DESC LIMIT N",
82+
),
83+
QueryPattern(
84+
intent="Sum/Avg of Y",
85+
sql=f"SELECT SUM({agg_field}) as total FROM {table}",
86+
),
87+
]
88+
89+
schema = EntitySchema(
90+
id=entity.id,
91+
entity_name=entity.name,
92+
display_name=entity.display_name or entity.name,
93+
description=entity.description,
94+
record_count=entity.record_count,
95+
fields=field_schemas,
96+
)
97+
return EntitySQLContext(entity_schema=schema, query_patterns=query_patterns)
98+
99+
100+
def build_sql_context(
101+
entities: list[Entity],
102+
resource_description: str = "",
103+
base_system_prompt: str = "",
104+
) -> SQLContext:
105+
"""Build the full SQL context from entities, prompts, and constraints."""
106+
return SQLContext(
107+
base_system_prompt=base_system_prompt or None,
108+
resource_description=resource_description or None,
109+
sql_expert_system_prompt=SQL_EXPERT_SYSTEM_PROMPT,
110+
constraints=SQL_CONSTRAINTS,
111+
entity_contexts=[build_entity_context(e) for e in entities],
112+
)
113+
114+
115+
def format_sql_context(ctx: SQLContext) -> str:
116+
"""Format a SQLContext as text for system prompt injection."""
117+
lines: list[str] = []
118+
119+
if ctx.base_system_prompt:
120+
lines.append("## Agent Instructions")
121+
lines.append("")
122+
lines.append(ctx.base_system_prompt)
123+
lines.append("")
124+
125+
if ctx.sql_expert_system_prompt:
126+
lines.append("## SQL Query Generation Guidelines")
127+
lines.append("")
128+
lines.append(ctx.sql_expert_system_prompt)
129+
lines.append("")
130+
131+
if ctx.constraints:
132+
lines.append("## SQL Constraints")
133+
lines.append("")
134+
lines.append(ctx.constraints)
135+
lines.append("")
136+
137+
if ctx.resource_description:
138+
lines.append("## Entity set description")
139+
lines.append("")
140+
lines.append(ctx.resource_description)
141+
lines.append("")
142+
143+
lines.append("## All available Data Fabric Entities")
144+
lines.append("")
145+
146+
for entity_ctx in ctx.entity_contexts:
147+
entity = entity_ctx.entity_schema
148+
lines.append(
149+
f"### Entity: {entity.display_name} (SQL table: `{entity.entity_name}`)"
150+
)
151+
if entity.description:
152+
lines.append(f"_{entity.description}_")
153+
lines.append("")
154+
lines.append("| Field | Type |")
155+
lines.append("|-------|------|")
156+
157+
for field in entity.fields:
158+
lines.append(f"| {field.name} | {field.display_type} |")
159+
160+
lines.append("")
161+
162+
lines.append(f"**Query Patterns for {entity.entity_name}:**")
163+
lines.append("")
164+
lines.append("| User Intent | SQL Pattern |")
165+
lines.append("|-------------|-------------|")
166+
for p in entity_ctx.query_patterns:
167+
lines.append(f"| '{p.intent}' | `{p.sql}` |")
168+
lines.append("")
169+
170+
return "\n".join(lines)
171+
172+
173+
def build(
174+
entities: list[Entity],
175+
resource_description: str = "",
176+
base_system_prompt: str = "",
177+
) -> str:
178+
"""Build the full SQL prompt text for the inner sub-graph LLM.
179+
180+
Combines agent system prompt, resource description, SQL guidelines,
181+
constraints, entity schemas, and query patterns into a single prompt string.
182+
183+
Args:
184+
entities: List of Entity objects with schema information.
185+
resource_description: Optional description of the resource/entity set.
186+
base_system_prompt: Optional system prompt from the outer agent.
187+
188+
Returns:
189+
Formatted prompt string for the inner LLM system message.
190+
"""
191+
if not entities:
192+
return ""
193+
194+
ctx = build_sql_context(entities, resource_description, base_system_prompt)
195+
return format_sql_context(ctx)

0 commit comments

Comments
 (0)