Skip to content

refactor(api): parse LLM tool parameters through a pydantic adapter - #44058

Open
gongfpp wants to merge 1 commit into
langgenius:mainfrom
gongfpp:refactor/llm-tool-parameters-pydantic
Open

gongfpp wants to merge 1 commit into
langgenius:mainfrom
gongfpp:refactor/llm-tool-parameters-pydantic

Conversation

@gongfpp

@gongfpp gongfpp commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Part of #33092 — the second of the two sites I claimed in my comment on the issue: the LLM-generated tool-parameter parse at the top of ToolEngine.agent_invoke (api/core/tools/tool_engine.py). The first one (provider configuration cache) is already open.

# before
with contextlib.suppress(Exception):
    tool_parameters = json.loads(tool_parameters)
if not isinstance(tool_parameters, dict):
    raise ValueError(f"tool_parameters should be a dict, but got a string: {tool_parameters}")

# after
with contextlib.suppress(ValidationError):
    tool_parameters = _tool_parameters_adapter.validate_json(tool_parameters)
if not isinstance(tool_parameters, dict):
    raise ValueError(f"tool_parameters should be a dict, but got a string: {tool_parameters}")

_tool_parameters_adapter: TypeAdapter[dict[str, Any]], so the success path is now typed dict[str, Any] instead of Any — the point of the issue ("since we need type info to avoid bugs"). The isinstance guard stays and is still load-bearing: when validation fails, the original string survives the suppress, and that is exactly what it catches.

I also narrowed the suppressed exception from Exception to ValidationError, since json.loads's contract is narrower than "anything can happen" and the blanket suppress was the reason this read as untyped.

The one behavioral delta, stated up front

I said on the issue I'd keep the ValueError message byte-identical, and for almost every input it is. One class differs, because the old code raised from the parsed value while the new code raises from the input — and this branch only runs when tool_parameters is a str:

