From 7515c083ea71b15148fd2ac949e34b8cf2270cb3 Mon Sep 17 00:00:00 2001 From: milind-jain-uipath Date: Thu, 2 Jul 2026 02:42:55 +0530 Subject: [PATCH 1/9] feat(datafabric-tool): ground relationship-field joins in text-to-SQL prompt Surface entity relationship (foreign-key) fields to the Data Fabric SQL sub-graph so it can join related entities. A relationship field stores the related record's Id; the prompt now instructs the model to join on related.Id = parent. and project explicit related columns. - FieldSchema carries the related entity's SQL table, join key (Id), and reference field; foreign-key fields are tagged "fk"; add is_relationship. - build_entity_context populates these from the SDK field metadata (reference_entity, reference_field, field_display_type). - The rendered schema adds a per-entity "Relationships" subsection with the join expression, gated to related entities present in the set. - The v1 prompt adds a RELATIONSHIP FIELDS section: LEFT JOIN for optional relationships, INNER JOIN when the related record must exist or is filtered. - SQL_CONSTRAINTS permits LEFT JOIN only for relationship/foreign-key joins on Id; general joins remain INNER-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../datafabric_prompt_builder.py | 45 +++++++++++ .../datafabric_tool/datafabric_prompts.py | 17 ++-- .../agent/tools/datafabric_tool/models.py | 13 ++++ .../agent/tools/datafabric_tool/prompts/v1.py | 25 ++++++ .../tools/test_datafabric_prompt_builder.py | 78 +++++++++++++++++-- 5 files changed, 164 insertions(+), 14 deletions(-) diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompt_builder.py b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompt_builder.py index 8154caf5e..ffb8e6524 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompt_builder.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompt_builder.py @@ -36,6 +36,14 @@ def build_entity_context(entity: Entity) -> EntitySQLContext: if field.is_hidden_field or field.is_system_field: continue type_name = field.sql_type.name if field.sql_type else "unknown" + ref_entity_table: str | None = None + ref_field_name: str | None = None + if field.is_foreign_key or field.field_display_type == "Relationship": + ref_entity = getattr(field, "reference_entity", None) + ref_entity_table = getattr(ref_entity, "name", None) + ref_field = getattr(field, "reference_field", None) + ref_definition = getattr(ref_field, "definition", None) + ref_field_name = getattr(ref_definition, "name", None) fs = FieldSchema( name=field.name, display_name=field.display_name, @@ -45,6 +53,8 @@ def build_entity_context(entity: Entity) -> EntitySQLContext: is_required=field.is_required, is_unique=field.is_unique, nullable=not field.is_required, + ref_entity_table=ref_entity_table, + ref_field_name=ref_field_name, ) field_schemas.append(fs) @@ -164,6 +174,8 @@ def format_sql_context(ctx: SQLContext) -> str: lines.append("## All available Data Fabric Entities") lines.append("") + entity_tables = {ec.entity_schema.entity_name for ec in ctx.entity_contexts} + for entity_ctx in ctx.entity_contexts: entity = entity_ctx.entity_schema lines.append( @@ -180,6 +192,39 @@ def format_sql_context(ctx: SQLContext) -> str: lines.append("") + # Relationship fields store the related record's Id; spell out the join + # so the model doesn't compare the FK column to a human-readable value. + # Only surface relationships whose target entity is in this set (and thus + # queryable) — a dangling reference would produce an unusable join. + relationships = [ + field + for field in entity.fields + if field.is_relationship and field.ref_entity_table in entity_tables + ] + if relationships: + lines.append(f"**Relationships for {entity.entity_name}:**") + lines.append( + f"_Join on the related entity's Id. Use LEFT JOIN to keep all {entity.entity_name} " + "rows (relationship may be unset); INNER JOIN when the related record must exist or " + "you filter on it. Project the specific related column you need — not `*`._" + ) + lines.append("") + for field in relationships: + join = ( + f"LEFT JOIN {field.ref_entity_table} " + f"ON {field.ref_entity_table}.{field.ref_join_key} = {entity.entity_name}.{field.name}" + ) + repr_hint = ( + f", representative field `{field.ref_entity_table}.{field.ref_field_name}`" + if field.ref_field_name + else "" + ) + lines.append( + f"- `{entity.entity_name}.{field.name}` → `{field.ref_entity_table}` " + f"(`{join}`{repr_hint})" + ) + lines.append("") + lines.append(f"**Query Patterns for {entity.entity_name}:**") lines.append("") lines.append("| User Intent | SQL Pattern |") diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompts.py b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompts.py index 6c1226dad..4dc078881 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompts.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompts.py @@ -157,10 +157,15 @@ ### 2. Multi-Entity Joins (≤4 adapters) - INNER JOIN chains via entity model (up to 4 tables) +- Equi-joins only (ON left.col = right.col) +- LEFT JOIN is allowed ONLY for relationship (foreign-key) joins on the related + entity's Id (see "Relationship fields" guidance) — use it for optional + relationships to keep parent rows - Shared intermediates **Examples:** - SELECT o.id, c.name FROM Order o INNER JOIN Customer c ON o.customer_id = c.id +- SELECT o.id, a.Name FROM Order o LEFT JOIN Account a ON a.Id = o.account -- relationship join, keeps orders with no account - Fields spanning 3-4 adapters with proper INNER JOIN chains ### 3. Predicate Distribution & Pushdown @@ -253,7 +258,7 @@ - Common Table Expressions (WITH/CTE) - Window functions (ROW_NUMBER, RANK, PARTITION BY) - Self-joins -- LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN (only INNER JOIN supported) +- RIGHT JOIN, FULL OUTER JOIN (general joins must be INNER; LEFT JOIN only for relationship/foreign-key joins on Id) - CROSS JOIN **Examples:** @@ -275,7 +280,7 @@ ### 4. ADVANCED_JOINS - More than 4 tables in JOIN chain -- LEFT JOIN +- LEFT JOIN for non-relationship joins (LEFT JOIN is allowed ONLY to join a relationship/foreign-key field to its related entity on Id) - RIGHT JOIN - FULL OUTER JOIN - CROSS JOIN @@ -283,9 +288,9 @@ - Non-equi joins (theta joins) **Examples:** -- SELECT * FROM t1 RIGHT JOIN t2 -- ❌ -- SELECT * FROM t1, t2 -- ❌ (implicit CROSS JOIN) -- SELECT * FROM Employee e1 JOIN Employee e2 ON e1.manager_id = e2.id -- ❌ (self-join) +- SELECT c.id FROM t1 c RIGHT JOIN t2 d ON d.id = c.fk -- ❌ +- SELECT c.id FROM t1 c, t2 d -- ❌ (implicit CROSS JOIN) +- SELECT e1.id FROM Employee e1 JOIN Employee e2 ON e1.manager_id = e2.id -- ❌ (self-join) ### 5. UNSUPPORTED_FUNCTIONS - Date/time manipulation functions (DATE_ADD, DATE_SUB, DATEDIFF) @@ -336,7 +341,7 @@ 1. **ALWAYS use explicit column names** - Never use SELECT * 2. **Use COUNT(column_name)** - Never use COUNT(*) -3. **Only INNER JOIN** - No LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN, or CROSS JOIN +3. **INNER JOIN by default; LEFT JOIN only for relationships** - General joins must be INNER JOIN (equi-join). LEFT JOIN is permitted ONLY to join a relationship (foreign-key) field to its related entity on Id — use it for optional relationships to keep parent rows, INNER JOIN when the related row must exist. No RIGHT JOIN, FULL OUTER JOIN, CROSS JOIN, or self-joins 4. **Maximum 4 tables** - No more than 4 tables in a JOIN chain 5. **No subqueries** - No subqueries in any clause 6. **No CTEs** - No WITH clauses diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/models.py b/src/uipath_langchain/agent/tools/datafabric_tool/models.py index 09f4436ee..886cdeee0 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/models.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/models.py @@ -17,6 +17,12 @@ class FieldSchema(BaseModel): is_required: bool = False is_unique: bool = False nullable: bool = True + # For relationship (foreign-key) fields: the related entity's SQL table and + # the column to join on. The field itself stores the related record's Id, so + # the join is always ``related. = .``. + ref_entity_table: str | None = None + ref_join_key: str = "Id" + ref_field_name: str | None = None @property def display_type(self) -> str: @@ -24,10 +30,17 @@ def display_type(self) -> str: modifiers = [] if self.is_required: modifiers.append("required") + if self.is_foreign_key: + modifiers.append("fk") if modifiers: return f"{self.type}, {', '.join(modifiers)}" return self.type + @property + def is_relationship(self) -> bool: + """True when this field references another entity that can be joined.""" + return self.is_foreign_key and self.ref_entity_table is not None + @property def is_numeric(self) -> bool: return self.type.lower() in NUMERIC_TYPES diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/prompts/v1.py b/src/uipath_langchain/agent/tools/datafabric_tool/prompts/v1.py index 539f2df39..c16754849 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/prompts/v1.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/prompts/v1.py @@ -128,6 +128,31 @@ BAD: SELECT name FROM t1 WHERE id IN (SELECT fk FROM t2 WHERE x = 1) GOOD: SELECT t1.name FROM t1 INNER JOIN t2 ON t1.id = t2.fk WHERE t2.x = 1 +RELATIONSHIP FIELDS (foreign keys): +- A relationship field (marked ``fk`` in the schema) stores the RELATED \ +record's Id (a GUID) — not its name, label, or any other attribute. Comparing \ +such a column to a human-readable value (e.g. ``WHERE Account = 'Acme'``) will \ +never match. +- To filter on, or return, the related entity's attributes, JOIN the related \ +entity on its Id and project the SPECIFIC column(s) you need — never \ +``SELECT parent.*``: + SELECT parent., related. FROM parent LEFT JOIN related ON related.Id = parent. + then put your filter/selection on ``related``'s columns \ +(e.g. ``WHERE related.Name = 'Acme'``). The exact join and the related entity's \ +representative field are listed under "Relationships for " in the entity \ +schemas above. +- Choose the join type by intent: + - LEFT JOIN when you want every parent row, including those whose \ +relationship is unset (the related columns come back NULL). Prefer this when the \ +question is about the parent entity and only enriches it with related data. + - INNER JOIN when the related record must exist, or when you filter on the \ +related entity's columns (e.g. "orders whose account region is APAC"). +- If you only need the related record's identifier itself, select the \ +relationship field directly — no JOIN. +- Only equi-joins on the related entity's Id are supported \ +(``JOIN related ON related.Id = parent.``); the related entity's \ +schema is one of the entities listed above. + ERROR RECOVERY (structured error taxonomy): If ``execute_sql`` returns an ``error`` field, classify it and apply the \ targeted fix: diff --git a/tests/agent/tools/test_datafabric_prompt_builder.py b/tests/agent/tools/test_datafabric_prompt_builder.py index 47034b144..73ea27537 100644 --- a/tests/agent/tools/test_datafabric_prompt_builder.py +++ b/tests/agent/tools/test_datafabric_prompt_builder.py @@ -3,9 +3,8 @@ from uipath_langchain.agent.tools.datafabric_tool.datafabric_prompt_builder import build -def _fake_field(**overrides): - return SimpleNamespace( - name="status", +def _fake_field(name="status", **overrides): + defaults = dict( display_name="Status", sql_type=SimpleNamespace(name="varchar"), description="The canonical workflow status", @@ -19,20 +18,36 @@ def _fake_field(**overrides): is_unique=False, is_hidden_field=False, is_system_field=False, + field_display_type=None, + reference_entity=None, + reference_field=None, + ) + defaults.update(overrides) + return SimpleNamespace(name=name, **defaults) + + +def _fake_fk_field(name="account", ref_table="Account", ref_field="Id", **overrides): + return _fake_field( + name=name, + display_name=name.title(), + description=f"Reference to {ref_table}", + is_foreign_key=True, + field_display_type="Relationship", + reference_entity=SimpleNamespace(name=ref_table), + reference_field=SimpleNamespace(definition=SimpleNamespace(name=ref_field)), **overrides, ) -def _fake_entity(*fields, **overrides): - return SimpleNamespace( +def _fake_entity(*fields, name="Ticket", **overrides): + defaults = dict( id="entity-1", - name="Ticket", display_name="Ticket", description="Support tickets", record_count=10, - fields=list(fields), - **overrides, ) + defaults.update(overrides) + return SimpleNamespace(name=name, fields=list(fields), **defaults) def test_build_renders_ecp_aware_prompt_strategy(): @@ -57,3 +72,50 @@ def test_build_includes_domain_guidance_in_rendered_prompt(): assert "## Domain Guidance" in prompt assert "Use business-friendly ticket language." in prompt + + +def test_relationship_field_renders_join_when_target_entity_present(): + order = _fake_entity(_fake_field(), _fake_fk_field(ref_field="Name"), name="Order", display_name="Order") + account = _fake_entity(_fake_field(name="Name"), name="Account", display_name="Account") + + prompt = build([order, account]) + + # The FK column is tagged; the join is spelled out against the target Id as a + # LEFT JOIN (keeps parent rows), and the representative field is surfaced. + assert "| account | varchar, fk |" in prompt + assert "**Relationships for Order:**" in prompt + assert "LEFT JOIN Account ON Account.Id = Order.account" in prompt + assert "representative field `Account.Name`" in prompt + + +def test_v1_prompt_documents_left_vs_inner_join_intent(): + prompt = build([_fake_entity(_fake_field())]) + + # The relationship guidance explains when to use LEFT vs INNER. + assert "LEFT JOIN" in prompt + assert "INNER JOIN" in prompt + + +def test_relationship_subsection_absent_when_no_foreign_keys(): + prompt = build([_fake_entity(_fake_field())]) + + # The rendered per-entity header (distinct from the static prompt guidance + # that mentions "Relationships for
") must not appear. + assert "**Relationships for Ticket:**" not in prompt + + +def test_relationship_omitted_when_target_entity_not_in_set(): + # Order references Account, but Account is not part of the entity set, so a + # join would be unusable — the relationship line must be suppressed. + order = _fake_entity(_fake_field(), _fake_fk_field(), name="Order", display_name="Order") + + prompt = build([order]) + + assert "**Relationships for Order:**" not in prompt + assert "INNER JOIN Account" not in prompt + + +def test_v1_prompt_documents_relationship_fields(): + prompt = build([_fake_entity(_fake_field())]) + + assert "RELATIONSHIP FIELDS" in prompt From f69f837b32969931bb9cb58f5a920d26e417dac6 Mon Sep 17 00:00:00 2001 From: milind-jain-uipath Date: Fri, 3 Jul 2026 03:00:34 +0530 Subject: [PATCH 2/9] style(datafabric-tool): apply ruff format to prompt builder tests Co-Authored-By: Claude Opus 4.8 (1M context) --- .../agent/tools/test_datafabric_prompt_builder.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/agent/tools/test_datafabric_prompt_builder.py b/tests/agent/tools/test_datafabric_prompt_builder.py index 73ea27537..b131c35a1 100644 --- a/tests/agent/tools/test_datafabric_prompt_builder.py +++ b/tests/agent/tools/test_datafabric_prompt_builder.py @@ -75,8 +75,15 @@ def test_build_includes_domain_guidance_in_rendered_prompt(): def test_relationship_field_renders_join_when_target_entity_present(): - order = _fake_entity(_fake_field(), _fake_fk_field(ref_field="Name"), name="Order", display_name="Order") - account = _fake_entity(_fake_field(name="Name"), name="Account", display_name="Account") + order = _fake_entity( + _fake_field(), + _fake_fk_field(ref_field="Name"), + name="Order", + display_name="Order", + ) + account = _fake_entity( + _fake_field(name="Name"), name="Account", display_name="Account" + ) prompt = build([order, account]) @@ -107,7 +114,9 @@ def test_relationship_subsection_absent_when_no_foreign_keys(): def test_relationship_omitted_when_target_entity_not_in_set(): # Order references Account, but Account is not part of the entity set, so a # join would be unusable — the relationship line must be suppressed. - order = _fake_entity(_fake_field(), _fake_fk_field(), name="Order", display_name="Order") + order = _fake_entity( + _fake_field(), _fake_fk_field(), name="Order", display_name="Order" + ) prompt = build([order]) From df939523b36ec2c9f5f03e8de419fb3a36c31e07 Mon Sep 17 00:00:00 2001 From: milind-jain-uipath Date: Fri, 3 Jul 2026 03:14:03 +0530 Subject: [PATCH 3/9] docs(datafabric-tool): tighten relationship-join prompt guidance - Drop the redundant "never SELECT parent.*" aside (SQL_CONSTRAINTS already forbids SELECT *). - Tie join-type choice to the relationship field's required flag: a required field -> INNER JOIN is safe (related record always exists); optional -> LEFT JOIN to keep parent rows where it is unset. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../agent/tools/datafabric_tool/prompts/v1.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/prompts/v1.py b/src/uipath_langchain/agent/tools/datafabric_tool/prompts/v1.py index c16754849..4c37a8ae8 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/prompts/v1.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/prompts/v1.py @@ -134,19 +134,22 @@ such a column to a human-readable value (e.g. ``WHERE Account = 'Acme'``) will \ never match. - To filter on, or return, the related entity's attributes, JOIN the related \ -entity on its Id and project the SPECIFIC column(s) you need — never \ -``SELECT parent.*``: +entity on its Id and project the specific column(s) you need: SELECT parent., related. FROM parent LEFT JOIN related ON related.Id = parent. then put your filter/selection on ``related``'s columns \ (e.g. ``WHERE related.Name = 'Acme'``). The exact join and the related entity's \ representative field are listed under "Relationships for
" in the entity \ schemas above. -- Choose the join type by intent: - - LEFT JOIN when you want every parent row, including those whose \ -relationship is unset (the related columns come back NULL). Prefer this when the \ -question is about the parent entity and only enriches it with related data. - - INNER JOIN when the related record must exist, or when you filter on the \ -related entity's columns (e.g. "orders whose account region is APAC"). +- Choose the join type by intent (the schema tags a relationship field \ +``required`` or not): + - LEFT JOIN when the relationship is optional (not ``required``) and you want \ +every parent row, including those where it is unset (the related columns come \ +back NULL). Use this when the question is about the parent entity and only \ +enriches it with related data. + - INNER JOIN when the relationship is marked ``required`` (the related record \ +always exists, so no parent rows are dropped), when the related record must \ +otherwise exist, or when you filter on the related entity's columns \ +(e.g. "orders whose account region is APAC"). - If you only need the related record's identifier itself, select the \ relationship field directly — no JOIN. - Only equi-joins on the related entity's Id are supported \ From 312b81ce34d3fe4ec8cd13c0746f5c4946817202 Mon Sep 17 00:00:00 2001 From: milind-jain-uipath Date: Fri, 3 Jul 2026 03:27:18 +0530 Subject: [PATCH 4/9] fix(datafabric-tool): tag Relationship-typed fields consistently Detect a relationship as (is_foreign_key OR fieldDisplayType == "Relationship") and use that single condition both to set is_foreign_key on the FieldSchema and to extract the reference target, so a Relationship-typed field without the is_foreign_key flag is still tagged fk and rendered in the Relationships section. Read field_display_type via getattr for safety. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../datafabric_prompt_builder.py | 11 ++++++++-- .../tools/test_datafabric_prompt_builder.py | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompt_builder.py b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompt_builder.py index ffb8e6524..fcbaf016b 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompt_builder.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_prompt_builder.py @@ -36,9 +36,16 @@ def build_entity_context(entity: Entity) -> EntitySQLContext: if field.is_hidden_field or field.is_system_field: continue type_name = field.sql_type.name if field.sql_type else "unknown" + # A relationship is either a declared foreign key or a Relationship-typed + # field; use the same condition to tag it and to extract its target, so + # the two never disagree. + is_relationship = ( + field.is_foreign_key + or getattr(field, "field_display_type", None) == "Relationship" + ) ref_entity_table: str | None = None ref_field_name: str | None = None - if field.is_foreign_key or field.field_display_type == "Relationship": + if is_relationship: ref_entity = getattr(field, "reference_entity", None) ref_entity_table = getattr(ref_entity, "name", None) ref_field = getattr(field, "reference_field", None) @@ -49,7 +56,7 @@ def build_entity_context(entity: Entity) -> EntitySQLContext: display_name=field.display_name, type=type_name, description=field.description, - is_foreign_key=field.is_foreign_key, + is_foreign_key=is_relationship, is_required=field.is_required, is_unique=field.is_unique, nullable=not field.is_required, diff --git a/tests/agent/tools/test_datafabric_prompt_builder.py b/tests/agent/tools/test_datafabric_prompt_builder.py index b131c35a1..98dcae3fb 100644 --- a/tests/agent/tools/test_datafabric_prompt_builder.py +++ b/tests/agent/tools/test_datafabric_prompt_builder.py @@ -95,6 +95,27 @@ def test_relationship_field_renders_join_when_target_entity_present(): assert "representative field `Account.Name`" in prompt +def test_relationship_detected_via_display_type_without_is_foreign_key(): + # A Relationship-typed field with is_foreign_key unset must still be tagged + # fk and rendered in the Relationships section. + relationship_field = _fake_field( + name="account", + display_name="Account", + field_display_type="Relationship", + reference_entity=SimpleNamespace(name="Account"), + reference_field=SimpleNamespace(definition=SimpleNamespace(name="Name")), + ) + order = _fake_entity(relationship_field, name="Order", display_name="Order") + account = _fake_entity( + _fake_field(name="Name"), name="Account", display_name="Account" + ) + + prompt = build([order, account]) + + assert "| account | varchar, fk |" in prompt + assert "LEFT JOIN Account ON Account.Id = Order.account" in prompt + + def test_v1_prompt_documents_left_vs_inner_join_intent(): prompt = build([_fake_entity(_fake_field())]) From 6aff8e670393c0039c47021a7ae0d168f967dbe1 Mon Sep 17 00:00:00 2001 From: milind-jain-uipath Date: Mon, 13 Jul 2026 02:52:41 +0530 Subject: [PATCH 5/9] feat(datafabric): send x-uipath-source: LOW_CODE_AGENT on query/execute Pass source=LOW_CODE_AGENT from the DF tool's execute_sql call so FQS types relationship (FK) fields as their scalar id, letting the agent's rel = Other.Id joins validate and execute. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../datafabric_tool/datafabric_subgraph.py | 5 +++++ tests/agent/tools/test_datafabric_subgraph.py | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 tests/agent/tools/test_datafabric_subgraph.py diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py index 591227962..9d9c3cde6 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py @@ -37,6 +37,10 @@ logger = logging.getLogger(__name__) +# Sent as the x-uipath-source header on query/execute so FQS types relationship +# (FK) fields as their scalar id, letting `rel = Other.Id` joins validate. +LOW_CODE_AGENT = "LOW_CODE_AGENT" + class DataFabricSubgraphState(BaseModel): """State for the inner Data Fabric ReAct sub-graph.""" @@ -57,6 +61,7 @@ async def __call__(self, sql_query: str) -> dict[str, Any]: try: records = await self._entities.query_entity_records_async( sql_query=sql_query, + source=LOW_CODE_AGENT, ) return { "records": records, diff --git a/tests/agent/tools/test_datafabric_subgraph.py b/tests/agent/tools/test_datafabric_subgraph.py new file mode 100644 index 000000000..b15d1434f --- /dev/null +++ b/tests/agent/tools/test_datafabric_subgraph.py @@ -0,0 +1,21 @@ +from unittest.mock import AsyncMock, MagicMock + +from uipath_langchain.agent.tools.datafabric_tool.datafabric_subgraph import ( + LOW_CODE_AGENT, + QueryExecutor, +) + + +async def test_query_executor_sends_low_code_agent_source() -> None: + """The agent tags its query/execute call as LOW_CODE_AGENT so FQS types + relationship fields as their scalar id (enabling `rel = Other.Id` joins).""" + entities = MagicMock() + entities.query_entity_records_async = AsyncMock(return_value=[{"id": 1}]) + + result = await QueryExecutor(entities)("SELECT id FROM TaskEntity LIMIT 10") + + entities.query_entity_records_async.assert_awaited_once_with( + sql_query="SELECT id FROM TaskEntity LIMIT 10", + source=LOW_CODE_AGENT, + ) + assert result["records"] == [{"id": 1}] From 71108334f72c4fb8c0a3f28fdce45e2e45330fde Mon Sep 17 00:00:00 2001 From: milind-jain-uipath Date: Mon, 13 Jul 2026 18:24:53 +0530 Subject: [PATCH 6/9] chore(datafabric): require uipath-platform>=0.2.8 for the query/execute source header The optional `source` parameter on query_entity_records[_async] (sent as the `x-uipath-source` header) lands in uipath-platform 0.2.8, so raise the floor and relock so the DF tool's source=LOW_CODE_AGENT call resolves against a version that supports it. Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19139fcc8..c9976e974 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath>=2.13.7, <2.14.0", "uipath-core>=0.5.29, <0.6.0", - "uipath-platform>=0.2.5, <0.3.0", + "uipath-platform>=0.2.8, <0.3.0", "uipath-runtime>=0.12.1, <0.13.0", "uipath-llm-client>=1.16.2, <1.17.0", "langgraph>=1.1.8, <2.0.0", diff --git a/uv.lock b/uv.lock index e313c588e..7abe70a9d 100644 --- a/uv.lock +++ b/uv.lock @@ -4588,7 +4588,7 @@ requires-dist = [ { name = "uipath-langchain-client", extras = ["openai"], specifier = ">=1.16.1,<1.17.0" }, { name = "uipath-langchain-client", extras = ["vertexai"], marker = "extra == 'vertex'", specifier = ">=1.16.1,<1.17.0" }, { name = "uipath-llm-client", specifier = ">=1.16.2,<1.17.0" }, - { name = "uipath-platform", specifier = ">=0.2.5,<0.3.0" }, + { name = "uipath-platform", specifier = ">=0.2.8,<0.3.0" }, { name = "uipath-runtime", specifier = ">=0.12.1,<0.13.0" }, ] provides-extras = ["anthropic", "vertex", "bedrock", "fireworks", "all"] @@ -4672,7 +4672,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.5" +version = "0.2.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -4682,9 +4682,9 @@ dependencies = [ { name = "truststore" }, { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/d5/497e6d681f8f19a6621875956562cc6da8c514568fb89b773844899c8656/uipath_platform-0.2.5.tar.gz", hash = "sha256:8e9e02111ca9485ee7289d75613a62d882c95d34beccb6f8b59c76050882cf2d", size = 424528, upload-time = "2026-07-08T08:58:56.058Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/b0/653776469d09b85c5741003800567fd21823f725faae7809aa7d26d0d26d/uipath_platform-0.2.8.tar.gz", hash = "sha256:d58ed06da22e01ba88edb02d664098ab3e3cf98291093fecfd6aaa651fe02f26", size = 425767, upload-time = "2026-07-13T12:51:12.489Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/bf/11bb0b11777568c4e079d37f3ec003e816ca9be4e57f0455cafbccc67212/uipath_platform-0.2.5-py3-none-any.whl", hash = "sha256:ee14cd49e88ac1cdfc81e392025cc6f5c54b924fd20491d72ad6d55a5bc15db7", size = 279891, upload-time = "2026-07-08T08:58:53.575Z" }, + { url = "https://files.pythonhosted.org/packages/eb/54/f30cab355abde9e50fb8785feff2bcbd5ab9a9990333139c6eda751e2786/uipath_platform-0.2.8-py3-none-any.whl", hash = "sha256:a076fd0f0c34ba89167144a9a406a4abdadc2a5bbb8802fe89994f4c552491fa", size = 280664, upload-time = "2026-07-13T12:51:10.752Z" }, ] [[package]] From eed18ca6fcfcc8603fbcf602e2456e5e2154fc45 Mon Sep 17 00:00:00 2001 From: milind-jain-uipath Date: Mon, 13 Jul 2026 18:43:39 +0530 Subject: [PATCH 7/9] refactor(datafabric): thread query/execute source through tool creation instead of hardcoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DF tool is shared by the low-code runtime (uipath-agents-python) and coded agents, so it no longer self-identifies as LOW_CODE_AGENT. create_tools_from_resources accepts an optional `source` that flows through create_context_tool → create_datafabric_query_tool → DataFabricTextQueryHandler → DataFabricGraph → QueryExecutor → query_entity_records_async(source=…). The caller supplies the value (the low-code runtime passes LOW_CODE_AGENT); defaults to None (no header). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../agent/tools/context_tool.py | 2 ++ .../datafabric_tool/datafabric_subgraph.py | 19 +++++++------ .../tools/datafabric_tool/datafabric_tool.py | 5 ++++ .../agent/tools/tool_factory.py | 7 ++--- tests/agent/tools/test_datafabric_subgraph.py | 27 ++++++++++++++----- 5 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 7c1c1a508..492dad86d 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -155,6 +155,7 @@ def create_context_tool( resource: AgentContextResourceConfig, llm: BaseChatModel | None = None, agent: LowCodeAgentDefinition | None = None, + source: str | None = None, ) -> StructuredTool | BaseTool | None: tool_name = sanitize_tool_name(resource.name) @@ -193,6 +194,7 @@ def create_context_tool( llm, tool_name=tool_name, agent_config={BASE_SYSTEM_PROMPT: _extract_system_prompt(agent)}, + source=source, ) assert resource.settings is not None diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py index 9d9c3cde6..4d0f128ab 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py @@ -37,10 +37,6 @@ logger = logging.getLogger(__name__) -# Sent as the x-uipath-source header on query/execute so FQS types relationship -# (FK) fields as their scalar id, letting `rel = Other.Id` joins validate. -LOW_CODE_AGENT = "LOW_CODE_AGENT" - class DataFabricSubgraphState(BaseModel): """State for the inner Data Fabric ReAct sub-graph.""" @@ -53,15 +49,18 @@ class DataFabricSubgraphState(BaseModel): class QueryExecutor: """Executes SQL queries against Data Fabric.""" - def __init__(self, entities_service: EntitiesService) -> None: + def __init__( + self, entities_service: EntitiesService, source: str | None = None + ) -> None: self._entities = entities_service + self._source = source async def __call__(self, sql_query: str) -> dict[str, Any]: logger.debug("execute_sql called with SQL: %s", sql_query) try: records = await self._entities.query_entity_records_async( sql_query=sql_query, - source=LOW_CODE_AGENT, + source=self._source, ) return { "records": records, @@ -93,10 +92,11 @@ def __init__( max_iterations: int = 25, resource_description: str = "", base_system_prompt: str = "", + source: str | None = None, ) -> None: self._max_iterations = max_iterations self._execute_sql_tool = self._create_execute_sql_tool( - entities_service, entities + entities_service, entities, source ) self._system_message = SystemMessage( content=datafabric_prompt_builder.build( @@ -208,6 +208,7 @@ def _create_execute_sql_tool( self, entities_service: EntitiesService, entities: list[Entity], + source: str | None = None, ) -> BaseTool: """Create the inner ``execute_sql`` tool.""" entity_names = ", ".join(e.name for e in entities) @@ -219,7 +220,7 @@ def _create_execute_sql_tool( "tables and columns. Retry with a corrected query on errors." ), args_schema=DataFabricExecuteSqlInput, - coroutine=QueryExecutor(entities_service), + coroutine=QueryExecutor(entities_service, source), metadata={"tool_type": "datafabric_sql"}, ) @@ -231,6 +232,7 @@ def create( max_iterations: int = 25, resource_description: str = "", base_system_prompt: str = "", + source: str | None = None, ) -> CompiledStateGraph[Any]: """Create and return a compiled Data Fabric sub-graph.""" graph = DataFabricGraph( @@ -240,5 +242,6 @@ def create( max_iterations, resource_description, base_system_prompt, + source, ) return graph.compiled_graph diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_tool.py b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_tool.py index aab4e4cfc..639303aae 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_tool.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_tool.py @@ -44,11 +44,13 @@ def __init__( llm: BaseChatModel, resource_description: str = "", base_system_prompt: str = "", + source: str | None = None, ) -> None: self._entity_set = entity_set self._llm = llm self._resource_description = resource_description self._base_system_prompt = base_system_prompt + self._source = source self._compiled: CompiledStateGraph[Any] | None = None self._init_lock = asyncio.Lock() @@ -82,6 +84,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, + source=self._source, ) return self._compiled @@ -144,6 +147,7 @@ def create_datafabric_query_tool( llm: BaseChatModel, tool_name: str = "query_datafabric", agent_config: dict[str, str] | None = None, + source: str | None = None, ) -> BaseTool: """Create the ``query_datafabric`` agentic tool. @@ -164,6 +168,7 @@ def create_datafabric_query_tool( llm=llm, resource_description=resource.description or "", base_system_prompt=config.get(BASE_SYSTEM_PROMPT, ""), + source=source, ) entity_lines = [] for e in entity_set: diff --git a/src/uipath_langchain/agent/tools/tool_factory.py b/src/uipath_langchain/agent/tools/tool_factory.py index f6a7fb4b7..dae70b802 100644 --- a/src/uipath_langchain/agent/tools/tool_factory.py +++ b/src/uipath_langchain/agent/tools/tool_factory.py @@ -45,7 +45,7 @@ def _is_user_token() -> bool: async def create_tools_from_resources( - agent: LowCodeAgentDefinition, llm: BaseChatModel + agent: LowCodeAgentDefinition, llm: BaseChatModel, source: str | None = None ) -> list[BaseTool]: tools: list[BaseTool] = [] @@ -75,7 +75,7 @@ async def create_tools_from_resources( type(resource).__name__, ) tool = await _build_tool_for_resource( - resource, llm, agent=agent, run_as_me=run_as_me + resource, llm, agent=agent, run_as_me=run_as_me, source=source ) if tool is not None: if isinstance(tool, list): @@ -100,12 +100,13 @@ async def _build_tool_for_resource( llm: BaseChatModel, agent: LowCodeAgentDefinition | None = None, run_as_me: bool = False, + source: str | None = None, ) -> BaseTool | list[BaseTool] | None: if isinstance(resource, AgentProcessToolResourceConfig): return create_process_tool(resource, run_as_me=run_as_me) elif isinstance(resource, AgentContextResourceConfig): - return create_context_tool(resource, llm=llm, agent=agent) + return create_context_tool(resource, llm=llm, agent=agent, source=source) elif isinstance(resource, AgentEscalationResourceConfig): return create_escalation_tool(resource, agent=agent) diff --git a/tests/agent/tools/test_datafabric_subgraph.py b/tests/agent/tools/test_datafabric_subgraph.py index b15d1434f..038efc0cc 100644 --- a/tests/agent/tools/test_datafabric_subgraph.py +++ b/tests/agent/tools/test_datafabric_subgraph.py @@ -1,21 +1,36 @@ from unittest.mock import AsyncMock, MagicMock from uipath_langchain.agent.tools.datafabric_tool.datafabric_subgraph import ( - LOW_CODE_AGENT, QueryExecutor, ) -async def test_query_executor_sends_low_code_agent_source() -> None: - """The agent tags its query/execute call as LOW_CODE_AGENT so FQS types - relationship fields as their scalar id (enabling `rel = Other.Id` joins).""" +async def test_query_executor_forwards_source() -> None: + """QueryExecutor forwards the caller-provided source as the query/execute + header value — the low-code runtime passes LOW_CODE_AGENT, coded agents pass + their own — so it is not hardcoded in this shared library.""" entities = MagicMock() entities.query_entity_records_async = AsyncMock(return_value=[{"id": 1}]) - result = await QueryExecutor(entities)("SELECT id FROM TaskEntity LIMIT 10") + result = await QueryExecutor(entities, "LOW_CODE_AGENT")( + "SELECT id FROM TaskEntity LIMIT 10" + ) entities.query_entity_records_async.assert_awaited_once_with( sql_query="SELECT id FROM TaskEntity LIMIT 10", - source=LOW_CODE_AGENT, + source="LOW_CODE_AGENT", ) assert result["records"] == [{"id": 1}] + + +async def test_query_executor_source_defaults_to_none() -> None: + """With no source provided, none is sent (the SDK omits the header).""" + entities = MagicMock() + entities.query_entity_records_async = AsyncMock(return_value=[]) + + await QueryExecutor(entities)("SELECT id FROM TaskEntity LIMIT 10") + + entities.query_entity_records_async.assert_awaited_once_with( + sql_query="SELECT id FROM TaskEntity LIMIT 10", + source=None, + ) From c6b465dca65648074136aa309e1b33018b590469 Mon Sep 17 00:00:00 2001 From: milind-jain-uipath Date: Mon, 13 Jul 2026 19:00:08 +0530 Subject: [PATCH 8/9] chore: bump version to 0.14.6 for the query/execute source param 0.14.5 is already released without the `source` parameter, so the source threading in this PR must publish as a new version. Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c9976e974..2f0e9864a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.14.5" +version = "0.14.6" description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/uv.lock b/uv.lock index 7abe70a9d..786a00874 100644 --- a/uv.lock +++ b/uv.lock @@ -4498,7 +4498,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.14.5" +version = "0.14.6" source = { editable = "." } dependencies = [ { name = "a2a-sdk" }, From e5f09e66e45e14defd61274ce6552bf4adc5374e Mon Sep 17 00:00:00 2001 From: milind-jain-uipath Date: Wed, 15 Jul 2026 21:26:13 +0530 Subject: [PATCH 9/9] feat(datafabric): request relationships_as_scalar on Data Fabric queries Replaces the query/execute `source` threading with the Data Fabric tool always passing `relationships_as_scalar=True` to `query_entity_records_async`, so relationship (FK) fields are typed as their scalar id and the agent's SQL can join on `relationshipField = Other.Id` (SDK 0.2.11, FQS queryOptions contract). Removes the source param from the tool-factory -> context -> datafabric-tool -> subgraph chain; requires uipath-platform>=0.2.11. Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 2 +- .../agent/tools/context_tool.py | 2 -- .../datafabric_tool/datafabric_subgraph.py | 17 +++++------- .../tools/datafabric_tool/datafabric_tool.py | 5 ---- .../agent/tools/tool_factory.py | 7 +++-- tests/agent/tools/test_datafabric_subgraph.py | 26 ++++--------------- 6 files changed, 15 insertions(+), 44 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aae33b9bc..2177f2747 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath>=2.13.7, <2.14.0", "uipath-core>=0.5.29, <0.6.0", - "uipath-platform>=0.2.8, <0.3.0", + "uipath-platform>=0.2.11, <0.3.0", "uipath-runtime>=0.12.1, <0.13.0", "uipath-llm-client>=1.16.3, <1.17.0", "langgraph>=1.1.8, <2.0.0", diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py index 492dad86d..7c1c1a508 100644 --- a/src/uipath_langchain/agent/tools/context_tool.py +++ b/src/uipath_langchain/agent/tools/context_tool.py @@ -155,7 +155,6 @@ def create_context_tool( resource: AgentContextResourceConfig, llm: BaseChatModel | None = None, agent: LowCodeAgentDefinition | None = None, - source: str | None = None, ) -> StructuredTool | BaseTool | None: tool_name = sanitize_tool_name(resource.name) @@ -194,7 +193,6 @@ def create_context_tool( llm, tool_name=tool_name, agent_config={BASE_SYSTEM_PROMPT: _extract_system_prompt(agent)}, - source=source, ) assert resource.settings is not None diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py index 4d0f128ab..629f71f47 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_subgraph.py @@ -49,18 +49,17 @@ class DataFabricSubgraphState(BaseModel): class QueryExecutor: """Executes SQL queries against Data Fabric.""" - def __init__( - self, entities_service: EntitiesService, source: str | None = None - ) -> None: + def __init__(self, entities_service: EntitiesService) -> None: self._entities = entities_service - self._source = source async def __call__(self, sql_query: str) -> dict[str, Any]: logger.debug("execute_sql called with SQL: %s", sql_query) try: + # Relationship (FK) fields are typed as their scalar id so the SQL the + # agent writes can join on `relationshipField = Other.Id`. records = await self._entities.query_entity_records_async( sql_query=sql_query, - source=self._source, + relationships_as_scalar=True, ) return { "records": records, @@ -92,11 +91,10 @@ def __init__( max_iterations: int = 25, resource_description: str = "", base_system_prompt: str = "", - source: str | None = None, ) -> None: self._max_iterations = max_iterations self._execute_sql_tool = self._create_execute_sql_tool( - entities_service, entities, source + entities_service, entities ) self._system_message = SystemMessage( content=datafabric_prompt_builder.build( @@ -208,7 +206,6 @@ def _create_execute_sql_tool( self, entities_service: EntitiesService, entities: list[Entity], - source: str | None = None, ) -> BaseTool: """Create the inner ``execute_sql`` tool.""" entity_names = ", ".join(e.name for e in entities) @@ -220,7 +217,7 @@ def _create_execute_sql_tool( "tables and columns. Retry with a corrected query on errors." ), args_schema=DataFabricExecuteSqlInput, - coroutine=QueryExecutor(entities_service, source), + coroutine=QueryExecutor(entities_service), metadata={"tool_type": "datafabric_sql"}, ) @@ -232,7 +229,6 @@ def create( max_iterations: int = 25, resource_description: str = "", base_system_prompt: str = "", - source: str | None = None, ) -> CompiledStateGraph[Any]: """Create and return a compiled Data Fabric sub-graph.""" graph = DataFabricGraph( @@ -242,6 +238,5 @@ def create( max_iterations, resource_description, base_system_prompt, - source, ) return graph.compiled_graph diff --git a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_tool.py b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_tool.py index 639303aae..aab4e4cfc 100644 --- a/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_tool.py +++ b/src/uipath_langchain/agent/tools/datafabric_tool/datafabric_tool.py @@ -44,13 +44,11 @@ def __init__( llm: BaseChatModel, resource_description: str = "", base_system_prompt: str = "", - source: str | None = None, ) -> None: self._entity_set = entity_set self._llm = llm self._resource_description = resource_description self._base_system_prompt = base_system_prompt - self._source = source self._compiled: CompiledStateGraph[Any] | None = None self._init_lock = asyncio.Lock() @@ -84,7 +82,6 @@ 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, - source=self._source, ) return self._compiled @@ -147,7 +144,6 @@ def create_datafabric_query_tool( llm: BaseChatModel, tool_name: str = "query_datafabric", agent_config: dict[str, str] | None = None, - source: str | None = None, ) -> BaseTool: """Create the ``query_datafabric`` agentic tool. @@ -168,7 +164,6 @@ def create_datafabric_query_tool( llm=llm, resource_description=resource.description or "", base_system_prompt=config.get(BASE_SYSTEM_PROMPT, ""), - source=source, ) entity_lines = [] for e in entity_set: diff --git a/src/uipath_langchain/agent/tools/tool_factory.py b/src/uipath_langchain/agent/tools/tool_factory.py index dae70b802..f6a7fb4b7 100644 --- a/src/uipath_langchain/agent/tools/tool_factory.py +++ b/src/uipath_langchain/agent/tools/tool_factory.py @@ -45,7 +45,7 @@ def _is_user_token() -> bool: async def create_tools_from_resources( - agent: LowCodeAgentDefinition, llm: BaseChatModel, source: str | None = None + agent: LowCodeAgentDefinition, llm: BaseChatModel ) -> list[BaseTool]: tools: list[BaseTool] = [] @@ -75,7 +75,7 @@ async def create_tools_from_resources( type(resource).__name__, ) tool = await _build_tool_for_resource( - resource, llm, agent=agent, run_as_me=run_as_me, source=source + resource, llm, agent=agent, run_as_me=run_as_me ) if tool is not None: if isinstance(tool, list): @@ -100,13 +100,12 @@ async def _build_tool_for_resource( llm: BaseChatModel, agent: LowCodeAgentDefinition | None = None, run_as_me: bool = False, - source: str | None = None, ) -> BaseTool | list[BaseTool] | None: if isinstance(resource, AgentProcessToolResourceConfig): return create_process_tool(resource, run_as_me=run_as_me) elif isinstance(resource, AgentContextResourceConfig): - return create_context_tool(resource, llm=llm, agent=agent, source=source) + return create_context_tool(resource, llm=llm, agent=agent) elif isinstance(resource, AgentEscalationResourceConfig): return create_escalation_tool(resource, agent=agent) diff --git a/tests/agent/tools/test_datafabric_subgraph.py b/tests/agent/tools/test_datafabric_subgraph.py index 038efc0cc..d5e8f10e5 100644 --- a/tests/agent/tools/test_datafabric_subgraph.py +++ b/tests/agent/tools/test_datafabric_subgraph.py @@ -5,32 +5,16 @@ ) -async def test_query_executor_forwards_source() -> None: - """QueryExecutor forwards the caller-provided source as the query/execute - header value — the low-code runtime passes LOW_CODE_AGENT, coded agents pass - their own — so it is not hardcoded in this shared library.""" +async def test_query_executor_requests_relationships_as_scalar() -> None: + """The Data Fabric tool always requests scalar relationship typing so the SQL + it writes can join on ``relationshipField = Other.Id``.""" entities = MagicMock() entities.query_entity_records_async = AsyncMock(return_value=[{"id": 1}]) - result = await QueryExecutor(entities, "LOW_CODE_AGENT")( - "SELECT id FROM TaskEntity LIMIT 10" - ) + result = await QueryExecutor(entities)("SELECT id FROM TaskEntity LIMIT 10") entities.query_entity_records_async.assert_awaited_once_with( sql_query="SELECT id FROM TaskEntity LIMIT 10", - source="LOW_CODE_AGENT", + relationships_as_scalar=True, ) assert result["records"] == [{"id": 1}] - - -async def test_query_executor_source_defaults_to_none() -> None: - """With no source provided, none is sent (the SDK omits the header).""" - entities = MagicMock() - entities.query_entity_records_async = AsyncMock(return_value=[]) - - await QueryExecutor(entities)("SELECT id FROM TaskEntity LIMIT 10") - - entities.query_entity_records_async.assert_awaited_once_with( - sql_query="SELECT id FROM TaskEntity LIMIT 10", - source=None, - )