Skip to content

Commit a35807b

Browse files
refactor(datafabric): inject ontology into system prompt, drop fetch_ontology tool
1 parent 7fab6d5 commit a35807b

9 files changed

Lines changed: 244 additions & 445 deletions

File tree

src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompt_builder.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,15 @@ def build_sql_context(
133133
)
134134

135135

136-
def format_sql_context(ctx: SQLContext, ontology_names: list[str] | None = None) -> str:
136+
def format_sql_context(ctx: SQLContext, ontology_text: str = "") -> str:
137137
"""Format a SQLContext as text for system prompt injection.
138138
139139
Args:
140140
ctx: The built SQL context (entities, prompts, constraints).
141-
ontology_names: Names of the ontologies attached to this agent. When
142-
non-empty, an "Available Ontology" section is added telling the LLM
143-
to call ``fetch_ontology`` before writing SQL — mirroring how the
144-
entity set is surfaced below.
141+
ontology_text: The fetched ontology OWL content. When non-empty, an
142+
"Available Ontology" section embeds it as the authoritative schema
143+
the LLM should ground its SQL on — mirroring how the entity set is
144+
surfaced below.
145145
"""
146146
lines: list[str] = []
147147

@@ -151,19 +151,16 @@ def format_sql_context(ctx: SQLContext, ontology_names: list[str] | None = None)
151151
lines.append(ctx.base_system_prompt)
152152
lines.append("")
153153

154-
if ontology_names:
155-
names = ", ".join(f"`{n}`" for n in ontology_names)
154+
if ontology_text:
156155
lines.append(
157156
"## Available Ontology (authoritative semantic schema)\n\n"
158-
f"This agent has a semantic ontology attached for these entities: "
159-
f"{names}. It is the authoritative source for the exact column names, "
160-
"value formats (date formats, codes, zero-padding), allowed values, "
161-
"and the relationships between entities — richer and more reliable "
162-
"than the field list below, which omits value formats and semantics.\n\n"
163-
"**Before writing any SQL, call the `fetch_ontology` tool once** to "
164-
"load it, then base your column names, filter values, and joins on "
165-
"what it says. The entity tables below are a quick reference only; "
166-
"the ontology is the source of truth when they disagree."
157+
"The ontology below is the authoritative source for the exact column "
158+
"names, value formats (date formats, codes, zero-padding), allowed "
159+
"values, and the relationships between entities — richer and more "
160+
"reliable than the field list further down, which omits value formats "
161+
"and semantics. Base your column names, filter values, and joins on "
162+
"it; when it and the entity tables disagree, the ontology wins.\n\n"
163+
f"{ontology_text}"
167164
)
168165
lines.append("")
169166

