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.
- {echotools-1.0.35 → echotools-2.0.0}/PKG-INFO +1 -1
- {echotools-1.0.35 → echotools-2.0.0}/pyproject.toml +9 -20
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/_version.py +1 -1
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/proxy_selector.py +16 -34
- echotools-2.0.0/src/echotools/fncall/protocols/__init__.py +14 -0
- echotools-1.0.35/src/echotools/fncall/protocols/antml.py → echotools-2.0.0/src/echotools/fncall/protocols/entml.py +35 -35
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/registry.py +16 -7
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/protocol/base.py +1 -6
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools.egg-info/SOURCES.txt +10 -8
- echotools-2.0.0/tests/test_completion.py +48 -0
- echotools-2.0.0/tests/test_config_extended.py +45 -0
- echotools-2.0.0/tests/test_coverage_core.py +515 -0
- echotools-2.0.0/tests/test_dispatch_extended.py +44 -0
- echotools-2.0.0/tests/test_events_event.py +18 -0
- echotools-2.0.0/tests/test_proxy_selector.py +27 -0
- echotools-2.0.0/tests/test_usage.py +37 -0
- echotools-2.0.0/tests/test_web_broker.py +50 -0
- echotools-1.0.35/src/echotools/fncall/protocols/__init__.py +0 -30
- echotools-1.0.35/src/echotools/fncall/protocols/bracket.py +0 -158
- echotools-1.0.35/src/echotools/fncall/protocols/custom.py +0 -51
- echotools-1.0.35/src/echotools/fncall/protocols/dsml.py +0 -244
- echotools-1.0.35/src/echotools/fncall/protocols/nous.py +0 -147
- echotools-1.0.35/src/echotools/fncall/protocols/original.py +0 -377
- echotools-1.0.35/src/echotools/fncall/protocols/xml.py +0 -251
- {echotools-1.0.35 → echotools-2.0.0}/README.md +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/setup.cfg +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/cache/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/cache/list_cache.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/cache/memory_cache.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/config/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/config/base.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/config/center.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/config/loader.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/config/merge.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/candidate.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/dispatcher.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/selector.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/dispatch/usage.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/errors/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/errors/base.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/errors/classify.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/errors/common.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/errors/http.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/events/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/events/bus.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/events/event.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/files/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/files/file_util.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/parsers/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/parsers/stream.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/parsers/xml_parser.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/prompt/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/prompt/history.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/prompt/inject.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/prompt/templates.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/shared/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/shared/coercion.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/shared/loop_detect.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/shared/normalization.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/fncall/shared/xml_helpers.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/ids/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/ids/generator.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/io/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/io/io_utils.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/keys.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/lifecycle/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/lifecycle/manager.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/lifecycle/updater.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/logger/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/logger/manager.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/network/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/network/http_utils.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/plugin/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/plugin/base.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/plugin/discovery.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/plugin/registry.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/process/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/process/port.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/protocol/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/proxy/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/proxy/manager.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/py.typed +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/retry/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/retry/retry.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/runtime/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/runtime/collector.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/scheduler/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/scheduler/scheduler.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/sdk/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/sdk/facade.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/conpty.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/local.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/sanitize.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/session.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/terminal/ssh.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/tracing/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/tracing/context.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/tracing/span.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/tracing/tracer.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/translate.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/watcher/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/watcher/file_watcher.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/__init__.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/application.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/broker.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/app.js +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/file-zone/file-zone.js +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/fonts/inter-400.woff2 +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/fonts/tailwind.min.css +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/input-box.css +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/input-box.js +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/motion-kit/motion-kit.js +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/send-button/send-button.js +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/sortable-list/sortable-list.css +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/sortable-list/sortable-list.js +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/style.css +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/text-input/text-input.js +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/input_box/voice-input/voice-input.js +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/middleware.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/stats.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools/web/utils.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_cache_retry.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_config.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_dispatch.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_dispatch_race.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_errors.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_errors_common.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_events.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_facade.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_fncall.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_ids.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_imports.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_io.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_keys.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_lifecycle.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_logger.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_merge.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_plugin.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_protocols.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_proxy.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_runtime.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_scheduler.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_tracing.py +0 -0
- {echotools-1.0.35 → echotools-2.0.0}/tests/test_web.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "echotools"
|
|
7
|
-
version = "
|
|
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
|
|
123
|
-
"*/
|
|
124
|
-
"*/
|
|
125
|
-
"*/
|
|
126
|
-
"*/
|
|
122
|
+
"*/fncall/*",
|
|
123
|
+
"*/config/*",
|
|
124
|
+
"*/proxy/*",
|
|
125
|
+
"*/plugin/*",
|
|
126
|
+
"*/protocol/*",
|
|
127
|
+
"*/web/application.py",
|
|
127
128
|
"*/web/middleware.py",
|
|
128
|
-
"*/web/
|
|
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 =
|
|
133
|
+
fail_under = 90
|
|
145
134
|
exclude_lines = [
|
|
146
135
|
"pragma: no cover",
|
|
147
136
|
"def __repr__",
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
"""
|
|
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
|
|
7
|
+
import random
|
|
11
8
|
import time
|
|
12
9
|
from dataclasses import dataclass
|
|
13
10
|
from pathlib import Path
|
|
14
11
|
|
|
15
|
-
|
|
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,
|
|
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 =
|
|
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 * (
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
"""
|
|
1
|
+
"""Entropy ML (entml) (entml) 协议实现。
|
|
2
2
|
|
|
3
|
-
使用 <
|
|
4
|
-
<
|
|
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
|
-
<
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
</
|
|
43
|
-
</
|
|
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 `<
|
|
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"<
|
|
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'<
|
|
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'<
|
|
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'<
|
|
73
|
+
r'<entml:parameters>([\s\S]*?)</entml:parameters>',
|
|
74
74
|
re.DOTALL,
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
# ---------------------------------------------------------------------------
|
|
78
|
-
#
|
|
78
|
+
# Entml 协议
|
|
79
79
|
# ---------------------------------------------------------------------------
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
class
|
|
83
|
-
"""
|
|
82
|
+
class EntmlProtocol(ToolProtocol):
|
|
83
|
+
"""Entropy ML (entml) 格式工具调用协议适配器。
|
|
84
84
|
|
|
85
|
-
使用 <
|
|
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 "
|
|
91
|
+
return "entml"
|
|
92
92
|
|
|
93
|
-
_TRIGGER = "<
|
|
94
|
-
_END_TAG = "</
|
|
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 = "<
|
|
137
|
+
_TRIGGER_PREFIX = "<entml:function_calls"
|
|
138
138
|
|
|
139
139
|
def detect_start(self, buffer: str) -> Tuple[bool, int]:
|
|
140
|
-
"""检测 buffer 中是否包含 ``<
|
|
140
|
+
"""检测 buffer 中是否包含 ``<entml:function_calls...>`` 触发标记。
|
|
141
141
|
|
|
142
|
-
容忍变体(如 ``<
|
|
143
|
-
``<
|
|
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 <
|
|
174
|
+
# Extract parameters - support both <entml:parameter> and <entml:parameters>
|
|
175
175
|
args: Dict[str, Any] = {}
|
|
176
176
|
|
|
177
|
-
# Try <
|
|
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 <
|
|
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
|
-
"""将已知的完整
|
|
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
|
-
"""从响应文本中移除 <
|
|
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 对象列表渲染为
|
|
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'<
|
|
246
|
-
f"<
|
|
247
|
-
f"</
|
|
245
|
+
f'<entml:invoke name="{name}">'
|
|
246
|
+
f"<entml:parameters>{args}</entml:parameters>"
|
|
247
|
+
f"</entml:invoke>"
|
|
248
248
|
)
|
|
249
|
-
return f"<
|
|
249
|
+
return f"<entml:function_calls>{''.join(parts)}</entml:function_calls>"
|
|
250
250
|
|
|
251
251
|
def supports_streaming(self) -> bool:
|
|
252
|
-
"""
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 = "
|
|
61
|
+
default_protocol: str = "entml",
|
|
53
62
|
custom_prompt_en: str = "",
|
|
54
63
|
custom_prompt_zh: str = "",
|
|
55
64
|
platform_id: str = "",
|
|
@@ -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/
|
|
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/
|
|
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)]
|