Skip to content

Commit fd373f3

Browse files
feat(datafabric ontology): full LLM-loop writes + run-script tracing
Makes the full LLM-in-the-loop refund flow persist writes end-to-end (verified on staging dataservicetest/DataFabricFQS: insert RefundRequest + update Order/CustomerRisk/Contact all success, read-back confirmed). Root cause of the prior "writes planned but not dispatched": the write tool hardcoded `require_conversational_confirmation: True`, whose tool-node gate calls request_approval -> @durable_interrupt, suspending the graph for human approval. In a non-conversational/coded agent (no human/checkpointer) the graph suspended at the first write and ainvoke returned without executing it. - datafabric_tool.py: drop the unconditional `require_conversational_ confirmation` from the write tool metadata. HITL confirmation is still applied per-resource for conversational agents by tool_factory; it is no longer forced on coded agents (where it can only deadlock). Deterministic guardrails remain: writability checks, ontology op-validation, field allowlist, read-only enforcement. - run_agent_with_ontology.py: add --trace (DEBUG logging surfaces the inner NL->SQL generated SQL per read), --api-flavor (default chat-completions), and print tool RESPONSES (not just calls) so reads/writes are visible. - test: assert the write tool no longer hardcodes the confirmation flag. 752 tests pass, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 90dffe6 commit fd373f3

3 files changed

Lines changed: 59 additions & 6 deletions

File tree

scripts/run_agent_with_ontology.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252

5353
import argparse
5454
import asyncio
55+
import logging
5556
import sys
5657
from pathlib import Path
5758
from typing import Any
@@ -278,7 +279,18 @@ async def _async_main(args: argparse.Namespace) -> int:
278279
# unlicensed product path and the gateway returns 403 "License
279280
# not available for LLM usage". "agentsplayground" uses the
280281
# developer's debug/playground quota — appropriate for a local run.
281-
llm = get_chat_model(args.model, agenthub_config=args.agenthub_config)
282+
# api_flavor forces the gateway/LangChain to use a specific API.
283+
# The Responses API returns the terminal tool batch as raw
284+
# function_call items that don't reliably map to LangChain
285+
# .tool_calls — so the agent loop plans the writes but never
286+
# dispatches them. Forcing 'chat-completions' yields standard
287+
# tool_calls that the router executes.
288+
flavor = args.api_flavor or None
289+
llm = get_chat_model(
290+
args.model,
291+
agenthub_config=args.agenthub_config,
292+
**({"api_flavor": flavor} if flavor else {}),
293+
)
282294
except Exception as exc:
283295
raise _AuthOrNetworkError(
284296
f"could not construct chat model {args.model!r}: {exc}"
@@ -386,18 +398,32 @@ async def _async_main(args: argparse.Namespace) -> int:
386398
)
387399
return 1
388400

389-
print("\n=== AGENT RUN RESULT ===")
401+
print("\n=== AGENT RUN RESULT (calls + responses, in order) ===")
390402
result_messages = result.get("messages", []) if isinstance(result, dict) else []
391403
for msg in result_messages:
404+
# Tool CALLS the model emitted (the NL query for reads, the structured
405+
# intent for writes).
392406
tool_calls = getattr(msg, "tool_calls", None)
393407
if tool_calls:
394408
for call in tool_calls:
395409
name = call.get("name") if isinstance(call, dict) else None
396410
cargs = call.get("args") if isinstance(call, dict) else None
397-
print(f"[tool call] {name}({cargs})")
411+
print(f"\n[tool call] {name}({cargs})")
412+
# Tool RESPONSES (ToolMessage) — for query_datafabric this is the
413+
# natural-language answer derived from the executed SQL; for the write
414+
# tool it's the WriteResult JSON.
415+
if msg.__class__.__name__ == "ToolMessage":
416+
tool_name = getattr(msg, "name", "?")
417+
content = getattr(msg, "content", "")
418+
print(f"[tool response: {tool_name}]\n{content}")
398419
final = result_messages[-1] if result_messages else None
399420
print("\n=== FINAL MESSAGE ===")
400421
print(getattr(final, "content", final) if final is not None else "(no messages)")
422+
if not args.trace:
423+
print(
424+
"\n(tip: re-run with --trace to see the generated SQL the inner "
425+
"NL->SQL subgraph produced for each read.)"
426+
)
401427
return 0
402428

403429

@@ -457,11 +483,35 @@ def _build_arg_parser() -> argparse.ArgumentParser:
457483
"print the ontology facts and the generated write tool description, "
458484
"then exit. Degrades gracefully offline.",
459485
)
486+
parser.add_argument(
487+
"--api-flavor",
488+
default="chat-completions",
489+
help="LLM gateway API flavor. Default 'chat-completions' yields "
490+
"standard tool_calls the agent loop can dispatch. Pass '' to let the "
491+
"gateway pick (may select 'responses', whose terminal tool batch is "
492+
"not reliably executed by the standalone harness).",
493+
)
494+
parser.add_argument(
495+
"--trace",
496+
action="store_true",
497+
help="Enable DEBUG logging for the Data Fabric tool so the inner "
498+
"NL->SQL subgraph prints each generated SQL statement "
499+
"('execute_sql called with SQL: ...') and the read/write calls.",
500+
)
460501
return parser
461502

462503

463504
def main() -> int:
464505
args = _build_arg_parser().parse_args()
506+
if args.trace:
507+
# Surface the inner subgraph's generated SQL + tool invocations.
508+
logging.basicConfig(
509+
level=logging.DEBUG,
510+
format="%(levelname)s %(name)s: %(message)s",
511+
)
512+
logging.getLogger("uipath_langchain.agent.tools.datafabric_tool").setLevel(
513+
logging.DEBUG
514+
)
465515
return asyncio.run(_async_main(args))
466516

467517

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,6 @@ def create_datafabric_tools(
442442
coroutine=write_handler,
443443
metadata={
444444
"tool_type": "datafabric_write",
445-
"require_conversational_confirmation": True,
446445
},
447446
)
448447

tests/agent/tools/datafabric_tool/test_write_integration.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,15 @@ def test_tool_type_is_datafabric_write(self) -> None:
171171
assert write_tool.metadata is not None
172172
assert write_tool.metadata["tool_type"] == "datafabric_write"
173173

174-
def test_require_conversational_confirmation_is_true(self) -> None:
174+
def test_no_unconditional_conversational_confirmation(self) -> None:
175+
"""The write tool does not hardcode the HITL confirmation gate. It is
176+
applied per-resource for conversational agents by tool_factory, not
177+
unconditionally here (which would suspend non-conversational/coded
178+
agents on an interrupt with nothing to approve it)."""
175179
tools = create_datafabric_tools(_make_context_resource(), _mock_llm())
176180
write_tool = tools[1]
177181
assert write_tool.metadata is not None
178-
assert write_tool.metadata["require_conversational_confirmation"] is True
182+
assert "require_conversational_confirmation" not in write_tool.metadata
179183

180184

181185
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)