echotools 2.3.6__tar.gz → 2.3.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {echotools-2.3.6 → echotools-2.3.7}/PKG-INFO +1 -1
  2. {echotools-2.3.6 → echotools-2.3.7}/pyproject.toml +1 -1
  3. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/__init__.py +4 -0
  4. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/parsers/xml_parser.py +4 -3
  5. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/protocols/entml.py +2 -1
  6. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/shared/__init__.py +4 -0
  7. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/shared/normalization.py +88 -1
  8. {echotools-2.3.6 → echotools-2.3.7}/src/echotools.egg-info/PKG-INFO +1 -1
  9. {echotools-2.3.6 → echotools-2.3.7}/tests/test_fncall.py +31 -0
  10. {echotools-2.3.6 → echotools-2.3.7}/README.md +0 -0
  11. {echotools-2.3.6 → echotools-2.3.7}/setup.cfg +0 -0
  12. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/__init__.py +0 -0
  13. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/_version.py +0 -0
  14. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/cache/__init__.py +0 -0
  15. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/cache/list_cache.py +0 -0
  16. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/cache/memory_cache.py +0 -0
  17. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/config/__init__.py +0 -0
  18. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/config/base.py +0 -0
  19. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/config/center.py +0 -0
  20. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/config/loader.py +0 -0
  21. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/config/merge.py +0 -0
  22. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/dispatch/__init__.py +0 -0
  23. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/dispatch/candidate.py +0 -0
  24. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/dispatch/dispatcher.py +0 -0
  25. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/dispatch/proxy_selector.py +0 -0
  26. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/dispatch/selector.py +0 -0
  27. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/dispatch/usage.py +0 -0
  28. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/errors/__init__.py +0 -0
  29. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/errors/base.py +0 -0
  30. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/errors/classify.py +0 -0
  31. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/errors/common.py +0 -0
  32. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/errors/http.py +0 -0
  33. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/events/__init__.py +0 -0
  34. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/events/bus.py +0 -0
  35. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/events/event.py +0 -0
  36. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/files/__init__.py +0 -0
  37. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/files/file_util.py +0 -0
  38. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/parsers/__init__.py +0 -0
  39. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/parsers/stream.py +0 -0
  40. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/prompt/__init__.py +0 -0
  41. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/prompt/history.py +0 -0
  42. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/prompt/inject.py +0 -0
  43. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/prompt/templates.py +0 -0
  44. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/protocols/__init__.py +0 -0
  45. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/registry.py +0 -0
  46. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/shared/coercion.py +0 -0
  47. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/shared/loop_detect.py +0 -0
  48. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/fncall/shared/xml_helpers.py +0 -0
  49. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/ids/__init__.py +0 -0
  50. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/ids/generator.py +0 -0
  51. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/io/__init__.py +0 -0
  52. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/io/io_utils.py +0 -0
  53. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/io/printstream.py +0 -0
  54. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/keys.py +0 -0
  55. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/lifecycle/__init__.py +0 -0
  56. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/lifecycle/manager.py +0 -0
  57. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/lifecycle/updater.py +0 -0
  58. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/logger/__init__.py +0 -0
  59. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/logger/manager.py +0 -0
  60. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/network/__init__.py +0 -0
  61. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/network/http_utils.py +0 -0
  62. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/plugin/__init__.py +0 -0
  63. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/plugin/base.py +0 -0
  64. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/plugin/discovery.py +0 -0
  65. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/plugin/registry.py +0 -0
  66. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/process/__init__.py +0 -0
  67. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/process/port.py +0 -0
  68. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/protocol/__init__.py +0 -0
  69. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/protocol/base.py +0 -0
  70. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/proxy/__init__.py +0 -0
  71. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/proxy/manager.py +0 -0
  72. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/py.typed +0 -0
  73. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/retry/__init__.py +0 -0
  74. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/retry/retry.py +0 -0
  75. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/runtime/__init__.py +0 -0
  76. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/runtime/collector.py +0 -0
  77. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/scheduler/__init__.py +0 -0
  78. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/scheduler/scheduler.py +0 -0
  79. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/sdk/__init__.py +0 -0
  80. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/sdk/facade.py +0 -0
  81. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/spinner/__init__.py +0 -0
  82. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/spinner/spinner.py +0 -0
  83. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/terminal/__init__.py +0 -0
  84. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/terminal/conpty.py +0 -0
  85. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/terminal/local.py +0 -0
  86. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/terminal/sanitize.py +0 -0
  87. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/terminal/session.py +0 -0
  88. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/terminal/ssh.py +0 -0
  89. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/terminal/tmux.py +0 -0
  90. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/tracing/__init__.py +0 -0
  91. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/tracing/context.py +0 -0
  92. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/tracing/span.py +0 -0
  93. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/tracing/tracer.py +0 -0
  94. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/translate.py +0 -0
  95. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/watcher/__init__.py +0 -0
  96. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/watcher/file_watcher.py +0 -0
  97. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/__init__.py +0 -0
  98. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/application.py +0 -0
  99. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/broker.py +0 -0
  100. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/app.js +0 -0
  101. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/file_zone/file-zone.js +0 -0
  102. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/fonts/inter-400.woff2 +0 -0
  103. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/fonts/inter-500.woff2 +0 -0
  104. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/fonts/inter-600.woff2 +0 -0
  105. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/fonts/tailwind.min.css +0 -0
  106. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/input-box.css +0 -0
  107. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/input-box.js +0 -0
  108. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/motion_kit/motion-kit.js +0 -0
  109. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/send_button/send-button.js +0 -0
  110. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/sortable_list/sortable-list.css +0 -0
  111. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/sortable_list/sortable-list.js +0 -0
  112. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/style.css +0 -0
  113. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/text_input/text-input.js +0 -0
  114. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/input_box/voice_input/voice-input.js +0 -0
  115. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/middleware.py +0 -0
  116. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/stats.py +0 -0
  117. {echotools-2.3.6 → echotools-2.3.7}/src/echotools/web/utils.py +0 -0
  118. {echotools-2.3.6 → echotools-2.3.7}/src/echotools.egg-info/SOURCES.txt +0 -0
  119. {echotools-2.3.6 → echotools-2.3.7}/src/echotools.egg-info/dependency_links.txt +0 -0
  120. {echotools-2.3.6 → echotools-2.3.7}/src/echotools.egg-info/requires.txt +0 -0
  121. {echotools-2.3.6 → echotools-2.3.7}/src/echotools.egg-info/top_level.txt +0 -0
  122. {echotools-2.3.6 → echotools-2.3.7}/tests/test_cache_retry.py +0 -0
  123. {echotools-2.3.6 → echotools-2.3.7}/tests/test_completion.py +0 -0
  124. {echotools-2.3.6 → echotools-2.3.7}/tests/test_config.py +0 -0
  125. {echotools-2.3.6 → echotools-2.3.7}/tests/test_config_extended.py +0 -0
  126. {echotools-2.3.6 → echotools-2.3.7}/tests/test_coverage_core.py +0 -0
  127. {echotools-2.3.6 → echotools-2.3.7}/tests/test_dispatch.py +0 -0
  128. {echotools-2.3.6 → echotools-2.3.7}/tests/test_dispatch_extended.py +0 -0
  129. {echotools-2.3.6 → echotools-2.3.7}/tests/test_dispatch_race.py +0 -0
  130. {echotools-2.3.6 → echotools-2.3.7}/tests/test_errors.py +0 -0
  131. {echotools-2.3.6 → echotools-2.3.7}/tests/test_errors_common.py +0 -0
  132. {echotools-2.3.6 → echotools-2.3.7}/tests/test_events.py +0 -0
  133. {echotools-2.3.6 → echotools-2.3.7}/tests/test_events_event.py +0 -0
  134. {echotools-2.3.6 → echotools-2.3.7}/tests/test_facade.py +0 -0
  135. {echotools-2.3.6 → echotools-2.3.7}/tests/test_ids.py +0 -0
  136. {echotools-2.3.6 → echotools-2.3.7}/tests/test_imports.py +0 -0
  137. {echotools-2.3.6 → echotools-2.3.7}/tests/test_io.py +0 -0
  138. {echotools-2.3.6 → echotools-2.3.7}/tests/test_keys.py +0 -0
  139. {echotools-2.3.6 → echotools-2.3.7}/tests/test_lifecycle.py +0 -0
  140. {echotools-2.3.6 → echotools-2.3.7}/tests/test_logger.py +0 -0
  141. {echotools-2.3.6 → echotools-2.3.7}/tests/test_merge.py +0 -0
  142. {echotools-2.3.6 → echotools-2.3.7}/tests/test_plugin.py +0 -0
  143. {echotools-2.3.6 → echotools-2.3.7}/tests/test_printstream.py +0 -0
  144. {echotools-2.3.6 → echotools-2.3.7}/tests/test_protocols.py +0 -0
  145. {echotools-2.3.6 → echotools-2.3.7}/tests/test_proxy.py +0 -0
  146. {echotools-2.3.6 → echotools-2.3.7}/tests/test_proxy_selector.py +0 -0
  147. {echotools-2.3.6 → echotools-2.3.7}/tests/test_runtime.py +0 -0
  148. {echotools-2.3.6 → echotools-2.3.7}/tests/test_scheduler.py +0 -0
  149. {echotools-2.3.6 → echotools-2.3.7}/tests/test_tracing.py +0 -0
  150. {echotools-2.3.6 → echotools-2.3.7}/tests/test_usage.py +0 -0
  151. {echotools-2.3.6 → echotools-2.3.7}/tests/test_web.py +0 -0
  152. {echotools-2.3.6 → echotools-2.3.7}/tests/test_web_broker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.6
