Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c6e73eb
feat(datafabric): add fetch_ontology tool to DF inner SQL agent
sankalp-uipath Jun 16, 2026
b67e170
Merge branch 'main' into feat/datafabric-ontology-fetch-tool
sankalp-uipath Jun 16, 2026
da19087
feat(datafabric): resolve ontology from agent.json binding (name + fo…
sankalp-uipath Jun 17, 2026
4c22b8f
refactor(datafabric): fetch ontology via SDK EntitiesService.get_onto…
sankalp-uipath Jun 17, 2026
68f7cbf
feat(datafabric): support multiple ontologies per context (ontologySet)
sankalp-uipath Jun 17, 2026
ab77d65
Merge remote-tracking branch 'origin/main' into feat/datafabric-ontol…
sankalp-uipath Jun 17, 2026
40acdec
fix(datafabric): end loop on any successful SQL; drop env-var ontolog…
sankalp-uipath Jun 22, 2026
7a5bb69
test(datafabric): cover ontology fetch tool, subgraph routing, and fa…
sankalp-uipath Jun 22, 2026
04f79c5
fix(datafabric): return only terminal tool msgs on END; drop ToolMess…
sankalp-uipath Jun 22, 2026
0ed6210
perf(datafabric): fetch configured ontologies concurrently (asyncio.g…
sankalp-uipath Jun 22, 2026
e9c4cfb
feat(datafabric): resolve ontologies via ontology_refs
sankalp-uipath Jun 23, 2026
be5ef26
Merge branch 'main' into feat/datafabric-ontology-fetch-tool
sankalp-uipath Jun 23, 2026
1fd7a30
chore: consume uipath dev build (#1728) to unblock CI
sankalp-uipath Jun 23, 2026
a871a0a
chore: revert temp dev-build pin; fix datafabric test mypy
sankalp-uipath Jun 23, 2026
dfdd3d6
Merge branch 'main' into feat/datafabric-ontology-fetch-tool
sankalp-uipath Jun 23, 2026
a07adb9
Merge branch 'main' into feat/datafabric-ontology-fetch-tool
sankalp-uipath Jun 24, 2026
54db78f
refactor(datafabric): resolve ontologies from nested ontologySet
sankalp-uipath Jun 25, 2026
941f3ff
refactor(datafabric): gather ontologies from datafabricontology context
sankalp-uipath Jun 25, 2026
a975707
feat(datafabric): fetch ontology R2RML alongside OWL
sankalp-uipath Jun 24, 2026
2647337
feat(datafabric): fetch ontology R2RML alongside OWL
sankalp-uipath Jun 24, 2026
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
12 changes: 11 additions & 1 deletion src/uipath_langchain/agent/tools/context_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,17 +158,27 @@ def create_context_tool(
) -> StructuredTool | BaseTool | None:
tool_name = sanitize_tool_name(resource.name)

# An ontology context is not a standalone tool — it only grounds the Data
# Fabric entity tool, which gathers it via resolve_context_ontologies.
if resource.context_type == AgentContextType.DATA_FABRIC_ONTOLOGY:
return None

if resource.context_type == AgentContextType.DATA_FABRIC_ENTITY_SET:
if llm is None:
raise ValueError("Data Fabric entity set tools require an LLM instance")
from .datafabric_tool import create_datafabric_query_tool
from .datafabric_tool import (
create_datafabric_query_tool,
resolve_context_ontologies,
)
from .datafabric_tool.datafabric_tool import BASE_SYSTEM_PROMPT

ontologies = resolve_context_ontologies(agent.resources if agent else [])
return create_datafabric_query_tool(
resource,
llm,
tool_name=tool_name,
agent_config={BASE_SYSTEM_PROMPT: _extract_system_prompt(agent)},
ontologies=ontologies,
)

assert resource.settings is not None
Expand Down
2 changes: 2 additions & 0 deletions src/uipath_langchain/agent/tools/datafabric_tool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from .datafabric_tool import (
create_datafabric_query_tool,
resolve_context_ontologies,
)

__all__ = [
"create_datafabric_query_tool",
"resolve_context_ontologies",
]
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from ..datafabric_query_tool import DataFabricQueryTool
from . import datafabric_prompt_builder
from .models import DataFabricExecuteSqlInput
from .ontology_fetch_tool import create_ontology_fetch_tool

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -88,18 +89,29 @@ def __init__(
max_iterations: int = 25,
resource_description: str = "",
base_system_prompt: str = "",
ontologies: list[tuple[str, str | None]] | None = None,
) -> None:
self._max_iterations = max_iterations
self._execute_sql_tool = self._create_execute_sql_tool(
entities_service, entities
)
# Inner toolset: always execute_sql; optionally an LLM-decided
# fetch_ontology tool when one or more ontologies are configured.
inner_tools: list[BaseTool] = [self._execute_sql_tool]
if ontologies:
inner_tools.append(
create_ontology_fetch_tool(entities_service, ontologies)
)
self._tools_by_name: dict[str, BaseTool] = {
tool.name: tool for tool in inner_tools
}
self._system_message = SystemMessage(
content=datafabric_prompt_builder.build(
entities, resource_description, base_system_prompt
)
)
self._inner_llm = llm.model_copy(update={"disable_streaming": True}).bind_tools(
[self._execute_sql_tool]
inner_tools
)

# Build and compile the graph
Expand Down Expand Up @@ -130,36 +142,69 @@ async def tool_node(self, state: DataFabricSubgraphState) -> dict[str, Any]:
results = await asyncio.gather(
*[self._execute_tool_call(tc) for tc in last.tool_calls]
)
tool_messages = [msg for msg, _ in results]
all_succeeded = bool(results) and all(success for _, success in results)
# End as soon as ANY tool call is a terminal success (a row-returning
# execute_sql). `any` not `all`: a non-terminal tool (e.g. fetch_ontology)
# co-issued in the same turn must not prevent a successful SQL from ending
# the loop.
any_succeeded = any(success for _, success in results)
# When short-circuiting to END, return ONLY the terminal-success
# ToolMessages so the outer agent's result is the query rows — not a
# co-issued fetch_ontology's OWL. On a non-terminal turn keep all messages
# so the inner LLM can use them on its next pass.
if any_succeeded:
tool_messages = [msg for msg, success in results if success]
else:
tool_messages = [msg for msg, _ in results]
return {
"messages": tool_messages,
"iteration_count": state.iteration_count + len(last.tool_calls),
"last_tool_success": all_succeeded,
"last_tool_success": any_succeeded,
}

async def _execute_tool_call(self, tool_call: ToolCall) -> tuple[ToolMessage, bool]:
"""Execute a single tool call and report whether it succeeded."""
"""Execute a single tool call and report whether it is a terminal success.

Dispatches by tool name so the sub-graph can host more than one tool
(e.g. ``execute_sql`` and ``fetch_ontology``). Only a successful
``execute_sql`` that returned rows is terminal; every other tool
(including ontology fetch) reports ``False`` so the router loops back to
the inner LLM, letting it use the result to write or refine SQL.
"""
name = tool_call.get("name", "")
args = tool_call.get("args", {})
tool = self._tools_by_name.get(name)
if tool is None:
return (
ToolMessage(
content=f"Unknown tool: {name}",
tool_call_id=tool_call["id"],
name=name,
),
False,
)
try:
result = await self._execute_sql_tool.ainvoke(args)
result = await tool.ainvoke(args)
except ValueError as e:
result = {
"records": [],
"total_count": 0,
"error": str(e),
"sql_query": args.get("sql_query", ""),
}
if name == self._execute_sql_tool.name:
result = {
"records": [],
"total_count": 0,
"error": str(e),
"sql_query": args.get("sql_query", ""),
}
else:
result = f"Tool '{name}' failed: {e}"
succeeded = (
isinstance(result, dict)
name == self._execute_sql_tool.name
and isinstance(result, dict)
and not result.get("error")
and result.get("total_count", 0) > 0
)
return (
ToolMessage(
content=str(result),
tool_call_id=tool_call["id"],
name="execute_sql",
name=name,
),
succeeded,
)
Expand Down Expand Up @@ -226,6 +271,7 @@ def create(
max_iterations: int = 25,
resource_description: str = "",
base_system_prompt: str = "",
ontologies: list[tuple[str, str | None]] | None = None,
) -> CompiledStateGraph[Any]:
"""Create and return a compiled Data Fabric sub-graph."""
graph = DataFabricGraph(
Expand All @@ -235,5 +281,6 @@ def create(
max_iterations,
resource_description,
base_system_prompt,
ontologies,
)
return graph.compiled_graph
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,28 @@
BASE_SYSTEM_PROMPT = "base_system_prompt"


def resolve_context_ontologies(
resources: list[Any],
) -> list[tuple[str, str | None]]:
"""Gather ontologies from the agent's ontology context(s).

An ontology is configured in a dedicated ontology context (``contextType``
``datafabricontology``) whose ``ontologySet`` mirrors the entity context's
``entitySet`` — by convention at most one such context per agent. Its
ontologies ground the Data Fabric query tool; each carries its own
``folderId``, so it is fetched from its own folder.
"""
ontologies: list[tuple[str, str | None]] = []
for resource in resources:
if (
isinstance(resource, AgentContextResourceConfig)
and resource.is_datafabric_ontology
):
for item in resource.ontology_set or []:
ontologies.append((item.name, item.folder_key))
return ontologies


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

Expand All @@ -44,11 +66,13 @@ def __init__(
llm: BaseChatModel,
resource_description: str = "",
base_system_prompt: str = "",
ontologies: list[tuple[str, str | None]] | None = None,
) -> None:
self._entity_set = entity_set
self._llm = llm
self._resource_description = resource_description
self._base_system_prompt = base_system_prompt
self._ontologies = ontologies or []
self._compiled: CompiledStateGraph[Any] | None = None
self._init_lock = asyncio.Lock()

Expand Down Expand Up @@ -82,6 +106,7 @@ async def _ensure_datafabric_graph(self) -> CompiledStateGraph[Any]:
entities_service=resolution.entities_service,
resource_description=self._resource_description,
base_system_prompt=self._base_system_prompt,
ontologies=self._ontologies,
)
return self._compiled

Expand Down Expand Up @@ -144,6 +169,7 @@ def create_datafabric_query_tool(
llm: BaseChatModel,
tool_name: str = "query_datafabric",
agent_config: dict[str, str] | None = None,
ontologies: list[tuple[str, str | None]] | None = None,
) -> BaseTool:
"""Create the ``query_datafabric`` agentic tool.

Expand All @@ -153,17 +179,23 @@ def create_datafabric_query_tool(
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.
ontologies: ``(name, folder_key)`` pairs resolved from the context's
nested ``ontology_set`` (see ``resolve_context_ontologies``).
Empty/None → no fetch tool is added. Resolution comes only from the
agent definition (the binding), never from process env.
"""
config = agent_config or {}
entity_set = [
DataFabricEntityItem.model_validate(item.model_dump(by_alias=True))
for item in (resource.entity_set or [])
]
ontologies = ontologies or []
handler = DataFabricTextQueryHandler(
entity_set=entity_set,
llm=llm,
resource_description=resource.description or "",
base_system_prompt=config.get(BASE_SYSTEM_PROMPT, ""),
ontologies=ontologies,
)
entity_lines = []
for e in entity_set:
Expand Down
9 changes: 9 additions & 0 deletions src/uipath_langchain/agent/tools/datafabric_tool/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,12 @@ class DataFabricExecuteSqlInput(BaseModel):
"Use exact table and column names from the entity schemas."
),
)


class OntologyFetchInput(BaseModel):
"""Input schema for the ontology fetch tool — intentionally empty.

The ontology name is pinned from configuration, never supplied by the
LLM, so the model cannot redirect the fetch to an arbitrary resource. The
tool simply triggers a fetch of the configured ontology.
"""
Loading