echotools 2.3.5__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.5 → echotools-2.3.7}/PKG-INFO +2 -2
  2. {echotools-2.3.5 → echotools-2.3.7}/pyproject.toml +3 -2
  3. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/__init__.py +4 -0
  4. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/parsers/xml_parser.py +4 -3
  5. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/protocols/entml.py +2 -1
  6. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/shared/__init__.py +4 -0
  7. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/shared/normalization.py +88 -1
  8. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/spinner/spinner.py +3 -3
  9. {echotools-2.3.5 → echotools-2.3.7}/src/echotools.egg-info/PKG-INFO +2 -2
  10. {echotools-2.3.5 → echotools-2.3.7}/src/echotools.egg-info/SOURCES.txt +7 -7
  11. {echotools-2.3.5 → echotools-2.3.7}/tests/test_fncall.py +31 -0
  12. {echotools-2.3.5 → echotools-2.3.7}/tests/test_printstream.py +15 -16
  13. {echotools-2.3.5 → echotools-2.3.7}/tests/test_protocols.py +5 -2
  14. {echotools-2.3.5 → echotools-2.3.7}/README.md +0 -0
  15. {echotools-2.3.5 → echotools-2.3.7}/setup.cfg +0 -0
  16. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/__init__.py +0 -0
  17. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/_version.py +0 -0
  18. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/cache/__init__.py +0 -0
  19. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/cache/list_cache.py +0 -0
  20. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/cache/memory_cache.py +0 -0
  21. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/config/__init__.py +0 -0
  22. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/config/base.py +0 -0
  23. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/config/center.py +0 -0
  24. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/config/loader.py +0 -0
  25. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/config/merge.py +0 -0
  26. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/dispatch/__init__.py +0 -0
  27. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/dispatch/candidate.py +0 -0
  28. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/dispatch/dispatcher.py +0 -0
  29. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/dispatch/proxy_selector.py +0 -0
  30. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/dispatch/selector.py +0 -0
  31. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/dispatch/usage.py +0 -0
  32. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/errors/__init__.py +0 -0
  33. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/errors/base.py +0 -0
  34. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/errors/classify.py +0 -0
  35. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/errors/common.py +0 -0
  36. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/errors/http.py +0 -0
  37. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/events/__init__.py +0 -0
  38. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/events/bus.py +0 -0
  39. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/events/event.py +0 -0
  40. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/files/__init__.py +0 -0
  41. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/files/file_util.py +0 -0
  42. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/parsers/__init__.py +0 -0
  43. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/parsers/stream.py +0 -0
  44. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/prompt/__init__.py +0 -0
  45. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/prompt/history.py +0 -0
  46. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/prompt/inject.py +0 -0
  47. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/prompt/templates.py +0 -0
  48. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/protocols/__init__.py +0 -0
  49. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/registry.py +0 -0
  50. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/shared/coercion.py +0 -0
  51. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/shared/loop_detect.py +0 -0
  52. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/fncall/shared/xml_helpers.py +0 -0
  53. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/ids/__init__.py +0 -0
  54. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/ids/generator.py +0 -0
  55. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/io/__init__.py +0 -0
  56. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/io/io_utils.py +0 -0
  57. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/io/printstream.py +0 -0
  58. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/keys.py +0 -0
  59. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/lifecycle/__init__.py +0 -0
  60. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/lifecycle/manager.py +0 -0
  61. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/lifecycle/updater.py +0 -0
  62. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/logger/__init__.py +0 -0
  63. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/logger/manager.py +0 -0
  64. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/network/__init__.py +0 -0
  65. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/network/http_utils.py +0 -0
  66. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/plugin/__init__.py +0 -0
  67. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/plugin/base.py +0 -0
  68. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/plugin/discovery.py +0 -0
  69. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/plugin/registry.py +0 -0
  70. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/process/__init__.py +0 -0
  71. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/process/port.py +0 -0
  72. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/protocol/__init__.py +0 -0
  73. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/protocol/base.py +0 -0
  74. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/proxy/__init__.py +0 -0
  75. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/proxy/manager.py +0 -0
  76. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/py.typed +0 -0
  77. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/retry/__init__.py +0 -0
  78. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/retry/retry.py +0 -0
  79. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/runtime/__init__.py +0 -0
  80. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/runtime/collector.py +0 -0
  81. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/scheduler/__init__.py +0 -0
  82. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/scheduler/scheduler.py +0 -0
  83. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/sdk/__init__.py +0 -0
  84. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/sdk/facade.py +0 -0
  85. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/spinner/__init__.py +0 -0
  86. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/terminal/__init__.py +0 -0
  87. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/terminal/conpty.py +0 -0
  88. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/terminal/local.py +0 -0
  89. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/terminal/sanitize.py +0 -0
  90. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/terminal/session.py +0 -0
  91. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/terminal/ssh.py +0 -0
  92. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/terminal/tmux.py +0 -0
  93. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/tracing/__init__.py +0 -0
  94. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/tracing/context.py +0 -0
  95. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/tracing/span.py +0 -0
  96. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/tracing/tracer.py +0 -0
  97. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/translate.py +0 -0
  98. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/watcher/__init__.py +0 -0
  99. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/watcher/file_watcher.py +0 -0
  100. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/__init__.py +0 -0
  101. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/application.py +0 -0
  102. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/broker.py +0 -0
  103. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/input_box/app.js +0 -0
  104. {echotools-2.3.5/src/echotools/web/input_box/file-zone → echotools-2.3.7/src/echotools/web/input_box/file_zone}/file-zone.js +0 -0
  105. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/input_box/fonts/inter-400.woff2 +0 -0
  106. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/input_box/fonts/inter-500.woff2 +0 -0
  107. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/input_box/fonts/inter-600.woff2 +0 -0
  108. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/input_box/fonts/tailwind.min.css +0 -0
  109. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/input_box/input-box.css +0 -0
  110. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/input_box/input-box.js +0 -0
  111. {echotools-2.3.5/src/echotools/web/input_box/motion-kit → echotools-2.3.7/src/echotools/web/input_box/motion_kit}/motion-kit.js +0 -0
  112. {echotools-2.3.5/src/echotools/web/input_box/send-button → echotools-2.3.7/src/echotools/web/input_box/send_button}/send-button.js +0 -0
  113. {echotools-2.3.5/src/echotools/web/input_box/sortable-list → echotools-2.3.7/src/echotools/web/input_box/sortable_list}/sortable-list.css +0 -0
  114. {echotools-2.3.5/src/echotools/web/input_box/sortable-list → echotools-2.3.7/src/echotools/web/input_box/sortable_list}/sortable-list.js +0 -0
  115. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/input_box/style.css +0 -0
  116. {echotools-2.3.5/src/echotools/web/input_box/text-input → echotools-2.3.7/src/echotools/web/input_box/text_input}/text-input.js +0 -0
  117. {echotools-2.3.5/src/echotools/web/input_box/voice-input → echotools-2.3.7/src/echotools/web/input_box/voice_input}/voice-input.js +0 -0
  118. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/middleware.py +0 -0
  119. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/stats.py +0 -0
  120. {echotools-2.3.5 → echotools-2.3.7}/src/echotools/web/utils.py +0 -0
  121. {echotools-2.3.5 → echotools-2.3.7}/src/echotools.egg-info/dependency_links.txt +0 -0
  122. {echotools-2.3.5 → echotools-2.3.7}/src/echotools.egg-info/requires.txt +0 -0
  123. {echotools-2.3.5 → echotools-2.3.7}/src/echotools.egg-info/top_level.txt +0 -0
  124. {echotools-2.3.5 → echotools-2.3.7}/tests/test_cache_retry.py +0 -0
  125. {echotools-2.3.5 → echotools-2.3.7}/tests/test_completion.py +0 -0
  126. {echotools-2.3.5 → echotools-2.3.7}/tests/test_config.py +0 -0
  127. {echotools-2.3.5 → echotools-2.3.7}/tests/test_config_extended.py +0 -0
  128. {echotools-2.3.5 → echotools-2.3.7}/tests/test_coverage_core.py +0 -0
  129. {echotools-2.3.5 → echotools-2.3.7}/tests/test_dispatch.py +0 -0
  130. {echotools-2.3.5 → echotools-2.3.7}/tests/test_dispatch_extended.py +0 -0
  131. {echotools-2.3.5 → echotools-2.3.7}/tests/test_dispatch_race.py +0 -0
  132. {echotools-2.3.5 → echotools-2.3.7}/tests/test_errors.py +0 -0
  133. {echotools-2.3.5 → echotools-2.3.7}/tests/test_errors_common.py +0 -0
  134. {echotools-2.3.5 → echotools-2.3.7}/tests/test_events.py +0 -0
  135. {echotools-2.3.5 → echotools-2.3.7}/tests/test_events_event.py +0 -0
  136. {echotools-2.3.5 → echotools-2.3.7}/tests/test_facade.py +0 -0
  137. {echotools-2.3.5 → echotools-2.3.7}/tests/test_ids.py +0 -0
  138. {echotools-2.3.5 → echotools-2.3.7}/tests/test_imports.py +0 -0
  139. {echotools-2.3.5 → echotools-2.3.7}/tests/test_io.py +0 -0
  140. {echotools-2.3.5 → echotools-2.3.7}/tests/test_keys.py +0 -0
  141. {echotools-2.3.5 → echotools-2.3.7}/tests/test_lifecycle.py +0 -0
  142. {echotools-2.3.5 → echotools-2.3.7}/tests/test_logger.py +0 -0
  143. {echotools-2.3.5 → echotools-2.3.7}/tests/test_merge.py +0 -0
  144. {echotools-2.3.5 → echotools-2.3.7}/tests/test_plugin.py +0 -0
  145. {echotools-2.3.5 → echotools-2.3.7}/tests/test_proxy.py +0 -0
  146. {echotools-2.3.5 → echotools-2.3.7}/tests/test_proxy_selector.py +0 -0
  147. {echotools-2.3.5 → echotools-2.3.7}/tests/test_runtime.py +0 -0
  148. {echotools-2.3.5 → echotools-2.3.7}/tests/test_scheduler.py +0 -0
  149. {echotools-2.3.5 → echotools-2.3.7}/tests/test_tracing.py +0 -0
  150. {echotools-2.3.5 → echotools-2.3.7}/tests/test_usage.py +0 -0
  151. {echotools-2.3.5 → echotools-2.3.7}/tests/test_web.py +0 -0
  152. {echotools-2.3.5 → echotools-2.3.7}/tests/test_web_broker.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.5