input before after
[1, 2] (valid JSON, not an object) ... got a string: [1, 2] ... got a string: [1, 2] — same
42, 3.14, not json, {"broken": , "" identical identical
"quoted" (a JSON string) ... got a string: quoted ... got a string: "quoted"
null ... got a string: None ... got a string: null
true ... got a string: True ... got a string: true
["a"] ... got a string: ['a'] ... got a string: ["a"]

I read the new text as the fix rather than the regression — the message says "got a string" and the raw string is what we actually got; the old text mixed the parsed value's repr into a sentence about a string. It is also an LLM-misbehavior path: no caller constructs those payloads deliberately. But I promised identical output on the issue, so the call is yours: say the word and I'll restore the old interpolation by parsing twice.

Screenshots

Not applicable — backend.

Verification

  • Old-vs-new harness, both branches replicated verbatim, over 11 payloads ({"a": 1}, nested objects, {}, [1, 2], ["a"], "quoted", 42, 3.14, null, true, not json, {"broken": , ""): same return value in every case, and the ValueError text differences are exactly the table above.
  • Deeply nested arrays (depth 50 / 200 / 1000 / 5000): same outcome, so the narrowed suppress does not turn a swallowed RecursionError into a crash.
  • ruff format --check and ruff check with the repo's api/.ruff.toml / api/tests/unit_tests/.ruff.toml: no new findings. (I only had individual files available, so ruff's isort also reports I001 on the unmodified test file — a single-file-outside-the-tree artifact; my changes introduce no import reordering beyond adding the one pydantic line, placed as in api/core/provider_manager.py.)
  • pyrefly check --python-version 3.13: no diagnostics on any changed line.
  • Not run: make type-check and the test suite — no api environment in this sandbox. The two new tests are written against the existing fixtures in test_tool_engine.py and follow test_agent_invoke_success's patch stack exactly, but I could not execute them, so the last box stays unticked until CI confirms.

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've verified the change and added or updated tests where meaningful regression risk justifies coverage. — two new cases cover the branch that had none (success reaching _invoke with the parsed dict, and ValueError for non-dict payloads).
  • I've updated the documentation accordingly.
  • I ran make lint && make type-check (backend) and vp staged (frontend) to appease the lint gods

From DeepSeek Harness

@ghfind-review ghfind-review Bot added the review: medium ghfind author score; see https://ghfind.com label Oct 14, 2026
@github-actions github-actions Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Oct 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Pyrefly Diff

base → PR
--- /tmp/pyrefly_base.txt	2026-10-14 10:56:22.377070314 +0000
+++ /tmp/pyrefly_pr.txt	2026-10-14 10:56:13.933026477 +0000
@@ -1607,15 +1607,15 @@
 ERROR `str` is not assignable to dict key `strategy` with type `bool | list[Unknown]` [bad-assignment]
    --> tests/unit_tests/controllers/console/app/test_app_response_models.py:446:34
 ERROR Cannot index into `object` [bad-index]
-   --> tests/unit_tests/controllers/console/app/test_audio.py:133:12
-ERROR Cannot index into `object` [bad-index]
    --> tests/unit_tests/controllers/console/app/test_audio.py:134:12
 ERROR Cannot index into `object` [bad-index]
    --> tests/unit_tests/controllers/console/app/test_audio.py:135:12
 ERROR Cannot index into `object` [bad-index]
-   --> tests/unit_tests/controllers/console/app/test_audio.py:136:21
+   --> tests/unit_tests/controllers/console/app/test_audio.py:136:12
+ERROR Cannot index into `object` [bad-index]
+   --> tests/unit_tests/controllers/console/app/test_audio.py:137:21
 ERROR Cannot index into `object` [bad-index]
-   --> tests/unit_tests/controllers/console/app/test_audio.py:149:17
+   --> tests/unit_tests/controllers/console/app/test_audio.py:150:17
 ERROR Argument `Literal['550e8400-e29b-41d4-a716-446655440000']` is not assignable to parameter `message_id` with type `UUID` in function `controllers.console.app.message._get_message_detail` [bad-argument-type]
    --> tests/unit_tests/controllers/console/app/test_message_api.py:182:20
 ERROR Object of class `NoneType` has no attribute `id` [missing-attribute]
@@ -1726,8 +1726,6 @@
    --> tests/unit_tests/controllers/console/workspace/test_endpoint.py:561:46
 ERROR Argument `Literal['team']` is not assignable to parameter `plan` with type `CloudPlan | None` in function `services.workspace.contracts.EffectiveCreditPool.__init__` [bad-argument-type]
    --> tests/unit_tests/controllers/console/workspace/test_model_providers.py:258:18
-ERROR `in` is not supported between `Literal['knowledge-retrieval']` and `object` [not-iterable]
-   --> tests/unit_tests/controllers/console/workspace/test_snippets.py:213:12
 ERROR Missing argument `tenant_model` in function `protected_view` [missing-argument]
    --> tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py:257:40
 ERROR Missing argument `user_model` in function `protected_view` [missing-argument]
@@ -3266,6 +3264,8 @@
    --> tests/unit_tests/core/datasource/entities/test_datasource_entities.py:268:12
 ERROR Object of type `ToolInvokeMessage.BlobChunkMessage | ToolInvokeMessage.BlobMessage | ToolInvokeMessage.FileMessage | ToolInvokeMessage.JsonMessage | ToolInvokeMessage.LogMessage | ToolInvokeMessage.RetrieverResourceMessage | ToolInvokeMessage.TextMessage | ToolInvokeMessage.VariableMessage | None` has no attribute `json_object` [missing-attribute]
    --> tests/unit_tests/core/datasource/entities/test_datasource_entities.py:271:12
+ERROR `in` is not supported between `Literal['Single Page']` and `None` [not-iterable]
+    --> tests/unit_tests/core/datasource/test_website_crawl.py:1009:16
 ERROR `None` is not subscriptable [unsupported-operation]
    --> tests/unit_tests/core/entities/test_entities_provider_configuration.py:622:12
 ERROR Could not find name `Mock` [unknown-name]
@@ -3774,6 +3774,286 @@
    --> tests/unit_tests/core/repositories/test_sqlalchemy_workflow_execution_repository.py:301:33
 ERROR `None` is not assignable to attribute `_creator_user_role` with type `CreatorUserRole` [bad-assignment]
    --> tests/unit_tests/core/repositories/test_sqlalchemy_workflow_execution_repository.py:306:35
+ERROR Cannot index into `bool` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:42:16
+ERROR Cannot index into `float` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:42:16
+ERROR Cannot index into `int` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:42:16
+ERROR Cannot index into `list[Any]` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:42:16
+ERROR Cannot index into `str` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:42:16
+ERROR `None` is not subscriptable [unsupported-operation]
+  --> tests/unit_tests/core/schemas/test_resolver.py:42:16
+ERROR Cannot index into `bool` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:43:16
+ERROR Cannot index into `float` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:43:16
+ERROR Cannot index into `int` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:43:16
+ERROR Cannot index into `list[Any]` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:43:16
+ERROR Cannot index into `str` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:43:16
+ERROR `None` is not subscriptable [unsupported-operation]
+  --> tests/unit_tests/core/schemas/test_resolver.py:43:16
+ERROR Cannot index into `bool` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:44:31
+ERROR Cannot index into `float` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:44:31
+ERROR Cannot index into `int` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:44:31
+ERROR Cannot index into `list[Any]` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:44:31
+ERROR Cannot index into `str` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:44:31
+ERROR `None` is not subscriptable [unsupported-operation]
+  --> tests/unit_tests/core/schemas/test_resolver.py:44:31
+ERROR `not in` is not supported between `Literal['$id']` and `bool` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:48:16
+ERROR `not in` is not supported between `Literal['$id']` and `float` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:48:16
+ERROR `not in` is not supported between `Literal['$id']` and `int` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:48:16
+ERROR `not in` is not supported between `Literal['$id']` and `None` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:48:16
+ERROR `not in` is not supported between `Literal['$schema']` and `bool` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:49:16
+ERROR `not in` is not supported between `Literal['$schema']` and `float` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:49:16
+ERROR `not in` is not supported between `Literal['$schema']` and `int` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:49:16
+ERROR `not in` is not supported between `Literal['$schema']` and `None` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:49:16
+ERROR `not in` is not supported between `Literal['version']` and `bool` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:50:16
+ERROR `not in` is not supported between `Literal['version']` and `float` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:50:16
+ERROR `not in` is not supported between `Literal['version']` and `int` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:50:16
+ERROR `not in` is not supported between `Literal['version']` and `None` [not-iterable]
+  --> tests/unit_tests/core/schemas/test_resolver.py:50:16
+ERROR Cannot index into `bool` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:65:16
+ERROR Cannot index into `float` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:65:16
+ERROR Cannot index into `int` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:65:16
+ERROR Cannot index into `list[Any]` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:65:16
+ERROR Cannot index into `str` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:65:16
+ERROR `None` is not subscriptable [unsupported-operation]
+  --> tests/unit_tests/core/schemas/test_resolver.py:65:16
+ERROR Cannot index into `bool` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:66:30
+ERROR Cannot index into `float` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:66:30
+ERROR Cannot index into `int` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:66:30
+ERROR Cannot index into `list[Any]` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:66:30
+ERROR Cannot index into `str` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:66:30
+ERROR `None` is not subscriptable [unsupported-operation]
+  --> tests/unit_tests/core/schemas/test_resolver.py:66:30
+ERROR Cannot index into `bool` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:91:16
+ERROR Cannot index into `float` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:91:16
+ERROR Cannot index into `int` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:91:16
+ERROR Cannot index into `list[Any]` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:91:16
+ERROR Cannot index into `str` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:91:16
+ERROR `None` is not subscriptable [unsupported-operation]
+  --> tests/unit_tests/core/schemas/test_resolver.py:91:16
+ERROR Cannot index into `bool` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:92:16
+ERROR Cannot index into `float` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:92:16
+ERROR Cannot index into `int` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:92:16
+ERROR Cannot index into `list[Any]` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:92:16
+ERROR Cannot index into `str` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:92:16
+ERROR `None` is not subscriptable [unsupported-operation]
+  --> tests/unit_tests/core/schemas/test_resolver.py:92:16
+ERROR Cannot index into `bool` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:95:24
+ERROR Cannot index into `float` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:95:24
+ERROR Cannot index into `int` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:95:24
+ERROR Cannot index into `list[Any]` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:95:24
+ERROR Cannot index into `str` [bad-index]
+  --> tests/unit_tests/core/schemas/test_resolver.py:95:24
+ERROR `None` is not subscriptable [unsupported-operation]
+  --> tests/unit_tests/core/schemas/test_resolver.py:95:24
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:112:16
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:112:16
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:112:16
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:112:16
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:112:16
+ERROR `None` is not subscriptable [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:112:16
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:115:16
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:115:16
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:115:16
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:115:16
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:115:16
+ERROR `None` is not subscriptable [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:115:16
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:116:16
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:116:16
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:116:16
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:116:16
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:116:16
+ERROR `None` is not subscriptable [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:116:16
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:133:16
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:133:16
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:133:16
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:133:16
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:133:16
+ERROR `None` is not subscriptable [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:133:16
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:134:16
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:134:16
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:134:16
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:134:16
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:134:16
+ERROR `None` is not subscriptable [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:134:16
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:135:16
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:135:16
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:135:16
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:135:16
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:135:16
+ERROR `None` is not subscriptable [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:135:16
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:136:16
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:136:16
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:136:16
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:136:16
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:136:16
+ERROR `None` is not subscriptable [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:136:16
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:150:16
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:150:16
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:150:16
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:150:16
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:150:16
+ERROR `in` is not supported between `Literal['$circular_ref']` and `bool` [not-iterable]
+   --> tests/unit_tests/core/schemas/test_resolver.py:170:16
+ERROR `in` is not supported between `Literal['$circular_ref']` and `float` [not-iterable]
+   --> tests/unit_tests/core/schemas/test_resolver.py:170:16
+ERROR `in` is not supported between `Literal['$circular_ref']` and `int` [not-iterable]
+   --> tests/unit_tests/core/schemas/test_resolver.py:170:16
+ERROR `in` is not supported between `Literal['$circular_ref']` and `None` [not-iterable]
+   --> tests/unit_tests/core/schemas/test_resolver.py:170:16
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:182:16
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:182:16
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:182:16
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:182:16
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:182:16
+ERROR `None` is not subscriptable [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:182:16
+ERROR Argument `Exception` is not assignable to parameter `object` with type `bool | dict[str, Any] | float | int | list[Any] | str | None` in function `list.append` [bad-argument-type]
+   --> tests/unit_tests/core/schemas/test_resolver.py:233:32
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:273:16
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:273:16
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:273:16
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:273:16
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:273:16
+ERROR `None` is not subscriptable [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:273:16
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:274:27
+ERROR Cannot index into `float` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:274:27
+ERROR Cannot index into `int` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:274:27
+ERROR Cannot index into `list[Any]` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:274:27
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:274:27
+ERROR `None` is not subscriptable [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:274:27
+ERROR Cannot set item in `str` [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:593:13
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:594:23
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:667:13
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:667:13
+ERROR Cannot set item in `str` [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:667:13
+ERROR Cannot set item in `dict[str, str]` [unsupported-operation]
+   --> tests/unit_tests/core/schemas/test_resolver.py:667:51
+ERROR `dict[@_, @_]` is not assignable to dict value type `str` [bad-assignment]
+   --> tests/unit_tests/core/schemas/test_resolver.py:667:84
+ERROR Cannot index into `bool` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:668:23
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:668:23
+ERROR Cannot index into `str` [bad-index]
+   --> tests/unit_tests/core/schemas/test_resolver.py:668:23
 ERROR Argument `Literal[core.entities.provider_entities.ProviderQuotaType.TRIAL]` is not assignable to parameter `quota_type` with type `models.enums.ProviderQuotaType | SQLCoreOperations[models.enums.ProviderQuotaType | None] | None` in function `models.provider.Provider.__init__` [bad-argument-type]
    --> tests/unit_tests/core/test_provider_manager.py:114:20
 ERROR Argument `Literal['llm']` is not assignable to parameter `model_type` with type `ModelType | SQLCoreOperations[ModelType]` in function `models.provider.ProviderModelSetting.__init__` [bad-argument-type]
@@ -4209,27 +4489,27 @@
 ERROR Class member `CancelFailingStreamBackendClient.cancel_run` overrides a member in a parent class but is missing an `@override` decorator [missing-override-decorator]
    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:391:9
 ERROR Argument `dict[str, str]` is not assignable to parameter `node_data` with type `DifyAgentNodeData` in function `core.workflow.nodes.agent_v2.agent_node.DifyAgentNode._extract_variable_selector_to_variable_mapping` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:492:19
+   --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:477:19
 ERROR Class member `_UnavailableParticipantStore.load_existing_node_execution_scope` overrides a member in a parent class but is missing an `@override` decorator [missing-override-decorator]
-   --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:611:13
+   --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:596:13
 ERROR Cannot unpack tuple[WorkflowAgentSessionScope, str, SessionSnapshot | None, str | None, str | None] (of size 5) into 3 values [bad-unpacking]
-   --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:814:5
+   --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:765:5
 ERROR Class member `_FailingSessionStore.load_or_create_node_execution_session` overrides a member in a parent class but is missing an `@override` decorator [missing-override-decorator]
-   --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:880:13
+   --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:831:13
 ERROR Attribute `graph_execution` of class `GraphRuntimeState` is a read-only property and cannot be set [read-only]
-   --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:958:5
+   --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:929:5
 ERROR Object of class `NodeEventBase` has no attribute `node_run_result` [missing-attribute]
-    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1029:14
+    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1010:14
 ERROR Class member `RetryingBackendClient._events` overrides a member in a parent class but is missing an `@override` decorator [missing-override-decorator]
-    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1088:13
+    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1069:13
 ERROR `dict[str, int | str] | dict[str, str]` is not assignable to attribute `output_payload` with type `dict[str, object]` [bad-assignment]
-    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1089:35
+    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1070:35
 ERROR Object of class `NodeEventBase` has no attribute `node_run_result` [missing-attribute]
-    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1109:14
+    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1090:14
 ERROR Object of class `NodeEventBase` has no attribute `node_run_result` [missing-attribute]
-    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1155:14
+    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1136:14
 ERROR Argument `dict[str, dict[str, list[dict[str, str]] | Unknown]]` is not assignable to parameter `node_data` with type `DifyAgentNodeData` in function `core.workflow.nodes.agent_v2.agent_node.DifyAgentNode._extract_variable_selector_to_variable_mapping` [bad-argument-type]
-    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1178:19
+    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_agent_node.py:1159:19
 ERROR Object of class `Executable` has no attribute `compile` [missing-attribute]
    --> tests/unit_tests/core/workflow/nodes/agent_v2/test_binding_resolver.py:447:26
 ERROR Object of class `Executable` has no attribute `compile` [missing-attribute]
@@ -5213,17 +5493,9 @@
 ERROR Argument `None` is not assignable to parameter `value` with type `str` in function `libs.custom_inputs.time_duration` [bad-argument-type]
   --> tests/unit_tests/libs/test_custom_inputs.py:68:27
 ERROR `dict[str, str]` is not assignable to attribute `description` with type `str | None` [bad-assignment]
-  --> tests/unit_tests/libs/test_external_api.py:82:29
+  --> tests/unit_tests/libs/test_external_api.py:69:29
 ERROR Argument `_PassthroughFormatter` is not assignable to parameter `error_body_formatter` with type `ErrorBodyFormatter | None` in function `libs.external_api.ExternalApi.__init__` [bad-argument-type]
-   --> tests/unit_tests/libs/test_external_api.py:250:72
-ERROR Argument `Unknown | None` is not assignable to parameter `blueprint` with type `Blueprint` in function `flask.sansio.app.App.register_blueprint` [bad-argument-type]
-   --> tests/unit_tests/libs/test_external_api.py:364:28
-ERROR Cannot index into `str` [bad-index]
-   --> tests/unit_tests/libs/test_external_api.py:400:12
-ERROR Cannot index into `bool` [bad-index]
-   --> tests/unit_tests/libs/test_external_api.py:401:12
-ERROR Cannot index into `str` [bad-index]
-   --> tests/unit_tests/libs/test_external_api.py:401:12
+   --> tests/unit_tests/libs/test_external_api.py:237:72
 ERROR Class member `User.get_id` overrides a member in a parent class but is missing an `@override` decorator [missing-override-decorator]
   --> tests/unit_tests/libs/test_flask_utils.py:17:9
 ERROR `str` is not assignable to dict key `error` with type `None` [bad-assignment]

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 83.00% 83.00% -0.00%
Strict coverage 82.66% 82.66% -0.00%
Typed symbols 85,597 85,601 +4
Untyped symbols 17,602 17,604 +2
Modules 4373 4373 0

@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.79%. Comparing base (ac0f594) to head (a06c836).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main   #44058       +/-   ##
===========================================
+ Coverage   68.81%   89.79%   +20.98%     
===========================================
  Files        6103     6239      +136     
  Lines      368940   390955    +22015     
  Branches    77825    79291     +1466     
===========================================
+ Hits       253897   351077    +97180     
+ Misses     112233    33780    -78453     
- Partials     2810     6098     +3288     
Flag Coverage Δ
api 89.64% <100.00%> (+37.30%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gongfpp

gongfpp commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: I chose to keep the issue's original byte-for-byte error-message compatibility promise. Commit 59be1b1 restores the legacy Python-value interpolation for valid JSON non-object inputs, while leaving invalid or excessively nested JSON as the original raw string; the typed Pydantic dict-validation success path is unchanged. I could not run the API test suite locally because this workspace has no uv/pytest environment; syntax compilation and git diff --check passed, so please rely on the PR's CI for test validation.

Replaces the blanket `contextlib.suppress(Exception)` around `json.loads` with
`TypeAdapter(dict[str, Any]).validate_json`, so the success path is typed as
`dict[str, Any]` instead of `Any`.

The error message stays byte-identical to the pre-change text: the old handler
parsed the string in place before raising, so the message showed the parsed
value for valid JSON and the raw string otherwise. `_legacy_parameter_text`
reproduces that for the message only.
@gongfpp
gongfpp force-pushed the refactor/llm-tool-parameters-pydantic branch from 59be1b1 to a06c836 Compare November 3, 2026 03:02
@gongfpp

gongfpp commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and reimplemented, because the branch had drifted 598 commits and the file changed underneath it.

What moved on main

ToolEngine.agent_invoke no longer takes trace_manager; the tracing refactor (#44673) replaced it with trace_scope: TraceScope | None, and Union[...] became |. The old head could not be applied as-is.

A defect in the previous head, found while rebuilding

The last commit on the old branch tried to restore the legacy message by re-parsing, with a narrow contextlib.suppress(json.JSONDecodeError, RecursionError). By the time that code ran, tool_parameters had already been replaced by the parsed value, so the second json.loads received a list, None or a bool and raised TypeError, which the suppress tuple does not catch. Measured on pydantic 2.14.2 with the two implementations side by side:

input previous head main (and this branch now)
["a"] TypeError: the JSON object must be str, bytes or bytearray, not list ValueError: ... got a string: ['a']
null TypeError: ... not NoneType ValueError: ... got a string: None
true TypeError: ... not bool ValueError: ... got a string: True

So that commit would have replaced a ValueError with a TypeError for three of the seven shapes its own test claimed to cover.

What this branch does instead

raw_parameters = tool_parameters
with contextlib.suppress(ValidationError):
    tool_parameters = _tool_parameters_adapter.validate_json(tool_parameters)
if not isinstance(tool_parameters, dict):
    raise ValueError(
        f"tool_parameters should be a dict, but got a string: {_legacy_parameter_text(raw_parameters)}"
    )

_tool_parameters_adapter: TypeAdapter[dict[str, Any]] types the success path as dict[str, Any] instead of Any, which is the point of #33092, and the suppress is narrowed from Exception to ValidationError. The message text is produced by _legacy_parameter_text, which parses only for the message, so the guarantee from my comment on the issue holds exactly rather than approximately.

Measured old-vs-new on pydantic 2.14.2, nine inputs, side by side: identical exception type and identical message for all nine — [1, 2], "quoted", null, true, ["a"], not json, ``, {"a": 1}, `{"broken": `.

Tests

api/tests/unit_tests/core/tools/test_tool_engine.py: happy path for a JSON-string payload, plus a parametrized rejection test that asserts the exact message for seven inputs (array, JSON string, null, boolean, string array, invalid JSON, empty string). Both new tests pass trace_scope=None, which the old head did not do and would have failed on.

Validation gaps

  • I could not run pytest here: the workspace has no dify Python environment, and I did not install one. The unit tests are unverified locally; CI carries that.
  • What I did run: ruff check and ruff format --check on both files (clean), python -m compileall on both files, and the nine-input behavioral comparison above with real pydantic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review: medium ghfind author score; see https://ghfind.com size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant