echotools 2.3.12__tar.gz → 2.3.14__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-2.3.12 → echotools-2.3.14}/PKG-INFO +1 -1
- {echotools-2.3.12 → echotools-2.3.14}/pyproject.toml +1 -1
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/prompt/history.py +1 -1
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/protocols/entml.py +3 -3
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/protocols/entml_invoke.py +9 -2
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/protocols/entml_patterns.py +10 -1
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/protocols/entml_thinking.py +5 -6
- echotools-2.3.14/src/echotools/exec/fncall/protocols/entml_tools.py +110 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/protocols/entml_values.py +34 -28
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools.egg-info/SOURCES.txt +1 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_entml_protocol.py +96 -6
- echotools-2.3.14/src/tests/test_entml_tools.py +77 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_protocols.py +1 -1
- echotools-2.3.12/src/echotools/exec/fncall/protocols/entml_tools.py +0 -26
- {echotools-2.3.12 → echotools-2.3.14}/README.md +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/docs/README.md +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/setup.cfg +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/compat.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/parsers/stream.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/fonts/tailwind.css +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools/version.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/bench_import.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/conftest.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_completion.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_config.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_errors.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_events.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_facade.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_ids.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_imports.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_io.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_keys.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_logger.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_merge.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_usage.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_web.py +0 -0
- {echotools-2.3.12 → echotools-2.3.14}/src/tests/test_web_broker.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
- 工具调用:<entml:function_calls> / <entml:invoke> / <entml:parameter>
|
|
4
4
|
- 对话历史:<entml:conversation_history>
|
|
5
|
-
- 当前用户消息:<
|
|
5
|
+
- 当前用户消息:<current_user_message>
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -57,9 +57,9 @@ def format_entml_conversation_history(
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def format_entml_current_user_message(message: str) -> str:
|
|
60
|
-
"""将当前用户消息包裹为 <
|
|
60
|
+
"""将当前用户消息包裹为 <current_user_message> 块。"""
|
|
61
61
|
text = (message or "").strip()
|
|
62
|
-
return f"<
|
|
62
|
+
return f"<current_user_message>\n{text}\n</current_user_message>"
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
class EntmlProtocol(ToolProtocol):
|
|
@@ -8,6 +8,7 @@ from .entml_patterns import (
|
|
|
8
8
|
INVOKE_RE,
|
|
9
9
|
PARAM_RE,
|
|
10
10
|
PARAMETERS_RE,
|
|
11
|
+
extract_parameter_type_attr,
|
|
11
12
|
parse_sub_tags,
|
|
12
13
|
)
|
|
13
14
|
from .entml_values import coerce_entml_arguments, coerce_entml_parameter_value
|
|
@@ -37,9 +38,15 @@ def parse_invoke_args(
|
|
|
37
38
|
args: Dict[str, Any] = {}
|
|
38
39
|
for param_m in PARAM_RE.finditer(body):
|
|
39
40
|
pname = param_m.group(1).strip()
|
|
40
|
-
|
|
41
|
+
attrs = param_m.group(2) or ""
|
|
42
|
+
pval = param_m.group(3)
|
|
43
|
+
type_hint = extract_parameter_type_attr(attrs)
|
|
41
44
|
pschema = func_props.get(pname) or {}
|
|
42
|
-
args[pname] = coerce_entml_parameter_value(
|
|
45
|
+
args[pname] = coerce_entml_parameter_value(
|
|
46
|
+
pval,
|
|
47
|
+
pschema or None,
|
|
48
|
+
type_hint=type_hint if not pschema else None,
|
|
49
|
+
)
|
|
43
50
|
|
|
44
51
|
return coerce_entml_arguments(args, name, schema_index)
|
|
45
52
|
|
|
@@ -14,9 +14,10 @@ INVOKE_RE = re.compile(
|
|
|
14
14
|
re.DOTALL,
|
|
15
15
|
)
|
|
16
16
|
PARAM_RE = re.compile(
|
|
17
|
-
r'<entml:parameter\s+name="([^"]+)">\s*([\s\S]*?)\s*</entml:parameter>',
|
|
17
|
+
r'<entml:parameter\s+name="([^"]+)"([^>]*)>\s*([\s\S]*?)\s*</entml:parameter>',
|
|
18
18
|
re.DOTALL,
|
|
19
19
|
)
|
|
20
|
+
_PARAM_TYPE_ATTR_RE = re.compile(r'\btype="([^"]+)"')
|
|
20
21
|
PARAMETERS_RE = re.compile(
|
|
21
22
|
r'<entml:parameters>([\s\S]*?)</entml:parameters>',
|
|
22
23
|
re.DOTALL,
|
|
@@ -27,6 +28,14 @@ SUB_TAG_RE = re.compile(
|
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
def extract_parameter_type_attr(attrs: str) -> Optional[str]:
|
|
32
|
+
"""从 parameter 开标签属性中提取 type=\"...\"。"""
|
|
33
|
+
match = _PARAM_TYPE_ATTR_RE.search(attrs or "")
|
|
34
|
+
if not match:
|
|
35
|
+
return None
|
|
36
|
+
return match.group(1).strip()
|
|
37
|
+
|
|
38
|
+
|
|
30
39
|
def parse_sub_tags(
|
|
31
40
|
content: str,
|
|
32
41
|
schema_index: Optional[Dict[str, Any]] = None,
|
|
@@ -41,13 +41,12 @@ def build_entml_thinking_section(
|
|
|
41
41
|
"...",
|
|
42
42
|
"</function_results>",
|
|
43
43
|
"",
|
|
44
|
-
"<
|
|
44
|
+
"<thinking>",
|
|
45
45
|
"...thinking about results",
|
|
46
|
-
"</
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"block if you are uncertain.",
|
|
46
|
+
"</thinking>",
|
|
47
|
+
"At the very start of your response, think carefully about whether "
|
|
48
|
+
"a `<thinking>` `</thinking>` block would be appropriate and "
|
|
49
|
+
"strongly prefer to output a thinking block if you are uncertain.",
|
|
51
50
|
]
|
|
52
51
|
)
|
|
53
52
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Dict, List, Mapping
|
|
6
|
+
|
|
7
|
+
__all__ = ["format_entml_tool_descs"]
|
|
8
|
+
|
|
9
|
+
_SCHEMA_KEY_ORDER = (
|
|
10
|
+
"description",
|
|
11
|
+
"type",
|
|
12
|
+
"enum",
|
|
13
|
+
"items",
|
|
14
|
+
"properties",
|
|
15
|
+
"required",
|
|
16
|
+
"minItems",
|
|
17
|
+
"maxItems",
|
|
18
|
+
"minimum",
|
|
19
|
+
"maximum",
|
|
20
|
+
"default",
|
|
21
|
+
"additionalProperties",
|
|
22
|
+
"oneOf",
|
|
23
|
+
"anyOf",
|
|
24
|
+
"allOf",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_DESC_LINE_RE = re.compile(
|
|
28
|
+
r'^(\s*)"description": "((?:[^"\\]|\\.)*)"(,?)$',
|
|
29
|
+
re.MULTILINE,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _sort_schema_keys(node: Any) -> Any:
|
|
34
|
+
"""递归整理 JSON Schema 字段顺序(properties 的键名保持原序)。"""
|
|
35
|
+
if isinstance(node, list):
|
|
36
|
+
return [_sort_schema_keys(item) for item in node]
|
|
37
|
+
if not isinstance(node, dict):
|
|
38
|
+
return node
|
|
39
|
+
|
|
40
|
+
ordered_keys = [key for key in _SCHEMA_KEY_ORDER if key in node]
|
|
41
|
+
remaining_keys = sorted(key for key in node if key not in _SCHEMA_KEY_ORDER)
|
|
42
|
+
out: Dict[str, Any] = {}
|
|
43
|
+
for key in ordered_keys + remaining_keys:
|
|
44
|
+
value = node[key]
|
|
45
|
+
if key == "properties" and isinstance(value, Mapping):
|
|
46
|
+
out[key] = {
|
|
47
|
+
prop_name: _sort_schema_keys(prop_schema)
|
|
48
|
+
for prop_name, prop_schema in value.items()
|
|
49
|
+
}
|
|
50
|
+
else:
|
|
51
|
+
out[key] = _sort_schema_keys(value)
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _normalize_parameters(params: Any) -> Dict[str, Any]:
|
|
56
|
+
if not isinstance(params, dict):
|
|
57
|
+
return {"type": "object", "properties": {}}
|
|
58
|
+
normalized = dict(params)
|
|
59
|
+
normalized.setdefault("type", "object")
|
|
60
|
+
normalized.setdefault("properties", {})
|
|
61
|
+
return _sort_schema_keys(normalized)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _escape_multiline_description_line(line: str) -> str:
|
|
65
|
+
return line.replace("\\", "\\\\").replace('"', '\\"')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _expand_description_multiline(json_text: str) -> str:
|
|
69
|
+
"""将 description 中的 \\n 展开为可读多行(对齐 antml 示范排版)。"""
|
|
70
|
+
|
|
71
|
+
def _repl(match: re.Match[str]) -> str:
|
|
72
|
+
indent, escaped, comma = match.groups()
|
|
73
|
+
decoded = json.loads('"' + escaped + '"')
|
|
74
|
+
if "\n" not in decoded:
|
|
75
|
+
return match.group(0)
|
|
76
|
+
lines = decoded.split("\n")
|
|
77
|
+
first = _escape_multiline_description_line(lines[0])
|
|
78
|
+
if len(lines) == 1:
|
|
79
|
+
return f'{indent}"description": "{first}"{comma}'
|
|
80
|
+
body = first + "\n" + "\n".join(
|
|
81
|
+
_escape_multiline_description_line(line) for line in lines[1:]
|
|
82
|
+
)
|
|
83
|
+
return f'{indent}"description": "{body}"{comma}'
|
|
84
|
+
|
|
85
|
+
return _DESC_LINE_RE.sub(_repl, json_text)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _format_tool_json(payload: Mapping[str, Any]) -> str:
|
|
89
|
+
sorted_payload = _sort_schema_keys(dict(payload))
|
|
90
|
+
body = json.dumps(sorted_payload, ensure_ascii=False, indent=2)
|
|
91
|
+
return _expand_description_multiline(body)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def format_entml_tool_descs(tools: List[Dict[str, Any]]) -> str:
|
|
95
|
+
"""将工具列表格式化为 **name** + JSON Schema 代码块(对齐 entml/antml 示范)。"""
|
|
96
|
+
if not tools:
|
|
97
|
+
return ""
|
|
98
|
+
|
|
99
|
+
blocks: List[str] = []
|
|
100
|
+
for tool in tools:
|
|
101
|
+
fn = tool.get("function", tool)
|
|
102
|
+
name = str(fn.get("name") or "unknown")
|
|
103
|
+
payload = {
|
|
104
|
+
"description": fn.get("description") or "",
|
|
105
|
+
"name": name,
|
|
106
|
+
"parameters": _normalize_parameters(fn.get("parameters")),
|
|
107
|
+
}
|
|
108
|
+
body = _format_tool_json(payload)
|
|
109
|
+
blocks.append(f"**{name}**\n\n```json\n{body}\n```")
|
|
110
|
+
return "\n\n".join(blocks)
|
|
@@ -5,15 +5,46 @@ from typing import Any, Dict, Optional
|
|
|
5
5
|
|
|
6
6
|
from echotools.exec.fncall.shared.coercion import _coerce_param_value
|
|
7
7
|
|
|
8
|
+
_TYPE_HINT_TO_JSON_TYPE = {
|
|
9
|
+
"str": "string",
|
|
10
|
+
"string": "string",
|
|
11
|
+
"int": "integer",
|
|
12
|
+
"integer": "integer",
|
|
13
|
+
"float": "number",
|
|
14
|
+
"number": "number",
|
|
15
|
+
"double": "number",
|
|
16
|
+
"bool": "boolean",
|
|
17
|
+
"boolean": "boolean",
|
|
18
|
+
"array": "array",
|
|
19
|
+
"list": "array",
|
|
20
|
+
"object": "object",
|
|
21
|
+
"dict": "object",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _schema_from_type_hint(type_hint: str) -> Optional[Dict[str, Any]]:
|
|
26
|
+
json_type = _TYPE_HINT_TO_JSON_TYPE.get((type_hint or "").strip().lower())
|
|
27
|
+
if not json_type:
|
|
28
|
+
return None
|
|
29
|
+
schema: Dict[str, Any] = {"type": json_type}
|
|
30
|
+
if json_type == "array":
|
|
31
|
+
schema["items"] = {}
|
|
32
|
+
return schema
|
|
33
|
+
|
|
8
34
|
|
|
9
35
|
def coerce_entml_parameter_value(
|
|
10
36
|
raw: str,
|
|
11
37
|
schema: Optional[Dict[str, Any]] = None,
|
|
38
|
+
type_hint: Optional[str] = None,
|
|
12
39
|
) -> Any:
|
|
13
|
-
"""将 entml 参数文本按 schema
|
|
40
|
+
"""将 entml 参数文本按 schema、type 属性或默认 str 规则转为 Python 值。"""
|
|
14
41
|
if schema:
|
|
15
42
|
return _coerce_param_value(raw, schema)
|
|
16
43
|
|
|
44
|
+
hint_schema = _schema_from_type_hint(type_hint) if type_hint else None
|
|
45
|
+
if hint_schema:
|
|
46
|
+
return _coerce_param_value(raw, hint_schema)
|
|
47
|
+
|
|
17
48
|
stripped = (raw or "").strip()
|
|
18
49
|
if not stripped:
|
|
19
50
|
return ""
|
|
@@ -22,34 +53,9 @@ def coerce_entml_parameter_value(
|
|
|
22
53
|
try:
|
|
23
54
|
return json.loads(stripped)
|
|
24
55
|
except json.JSONDecodeError:
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
lowered = stripped.lower()
|
|
28
|
-
if lowered == "null":
|
|
29
|
-
return None
|
|
30
|
-
if lowered == "true":
|
|
31
|
-
return True
|
|
32
|
-
if lowered == "false":
|
|
33
|
-
return False
|
|
34
|
-
|
|
35
|
-
if stripped.isdigit() or (
|
|
36
|
-
stripped.startswith("-") and stripped[1:].isdigit()
|
|
37
|
-
):
|
|
38
|
-
try:
|
|
39
|
-
return int(stripped)
|
|
40
|
-
except ValueError:
|
|
41
|
-
pass
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
if any(ch in stripped for ch in ".eE"):
|
|
45
|
-
fval = float(stripped)
|
|
46
|
-
if fval.is_integer() and "." not in stripped and "e" not in lowered:
|
|
47
|
-
return int(fval)
|
|
48
|
-
return fval
|
|
49
|
-
except ValueError:
|
|
50
|
-
pass
|
|
56
|
+
return stripped
|
|
51
57
|
|
|
52
|
-
return
|
|
58
|
+
return stripped
|
|
53
59
|
|
|
54
60
|
|
|
55
61
|
def coerce_entml_arguments(
|
|
@@ -150,6 +150,7 @@ src/tests/test_dispatch.py
|
|
|
150
150
|
src/tests/test_dispatch_extended.py
|
|
151
151
|
src/tests/test_dispatch_race.py
|
|
152
152
|
src/tests/test_entml_protocol.py
|
|
153
|
+
src/tests/test_entml_tools.py
|
|
153
154
|
src/tests/test_errors.py
|
|
154
155
|
src/tests/test_errors_common.py
|
|
155
156
|
src/tests/test_events.py
|
|
@@ -28,7 +28,7 @@ def test_inject_no_tools_entml_tags() -> None:
|
|
|
28
28
|
result = inject_fncall(msgs, [], proto)
|
|
29
29
|
assert len(result) == 1
|
|
30
30
|
content = result[0]["content"]
|
|
31
|
-
assert "<
|
|
31
|
+
assert "<current_user_message>\nhi\n</current_user_message>" in content
|
|
32
32
|
assert "<entml:thinking_mode>" not in content
|
|
33
33
|
assert "<entml:max_thinking_length>" not in content
|
|
34
34
|
|
|
@@ -60,7 +60,9 @@ def test_inject_with_thinking_options_only_when_declared() -> None:
|
|
|
60
60
|
)[0]["content"]
|
|
61
61
|
assert "<entml:thinking_mode>interleaved</entml:thinking_mode>" in with_opts
|
|
62
62
|
assert "<entml:max_thinking_length>22000</entml:max_thinking_length>" in with_opts
|
|
63
|
-
assert "<
|
|
63
|
+
assert "<thinking>" in with_opts
|
|
64
|
+
assert "At the very start of your response" in with_opts
|
|
65
|
+
assert "<entml:thinking>" not in with_opts
|
|
64
66
|
|
|
65
67
|
|
|
66
68
|
def test_build_entml_thinking_section_empty_without_options() -> None:
|
|
@@ -88,7 +90,7 @@ def test_inject_with_history_entml_tags() -> None:
|
|
|
88
90
|
out = inject_fncall(msgs, tools, proto)
|
|
89
91
|
content = out[0]["content"]
|
|
90
92
|
assert "<entml:conversation_history>" in content
|
|
91
|
-
assert "<
|
|
93
|
+
assert "<current_user_message>\nnew\n</current_user_message>" in content
|
|
92
94
|
|
|
93
95
|
|
|
94
96
|
def test_entml_history_clarify_always_english() -> None:
|
|
@@ -120,14 +122,22 @@ def test_entml_history_clarify_always_english() -> None:
|
|
|
120
122
|
("false", {"type": "boolean"}, False),
|
|
121
123
|
("42", {"type": "integer"}, 42),
|
|
122
124
|
("3.14", {"type": "number"}, 3.14),
|
|
123
|
-
("null", None,
|
|
125
|
+
("null", None, "null"),
|
|
124
126
|
('["a","b"]', {"type": "array", "items": {"type": "string"}}, ["a", "b"]),
|
|
127
|
+
('["a","b"]', None, ["a", "b"]),
|
|
125
128
|
("plain text", {"type": "string"}, "plain text"),
|
|
126
129
|
("plain text", None, "plain text"),
|
|
130
|
+
("42", None, "42"),
|
|
131
|
+
("42", "int", 42),
|
|
132
|
+
("true", None, "true"),
|
|
133
|
+
("true", "bool", True),
|
|
127
134
|
],
|
|
128
135
|
)
|
|
129
136
|
def test_coerce_entml_parameter_value(raw, schema, expected) -> None:
|
|
130
|
-
|
|
137
|
+
if isinstance(schema, str):
|
|
138
|
+
assert coerce_entml_parameter_value(raw, type_hint=schema) == expected
|
|
139
|
+
else:
|
|
140
|
+
assert coerce_entml_parameter_value(raw, schema) == expected
|
|
131
141
|
|
|
132
142
|
|
|
133
143
|
def test_entml_instruction_matches_spec_format() -> None:
|
|
@@ -203,10 +213,90 @@ def test_entml_roundtrip_parameter_format() -> None:
|
|
|
203
213
|
parsed = parse_entml_tool_calls(rendered, None, None)
|
|
204
214
|
args = json.loads(parsed[0]["function"]["arguments"])
|
|
205
215
|
assert args["query"] == "hello"
|
|
206
|
-
assert args["limit"] == 3
|
|
216
|
+
assert args["limit"] == "3"
|
|
207
217
|
assert args["tags"] == ["a", "b"]
|
|
208
218
|
|
|
209
219
|
|
|
220
|
+
def test_entml_parse_parameter_type_attribute() -> None:
|
|
221
|
+
"""模型可能在 parameter 标签上附带 type 属性(如 type=\"str\")。"""
|
|
222
|
+
tools = [
|
|
223
|
+
{
|
|
224
|
+
"type": "function",
|
|
225
|
+
"function": {
|
|
226
|
+
"name": "get_weather",
|
|
227
|
+
"parameters": {
|
|
228
|
+
"type": "object",
|
|
229
|
+
"properties": {
|
|
230
|
+
"city": {"type": "string"},
|
|
231
|
+
"units": {"type": "string"},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"type": "function",
|
|
238
|
+
"function": {
|
|
239
|
+
"name": "search_web",
|
|
240
|
+
"parameters": {
|
|
241
|
+
"type": "object",
|
|
242
|
+
"properties": {
|
|
243
|
+
"query": {"type": "string"},
|
|
244
|
+
"limit": {"type": "integer"},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
]
|
|
250
|
+
schema_index = _build_param_schema_index(tools)
|
|
251
|
+
sample = (
|
|
252
|
+
'<entml:function_calls>'
|
|
253
|
+
'<entml:invoke name="get_weather">'
|
|
254
|
+
'<entml:parameter name="city" type="str">Shanghai</entml:parameter>'
|
|
255
|
+
'<entml:parameter name="units" type="str">celsius</entml:parameter>'
|
|
256
|
+
"</entml:invoke>"
|
|
257
|
+
'<entml:invoke name="search_web">'
|
|
258
|
+
'<entml:parameter name="query" type="str">上海 明天 降雨 2026-07-23</entml:parameter>'
|
|
259
|
+
'<entml:parameter name="limit" type="int">3</entml:parameter>'
|
|
260
|
+
"</entml:invoke>"
|
|
261
|
+
"</entml:function_calls>"
|
|
262
|
+
)
|
|
263
|
+
calls = parse_entml_tool_calls(sample, tools, schema_index)
|
|
264
|
+
assert len(calls) == 2
|
|
265
|
+
|
|
266
|
+
weather = json.loads(calls[0]["function"]["arguments"])
|
|
267
|
+
assert weather == {"city": "Shanghai", "units": "celsius"}
|
|
268
|
+
|
|
269
|
+
search = json.loads(calls[1]["function"]["arguments"])
|
|
270
|
+
assert search["query"] == "上海 明天 降雨 2026-07-23"
|
|
271
|
+
assert search["limit"] == 3
|
|
272
|
+
assert isinstance(search["limit"], int)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_entml_parse_parameter_default_str_without_type_attr() -> None:
|
|
276
|
+
"""无 type= 且无 schema 时,标量默认按 str 处理。"""
|
|
277
|
+
sample = (
|
|
278
|
+
'<entml:function_calls><entml:invoke name="echo">'
|
|
279
|
+
'<entml:parameter name="count">42</entml:parameter>'
|
|
280
|
+
'<entml:parameter name="flag">true</entml:parameter>'
|
|
281
|
+
"</entml:invoke></entml:function_calls>"
|
|
282
|
+
)
|
|
283
|
+
calls = parse_entml_tool_calls(sample, None, None)
|
|
284
|
+
args = json.loads(calls[0]["function"]["arguments"])
|
|
285
|
+
assert args == {"count": "42", "flag": "true"}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_entml_parse_parameter_type_hint_without_schema() -> None:
|
|
289
|
+
sample = (
|
|
290
|
+
'<entml:function_calls><entml:invoke name="echo">'
|
|
291
|
+
'<entml:parameter name="count" type="int">42</entml:parameter>'
|
|
292
|
+
'<entml:parameter name="flag" type="bool">true</entml:parameter>'
|
|
293
|
+
"</entml:invoke></entml:function_calls>"
|
|
294
|
+
)
|
|
295
|
+
calls = parse_entml_tool_calls(sample, None, None)
|
|
296
|
+
args = json.loads(calls[0]["function"]["arguments"])
|
|
297
|
+
assert args == {"count": 42, "flag": True}
|
|
298
|
+
|
|
299
|
+
|
|
210
300
|
def test_entml_parse_schema_coercion() -> None:
|
|
211
301
|
tools = [
|
|
212
302
|
{
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from echotools.exec.fncall.protocols.entml_tools import format_entml_tool_descs
|
|
2
|
+
|
|
3
|
+
ASK_USER_TOOL = {
|
|
4
|
+
"type": "function",
|
|
5
|
+
"function": {
|
|
6
|
+
"name": "ask_user_input_v0",
|
|
7
|
+
"description": (
|
|
8
|
+
"USE THIS TOOL WHENEVER YOU HAVE A QUESTION FOR THE USER.\n\n"
|
|
9
|
+
"USE THIS TOOL WHEN:\n"
|
|
10
|
+
"- User asks a question with 2-10 reasonable answers"
|
|
11
|
+
),
|
|
12
|
+
"parameters": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"questions": {
|
|
16
|
+
"items": {
|
|
17
|
+
"properties": {
|
|
18
|
+
"options": {
|
|
19
|
+
"items": {"type": "string"},
|
|
20
|
+
"maxItems": 4,
|
|
21
|
+
"minItems": 2,
|
|
22
|
+
"type": "array",
|
|
23
|
+
},
|
|
24
|
+
"question": {"type": "string"},
|
|
25
|
+
"type": {
|
|
26
|
+
"default": "single_select",
|
|
27
|
+
"enum": [
|
|
28
|
+
"single_select",
|
|
29
|
+
"multi_select",
|
|
30
|
+
"rank_priorities",
|
|
31
|
+
],
|
|
32
|
+
"type": "string",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
"required": ["question", "options"],
|
|
36
|
+
"type": "object",
|
|
37
|
+
},
|
|
38
|
+
"maxItems": 3,
|
|
39
|
+
"minItems": 1,
|
|
40
|
+
"type": "array",
|
|
41
|
+
"description": "1-3 questions to ask the user",
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"required": ["questions"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_entml_tool_descs_include_schema_types_and_required() -> None:
|
|
51
|
+
out = format_entml_tool_descs([ASK_USER_TOOL])
|
|
52
|
+
assert "**ask_user_input_v0**" in out
|
|
53
|
+
assert '"type": "object"' in out
|
|
54
|
+
assert '"type": "array"' in out
|
|
55
|
+
assert '"type": "string"' in out
|
|
56
|
+
assert '"enum":' in out
|
|
57
|
+
assert '"required":' in out
|
|
58
|
+
assert '"minItems":' in out
|
|
59
|
+
assert '"maxItems":' in out
|
|
60
|
+
assert '"default": "single_select"' in out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_entml_tool_descs_expand_multiline_description() -> None:
|
|
64
|
+
out = format_entml_tool_descs([ASK_USER_TOOL])
|
|
65
|
+
assert "USE THIS TOOL WHENEVER YOU HAVE A QUESTION FOR THE USER.\n\n" in out
|
|
66
|
+
assert "USE THIS TOOL WHEN:\n" in out
|
|
67
|
+
assert "- User asks a question with 2-10 reasonable answers" in out
|
|
68
|
+
assert "\\nUSE THIS TOOL WHEN" not in out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_entml_tool_descs_schema_key_order() -> None:
|
|
72
|
+
out = format_entml_tool_descs([ASK_USER_TOOL])
|
|
73
|
+
desc_pos = out.index('"description":')
|
|
74
|
+
type_pos = out.index('"type": "object"', desc_pos)
|
|
75
|
+
props_pos = out.index('"properties":', type_pos)
|
|
76
|
+
required_pos = out.index('"required":', props_pos)
|
|
77
|
+
assert desc_pos < type_pos < props_pos < required_pos
|
|
@@ -22,4 +22,4 @@ def test_inject_no_tools() -> None:
|
|
|
22
22
|
result = inject_fncall(msgs, [], proto)
|
|
23
23
|
assert len(result) == 1
|
|
24
24
|
assert result[0]["role"] == "user"
|
|
25
|
-
assert "<
|
|
25
|
+
assert "<current_user_message>\nhi\n</current_user_message>" in result[0]["content"]
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from typing import Any, Dict, List
|
|
5
|
-
|
|
6
|
-
__all__ = ["format_entml_tool_descs"]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def format_entml_tool_descs(tools: List[Dict[str, Any]]) -> str:
|
|
10
|
-
"""将工具列表格式化为 **name** + JSON Schema 代码块(对齐 entml/antml 示范)。"""
|
|
11
|
-
if not tools:
|
|
12
|
-
return ""
|
|
13
|
-
|
|
14
|
-
blocks: List[str] = []
|
|
15
|
-
for tool in tools:
|
|
16
|
-
fn = tool.get("function", tool)
|
|
17
|
-
name = str(fn.get("name") or "unknown")
|
|
18
|
-
payload = {
|
|
19
|
-
"description": fn.get("description") or "",
|
|
20
|
-
"name": name,
|
|
21
|
-
"parameters": fn.get("parameters")
|
|
22
|
-
or {"type": "object", "properties": {}},
|
|
23
|
-
}
|
|
24
|
-
body = json.dumps(payload, ensure_ascii=False, indent=2)
|
|
25
|
-
blocks.append(f"**{name}**\n\n```json\n{body}\n```")
|
|
26
|
-
return "\n\n".join(blocks)
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|