@@ -220,7 +217,7 @@ def build(
220217
resource_description: str = "",
221218
base_system_prompt: str = "",
222219
prompt_version: str | None = None,
223-
ontology_names: list[str] | None = None,
220+
ontology_text: str = "",
224221
) -> str:
225222
"""Build the full SQL prompt text for the inner sub-graph LLM.
226223
@@ -234,10 +231,9 @@ def build(
234231
base_system_prompt: Optional system prompt from the outer agent.
235232
prompt_version: Optional version key (e.g. ``"v0"``, ``"v1"``).
236233
Defaults to the registry's default.
237-
ontology_names: Names of ontologies attached to this agent. When
238-
non-empty, an "Available Ontology" section instructs the LLM to call
239-
``fetch_ontology`` before writing SQL. Pass these only when the
240-
fetch_ontology tool is actually bound.
234+
ontology_text: The fetched ontology OWL content. When non-empty, an
235+
"Available Ontology" section embeds it so the LLM grounds its SQL on
236+
the ontology. Empty string → no ontology section.
241237
242238
Returns:
243239
Formatted prompt string for the inner LLM system message.
@@ -251,4 +247,4 @@ def build(
251247
base_system_prompt,
252248
prompt_version=prompt_version,
253249
)
254-
return format_sql_context(ctx, ontology_names=ontology_names)
250+
return format_sql_context(ctx, ontology_text=ontology_text)

src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py

Lines changed: 21 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,11 @@
2929
from langgraph.graph.message import add_messages
3030
from langgraph.graph.state import CompiledStateGraph
3131
from pydantic import BaseModel
32-
from uipath.core.feature_flags import FeatureFlags
3332
from uipath.platform.entities import EntitiesService, Entity
3433

3534
from ..datafabric_query_tool import DataFabricQueryTool
3635
from . import datafabric_prompt_builder
37-
from .datafabric_tool import DATAFABRIC_ONTOLOGY_FF
3836
from .models import DataFabricExecuteSqlInput
39-
from .ontology_fetch_tool import create_ontology_fetch_tool
4037

4138
logger = logging.getLogger(__name__)
4239

@@ -91,40 +88,25 @@ def __init__(
9188
max_iterations: int = 25,
9289
resource_description: str = "",
9390
base_system_prompt: str = "",
94-
ontologies: list[tuple[str, str | None]] | None = None,
91+
ontology_text: str = "",
9592
) -> None:
9693
self._max_iterations = max_iterations
9794
self._execute_sql_tool = self._create_execute_sql_tool(
9895
entities_service, entities
9996
)
100-
# Inner toolset: always execute_sql; optionally an LLM-decided
101-
# fetch_ontology tool, added only when ontologies are configured AND the
102-
# DataFabricOntologyEnabled feature flag is on. The flag decides which
103-
# graph gets built — off → the original entities-only graph.
104-
inner_tools: list[BaseTool] = [self._execute_sql_tool]
105-
ontology_names: list[str] = []
106-
if ontologies and FeatureFlags.is_flag_enabled(
107-
DATAFABRIC_ONTOLOGY_FF, default=False
108-
):
109-
inner_tools.append(
110-
create_ontology_fetch_tool(entities_service, ontologies)
111-
)
112-
ontology_names = [name for name, _ in ontologies]
113-
self._tools_by_name: dict[str, BaseTool] = {
114-
tool.name: tool for tool in inner_tools
115-
}
116-
# Surface the ontology in the system prompt only when its fetch tool is
117-
# actually bound — otherwise the LLM is told to call a tool it lacks.
97+
# The ontology (when configured and enabled) is fetched deterministically
98+
# upstream and embedded directly in the system prompt — the inner agent
99+
# still has a single tool, execute_sql.
118100
self._system_message = SystemMessage(
119101
content=datafabric_prompt_builder.build(
120102
entities,
121103
resource_description,
122104
base_system_prompt,
123-
ontology_names=ontology_names,
105+
ontology_text=ontology_text,
124106
)
125107
)
126108
self._inner_llm = llm.model_copy(update={"disable_streaming": True}).bind_tools(
127-
inner_tools
109+
[self._execute_sql_tool]
128110
)
129111

130112
# Build and compile the graph
@@ -155,69 +137,36 @@ async def tool_node(self, state: DataFabricSubgraphState) -> dict[str, Any]:
155137
results = await asyncio.gather(
156138
*[self._execute_tool_call(tc) for tc in last.tool_calls]
157139
)
158-
# End as soon as ANY tool call is a terminal success (a row-returning
159-
# execute_sql). `any` not `all`: a non-terminal tool (e.g. fetch_ontology)
160-
# co-issued in the same turn must not prevent a successful SQL from ending
161-
# the loop.
162-
any_succeeded = any(success for _, success in results)
163-
# When short-circuiting to END, return ONLY the terminal-success
164-
# ToolMessages so the outer agent's result is the query rows — not a
165-
# co-issued fetch_ontology's OWL. On a non-terminal turn keep all messages
166-
# so the inner LLM can use them on its next pass.
167-
if any_succeeded:
168-
tool_messages = [msg for msg, success in results if success]
169-
else:
170-
tool_messages = [msg for msg, _ in results]
140+
tool_messages = [msg for msg, _ in results]
141+
all_succeeded = bool(results) and all(success for _, success in results)
171142
return {
172143
"messages": tool_messages,
173144
"iteration_count": state.iteration_count + len(last.tool_calls),
174-
"last_tool_success": any_succeeded,
145+
"last_tool_success": all_succeeded,
175146
}
176147

177148
async def _execute_tool_call(self, tool_call: ToolCall) -> tuple[ToolMessage, bool]:
178-
"""Execute a single tool call and report whether it is a terminal success.
179-
180-
Dispatches by tool name so the sub-graph can host more than one tool
181-
(e.g. ``execute_sql`` and ``fetch_ontology``). Only a successful
182-
``execute_sql`` that returned rows is terminal; every other tool
183-
(including ontology fetch) reports ``False`` so the router loops back to
184-
the inner LLM, letting it use the result to write or refine SQL.
185-
"""
186-
name = tool_call.get("name", "")
149+
"""Execute a single tool call and report whether it succeeded."""
187150
args = tool_call.get("args", {})
188-
tool = self._tools_by_name.get(name)
189-
if tool is None:
190-
return (
191-
ToolMessage(
192-
content=f"Unknown tool: {name}",
193-
tool_call_id=tool_call["id"],
194-
name=name,
195-
),
196-
False,
197-
)
198151
try:
199-
result = await tool.ainvoke(args)
152+
result = await self._execute_sql_tool.ainvoke(args)
200153
except ValueError as e:
201-
if name == self._execute_sql_tool.name:
202-
result = {
203-
"records": [],
204-
"total_count": 0,
205-
"error": str(e),
206-
"sql_query": args.get("sql_query", ""),
207-
}
208-
else:
209-
result = f"Tool '{name}' failed: {e}"
154+
result = {
155+
"records": [],
156+
"total_count": 0,
157+
"error": str(e),
158+
"sql_query": args.get("sql_query", ""),
159+
}
210160
succeeded = (
211-
name == self._execute_sql_tool.name
212-
and isinstance(result, dict)
161+
isinstance(result, dict)
213162
and not result.get("error")
214163
and result.get("total_count", 0) > 0
215164
)
216165
return (
217166
ToolMessage(
218167
content=str(result),
219168
tool_call_id=tool_call["id"],
220-
name=name,
169+
name="execute_sql",
221170
),
222171
succeeded,
223172
)
@@ -284,7 +233,7 @@ def create(
284233
max_iterations: int = 25,
285234
resource_description: str = "",
286235
base_system_prompt: str = "",
287-
ontologies: list[tuple[str, str | None]] | None = None,
236+
ontology_text: str = "",
288237
) -> CompiledStateGraph[Any]:
289238
"""Create and return a compiled Data Fabric sub-graph."""
290239
graph = DataFabricGraph(
@@ -294,6 +243,6 @@ def create(
294243
max_iterations,
295244
resource_description,
296245
base_system_prompt,
297-
ontologies,
246+
ontology_text,
298247
)
299248
return graph.compiled_graph

src/uipath_langchain/agent/tools/datafabric_tool/datafabric_tool.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,11 @@ async def _ensure_datafabric_graph(self) -> CompiledStateGraph[Any]:
9595
if self._compiled is not None:
9696
return self._compiled
9797

98+
from uipath.core.feature_flags import FeatureFlags
9899
from uipath.platform import UiPath
99100

100101
from .datafabric_subgraph import DataFabricGraph
102+
from .ontology_fetcher import fetch_ontology_text
101103

102104
sdk = UiPath()
103105
resolution = await sdk.entities.resolve_entity_set_async(self._entity_set)
@@ -106,13 +108,23 @@ async def _ensure_datafabric_graph(self) -> CompiledStateGraph[Any]:
106108
"No Data Fabric entity schemas could be fetched. "
107109
"Check entity identifiers and permissions."
108110
)
111+
# Deterministically fetch the ontology (when configured AND the flag
112+
# is on) and embed it in the inner system prompt — the LLM never has
113+
# to decide to fetch it.
114+
ontology_text = ""
115+
if self._ontologies and FeatureFlags.is_flag_enabled(
116+
DATAFABRIC_ONTOLOGY_FF, default=False
117+
):
118+
ontology_text = await fetch_ontology_text(
119+
resolution.entities_service, self._ontologies
120+
)
109121
self._compiled = DataFabricGraph.create(
110122
llm=self._llm,
111123
entities=resolution.entities,
112124
entities_service=resolution.entities_service,
113125
resource_description=self._resource_description,
114126
base_system_prompt=self._base_system_prompt,
115-
ontologies=self._ontologies,
127+
ontology_text=ontology_text,
116128
)
117129
return self._compiled
118130

src/uipath_langchain/agent/tools/datafabric_tool/models.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,3 @@ class DataFabricExecuteSqlInput(BaseModel):
9494
"Use exact table and column names from the entity schemas."
9595
),
9696
)
97-
98-
99-
class OntologyFetchInput(BaseModel):
100-
"""Input schema for the ontology fetch tool — intentionally empty.
101-
102-
The ontology name is pinned from configuration, never supplied by the
103-
LLM, so the model cannot redirect the fetch to an arbitrary resource. The
104-
tool simply triggers a fetch of the configured ontology.
105-
"""

0 commit comments

Comments
 (0)