echotools 2.3.11__tar.gz → 2.3.13__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.11 → echotools-2.3.13}/PKG-INFO +1 -1
- {echotools-2.3.11 → echotools-2.3.13}/pyproject.toml +1 -1
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/prompt/history.py +1 -1
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/protocols/entml.py +20 -26
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/protocols/entml_invoke.py +9 -2
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/protocols/entml_patterns.py +10 -1
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/protocols/entml_thinking.py +16 -4
- echotools-2.3.13/src/echotools/exec/fncall/protocols/entml_tools.py +110 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/protocols/entml_values.py +34 -28
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools.egg-info/SOURCES.txt +2 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_entml_protocol.py +132 -13
- echotools-2.3.13/src/tests/test_entml_tools.py +77 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_protocols.py +1 -1
- {echotools-2.3.11 → echotools-2.3.13}/README.md +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/docs/README.md +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/setup.cfg +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/compat.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/parsers/stream.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/fonts/tailwind.css +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools/version.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/bench_import.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/conftest.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_completion.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_config.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_errors.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_events.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_facade.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_ids.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_imports.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_io.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_keys.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_logger.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_merge.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_usage.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_web.py +0 -0
- {echotools-2.3.11 → echotools-2.3.13}/src/tests/test_web_broker.py +0 -0
|
@@ -9,44 +9,35 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from typing import Any, Dict, List, Optional, Tuple
|
|
11
11
|
|
|
12
|
-
from echotools.exec.fncall.prompt.templates import
|
|
13
|
-
_HISTORY_CLARIFY_EN,
|
|
14
|
-
_HISTORY_CLARIFY_ZH,
|
|
15
|
-
)
|
|
12
|
+
from echotools.exec.fncall.prompt.templates import _HISTORY_CLARIFY_EN
|
|
16
13
|
from echotools.exec.fncall.protocols.entml_invoke import (
|
|
17
14
|
format_entml_tool_calls,
|
|
18
15
|
parse_entml_tool_calls,
|
|
19
16
|
)
|
|
20
17
|
from echotools.exec.fncall.protocols.entml_patterns import BLOCK_RE
|
|
21
18
|
from echotools.exec.fncall.protocols.entml_thinking import build_entml_thinking_section
|
|
19
|
+
from echotools.exec.fncall.protocols.entml_tools import format_entml_tool_descs
|
|
22
20
|
from echotools.exec.fncall.shared.coercion import _build_param_schema_index
|
|
23
21
|
from echotools.exec.fncall.shared.normalization import normalize_tool_calls
|
|
24
22
|
from echotools.exec.protocol.base import ToolProtocol
|
|
25
23
|
|
|
26
24
|
_ENTML_INSTRUCTION = """\
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
All functions are defined inside a `<functions>` wrapper block. Each function is a JSON object inside a `<function>` tag containing `description`, `name`, and `parameters` (JSON Schema).
|
|
30
|
-
|
|
31
|
-
**Function Invocation Syntax:**
|
|
32
|
-
|
|
33
|
-
When calling tools, respond with ONLY the following XML block format:
|
|
25
|
+
In this environment you have access to a set of tools you can use to answer the user's question.
|
|
26
|
+
You can invoke functions by writing a "<entml:function_calls>" block like the following as part of your reply to the user:
|
|
34
27
|
|
|
35
28
|
<entml:function_calls>
|
|
36
|
-
<entml:invoke name="
|
|
37
|
-
<entml:parameter name="
|
|
29
|
+
<entml:invoke name="$FUNCTION_NAME">
|
|
30
|
+
<entml:parameter name="$PARAMETER_NAME">$PARAMETER_VALUE</entml:parameter>
|
|
31
|
+
...
|
|
32
|
+
</entml:invoke>
|
|
33
|
+
<entml:invoke name="$FUNCTION_NAME2">
|
|
34
|
+
...
|
|
38
35
|
</entml:invoke>
|
|
39
36
|
</entml:function_calls>
|
|
40
37
|
|
|
41
|
-
String and scalar parameters should be specified as
|
|
38
|
+
String and scalar parameters should be specified as is, while lists and objects should use JSON format.
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
## Function Call Instructions
|
|
46
|
-
|
|
47
|
-
Answer the user's request using the relevant tool(s), if they are available. Check that all required parameters are provided or can reasonably be inferred from context. If there are no relevant tools or missing required parameter values, ask the user. If the user provides a specific value for a parameter (e.g., in quotes), use that value EXACTLY. Do NOT make up values for or ask about optional parameters.
|
|
48
|
-
|
|
49
|
-
If you intend to call multiple tools and there are no dependencies between the calls, make all independent calls in the same function_calls block. Otherwise, wait for previous calls to finish to determine dependent values (do NOT use placeholders or guess missing parameters).
|
|
40
|
+
Here are the functions available in JSONSchema format:
|
|
50
41
|
"""
|
|
51
42
|
|
|
52
43
|
|
|
@@ -68,7 +59,7 @@ def format_entml_conversation_history(
|
|
|
68
59
|
def format_entml_current_user_message(message: str) -> str:
|
|
69
60
|
"""将当前用户消息包裹为 <entml:current_user_message> 块。"""
|
|
70
61
|
text = (message or "").strip()
|
|
71
|
-
return f"<entml:current_user_message>\n{text}\n</
|
|
62
|
+
return f"<entml:current_user_message>\n{text}\n</current_user_message>"
|
|
72
63
|
|
|
73
64
|
|
|
74
65
|
class EntmlProtocol(ToolProtocol):
|
|
@@ -84,6 +75,10 @@ class EntmlProtocol(ToolProtocol):
|
|
|
84
75
|
def get_trigger_tags(self) -> List[str]:
|
|
85
76
|
return [self._TRIGGER]
|
|
86
77
|
|
|
78
|
+
@staticmethod
|
|
79
|
+
def format_tool_descs(tools: List[Dict[str, Any]]) -> str:
|
|
80
|
+
return format_entml_tool_descs(tools)
|
|
81
|
+
|
|
87
82
|
def render_prompt(
|
|
88
83
|
self,
|
|
89
84
|
tool_descs: str,
|
|
@@ -94,11 +89,11 @@ class EntmlProtocol(ToolProtocol):
|
|
|
94
89
|
current_user_message: str = "",
|
|
95
90
|
protocol_options: Optional[Dict[str, Any]] = None,
|
|
96
91
|
) -> str:
|
|
97
|
-
sections: List[str] = [_ENTML_INSTRUCTION]
|
|
92
|
+
sections: List[str] = [_ENTML_INSTRUCTION + tool_descs]
|
|
93
|
+
|
|
98
94
|
thinking_section = build_entml_thinking_section(protocol_options)
|
|
99
95
|
if thinking_section:
|
|
100
96
|
sections.append(thinking_section)
|
|
101
|
-
sections.append("<functions>\n" + tool_descs + "\n</functions>")
|
|
102
97
|
|
|
103
98
|
if user_system_prompt and user_system_prompt.strip():
|
|
104
99
|
sections.append(
|
|
@@ -106,9 +101,8 @@ class EntmlProtocol(ToolProtocol):
|
|
|
106
101
|
)
|
|
107
102
|
|
|
108
103
|
if history_text:
|
|
109
|
-
clarify = _HISTORY_CLARIFY_ZH if lang == "zh" else _HISTORY_CLARIFY_EN
|
|
110
104
|
sections.append(
|
|
111
|
-
format_entml_conversation_history(history_text,
|
|
105
|
+
format_entml_conversation_history(history_text, _HISTORY_CLARIFY_EN)
|
|
112
106
|
)
|
|
113
107
|
|
|
114
108
|
if loop_warning:
|
|
@@ -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,
|
|
@@ -14,7 +14,7 @@ def build_entml_thinking_section(
|
|
|
14
14
|
if thinking_mode is None and max_thinking_length is None:
|
|
15
15
|
return ""
|
|
16
16
|
|
|
17
|
-
lines = [
|
|
17
|
+
lines: list[str] = []
|
|
18
18
|
if thinking_mode is not None:
|
|
19
19
|
mode = str(thinking_mode).strip()
|
|
20
20
|
if mode:
|
|
@@ -30,12 +30,24 @@ def build_entml_thinking_section(
|
|
|
30
30
|
[
|
|
31
31
|
"",
|
|
32
32
|
"If the thinking_mode is interleaved or auto, then after function "
|
|
33
|
-
"results you should strongly consider outputting a thinking block "
|
|
34
|
-
"
|
|
33
|
+
"results you should strongly consider outputting a thinking block. "
|
|
34
|
+
"Here is an example:",
|
|
35
|
+
"",
|
|
36
|
+
"<entml:function_calls>",
|
|
37
|
+
"...",
|
|
38
|
+
"</entml:function_calls>",
|
|
39
|
+
"",
|
|
40
|
+
"<function_results>",
|
|
41
|
+
"...",
|
|
42
|
+
"</function_results>",
|
|
35
43
|
"",
|
|
36
44
|
"<entml:thinking>",
|
|
37
|
-
"...thinking about results
|
|
45
|
+
"...thinking about results",
|
|
38
46
|
"</entml:thinking>",
|
|
47
|
+
"Whenever you have the result of a function call, think carefully "
|
|
48
|
+
"about whether an `<entml:thinking>` `</entml:thinking>` block "
|
|
49
|
+
"would be appropriate and strongly prefer to output a thinking "
|
|
50
|
+
"block if you are uncertain.",
|
|
39
51
|
]
|
|
40
52
|
)
|
|
41
53
|
|
|
@@ -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(
|
|
@@ -58,6 +58,7 @@ src/echotools/exec/fncall/protocols/entml.py
|
|
|
58
58
|
src/echotools/exec/fncall/protocols/entml_invoke.py
|
|
59
59
|
src/echotools/exec/fncall/protocols/entml_patterns.py
|
|
60
60
|
src/echotools/exec/fncall/protocols/entml_thinking.py
|
|
61
|
+
src/echotools/exec/fncall/protocols/entml_tools.py
|
|
61
62
|
src/echotools/exec/fncall/protocols/entml_values.py
|
|
62
63
|
src/echotools/exec/fncall/shared/__init__.py
|
|
63
64
|
src/echotools/exec/fncall/shared/coercion.py
|
|
@@ -149,6 +150,7 @@ src/tests/test_dispatch.py
|
|
|
149
150
|
src/tests/test_dispatch_extended.py
|
|
150
151
|
src/tests/test_dispatch_race.py
|
|
151
152
|
src/tests/test_entml_protocol.py
|
|
153
|
+
src/tests/test_entml_tools.py
|
|
152
154
|
src/tests/test_errors.py
|
|
153
155
|
src/tests/test_errors_common.py
|
|
154
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 "<entml:current_user_message>\nhi\n</
|
|
31
|
+
assert "<entml: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
|
|
|
@@ -88,7 +88,29 @@ def test_inject_with_history_entml_tags() -> None:
|
|
|
88
88
|
out = inject_fncall(msgs, tools, proto)
|
|
89
89
|
content = out[0]["content"]
|
|
90
90
|
assert "<entml:conversation_history>" in content
|
|
91
|
-
assert "<entml:current_user_message>\nnew\n</
|
|
91
|
+
assert "<entml:current_user_message>\nnew\n</current_user_message>" in content
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_entml_history_clarify_always_english() -> None:
|
|
95
|
+
proto = get_protocol("entml")
|
|
96
|
+
tools = [
|
|
97
|
+
{
|
|
98
|
+
"type": "function",
|
|
99
|
+
"function": {
|
|
100
|
+
"name": "search",
|
|
101
|
+
"description": "Search",
|
|
102
|
+
"parameters": {"type": "object", "properties": {}},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
msgs = [
|
|
107
|
+
{"role": "user", "content": "old"},
|
|
108
|
+
{"role": "assistant", "content": "ok"},
|
|
109
|
+
{"role": "user", "content": "new"},
|
|
110
|
+
]
|
|
111
|
+
content = inject_fncall(msgs, tools, proto, lang="zh")[0]["content"]
|
|
112
|
+
assert "The following is a transcript of completed interactions." in content
|
|
113
|
+
assert "以下是已完成的交互记录" not in content
|
|
92
114
|
|
|
93
115
|
|
|
94
116
|
@pytest.mark.parametrize(
|
|
@@ -98,21 +120,27 @@ def test_inject_with_history_entml_tags() -> None:
|
|
|
98
120
|
("false", {"type": "boolean"}, False),
|
|
99
121
|
("42", {"type": "integer"}, 42),
|
|
100
122
|
("3.14", {"type": "number"}, 3.14),
|
|
101
|
-
("null", None,
|
|
123
|
+
("null", None, "null"),
|
|
102
124
|
('["a","b"]', {"type": "array", "items": {"type": "string"}}, ["a", "b"]),
|
|
125
|
+
('["a","b"]', None, ["a", "b"]),
|
|
103
126
|
("plain text", {"type": "string"}, "plain text"),
|
|
104
127
|
("plain text", None, "plain text"),
|
|
128
|
+
("42", None, "42"),
|
|
129
|
+
("42", "int", 42),
|
|
130
|
+
("true", None, "true"),
|
|
131
|
+
("true", "bool", True),
|
|
105
132
|
],
|
|
106
133
|
)
|
|
107
134
|
def test_coerce_entml_parameter_value(raw, schema, expected) -> None:
|
|
108
|
-
|
|
135
|
+
if isinstance(schema, str):
|
|
136
|
+
assert coerce_entml_parameter_value(raw, type_hint=schema) == expected
|
|
137
|
+
else:
|
|
138
|
+
assert coerce_entml_parameter_value(raw, schema) == expected
|
|
109
139
|
|
|
110
140
|
|
|
111
141
|
def test_entml_instruction_matches_spec_format() -> None:
|
|
112
|
-
"""
|
|
142
|
+
"""示范格式:环境说明 + JSONSchema 工具块 + entml 调用标签。"""
|
|
113
143
|
proto = get_protocol("entml")
|
|
114
|
-
from echotools.exec.fncall.shared.normalization import format_tool_descs
|
|
115
|
-
|
|
116
144
|
tools = [
|
|
117
145
|
{
|
|
118
146
|
"type": "function",
|
|
@@ -134,17 +162,28 @@ def test_entml_instruction_matches_spec_format() -> None:
|
|
|
134
162
|
}
|
|
135
163
|
]
|
|
136
164
|
prompt = proto.render_prompt(
|
|
137
|
-
tool_descs=format_tool_descs(tools),
|
|
165
|
+
tool_descs=proto.format_tool_descs(tools),
|
|
138
166
|
lang="en",
|
|
139
167
|
current_user_message="pick one",
|
|
168
|
+
protocol_options={
|
|
169
|
+
"thinking_mode": "interleaved",
|
|
170
|
+
"max_thinking_length": 22000,
|
|
171
|
+
},
|
|
140
172
|
)
|
|
173
|
+
assert "In this environment you have access to a set of tools" in prompt
|
|
174
|
+
assert "Here are the functions available in JSONSchema format:" in prompt
|
|
175
|
+
assert "**ask_user_input_v0**" in prompt
|
|
176
|
+
assert '"name": "ask_user_input_v0"' in prompt
|
|
141
177
|
assert "<entml:function_calls>" in prompt
|
|
142
|
-
assert '<entml:invoke name="' in prompt
|
|
143
|
-
assert '<entml:parameter name="' in prompt
|
|
144
|
-
assert "String and scalar parameters should be specified as
|
|
178
|
+
assert '<entml:invoke name="$FUNCTION_NAME">' in prompt
|
|
179
|
+
assert '<entml:parameter name="$PARAMETER_NAME">' in prompt
|
|
180
|
+
assert "String and scalar parameters should be specified as is" in prompt
|
|
181
|
+
assert "<entml:thinking_mode>interleaved</entml:thinking_mode>" in prompt
|
|
182
|
+
assert "<entml:max_thinking_length>22000</entml:max_thinking_length>" in prompt
|
|
183
|
+
assert "<function_results>" in prompt
|
|
145
184
|
assert "<entml:conversation_history>" not in prompt
|
|
146
185
|
assert "<entml:history>" not in prompt
|
|
147
|
-
assert "<
|
|
186
|
+
assert "<functions>" not in prompt
|
|
148
187
|
|
|
149
188
|
|
|
150
189
|
def test_entml_roundtrip_parameter_format() -> None:
|
|
@@ -172,10 +211,90 @@ def test_entml_roundtrip_parameter_format() -> None:
|
|
|
172
211
|
parsed = parse_entml_tool_calls(rendered, None, None)
|
|
173
212
|
args = json.loads(parsed[0]["function"]["arguments"])
|
|
174
213
|
assert args["query"] == "hello"
|
|
175
|
-
assert args["limit"] == 3
|
|
214
|
+
assert args["limit"] == "3"
|
|
176
215
|
assert args["tags"] == ["a", "b"]
|
|
177
216
|
|
|
178
217
|
|
|
218
|
+
def test_entml_parse_parameter_type_attribute() -> None:
|
|
219
|
+
"""模型可能在 parameter 标签上附带 type 属性(如 type=\"str\")。"""
|
|
220
|
+
tools = [
|
|
221
|
+
{
|
|
222
|
+
"type": "function",
|
|
223
|
+
"function": {
|
|
224
|
+
"name": "get_weather",
|
|
225
|
+
"parameters": {
|
|
226
|
+
"type": "object",
|
|
227
|
+
"properties": {
|
|
228
|
+
"city": {"type": "string"},
|
|
229
|
+
"units": {"type": "string"},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"type": "function",
|
|
236
|
+
"function": {
|
|
237
|
+
"name": "search_web",
|
|
238
|
+
"parameters": {
|
|
239
|
+
"type": "object",
|
|
240
|
+
"properties": {
|
|
241
|
+
"query": {"type": "string"},
|
|
242
|
+
"limit": {"type": "integer"},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
]
|
|
248
|
+
schema_index = _build_param_schema_index(tools)
|
|
249
|
+
sample = (
|
|
250
|
+
'<entml:function_calls>'
|
|
251
|
+
'<entml:invoke name="get_weather">'
|
|
252
|
+
'<entml:parameter name="city" type="str">Shanghai</entml:parameter>'
|
|
253
|
+
'<entml:parameter name="units" type="str">celsius</entml:parameter>'
|
|
254
|
+
"</entml:invoke>"
|
|
255
|
+
'<entml:invoke name="search_web">'
|
|
256
|
+
'<entml:parameter name="query" type="str">上海 明天 降雨 2026-07-23</entml:parameter>'
|
|
257
|
+
'<entml:parameter name="limit" type="int">3</entml:parameter>'
|
|
258
|
+
"</entml:invoke>"
|
|
259
|
+
"</entml:function_calls>"
|
|
260
|
+
)
|
|
261
|
+
calls = parse_entml_tool_calls(sample, tools, schema_index)
|
|
262
|
+
assert len(calls) == 2
|
|
263
|
+
|
|
264
|
+
weather = json.loads(calls[0]["function"]["arguments"])
|
|
265
|
+
assert weather == {"city": "Shanghai", "units": "celsius"}
|
|
266
|
+
|
|
267
|
+
search = json.loads(calls[1]["function"]["arguments"])
|
|
268
|
+
assert search["query"] == "上海 明天 降雨 2026-07-23"
|
|
269
|
+
assert search["limit"] == 3
|
|
270
|
+
assert isinstance(search["limit"], int)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_entml_parse_parameter_default_str_without_type_attr() -> None:
|
|
274
|
+
"""无 type= 且无 schema 时,标量默认按 str 处理。"""
|
|
275
|
+
sample = (
|
|
276
|
+
'<entml:function_calls><entml:invoke name="echo">'
|
|
277
|
+
'<entml:parameter name="count">42</entml:parameter>'
|
|
278
|
+
'<entml:parameter name="flag">true</entml:parameter>'
|
|
279
|
+
"</entml:invoke></entml:function_calls>"
|
|
280
|
+
)
|
|
281
|
+
calls = parse_entml_tool_calls(sample, None, None)
|
|
282
|
+
args = json.loads(calls[0]["function"]["arguments"])
|
|
283
|
+
assert args == {"count": "42", "flag": "true"}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_entml_parse_parameter_type_hint_without_schema() -> None:
|
|
287
|
+
sample = (
|
|
288
|
+
'<entml:function_calls><entml:invoke name="echo">'
|
|
289
|
+
'<entml:parameter name="count" type="int">42</entml:parameter>'
|
|
290
|
+
'<entml:parameter name="flag" type="bool">true</entml:parameter>'
|
|
291
|
+
"</entml:invoke></entml:function_calls>"
|
|
292
|
+
)
|
|
293
|
+
calls = parse_entml_tool_calls(sample, None, None)
|
|
294
|
+
args = json.loads(calls[0]["function"]["arguments"])
|
|
295
|
+
assert args == {"count": 42, "flag": True}
|
|
296
|
+
|
|
297
|
+
|
|
179
298
|
def test_entml_parse_schema_coercion() -> None:
|
|
180
299
|
tools = [
|
|
181
300
|
{
|