3
+ Version: 2.3.7
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.3.6"
7
+ version = "2.3.7"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -16,6 +16,8 @@ from echotools.fncall.shared.loop_detect import (
16
16
  from echotools.fncall.shared.normalization import (
17
17
  format_tool_descs,
18
18
  normalize_content,
19
+ normalize_tool_call,
20
+ normalize_tool_calls,
19
21
  )
20
22
  from echotools.protocol.base import (
21
23
  ToolProtocol,
@@ -30,6 +32,8 @@ __all__ = [
30
32
  "FncallStreamParser",
31
33
  "format_tool_descs",
32
34
  "normalize_content",
35
+ "normalize_tool_call",
36
+ "normalize_tool_calls",
33
37
  "detect_tool_loop",
34
38
  "LoopDetectionResult",
35
39
  "ToolProtocol",
@@ -14,6 +14,7 @@ from echotools.fncall.shared.coercion import (
14
14
  _build_param_schema_index,
15
15
  _coerce_param_value,
16
16
  )
17
+ from echotools.fncall.shared.normalization import normalize_tool_calls
17
18
  from echotools.fncall.shared.xml_helpers import (
18
19
  _PROVIDER_BLOCK_RE,
19
20
  _PROVIDER_INVOKE_RE,
@@ -208,7 +209,7 @@ def parse_fncall(
208
209
  clean = _FUNC_RE.sub("", clean)
209
210
  clean = clean.strip()
210
211
 
211
- return clean, calls
212
+ return clean, normalize_tool_calls(calls, tools)
212
213
 
213
214
  def parse_fncall_xml(
214
215
  xml: str,
@@ -262,7 +263,7 @@ def parse_fncall_xml(
262
263
  except Exception as exc:
263
264
  logger.warning("parse_fncall_xml 解析失败: %s", exc)
264
265
 
265
- return tool_calls
266
+ return normalize_tool_calls(tool_calls, tools)
266
267
 
267
268
 
268
269
  def parse_fncall_managed_xml(
@@ -319,4 +320,4 @@ def parse_fncall_managed_xml(
319
320
  except Exception as exc:
320
321
  logger.warning("parse_fncall_managed_xml 解析失败: %s", exc)
321
322
 
322
- return tool_calls
323
+ return normalize_tool_calls(tool_calls, tools)
@@ -18,6 +18,7 @@ from echotools.fncall.shared.coercion import (
18
18
  _build_param_schema_index,
19
19
  _coerce_param_value,
20
20
  )
21
+ from echotools.fncall.shared.normalization import normalize_tool_calls
21
22
  from echotools.logger.manager import get_logger
22
23
  from echotools.protocol.base import ToolProtocol
23
24
 
@@ -245,7 +246,7 @@ class EntmlProtocol(ToolProtocol):
245
246
  if tool_calls:
246
247
  clean = _BLOCK_RE.sub("", text).strip()
247
248
 
248
- return (clean, tool_calls)
249
+ return (clean, normalize_tool_calls(tool_calls, tools))
249
250
 
250
251
  def parse_fragment(
251
252
  self,
@@ -11,12 +11,16 @@ from echotools.fncall.shared.loop_detect import (
11
11
  from echotools.fncall.shared.normalization import (
12
12
  format_tool_descs,
13
13
  normalize_content,
14
+ normalize_tool_call,
15
+ normalize_tool_calls,
14
16
  )
15
17
  from echotools.ids.generator import uuid7
16
18
 
17
19
  __all__ = [
18
20
  "normalize_content",
19
21
  "format_tool_descs",
22
+ "normalize_tool_call",
23
+ "normalize_tool_calls",
20
24
  "detect_tool_loop",
21
25
  "LoopDetectionResult",
22
26
  "uuid7",
@@ -5,8 +5,14 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import ast
8
9
  import json
9
- from typing import Any, List
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from echotools.fncall.shared.coercion import _build_param_schema_index
13
+ from echotools.logger.manager import get_logger
14
+
15
+ logger = get_logger(__name__)
10
16
 
11
17
  _LT = chr(60)
12
18
  _GT = chr(62)
@@ -168,3 +174,84 @@ def format_tool_descs(tools):
168
174
  parts.append(chr(10).join(lines))
169
175
 
170
176
  return (chr(10) + chr(10)).join(parts)
177
+
178
+
179
+ def _try_parse_relaxed_literal(text: str) -> Optional[Any]:
180
+ """仅对 array/object 形态尝试 JSON 或 Python literal 解析。"""
181
+ stripped = text.strip()
182
+ if not stripped or stripped[0] not in "[{" or stripped[-1] not in "]}":
183
+ return None
184
+ try:
185
+ return json.loads(stripped)
186
+ except json.JSONDecodeError:
187
+ pass
188
+ try:
189
+ return ast.literal_eval(stripped)
190
+ except (ValueError, SyntaxError):
191
+ return None
192
+
193
+
194
+ def _normalize_value(value: Any, schema: Optional[Dict[str, Any]] = None) -> Any:
195
+ if isinstance(value, str):
196
+ parsed = _try_parse_relaxed_literal(value)
197
+ if parsed is not None:
198
+ value = parsed
199
+ if isinstance(value, list):
200
+ item_schema = (schema or {}).get("items") if schema else None
201
+ if isinstance(item_schema, dict):
202
+ return [_normalize_value(item, item_schema) for item in value]
203
+ return [_normalize_value(item) for item in value]
204
+ if isinstance(value, dict):
205
+ props = (schema or {}).get("properties") or {}
206
+ return {
207
+ key: _normalize_value(
208
+ val,
209
+ props.get(key) if isinstance(props.get(key), dict) else None,
210
+ )
211
+ for key, val in value.items()
212
+ }
213
+ return value
214
+
215
+
216
+ def normalize_tool_call(
217
+ tc: Dict[str, Any],
218
+ tools: Optional[List[Dict[str, Any]]] = None,
219
+ ) -> Dict[str, Any]:
220
+ """将 tool call arguments 中的 Python 字面量字符串还原为合法 JSON 结构。"""
221
+ func = tc.get("function") or {}
222
+ name = str(func.get("name") or "")
223
+ raw_args = func.get("arguments", "{}")
224
+ try:
225
+ args = json.loads(raw_args) if isinstance(raw_args, str) else dict(raw_args)
226
+ except (json.JSONDecodeError, TypeError, ValueError):
227
+ logger.warning("normalize_tool_call: invalid arguments JSON for %s", name)
228
+ return tc
229
+
230
+ schema_index = _build_param_schema_index(tools) if tools else {}
231
+ param_schemas = schema_index.get(name, {})
232
+
233
+ if isinstance(args, dict):
234
+ args = {
235
+ key: _normalize_value(
236
+ val,
237
+ param_schemas.get(key) if param_schemas else None,
238
+ )
239
+ for key, val in args.items()
240
+ }
241
+ else:
242
+ args = _normalize_value(args)
243
+
244
+ normalized = dict(tc)
245
+ normalized["function"] = dict(func)
246
+ normalized["function"]["arguments"] = json.dumps(args, ensure_ascii=False)
247
+ return normalized
248
+
249
+
250
+ def normalize_tool_calls(
251
+ tool_calls: Optional[List[Dict[str, Any]]],
252
+ tools: Optional[List[Dict[str, Any]]] = None,
253
+ ) -> List[Dict[str, Any]]:
254
+ """批量规范化 OpenAI 格式 tool_calls。"""
255
+ if not tool_calls:
256
+ return []
257
+ return [normalize_tool_call(tc, tools) for tc in tool_calls]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.6
3
+ Version: 2.3.7
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+
3
5
  import pytest
4
6
 
5
7
  from echotools.fncall import get_protocol, inject_fncall, list_protocols
8
+ from echotools.fncall.shared.normalization import normalize_tool_call
6
9
 
7
10
 
8
11
  @pytest.mark.parametrize(
@@ -43,6 +46,34 @@ def test_get_protocol_platform_mapping() -> None:
43
46
  assert proto.id == "entml"
44
47
 
45
48
 
49
+ def test_normalize_tool_call_python_literal() -> None:
50
+ tools = [
51
+ {
52
+ "type": "function",
53
+ "function": {
54
+ "name": "run",
55
+ "parameters": {
56
+ "type": "object",
57
+ "properties": {
58
+ "items": {"type": "array", "items": {"type": "string"}},
59
+ },
60
+ },
61
+ },
62
+ }
63
+ ]
64
+ tc = {
65
+ "id": "call_0001",
66
+ "type": "function",
67
+ "function": {
68
+ "name": "run",
69
+ "arguments": json.dumps({"items": "['a', 'b']"}),
70
+ },
71
+ }
72
+ out = normalize_tool_call(tc, tools)
73
+ args = json.loads(out["function"]["arguments"])
74
+ assert args["items"] == ["a", "b"]
75
+
76
+
46
77
  def test_inject_with_tools_and_dump(tmp_path) -> None:
47
78
  proto = get_protocol("entml")
48
79
  tools = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes