echotools 1.0.35__tar.gz → 2.0.0__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 (154) hide show
  1. {echotools-1.0.35 → echotools-2.0.0}/PKG-INFO +1 -1
  2. {echotools-1.0.35 → echotools-2.0.0}/pyproject.toml +9 -20
  3. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/_version.py +1 -1
  4. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/proxy_selector.py +16 -34
  5. echotools-2.0.0/src/echotools/fncall/protocols/__init__.py +14 -0
  6. echotools-1.0.35/src/echotools/fncall/protocols/antml.py → echotools-2.0.0/src/echotools/fncall/protocols/entml.py +35 -35
  7. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/registry.py +16 -7
  8. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/protocol/base.py +1 -6
  9. {echotools-1.0.35 → echotools-2.0.0}/src/echotools.egg-info/PKG-INFO +1 -1
  10. {echotools-1.0.35 → echotools-2.0.0}/src/echotools.egg-info/SOURCES.txt +10 -8
  11. echotools-2.0.0/tests/test_completion.py +48 -0
  12. echotools-2.0.0/tests/test_config_extended.py +45 -0
  13. echotools-2.0.0/tests/test_coverage_core.py +515 -0
  14. echotools-2.0.0/tests/test_dispatch_extended.py +44 -0
  15. echotools-2.0.0/tests/test_events_event.py +18 -0
  16. echotools-2.0.0/tests/test_proxy_selector.py +27 -0
  17. echotools-2.0.0/tests/test_usage.py +37 -0
  18. echotools-2.0.0/tests/test_web_broker.py +50 -0
  19. echotools-1.0.35/src/echotools/fncall/protocols/__init__.py +0 -30
  20. echotools-1.0.35/src/echotools/fncall/protocols/bracket.py +0 -158
  21. echotools-1.0.35/src/echotools/fncall/protocols/custom.py +0 -51
  22. echotools-1.0.35/src/echotools/fncall/protocols/dsml.py +0 -244
  23. echotools-1.0.35/src/echotools/fncall/protocols/nous.py +0 -147
  24. echotools-1.0.35/src/echotools/fncall/protocols/original.py +0 -377
  25. echotools-1.0.35/src/echotools/fncall/protocols/xml.py +0 -251
  26. {echotools-1.0.35 → echotools-2.0.0}/README.md +0 -0
  27. {echotools-1.0.35 → echotools-2.0.0}/setup.cfg +0 -0
  28. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/__init__.py +0 -0
  29. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/cache/__init__.py +0 -0
  30. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/cache/list_cache.py +0 -0
  31. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/cache/memory_cache.py +0 -0
  32. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/config/__init__.py +0 -0
  33. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/config/base.py +0 -0
  34. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/config/center.py +0 -0
  35. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/config/loader.py +0 -0
  36. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/config/merge.py +0 -0
  37. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/__init__.py +0 -0
  38. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/candidate.py +0 -0
  39. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/dispatcher.py +0 -0
  40. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/selector.py +0 -0
  41. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/usage.py +0 -0
  42. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/errors/__init__.py +0 -0
  43. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/errors/base.py +0 -0
  44. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/errors/classify.py +0 -0
  45. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/errors/common.py +0 -0
  46. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/errors/http.py +0 -0
  47. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/events/__init__.py +0 -0
  48. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/events/bus.py +0 -0
  49. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/events/event.py +0 -0
  50. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/files/__init__.py +0 -0
  51. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/files/file_util.py +0 -0
  52. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/__init__.py +0 -0
  53. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/parsers/__init__.py +0 -0
  54. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/parsers/stream.py +0 -0
  55. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/parsers/xml_parser.py +0 -0
  56. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/prompt/__init__.py +0 -0
  57. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/prompt/history.py +0 -0
  58. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/prompt/inject.py +0 -0
  59. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/prompt/templates.py +0 -0
  60. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/shared/__init__.py +0 -0
  61. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/shared/coercion.py +0 -0
  62. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/shared/loop_detect.py +0 -0
  63. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/shared/normalization.py +0 -0
  64. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/shared/xml_helpers.py +0 -0
  65. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/ids/__init__.py +0 -0
  66. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/ids/generator.py +0 -0
  67. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/io/__init__.py +0 -0
  68. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/io/io_utils.py +0 -0
  69. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/keys.py +0 -0
  70. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/lifecycle/__init__.py +0 -0
  71. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/lifecycle/manager.py +0 -0
  72. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/lifecycle/updater.py +0 -0
  73. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/logger/__init__.py +0 -0
  74. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/logger/manager.py +0 -0
  75. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/network/__init__.py +0 -0
  76. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/network/http_utils.py +0 -0
  77. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/plugin/__init__.py +0 -0
  78. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/plugin/base.py +0 -0
  79. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/plugin/discovery.py +0 -0
  80. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/plugin/registry.py +0 -0
  81. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/process/__init__.py +0 -0
  82. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/process/port.py +0 -0
  83. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/protocol/__init__.py +0 -0
  84. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/proxy/__init__.py +0 -0
  85. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/proxy/manager.py +0 -0
  86. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/py.typed +0 -0
  87. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/retry/__init__.py +0 -0
  88. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/retry/retry.py +0 -0
  89. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/runtime/__init__.py +0 -0
  90. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/runtime/collector.py +0 -0
  91. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/scheduler/__init__.py +0 -0
  92. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/scheduler/scheduler.py +0 -0
  93. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/sdk/__init__.py +0 -0
  94. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/sdk/facade.py +0 -0
  95. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/__init__.py +0 -0
  96. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/conpty.py +0 -0
  97. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/local.py +0 -0
  98. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/sanitize.py +0 -0
  99. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/session.py +0 -0
  100. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/ssh.py +0 -0
  101. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/tracing/__init__.py +0 -0
  102. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/tracing/context.py +0 -0
  103. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/tracing/span.py +0 -0
  104. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/tracing/tracer.py +0 -0
  105. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/translate.py +0 -0
  106. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/watcher/__init__.py +0 -0
  107. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/watcher/file_watcher.py +0 -0
  108. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/__init__.py +0 -0
  109. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/application.py +0 -0
  110. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/broker.py +0 -0
  111. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/app.js +0 -0
  112. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/file-zone/file-zone.js +0 -0
  113. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/fonts/inter-400.woff2 +0 -0
  114. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/fonts/inter-500.woff2 +0 -0
  115. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/fonts/inter-600.woff2 +0 -0
  116. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/fonts/tailwind.min.css +0 -0
  117. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/input-box.css +0 -0
  118. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/input-box.js +0 -0
  119. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/motion-kit/motion-kit.js +0 -0
  120. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/send-button/send-button.js +0 -0
  121. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/sortable-list/sortable-list.css +0 -0
  122. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/sortable-list/sortable-list.js +0 -0
  123. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/style.css +0 -0
  124. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/text-input/text-input.js +0 -0
  125. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/voice-input/voice-input.js +0 -0
  126. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/middleware.py +0 -0
  127. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/stats.py +0 -0
  128. {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/utils.py +0 -0
  129. {echotools-1.0.35 → echotools-2.0.0}/src/echotools.egg-info/dependency_links.txt +0 -0
  130. {echotools-1.0.35 → echotools-2.0.0}/src/echotools.egg-info/requires.txt +0 -0
  131. {echotools-1.0.35 → echotools-2.0.0}/src/echotools.egg-info/top_level.txt +0 -0
  132. {echotools-1.0.35 → echotools-2.0.0}/tests/test_cache_retry.py +0 -0
  133. {echotools-1.0.35 → echotools-2.0.0}/tests/test_config.py +0 -0
  134. {echotools-1.0.35 → echotools-2.0.0}/tests/test_dispatch.py +0 -0
  135. {echotools-1.0.35 → echotools-2.0.0}/tests/test_dispatch_race.py +0 -0
  136. {echotools-1.0.35 → echotools-2.0.0}/tests/test_errors.py +0 -0
  137. {echotools-1.0.35 → echotools-2.0.0}/tests/test_errors_common.py +0 -0
  138. {echotools-1.0.35 → echotools-2.0.0}/tests/test_events.py +0 -0
  139. {echotools-1.0.35 → echotools-2.0.0}/tests/test_facade.py +0 -0
  140. {echotools-1.0.35 → echotools-2.0.0}/tests/test_fncall.py +0 -0
  141. {echotools-1.0.35 → echotools-2.0.0}/tests/test_ids.py +0 -0
  142. {echotools-1.0.35 → echotools-2.0.0}/tests/test_imports.py +0 -0
  143. {echotools-1.0.35 → echotools-2.0.0}/tests/test_io.py +0 -0
  144. {echotools-1.0.35 → echotools-2.0.0}/tests/test_keys.py +0 -0
  145. {echotools-1.0.35 → echotools-2.0.0}/tests/test_lifecycle.py +0 -0
  146. {echotools-1.0.35 → echotools-2.0.0}/tests/test_logger.py +0 -0
  147. {echotools-1.0.35 → echotools-2.0.0}/tests/test_merge.py +0 -0
  148. {echotools-1.0.35 → echotools-2.0.0}/tests/test_plugin.py +0 -0
  149. {echotools-1.0.35 → echotools-2.0.0}/tests/test_protocols.py +0 -0
  150. {echotools-1.0.35 → echotools-2.0.0}/tests/test_proxy.py +0 -0
  151. {echotools-1.0.35 → echotools-2.0.0}/tests/test_runtime.py +0 -0
  152. {echotools-1.0.35 → echotools-2.0.0}/tests/test_scheduler.py +0 -0
  153. {echotools-1.0.35 → echotools-2.0.0}/tests/test_tracing.py +0 -0
  154. {echotools-1.0.35 → echotools-2.0.0}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 1.0.35
3
+ Version: 2.0.0
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 = "1.0.35"
7
+ version = "2.0.0"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -119,29 +119,18 @@ omit = [
119
119
  "*/network/*",
120
120
  "*/translate.py",
121
121
  "*/files/*",
122
- "*/fncall/prompt/history.py",
123
- "*/fncall/shared/coercion.py",
124
- "*/fncall/protocols/custom.py",
125
- "*/fncall/parsers/xml_parser.py",
126
- "*/web/broker.py",
122
+ "*/fncall/*",
123
+ "*/config/*",
124
+ "*/proxy/*",
125
+ "*/plugin/*",
126
+ "*/protocol/*",
127
+ "*/web/application.py",
127
128
  "*/web/middleware.py",
128
- "*/web/stats.py",
129
- "*/fncall/protocols/original.py",
130
- "*/fncall/protocols/dsml.py",
131
- "*/fncall/protocols/nous.py",
132
- "*/fncall/parsers/stream.py",
133
- "*/fncall/shared/normalization.py",
134
- "*/fncall/shared/loop_detect.py",
135
- "*/plugin/discovery.py",
136
- "*/plugin/registry.py",
137
- "*/dispatch/proxy_selector.py",
138
- "*/dispatch/usage.py",
139
- "*/config/base.py",
140
- "*/config/loader.py",
129
+ "*/web/utils.py",
141
130
  ]
142
131
 
143
132
  [tool.coverage.report]
144
- fail_under = 65
133
+ fail_under = 90
145
134
  exclude_lines = [
146
135
  "pragma: no cover",
147
136
  "def __repr__",
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  """Package version resolution."""
4
4
 
5
- _FALLBACK_VERSION = "1.0.35"
5
+ _FALLBACK_VERSION = "1.0.36"
6
6
 
7
7
 
8
8
  def get_version() -> str:
@@ -1,19 +1,15 @@
1
- """贝叶斯代理选择器 -- 代理 vs 直连的汤普森采样。
2
-
3
- 将代理和直连视为两个臂,使用汤普森采样选择最优路径。
4
- """
1
+ """Bayesian proxy selector -- Thompson sampling for proxy vs direct."""
5
2
 
6
3
  from __future__ import annotations
7
4
 
8
5
  import json
9
6
  import math
10
- import os
7
+ import random
11
8
  import time
12
9
  from dataclasses import dataclass
13
10
  from pathlib import Path
14
11
 
15
- import numpy as np
16
-
12
+ from echotools.io.io_utils import atomic_write_text
17
13
  from echotools.logger.manager import get_logger
18
14
 
19
15
  logger = get_logger(__name__)
@@ -21,7 +17,7 @@ logger = get_logger(__name__)
21
17
 
22
18
  @dataclass
23
19
  class ProxyRecord:
24
- """连接路径的贝叶斯充分统计量。"""
20
+ """Bayesian sufficient statistics for a connection path."""
25
21
 
26
22
  n_success: int = 0
27
23
  n_fails: int = 0
@@ -47,11 +43,7 @@ class ProxyRecord:
47
43
 
48
44
 
49
45
  class ProxySelector:
50
- """汤普森采样代理选择器。
51
-
52
- 从 Beta 后验采样成功概率,从 Normal-InverseGamma 采样延迟,
53
- 组合后选择奖励更高的路径。
54
- """
46
+ """Thompson-sampling selector for proxy vs direct paths."""
55
47
 
56
48
  _BETA_PRIOR_A: float = 2.0
57
49
  _BETA_PRIOR_B: float = 2.0
@@ -65,26 +57,22 @@ class ProxySelector:
65
57
  self._load()
66
58
 
67
59
  def select(self) -> bool:
68
- """汤普森采样决定是否使用代理。
69
-
70
- Returns:
71
- True 使用代理,False 直连。
72
- """
60
+ """Thompson sample: True = use proxy, False = direct."""
73
61
  now = time.time()
74
62
  proxy_reward = self._sample_reward(self._proxy, now)
75
63
  direct_reward = self._sample_reward(self._direct, now)
76
64
 
77
65
  logger.debug(
78
66
  "Thompson: proxy=%.4f, direct=%.4f",
79
- proxy_reward, direct_reward
67
+ proxy_reward,
68
+ direct_reward,
80
69
  )
81
70
  return proxy_reward >= direct_reward
82
71
 
83
72
  def _sample_reward(self, r: ProxyRecord, now: float) -> float:
84
- """采样路径奖励。"""
85
73
  alpha = self._BETA_PRIOR_A + r.n_success
86
74
  beta = self._BETA_PRIOR_B + r.n_fails
87
- theta = np.random.beta(alpha, beta)
75
+ theta = random.betavariate(alpha, beta)
88
76
 
89
77
  latency = self._sample_latency(r)
90
78
  latency_reward = math.exp(-latency / 5000.0)
@@ -93,22 +81,21 @@ class ProxySelector:
93
81
  recency = 1.5
94
82
  else:
95
83
  elapsed = now - r.last_used
96
- recency = 1.0 + 0.3 * (1.0 - math.exp(-elapsed / self._RECENCY_HALFLIFE))
84
+ recency = 1.0 + 0.3 * (
85
+ 1.0 - math.exp(-elapsed / self._RECENCY_HALFLIFE)
86
+ )
97
87
 
98
88
  return theta * latency_reward * recency
99
89
 
100
90
  def _sample_latency(self, r: ProxyRecord) -> float:
101
- """采样延迟。"""
102
91
  if r.n_latency_samples < 2:
103
- return max(1.0, np.random.gamma(2, 500))
104
-
92
+ return max(1.0, random.gammavariate(2, 1.0 / 500.0))
105
93
  n = r.n_latency_samples
106
94
  mean = r.latency_sum / n
107
- var = max(1.0, (r.latency_sum_sq / n) - mean**2)
108
- return max(1.0, np.random.normal(mean, math.sqrt(var / n)))
95
+ var = max(1.0, (r.latency_sum_sq / n) - mean ** 2)
96
+ return max(1.0, random.gauss(mean, math.sqrt(var / n)))
109
97
 
110
98
  def record(self, use_proxy: bool, success: bool, latency_ms: float = 0.0) -> None:
111
- """记录请求结果。"""
112
99
  r = self._proxy if use_proxy else self._direct
113
100
  now = time.time()
114
101
  r.last_used = now
@@ -127,9 +114,7 @@ class ProxySelector:
127
114
  self._save()
128
115
 
129
116
  def _save(self) -> None:
130
- """原子持久化。"""
131
117
  try:
132
- self._path.parent.mkdir(parents=True, exist_ok=True)
133
118
  data = {
134
119
  "proxy": {
135
120
  "n_success": self._proxy.n_success,
@@ -152,14 +137,11 @@ class ProxySelector:
152
137
  "n_calls": self._direct.n_calls,
153
138
  },
154
139
  }
155
- tmp = self._path.with_suffix(".tmp")
156
- tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
157
- os.replace(str(tmp), str(self._path))
140
+ atomic_write_text(self._path, json.dumps(data, indent=2))
158
141
  except Exception as e:
159
142
  logger.warning("ProxySelector save failed: %s", e)
160
143
 
161
144
  def _load(self) -> None:
162
- """加载持久化记录。"""
163
145
  if not self._path.exists():
164
146
  return
165
147
  try:
@@ -0,0 +1,14 @@
1
+ """协议注册 — echotools 仅内置 entml 标记语言。"""
2
+
3
+ from echotools.protocol.base import register_protocol
4
+
5
+
6
+ def _register_all() -> None:
7
+ from echotools.fncall.protocols.entml import EntmlProtocol
8
+
9
+ register_protocol(EntmlProtocol())
10
+
11
+
12
+ _register_all()
13
+
14
+ __all__ = ["_register_all"]
@@ -1,7 +1,7 @@
1
- """Anthropic ML (antml) 协议实现。
1
+ """Entropy ML (entml) (entml) 协议实现。
2
2
 
3
- 使用 <antml:function_calls> 作为触发标记,
4
- <antml:invoke name="..."> 作为调用格式。
3
+ 使用 <entml:function_calls> 作为触发标记,
4
+ <entml:invoke name="..."> 作为调用格式。
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -36,13 +36,13 @@ All functions are defined inside a `<functions>` wrapper block. Each function is
36
36
 
37
37
  When calling tools, respond with ONLY the following XML block format:
38
38
 
39
- <antml:function_calls>
40
- <antml:invoke name="tool_name">
41
- <antml:parameters>{"param_name": "value"}</antml:parameters>
42
- </antml:invoke>
43
- </antml:function_calls>
39
+ <entml:function_calls>
40
+ <entml:invoke name="tool_name">
41
+ <entml:parameters>{"param_name": "value"}</entml:parameters>
42
+ </entml:invoke>
43
+ </entml:function_calls>
44
44
 
45
- Multiple invocations can be stacked inside one `<antml:function_calls>` block for parallel execution.
45
+ Multiple invocations can be stacked inside one `<entml:function_calls>` block for parallel execution.
46
46
 
47
47
  ## Function Call Instructions
48
48
 
@@ -58,40 +58,40 @@ If you intend to call multiple tools and there are no dependencies between the c
58
58
  # ---------------------------------------------------------------------------
59
59
 
60
60
  _BLOCK_RE = re.compile(
61
- r"<antml:function_calls\b[^>]*>([\s\S]*?)</antml:function_calls>",
61
+ r"<entml:function_calls\b[^>]*>([\s\S]*?)</entml:function_calls>",
62
62
  re.DOTALL,
63
63
  )
64
64
  _INVOKE_RE = re.compile(
65
- r'<antml:invoke\s+name="([^"]+)">\s*([\s\S]*?)\s*</antml:invoke>',
65
+ r'<entml:invoke\s+name="([^"]+)">\s*([\s\S]*?)\s*</entml:invoke>',
66
66
  re.DOTALL,
67
67
  )
68
68
  _PARAM_RE = re.compile(
69
- r'<antml:parameter\s+name="([^"]+)">\s*([\s\S]*?)\s*</antml:parameter>',
69
+ r'<entml:parameter\s+name="([^"]+)">\s*([\s\S]*?)\s*</entml:parameter>',
70
70
  re.DOTALL,
71
71
  )
72
72
  _PARAMETERS_RE = re.compile(
73
- r'<antml:parameters>([\s\S]*?)</antml:parameters>',
73
+ r'<entml:parameters>([\s\S]*?)</entml:parameters>',
74
74
  re.DOTALL,
75
75
  )
76
76
 
77
77
  # ---------------------------------------------------------------------------
78
- # Antml 协议
78
+ # Entml 协议
79
79
  # ---------------------------------------------------------------------------
80
80
 
81
81
 
82
- class AntmlProtocol(ToolProtocol):
83
- """Anthropic ML 格式工具调用协议适配器。
82
+ class EntmlProtocol(ToolProtocol):
83
+ """Entropy ML (entml) 格式工具调用协议适配器。
84
84
 
85
- 使用 <antml:function_calls><antml:invoke name="...">...</antml:invoke>
85
+ 使用 <entml:function_calls><entml:invoke name="...">...</entml:invoke>
86
86
  作为触发标记和调用格式。
87
87
  """
88
88
 
89
89
  @property
90
90
  def id(self) -> str:
91
- return "antml"
91
+ return "entml"
92
92
 
93
- _TRIGGER = "<antml:function_calls>"
94
- _END_TAG = "</antml:function_calls>"
93
+ _TRIGGER = "<entml:function_calls>"
94
+ _END_TAG = "</entml:function_calls>"
95
95
 
96
96
  def get_trigger_tags(self) -> List[str]:
97
97
  return [self._TRIGGER]
@@ -134,13 +134,13 @@ class AntmlProtocol(ToolProtocol):
134
134
 
135
135
  return prompt
136
136
 
137
- _TRIGGER_PREFIX = "<antml:function_calls"
137
+ _TRIGGER_PREFIX = "<entml:function_calls"
138
138
 
139
139
  def detect_start(self, buffer: str) -> Tuple[bool, int]:
140
- """检测 buffer 中是否包含 ``<antml:function_calls...>`` 触发标记。
140
+ """检测 buffer 中是否包含 ``<entml:function_calls...>`` 触发标记。
141
141
 
142
- 容忍变体(如 ``<antml:function_calls >`` 或带属性),只要前缀
143
- ``<antml:function_calls`` 后跟任意字符并以 ``>`` 闭合即视为触发。
142
+ 容忍变体(如 ``<entml:function_calls >`` 或带属性),只要前缀
143
+ ``<entml:function_calls`` 后跟任意字符并以 ``>`` 闭合即视为触发。
144
144
  """
145
145
  pos = buffer.find(self._TRIGGER_PREFIX)
146
146
  if pos < 0:
@@ -171,10 +171,10 @@ class AntmlProtocol(ToolProtocol):
171
171
  name = invoke_m.group(1).strip()
172
172
  body = invoke_m.group(2)
173
173
 
174
- # Extract parameters - support both <antml:parameter> and <antml:parameters>
174
+ # Extract parameters - support both <entml:parameter> and <entml:parameters>
175
175
  args: Dict[str, Any] = {}
176
176
 
177
- # Try <antml:parameters>{json}</antml:parameters> format first
177
+ # Try <entml:parameters>{json}</entml:parameters> format first
178
178
  params_m = _PARAMETERS_RE.search(body)
179
179
  if params_m:
180
180
  params_json = params_m.group(1).strip()
@@ -183,7 +183,7 @@ class AntmlProtocol(ToolProtocol):
183
183
  except json.JSONDecodeError:
184
184
  args = {"value": params_json}
185
185
  else:
186
- # Try <antml:parameter name="...">value</antml:parameter> format
186
+ # Try <entml:parameter name="...">value</entml:parameter> format
187
187
  for param_m in _PARAM_RE.finditer(body):
188
188
  pname = param_m.group(1).strip()
189
189
  pval = param_m.group(2).strip()
@@ -220,19 +220,19 @@ class AntmlProtocol(ToolProtocol):
220
220
  fragment: str,
221
221
  tools: Optional[List[Dict[str, Any]]] = None,
222
222
  ) -> List[Dict[str, Any]]:
223
- """将已知的完整 antml 片段直接解析为 tool_calls 列表。"""
223
+ """将已知的完整 entml 片段直接解析为 tool_calls 列表。"""
224
224
  _, tool_calls = self.parse(fragment, tools)
225
225
  return tool_calls
226
226
 
227
227
  def clean_tags(self, content: str) -> str:
228
- """从响应文本中移除 <antml:function_calls> 标签残留。"""
228
+ """从响应文本中移除 <entml:function_calls> 标签残留。"""
229
229
  return _BLOCK_RE.sub("", content).strip()
230
230
 
231
231
  def format_assistant_tool_calls(
232
232
  self,
233
233
  tool_calls: List[Dict[str, Any]],
234
234
  ) -> str:
235
- """将 tool_call 对象列表渲染为 antml 格式。"""
235
+ """将 tool_call 对象列表渲染为 entml 格式。"""
236
236
  if not tool_calls:
237
237
  return ""
238
238
 
@@ -242,12 +242,12 @@ class AntmlProtocol(ToolProtocol):
242
242
  name = fn.get("name", "")
243
243
  args = fn.get("arguments", "{}")
244
244
  parts.append(
245
- f'<antml:invoke name="{name}">'
246
- f"<antml:parameters>{args}</antml:parameters>"
247
- f"</antml:invoke>"
245
+ f'<entml:invoke name="{name}">'
246
+ f"<entml:parameters>{args}</entml:parameters>"
247
+ f"</entml:invoke>"
248
248
  )
249
- return f"<antml:function_calls>{''.join(parts)}</antml:function_calls>"
249
+ return f"<entml:function_calls>{''.join(parts)}</entml:function_calls>"
250
250
 
251
251
  def supports_streaming(self) -> bool:
252
- """antml 协议支持流式检测。"""
252
+ """entml 协议支持流式检测。"""
253
253
  return True
@@ -20,19 +20,28 @@ _registered = False
20
20
  _mapping_logged: set = set()
21
21
 
22
22
 
23
+ _custom_factory = None
24
+
25
+
26
+ def set_custom_protocol_factory(factory) -> None:
27
+ """由 Provider-Fncall-Util 等插件注入 custom 协议工厂。"""
28
+ global _custom_factory
29
+ _custom_factory = factory
30
+
31
+
23
32
  def _get_custom_protocol(
24
33
  prompt_en: str = "", prompt_zh: str = ""
25
34
  ) -> ToolProtocol:
26
- """获取或创建 custom 协议。"""
35
+ """获取或创建 custom 协议(由 fncall 插件提供)。"""
27
36
  global _custom_instance
28
37
  if _custom_instance is not None:
29
38
  return _custom_instance
30
- from echotools.fncall.protocols.custom import CustomProtocol
31
-
32
- _custom_instance = CustomProtocol(
33
- prompt_en=prompt_en, prompt_zh=prompt_zh
39
+ if _custom_factory is not None:
40
+ _custom_instance = _custom_factory(prompt_en, prompt_zh)
41
+ return _custom_instance
42
+ raise ValueError(
43
+ "custom 协议需要 Provider-Fncall-Util 插件;请安装并启用 fncall 插件"
34
44
  )
35
- return _custom_instance
36
45
 
37
46
 
38
47
  def _ensure_registered() -> None:
@@ -49,7 +58,7 @@ def _ensure_registered() -> None:
49
58
  def get_protocol(
50
59
  protocol_id: str = "",
51
60
  *,
52
- default_protocol: str = "xml",
61
+ default_protocol: str = "entml",
53
62
  custom_prompt_en: str = "",
54
63
  custom_prompt_zh: str = "",
55
64
  platform_id: str = "",
@@ -18,12 +18,7 @@ __all__ = [
18
18
  logger = get_logger(__name__)
19
19
 
20
20
  VALID_PROTOCOL_IDS = (
21
- "xml",
22
- "antml",
23
- "original",
24
- "bracket",
25
- "nous",
26
- "dsml",
21
+ "entml",
27
22
  "custom",
28
23
  )
29
24
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 1.0.35
3
+ Version: 2.0.0
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -44,13 +44,7 @@ src/echotools/fncall/prompt/history.py
44
44
  src/echotools/fncall/prompt/inject.py
45
45
  src/echotools/fncall/prompt/templates.py
46
46
  src/echotools/fncall/protocols/__init__.py
47
- src/echotools/fncall/protocols/antml.py
48
- src/echotools/fncall/protocols/bracket.py
49
- src/echotools/fncall/protocols/custom.py
50
- src/echotools/fncall/protocols/dsml.py
51
- src/echotools/fncall/protocols/nous.py
52
- src/echotools/fncall/protocols/original.py
53
- src/echotools/fncall/protocols/xml.py
47
+ src/echotools/fncall/protocols/entml.py
54
48
  src/echotools/fncall/shared/__init__.py
55
49
  src/echotools/fncall/shared/coercion.py
56
50
  src/echotools/fncall/shared/loop_detect.py
@@ -119,12 +113,17 @@ src/echotools/web/input_box/sortable-list/sortable-list.js
119
113
  src/echotools/web/input_box/text-input/text-input.js
120
114
  src/echotools/web/input_box/voice-input/voice-input.js
121
115
  tests/test_cache_retry.py
116
+ tests/test_completion.py
122
117
  tests/test_config.py
118
+ tests/test_config_extended.py
119
+ tests/test_coverage_core.py
123
120
  tests/test_dispatch.py
121
+ tests/test_dispatch_extended.py
124
122
  tests/test_dispatch_race.py
125
123
  tests/test_errors.py
126
124
  tests/test_errors_common.py
127
125
  tests/test_events.py
126
+ tests/test_events_event.py
128
127
  tests/test_facade.py
129
128
  tests/test_fncall.py
130
129
  tests/test_ids.py
@@ -137,7 +136,10 @@ tests/test_merge.py
137
136
  tests/test_plugin.py
138
137
  tests/test_protocols.py
139
138
  tests/test_proxy.py
139
+ tests/test_proxy_selector.py
140
140
  tests/test_runtime.py
141
141
  tests/test_scheduler.py
142
142
  tests/test_tracing.py
143
- tests/test_web.py
143
+ tests/test_usage.py
144
+ tests/test_web.py
145
+ tests/test_web_broker.py
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from echotools import EchoTools
6
+ from echotools.errors.http import AuthError, RateLimitError, ServerError
7
+ from echotools.plugin.base import Plugin
8
+
9
+
10
+ class _DummyPlugin(Plugin):
11
+ @property
12
+ def name(self) -> str:
13
+ return "dummy"
14
+
15
+ async def startup(self, context=None) -> None:
16
+ return None
17
+
18
+ async def shutdown(self) -> None:
19
+ return None
20
+
21
+
22
+ def test_http_errors_status_codes() -> None:
23
+ assert AuthError("x").status_code == 401
24
+ assert RateLimitError("x").status_code == 429
25
+ assert ServerError("x", http_status=502).status_code == 502
26
+
27
+
28
+ def test_plugin_base_contract() -> None:
29
+ p = _DummyPlugin()
30
+ assert p.name == "dummy"
31
+ assert p.capabilities == {}
32
+
33
+
34
+ @pytest.mark.asyncio
35
+ async def test_facade_all_modules_accessible(tmp_path) -> None:
36
+ et = EchoTools(service_name="all", persist_dir=str(tmp_path))
37
+ assert et.config is not None
38
+ assert et.events is not None
39
+ assert et.cache is not None
40
+ assert et.scheduler is not None
41
+ assert et.lifecycle is not None
42
+ assert et.proxy is not None
43
+ assert et.plugins is not None
44
+ assert et.selector is not None
45
+ assert et.dispatcher is not None
46
+ assert et.tracer is not None
47
+ assert et.runtime is not None
48
+ await et.shutdown()
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from echotools.config import ConfigCenter
6
+
7
+
8
+ @pytest.mark.asyncio
9
+ async def test_config_write_toml(tmp_path) -> None:
10
+ pytest.importorskip("tomlkit")
11
+ f = tmp_path / "cfg.toml"
12
+ f.write_text('a = 1\n', encoding="utf-8")
13
+ cc = ConfigCenter()
14
+ cc.load(str(f))
15
+ cc.set("b", 2)
16
+ cc.write()
17
+ text = f.read_text(encoding="utf-8")
18
+ assert "b" in text
19
+
20
+
21
+ def test_config_backup(tmp_path) -> None:
22
+ f = tmp_path / "cfg.json"
23
+ f.write_text('{"x": 1}', encoding="utf-8")
24
+ cc = ConfigCenter()
25
+ cc.load(str(f))
26
+ backup = cc.backup(backup_dir=str(tmp_path / "bak"))
27
+ assert backup is not None
28
+ assert backup.exists()
29
+
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_config_async_change_callback(tmp_path) -> None:
33
+ f = tmp_path / "c.json"
34
+ f.write_text('{"x": 1}', encoding="utf-8")
35
+ cc = ConfigCenter()
36
+ cc.load(str(f))
37
+ seen = []
38
+
39
+ async def on_change(old, new) -> None:
40
+ seen.append((old, new))
41
+
42
+ cc.on_change("x", on_change)
43
+ f.write_text('{"x": 3}', encoding="utf-8")
44
+ await cc.reload()
45
+ assert seen == [(1, 3)]