3
+ Version: 2.3.7
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
- License-Expression: MIT
6
+ License: MIT
7
7
  Project-URL: Homepage, https://github.com/nichengfuben/echotools
8
8
  Project-URL: Repository, https://github.com/nichengfuben/echotools
9
9
  Project-URL: Issues, https://github.com/nichengfuben/echotools/issues
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.3.5"
7
+ version = "2.3.7"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
11
- license = "MIT"
11
+ license = { text = "MIT" }
12
12
  authors = [{ name = "nichengfuben" }]
13
13
  classifiers = [
14
14
  "Development Status :: 4 - Beta",
@@ -111,6 +111,7 @@ omit = [
111
111
  "*/tests/*",
112
112
  "*/__init__.py",
113
113
  "*/terminal/*",
114
+ "*/spinner/*",
114
115
  "*/web/input_box/*",
115
116
  "*/lifecycle/updater.py",
116
117
  "*/watcher/*",
@@ -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]
@@ -30,10 +30,10 @@ def get_default_characters() -> List[str]:
30
30
  def hue_to_rgb(hue: float) -> Tuple[int, int, int]:
31
31
  h = ((hue % 360) + 360) % 360
32
32
  s = 0.7
33
- l = 0.6
34
- c = (1 - abs(2 * l - 1)) * s
33
+ lightness = 0.6
34
+ c = (1 - abs(2 * lightness - 1)) * s
35
35
  x = c * (1 - abs((h / 60) % 2 - 1))
36
- m = l - c / 2
36
+ m = lightness - c / 2
37
37
  r = g = b = 0.0
38
38
  if h < 60:
39
39
  r = c
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.5
3
+ Version: 2.3.7
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
- License-Expression: MIT
6
+ License: MIT
7
7
  Project-URL: Homepage, https://github.com/nichengfuben/echotools
8
8
  Project-URL: Repository, https://github.com/nichengfuben/echotools
9
9
  Project-URL: Issues, https://github.com/nichengfuben/echotools/issues
@@ -105,17 +105,17 @@ src/echotools/web/input_box/app.js
105
105
  src/echotools/web/input_box/input-box.css
106
106
  src/echotools/web/input_box/input-box.js
107
107
  src/echotools/web/input_box/style.css
108
- src/echotools/web/input_box/file-zone/file-zone.js
108
+ src/echotools/web/input_box/file_zone/file-zone.js
109
109
  src/echotools/web/input_box/fonts/inter-400.woff2
110
110
  src/echotools/web/input_box/fonts/inter-500.woff2
111
111
  src/echotools/web/input_box/fonts/inter-600.woff2
112
112
  src/echotools/web/input_box/fonts/tailwind.min.css
113
- src/echotools/web/input_box/motion-kit/motion-kit.js
114
- src/echotools/web/input_box/send-button/send-button.js
115
- src/echotools/web/input_box/sortable-list/sortable-list.css
116
- src/echotools/web/input_box/sortable-list/sortable-list.js
117
- src/echotools/web/input_box/text-input/text-input.js
118
- src/echotools/web/input_box/voice-input/voice-input.js
113
+ src/echotools/web/input_box/motion_kit/motion-kit.js
114
+ src/echotools/web/input_box/send_button/send-button.js
115
+ src/echotools/web/input_box/sortable_list/sortable-list.css
116
+ src/echotools/web/input_box/sortable_list/sortable-list.js
117
+ src/echotools/web/input_box/text_input/text-input.js
118
+ src/echotools/web/input_box/voice_input/voice-input.js
119
119
  tests/test_cache_retry.py
120
120
  tests/test_completion.py
121
121
  tests/test_config.py
@@ -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 = [
@@ -4,10 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import sys
6
6
  import time
7
- from io import StringIO
8
- from unittest.mock import MagicMock, patch
9
-
10
- import pytest
7
+ from unittest.mock import patch
11
8
 
12
9
  from echotools.io.printstream import (
13
10
  PrintStream,
@@ -294,21 +291,23 @@ class TestPrintStreamIntegration:
294
291
  stream.start()
295
292
 
296
293
  messages = ["First", "Second", "Third"]
297
- for msg in messages:
298
- stream.add_to_buffer(msg)
299
-
300
- # Give time for processing
301
- time.sleep(0.5)
302
-
303
- # Flush and check order
304
294
  with patch.object(sys.stdout, "write") as mock_write:
295
+ for msg in messages:
296
+ stream.add_to_buffer(msg)
297
+
298
+ # Give time for processing
299
+ time.sleep(0.5)
305
300
  stream.flush_remaining()
301
+
306
302
  # Check that messages were written in order
307
- calls = [call[0][0] for call in mock_write.call_args_list]
308
- # The order should be preserved
309
- assert "First" in calls
310
- assert "Second" in calls
311
- assert "Third" in calls
303
+ written = "".join(call[0][0] for call in mock_write.call_args_list)
304
+ first_idx = written.find("First")
305
+ second_idx = written.find("Second")
306
+ third_idx = written.find("Third")
307
+ assert first_idx != -1
308
+ assert second_idx != -1
309
+ assert third_idx != -1
310
+ assert first_idx < second_idx < third_idx
312
311
 
313
312
  stream.stop()
314
313
 
@@ -16,7 +16,10 @@ def test_entml_protocol_parse() -> None:
16
16
 
17
17
 
18
18
  def test_inject_no_tools() -> None:
19
- """无工具时原样返回。"""
19
+ """无工具时仍包裹历史/当前消息模板,但不注入工具定义。"""
20
20
  proto = get_protocol("entml")
21
21
  msgs = [{"role": "user", "content": "hi"}]
22
- assert inject_fncall(msgs, [], proto) == msgs
22
+ result = inject_fncall(msgs, [], proto)
23
+ assert len(result) == 1
24
+ assert result[0]["role"] == "user"
25
+ assert "<current_user_message>\nhi\n</current_user_message>" in result[0]["content"]
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