echotools 2.3.40__tar.gz → 2.3.43__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.40 → echotools-2.3.43}/PKG-INFO +1 -1
- {echotools-2.3.40 → echotools-2.3.43}/pyproject.toml +1 -1
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/parsers/stream.py +3 -3
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/prompt/history.py +39 -0
- echotools-2.3.43/src/echotools/exec/fncall/prompt/history_format.py +116 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/prompt/inject.py +6 -5
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/prompt/prompt_helpers.py +1 -1
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/entml.py +29 -6
- echotools-2.3.43/src/echotools/exec/fncall/protocols/entml_think/__init__.py +31 -0
- echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_thinking.py → echotools-2.3.43/src/echotools/exec/fncall/protocols/entml_think/core.py +21 -10
- echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_thinking_parse.py → echotools-2.3.43/src/echotools/exec/fncall/protocols/entml_think/parse.py +47 -38
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools.egg-info/SOURCES.txt +4 -4
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_entml_protocol.py +47 -14
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_entml_thinking_history.py +1 -1
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_entml_thinking_parse.py +1 -1
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_fncall_stream_thinking.py +1 -1
- echotools-2.3.40/src/echotools/exec/fncall/prompt/history_format.py +0 -89
- echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_sanitize.py +0 -24
- {echotools-2.3.40 → echotools-2.3.43}/README.md +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/docs/README.md +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/setup.cfg +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/compat.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
- /echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_thinking_history.py → /echotools-2.3.43/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/fonts/tailwind.css +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools/version.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/bench_import.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/conftest.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_completion.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_config.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_console.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_errors.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_events.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_facade.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_ids.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_imports.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_io.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_keys.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_logger.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_merge.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_usage.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_web.py +0 -0
- {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_web_broker.py +0 -0
|
@@ -13,7 +13,7 @@ from echotools.exec.protocol.base import ToolProtocol
|
|
|
13
13
|
# 懒导入:仅 entml 协议需要
|
|
14
14
|
def _make_thinking_filter(protocol: ToolProtocol):
|
|
15
15
|
if getattr(protocol, "id", None) == "entml":
|
|
16
|
-
from echotools.exec.fncall.protocols.
|
|
16
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
17
17
|
EntmlThinkingStreamFilter,
|
|
18
18
|
)
|
|
19
19
|
return EntmlThinkingStreamFilter()
|
|
@@ -177,7 +177,7 @@ class FncallStreamParser:
|
|
|
177
177
|
self._waiting_tail = ""
|
|
178
178
|
|
|
179
179
|
if self._thinking_filter is not None:
|
|
180
|
-
from echotools.exec.fncall.protocols.
|
|
180
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
181
181
|
has_unclosed_entml_thinking,
|
|
182
182
|
)
|
|
183
183
|
if (
|
|
@@ -262,7 +262,7 @@ class FncallStreamParser:
|
|
|
262
262
|
|
|
263
263
|
# 兜底:剥离残留 <entml:thinking>(holdback 边界/分片异常时)
|
|
264
264
|
if self._thinking_filter is not None and clean_text:
|
|
265
|
-
from echotools.exec.fncall.protocols.
|
|
265
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
266
266
|
split_entml_thinking,
|
|
267
267
|
)
|
|
268
268
|
clean_text, more_thinking = split_entml_thinking(clean_text)
|
|
@@ -39,6 +39,45 @@ _TOOL_CALL_MARKER_RE = re.compile(
|
|
|
39
39
|
re.MULTILINE,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
+
# entml 标签清理正则
|
|
43
|
+
_ENTML_FNCALL_BLOCK_RE = re.compile(
|
|
44
|
+
r"<entml:function_calls\b[^>]*>.*?</entml:function_calls>",
|
|
45
|
+
re.DOTALL,
|
|
46
|
+
)
|
|
47
|
+
_ENTML_INVOKE_RE = re.compile(
|
|
48
|
+
r'<entml:invoke\s+name="[^"]+"\s*>.*?</entml:invoke>',
|
|
49
|
+
re.DOTALL,
|
|
50
|
+
)
|
|
51
|
+
_ENTML_PARAMETERS_RE = re.compile(
|
|
52
|
+
r'<entml:parameters>.*?</entml:parameters>',
|
|
53
|
+
re.DOTALL,
|
|
54
|
+
)
|
|
55
|
+
_ENTML_PARAM_RE = re.compile(
|
|
56
|
+
r'<entml:parameter\s+name="[^"]+"[^>]*>.*?</entml:parameter>',
|
|
57
|
+
re.DOTALL,
|
|
58
|
+
)
|
|
59
|
+
# 清理任意 entml:* 标签
|
|
60
|
+
_ENTML_ANY_TAG_RE = re.compile(
|
|
61
|
+
r'<entml:[a-zA-Z_][\w]*\b[^>]*>.*?</entml:[a-zA-Z_][\w]*>',
|
|
62
|
+
re.DOTALL,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _clean_entml_tags(content: str) -> str:
|
|
67
|
+
"""移除内容中的所有 entml 标签及其内部内容。"""
|
|
68
|
+
if not content:
|
|
69
|
+
return content
|
|
70
|
+
cleaned = content
|
|
71
|
+
cleaned = _ENTML_FNCALL_BLOCK_RE.sub("", cleaned)
|
|
72
|
+
cleaned = _ENTML_INVOKE_RE.sub("", cleaned)
|
|
73
|
+
cleaned = _ENTML_PARAMETERS_RE.sub("", cleaned)
|
|
74
|
+
cleaned = _ENTML_PARAM_RE.sub("", cleaned)
|
|
75
|
+
cleaned = _ENTML_ANY_TAG_RE.sub("", cleaned)
|
|
76
|
+
# 清理多余空行
|
|
77
|
+
lines = [line for line in cleaned.splitlines() if line.strip()]
|
|
78
|
+
return "\n".join(lines)
|
|
79
|
+
|
|
80
|
+
|
|
42
81
|
# ---------------------------------------------------------------------------
|
|
43
82
|
# 渲染工具调用 / 工具结果
|
|
44
83
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Conversation history block formatting."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from echotools.exec.fncall.shared.normalization import normalize_content
|
|
8
|
+
|
|
9
|
+
from .prompt_helpers import (
|
|
10
|
+
format_assistant_block,
|
|
11
|
+
format_assistant_block_with_results,
|
|
12
|
+
format_tool_block,
|
|
13
|
+
format_user_block,
|
|
14
|
+
join_history_parts,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def history_contains_tool_calls(messages: List[Dict[str, Any]]) -> bool:
|
|
19
|
+
"""历史消息中是否包含 assistant tool_calls 或 tool 结果。"""
|
|
20
|
+
for m in messages:
|
|
21
|
+
role = m.get("role") or "user"
|
|
22
|
+
if role == "assistant" and m.get("tool_calls"):
|
|
23
|
+
return True
|
|
24
|
+
if role == "tool":
|
|
25
|
+
return True
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _append_user_part(
|
|
30
|
+
parts: List[Tuple[str, bool]],
|
|
31
|
+
m: Dict[str, Any],
|
|
32
|
+
clean_fn: Any,
|
|
33
|
+
) -> None:
|
|
34
|
+
content_str = normalize_content(m.get("content", ""))
|
|
35
|
+
parts.append((format_user_block(content_str, clean_fn), False))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _append_assistant_part(
|
|
39
|
+
parts: List[Tuple[str, bool]],
|
|
40
|
+
messages: List[Dict[str, Any]],
|
|
41
|
+
index: int,
|
|
42
|
+
protocol: Optional[Any],
|
|
43
|
+
call_id_to_name: Dict[str, str],
|
|
44
|
+
seen_assistant_keys: set,
|
|
45
|
+
*,
|
|
46
|
+
include_thinking_in_history: bool,
|
|
47
|
+
) -> int:
|
|
48
|
+
m = messages[index]
|
|
49
|
+
content_str = normalize_content(m.get("content", ""))
|
|
50
|
+
tcs = m.get("tool_calls") or []
|
|
51
|
+
if tcs:
|
|
52
|
+
tool_msgs = []
|
|
53
|
+
j = index + 1
|
|
54
|
+
while j < len(messages) and (messages[j].get("role") or "user") == "tool":
|
|
55
|
+
tool_msgs.append(messages[j])
|
|
56
|
+
j += 1
|
|
57
|
+
blocks = format_assistant_block_with_results(
|
|
58
|
+
m, tool_msgs, protocol, call_id_to_name, seen_assistant_keys,
|
|
59
|
+
include_thinking_in_history=include_thinking_in_history,
|
|
60
|
+
)
|
|
61
|
+
for block in blocks:
|
|
62
|
+
parts.append((block, False))
|
|
63
|
+
return j
|
|
64
|
+
|
|
65
|
+
blocks = format_assistant_block(
|
|
66
|
+
m, content_str, protocol, call_id_to_name, seen_assistant_keys,
|
|
67
|
+
include_thinking_in_history=include_thinking_in_history,
|
|
68
|
+
)
|
|
69
|
+
for block in blocks:
|
|
70
|
+
parts.append((block, False))
|
|
71
|
+
return index + 1
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _format_conversation_history(
|
|
75
|
+
messages: List[Dict[str, Any]],
|
|
76
|
+
protocol: Optional[Any] = None,
|
|
77
|
+
*,
|
|
78
|
+
include_thinking_in_history: bool = False,
|
|
79
|
+
) -> str:
|
|
80
|
+
if not messages:
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
call_id_to_name: Dict[str, str] = {}
|
|
84
|
+
seen_assistant_keys = set()
|
|
85
|
+
parts: List[Tuple[str, bool]] = []
|
|
86
|
+
clean_fn = protocol.clean_tags if protocol and hasattr(protocol, "clean_tags") else None
|
|
87
|
+
|
|
88
|
+
i = 0
|
|
89
|
+
while i < len(messages):
|
|
90
|
+
m = messages[i]
|
|
91
|
+
role: str = m.get("role") or "user"
|
|
92
|
+
|
|
93
|
+
if role == "user":
|
|
94
|
+
_append_user_part(parts, m, clean_fn)
|
|
95
|
+
i += 1
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
if role == "assistant":
|
|
99
|
+
i = _append_assistant_part(
|
|
100
|
+
parts, messages, i, protocol, call_id_to_name,
|
|
101
|
+
seen_assistant_keys,
|
|
102
|
+
include_thinking_in_history=include_thinking_in_history,
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
if role == "tool":
|
|
107
|
+
content_str = normalize_content(m.get("content", ""))
|
|
108
|
+
parts.append((format_tool_block(m, content_str, call_id_to_name), True))
|
|
109
|
+
i += 1
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
content_str = normalize_content(m.get("content", ""))
|
|
113
|
+
parts.append((f"<{role}>\n{content_str}\n</{role}>", False))
|
|
114
|
+
i += 1
|
|
115
|
+
|
|
116
|
+
return join_history_parts(parts)
|
|
@@ -22,8 +22,8 @@ from echotools.exec.fncall.protocols.entml import (
|
|
|
22
22
|
format_entml_conversation_history,
|
|
23
23
|
format_entml_current_user_message,
|
|
24
24
|
)
|
|
25
|
-
from echotools.exec.fncall.protocols.
|
|
26
|
-
from echotools.exec.fncall.protocols.
|
|
25
|
+
from echotools.exec.fncall.protocols.entml_think.core import build_entml_thinking_section
|
|
26
|
+
from echotools.exec.fncall.protocols.entml_think.hist import (
|
|
27
27
|
apply_thinking_history_policy,
|
|
28
28
|
parse_include_thinking_in_history,
|
|
29
29
|
)
|
|
@@ -145,12 +145,13 @@ def inject_fncall(
|
|
|
145
145
|
if not tools:
|
|
146
146
|
if protocol.id == "entml":
|
|
147
147
|
sections: List[str] = []
|
|
148
|
-
thinking_section = build_entml_thinking_section(protocol_options)
|
|
149
|
-
if thinking_section:
|
|
150
|
-
sections.append(thinking_section)
|
|
151
148
|
if history_text.strip():
|
|
152
149
|
sections.append(format_entml_conversation_history(history_text))
|
|
153
150
|
sections.append(format_entml_current_user_message(current_user_message))
|
|
151
|
+
# thinking 放在最后,超限截断时优先保留在 send_text 尾部
|
|
152
|
+
thinking_section = build_entml_thinking_section(protocol_options)
|
|
153
|
+
if thinking_section:
|
|
154
|
+
sections.append(thinking_section)
|
|
154
155
|
prompt = "\n\n".join(sections)
|
|
155
156
|
else:
|
|
156
157
|
prompt = build_no_tools_prompt(history_text, current_user_message)
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
4
4
|
|
|
5
5
|
from echotools.base.logger.manager import get_logger
|
|
6
|
-
from echotools.exec.fncall.protocols.
|
|
6
|
+
from echotools.exec.fncall.protocols.entml_think.hist import (
|
|
7
7
|
extract_reasoning_text,
|
|
8
8
|
format_entml_thinking_history_block,
|
|
9
9
|
)
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json as _json
|
|
11
|
+
import re
|
|
11
12
|
from typing import Any, Dict, List, Optional, Tuple
|
|
12
13
|
|
|
13
14
|
from echotools.exec.fncall.prompt.templates import (
|
|
@@ -18,8 +19,7 @@ from echotools.exec.fncall.protocols.entml_invoke import (
|
|
|
18
19
|
parse_entml_tool_calls,
|
|
19
20
|
)
|
|
20
21
|
from echotools.exec.fncall.protocols.entml_patterns import BLOCK_RE
|
|
21
|
-
from echotools.exec.fncall.protocols.
|
|
22
|
-
from echotools.exec.fncall.protocols.entml_thinking import build_entml_thinking_section
|
|
22
|
+
from echotools.exec.fncall.protocols.entml_think.core import build_entml_thinking_section
|
|
23
23
|
from echotools.exec.fncall.protocols.entml_tools import format_entml_tool_descs
|
|
24
24
|
from echotools.exec.fncall.shared.coercion import _build_param_schema_index
|
|
25
25
|
from echotools.exec.fncall.shared.normalization import (
|
|
@@ -28,6 +28,27 @@ from echotools.exec.fncall.shared.normalization import (
|
|
|
28
28
|
)
|
|
29
29
|
from echotools.exec.protocol.base import ToolProtocol
|
|
30
30
|
|
|
31
|
+
# 仅按 entml: 前缀剥离标签,不区分具体标签名。
|
|
32
|
+
_ENTML_PAIR_RE = re.compile(
|
|
33
|
+
r"<entml:[a-zA-Z_][\w]*\b[^>]*>.*?</entml:[a-zA-Z_][\w]*>",
|
|
34
|
+
re.DOTALL,
|
|
35
|
+
)
|
|
36
|
+
_ENTML_SELF_CLOSING_RE = re.compile(r"<entml:[a-zA-Z_][\w]*\b[^>]*/>", re.DOTALL)
|
|
37
|
+
_ENTML_ORPHAN_CLOSE_RE = re.compile(r"</entml:[a-zA-Z_][\w]*>", re.DOTALL)
|
|
38
|
+
_ENTML_ORPHAN_OPEN_RE = re.compile(r"<entml:[a-zA-Z_][\w]*\b[^>]*>", re.DOTALL)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def strip_entml_from_content(content: str) -> str:
|
|
42
|
+
"""从 user 消息正文剥离所有 entml:* 标签及残留开闭标签。"""
|
|
43
|
+
if not content:
|
|
44
|
+
return content
|
|
45
|
+
cleaned = content
|
|
46
|
+
cleaned = _ENTML_PAIR_RE.sub("", cleaned)
|
|
47
|
+
cleaned = _ENTML_SELF_CLOSING_RE.sub("", cleaned)
|
|
48
|
+
cleaned = _ENTML_ORPHAN_CLOSE_RE.sub("", cleaned)
|
|
49
|
+
cleaned = _ENTML_ORPHAN_OPEN_RE.sub("", cleaned)
|
|
50
|
+
return cleaned.strip()
|
|
51
|
+
|
|
31
52
|
|
|
32
53
|
def _parse_tool_call_args(tc: Dict[str, Any]) -> tuple[str, Dict[str, Any]]:
|
|
33
54
|
fn = tc.get("function") or {}
|
|
@@ -141,10 +162,6 @@ class EntmlProtocol(ToolProtocol):
|
|
|
141
162
|
else:
|
|
142
163
|
sections = [_ENTML_INSTRUCTION.rstrip()]
|
|
143
164
|
|
|
144
|
-
thinking_section = build_entml_thinking_section(protocol_options)
|
|
145
|
-
if thinking_section:
|
|
146
|
-
sections.append(thinking_section)
|
|
147
|
-
|
|
148
165
|
if user_system_prompt and user_system_prompt.strip():
|
|
149
166
|
sections.append(
|
|
150
167
|
f"<user_system_prompt>\n{user_system_prompt.strip()}\n</user_system_prompt>"
|
|
@@ -161,6 +178,12 @@ class EntmlProtocol(ToolProtocol):
|
|
|
161
178
|
sections.append(f"<loop_warning>\n{loop_warning}\n</loop_warning>")
|
|
162
179
|
|
|
163
180
|
sections.append(format_entml_current_user_message(current_user_message))
|
|
181
|
+
|
|
182
|
+
# thinking 放在最后,超限截断时优先保留在 send_text 尾部
|
|
183
|
+
thinking_section = build_entml_thinking_section(protocol_options)
|
|
184
|
+
if thinking_section:
|
|
185
|
+
sections.append(thinking_section)
|
|
186
|
+
|
|
164
187
|
return "\n\n".join(sections)
|
|
165
188
|
|
|
166
189
|
def detect_start(self, buffer: str) -> Tuple[bool, int]:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Entml thinking: prompt section, history blocks, stream parsing."""
|
|
2
|
+
|
|
3
|
+
from echotools.exec.fncall.protocols.entml_think.core import (
|
|
4
|
+
build_entml_thinking_section,
|
|
5
|
+
parse_max_thinking_length,
|
|
6
|
+
)
|
|
7
|
+
from echotools.exec.fncall.protocols.entml_think.hist import (
|
|
8
|
+
apply_thinking_history_policy,
|
|
9
|
+
extract_reasoning_text,
|
|
10
|
+
format_entml_thinking_history_block,
|
|
11
|
+
parse_include_thinking_in_history,
|
|
12
|
+
parse_interleaved_history,
|
|
13
|
+
)
|
|
14
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
15
|
+
EntmlThinkingStreamFilter,
|
|
16
|
+
has_unclosed_entml_thinking,
|
|
17
|
+
split_entml_thinking,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"EntmlThinkingStreamFilter",
|
|
22
|
+
"apply_thinking_history_policy",
|
|
23
|
+
"build_entml_thinking_section",
|
|
24
|
+
"extract_reasoning_text",
|
|
25
|
+
"format_entml_thinking_history_block",
|
|
26
|
+
"has_unclosed_entml_thinking",
|
|
27
|
+
"parse_include_thinking_in_history",
|
|
28
|
+
"parse_interleaved_history",
|
|
29
|
+
"parse_max_thinking_length",
|
|
30
|
+
"split_entml_thinking",
|
|
31
|
+
]
|
|
@@ -53,11 +53,11 @@ _INJECTION_MODE_ALIASES: Dict[str, str] = {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
_DEFAULT_MAX_BY_LEVEL: Dict[str, int] = {
|
|
56
|
-
"low":
|
|
57
|
-
"medium":
|
|
58
|
-
"high":
|
|
59
|
-
"xhigh":
|
|
60
|
-
"max":
|
|
56
|
+
"low": 12800,
|
|
57
|
+
"medium": 25600,
|
|
58
|
+
"high": 64000,
|
|
59
|
+
"xhigh": 102400,
|
|
60
|
+
"max": 134736,
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
_LEGACY_ON_DEFAULT_MAX = _DEFAULT_MAX_BY_LEVEL["medium"]
|
|
@@ -120,9 +120,12 @@ def parse_max_thinking_length(value: Any) -> Optional[int]:
|
|
|
120
120
|
def resolve_thinking_injection(
|
|
121
121
|
protocol_options: Optional[Dict[str, Any]] = None,
|
|
122
122
|
) -> Optional[Tuple[str, Optional[int]]]:
|
|
123
|
-
"""解析注入用 thinking_mode
|
|
123
|
+
"""解析注入用 thinking_mode 与 max_thinking_length。
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
- thinking_level 为 low|medium|high|xhigh|max 时,thinking_mode 直接传该挡位名。
|
|
126
|
+
- 仅 thinking_mode=on(无 level)时用 ``on``,并按 medium 默认长度。
|
|
127
|
+
- auto 传 ``auto``,无默认 max_thinking_length。
|
|
128
|
+
- none / off 返回 None,不注入任何思考相关内容。
|
|
126
129
|
"""
|
|
127
130
|
opts = protocol_options or {}
|
|
128
131
|
|
|
@@ -130,8 +133,12 @@ def resolve_thinking_injection(
|
|
|
130
133
|
if level is not None:
|
|
131
134
|
if level == "none":
|
|
132
135
|
return None
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
if level == "auto":
|
|
137
|
+
injection_mode = "auto"
|
|
138
|
+
default_max = None
|
|
139
|
+
else:
|
|
140
|
+
injection_mode = level
|
|
141
|
+
default_max = default_max_thinking_length_for_level(level)
|
|
135
142
|
else:
|
|
136
143
|
mode = normalize_thinking_mode(opts.get("thinking_mode"))
|
|
137
144
|
if mode is None or mode == "off":
|
|
@@ -144,8 +151,12 @@ def resolve_thinking_injection(
|
|
|
144
151
|
return injection_mode, max_length
|
|
145
152
|
|
|
146
153
|
|
|
154
|
+
def _uses_forced_thinking_behavior(injection_mode: str) -> bool:
|
|
155
|
+
return injection_mode != "auto"
|
|
156
|
+
|
|
157
|
+
|
|
147
158
|
def _format_thinking_behavior(injection_mode: str) -> str:
|
|
148
|
-
body = _THINKING_BEHAVIOR_ON if injection_mode
|
|
159
|
+
body = _THINKING_BEHAVIOR_ON if _uses_forced_thinking_behavior(injection_mode) else _THINKING_BEHAVIOR_AUTO
|
|
149
160
|
return f"<thinking_behavior>\n{body}\n</thinking_behavior>"
|
|
150
161
|
|
|
151
162
|
|
|
@@ -92,6 +92,49 @@ class EntmlThinkingStreamFilter:
|
|
|
92
92
|
return True
|
|
93
93
|
return False
|
|
94
94
|
|
|
95
|
+
def _feed_in_block(self, out: List[Tuple[str, str]]) -> bool:
|
|
96
|
+
"""处理块内 pending;返回 False 表示应退出 feed 主循环。"""
|
|
97
|
+
close_at = self._pending.find(_THINKING_CLOSE)
|
|
98
|
+
if close_at >= 0:
|
|
99
|
+
piece = self._pending[:close_at]
|
|
100
|
+
emitted = self._emit_thinking_piece(piece)
|
|
101
|
+
if emitted:
|
|
102
|
+
out.append(("thinking", emitted))
|
|
103
|
+
self._in_block = False
|
|
104
|
+
self._thinking_started = False
|
|
105
|
+
self._pending = self._pending[close_at + len(_THINKING_CLOSE) :]
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
|
|
109
|
+
if safe:
|
|
110
|
+
emitted = self._emit_thinking_piece(safe)
|
|
111
|
+
if emitted:
|
|
112
|
+
out.append(("thinking", emitted))
|
|
113
|
+
self._pending = hold
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def _feed_before_block(self, out: List[Tuple[str, str]]) -> bool:
|
|
117
|
+
"""开标签之前或尚未进入块;返回 False 表示应退出 feed 主循环。"""
|
|
118
|
+
open_at = self._pending.find(_THINKING_OPEN_PREFIX)
|
|
119
|
+
if open_at < 0:
|
|
120
|
+
safe, self._pending = _hold_prefix(self._pending, _THINKING_OPEN_PREFIX)
|
|
121
|
+
if safe:
|
|
122
|
+
out.append(("content", safe))
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
if open_at > 0:
|
|
126
|
+
out.append(("content", self._pending[:open_at]))
|
|
127
|
+
self._pending = self._pending[open_at:]
|
|
128
|
+
|
|
129
|
+
gt = self._pending.find(">")
|
|
130
|
+
if gt < 0:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
self._in_block = True
|
|
134
|
+
self._thinking_started = False
|
|
135
|
+
self._pending = self._pending[gt + 1 :]
|
|
136
|
+
return True
|
|
137
|
+
|
|
95
138
|
def feed(self, chunk: str) -> List[Tuple[str, str]]:
|
|
96
139
|
"""返回 [(kind, text), ...],kind 为 content 或 thinking。"""
|
|
97
140
|
if not chunk:
|
|
@@ -102,46 +145,12 @@ class EntmlThinkingStreamFilter:
|
|
|
102
145
|
|
|
103
146
|
while self._pending:
|
|
104
147
|
if self._in_block:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if emitted:
|
|
110
|
-
out.append(("thinking", emitted))
|
|
111
|
-
self._in_block = False
|
|
112
|
-
self._thinking_started = False
|
|
113
|
-
self._pending = self._pending[close_at + len(_THINKING_CLOSE) :]
|
|
114
|
-
continue
|
|
115
|
-
|
|
116
|
-
# 关闭标签可能被分片:hold 真前缀,其余立即作为 thinking 输出
|
|
117
|
-
safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
|
|
118
|
-
if safe:
|
|
119
|
-
emitted = self._emit_thinking_piece(safe)
|
|
120
|
-
if emitted:
|
|
121
|
-
out.append(("thinking", emitted))
|
|
122
|
-
self._pending = hold
|
|
123
|
-
break
|
|
124
|
-
|
|
125
|
-
open_at = self._pending.find(_THINKING_OPEN_PREFIX)
|
|
126
|
-
if open_at < 0:
|
|
127
|
-
safe, self._pending = _hold_prefix(self._pending, _THINKING_OPEN_PREFIX)
|
|
128
|
-
if safe:
|
|
129
|
-
out.append(("content", safe))
|
|
148
|
+
if not self._feed_in_block(out):
|
|
149
|
+
break
|
|
150
|
+
continue
|
|
151
|
+
if not self._feed_before_block(out):
|
|
130
152
|
break
|
|
131
153
|
|
|
132
|
-
if open_at > 0:
|
|
133
|
-
out.append(("content", self._pending[:open_at]))
|
|
134
|
-
self._pending = self._pending[open_at:]
|
|
135
|
-
|
|
136
|
-
gt = self._pending.find(">")
|
|
137
|
-
if gt < 0:
|
|
138
|
-
# 开标签属性未收齐
|
|
139
|
-
break
|
|
140
|
-
|
|
141
|
-
self._in_block = True
|
|
142
|
-
self._thinking_started = False
|
|
143
|
-
self._pending = self._pending[gt + 1 :]
|
|
144
|
-
|
|
145
154
|
return out
|
|
146
155
|
|
|
147
156
|
def _emit_thinking_piece(self, piece: str) -> str:
|
|
@@ -57,12 +57,12 @@ src/echotools/exec/fncall/protocols/__init__.py
|
|
|
57
57
|
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
|
-
src/echotools/exec/fncall/protocols/entml_sanitize.py
|
|
61
|
-
src/echotools/exec/fncall/protocols/entml_thinking.py
|
|
62
|
-
src/echotools/exec/fncall/protocols/entml_thinking_history.py
|
|
63
|
-
src/echotools/exec/fncall/protocols/entml_thinking_parse.py
|
|
64
60
|
src/echotools/exec/fncall/protocols/entml_tools.py
|
|
65
61
|
src/echotools/exec/fncall/protocols/entml_values.py
|
|
62
|
+
src/echotools/exec/fncall/protocols/entml_think/__init__.py
|
|
63
|
+
src/echotools/exec/fncall/protocols/entml_think/core.py
|
|
64
|
+
src/echotools/exec/fncall/protocols/entml_think/hist.py
|
|
65
|
+
src/echotools/exec/fncall/protocols/entml_think/parse.py
|
|
66
66
|
src/echotools/exec/fncall/shared/__init__.py
|
|
67
67
|
src/echotools/exec/fncall/shared/coercion.py
|
|
68
68
|
src/echotools/exec/fncall/shared/loop_detect.py
|
|
@@ -6,7 +6,7 @@ import pytest
|
|
|
6
6
|
|
|
7
7
|
from echotools.exec.fncall import get_protocol, inject_fncall
|
|
8
8
|
from echotools.exec.fncall.protocols.entml_invoke import parse_entml_tool_calls
|
|
9
|
-
from echotools.exec.fncall.protocols.
|
|
9
|
+
from echotools.exec.fncall.protocols.entml_think.core import (
|
|
10
10
|
build_entml_thinking_section,
|
|
11
11
|
default_max_thinking_length_for_level,
|
|
12
12
|
normalize_thinking_level,
|
|
@@ -77,7 +77,7 @@ def test_build_entml_thinking_section_empty_without_options() -> None:
|
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
def test_parse_max_thinking_length() -> None:
|
|
80
|
-
from echotools.exec.fncall.protocols.
|
|
80
|
+
from echotools.exec.fncall.protocols.entml_think.core import parse_max_thinking_length
|
|
81
81
|
|
|
82
82
|
assert parse_max_thinking_length(None) is None
|
|
83
83
|
assert parse_max_thinking_length("") is None
|
|
@@ -128,11 +128,11 @@ def test_normalize_thinking_level(raw, expected) -> None:
|
|
|
128
128
|
@pytest.mark.parametrize(
|
|
129
129
|
"level,expected",
|
|
130
130
|
[
|
|
131
|
-
("low",
|
|
132
|
-
("medium",
|
|
133
|
-
("high",
|
|
134
|
-
("xhigh",
|
|
135
|
-
("max",
|
|
131
|
+
("low", 12800),
|
|
132
|
+
("medium", 25600),
|
|
133
|
+
("high", 64000),
|
|
134
|
+
("xhigh", 102400),
|
|
135
|
+
("max", 134736),
|
|
136
136
|
("none", None),
|
|
137
137
|
("auto", None),
|
|
138
138
|
],
|
|
@@ -143,13 +143,13 @@ def test_default_max_thinking_length_for_level(level, expected) -> None:
|
|
|
143
143
|
|
|
144
144
|
def test_resolve_thinking_injection_levels() -> None:
|
|
145
145
|
assert resolve_thinking_injection({"thinking_level": "none"}) is None
|
|
146
|
-
assert resolve_thinking_injection({"thinking_level": "low"}) == ("
|
|
147
|
-
assert resolve_thinking_injection({"thinking_level": "high"}) == ("
|
|
146
|
+
assert resolve_thinking_injection({"thinking_level": "low"}) == ("low", 12800)
|
|
147
|
+
assert resolve_thinking_injection({"thinking_level": "high"}) == ("high", 64000)
|
|
148
148
|
assert resolve_thinking_injection({"thinking_level": "auto"}) == ("auto", None)
|
|
149
149
|
assert resolve_thinking_injection({
|
|
150
150
|
"thinking_level": "max",
|
|
151
151
|
"max_thinking_length": 1000,
|
|
152
|
-
}) == ("
|
|
152
|
+
}) == ("max", 1000)
|
|
153
153
|
|
|
154
154
|
|
|
155
155
|
def test_thinking_prompt_off() -> None:
|
|
@@ -163,18 +163,24 @@ def test_thinking_prompt_off() -> None:
|
|
|
163
163
|
def test_thinking_prompt_on_without_max_length() -> None:
|
|
164
164
|
section = build_entml_thinking_section({"thinking_mode": "on"})
|
|
165
165
|
assert "<entml:thinking_mode>on</entml:thinking_mode>" in section
|
|
166
|
-
assert "<entml:max_thinking_length>
|
|
166
|
+
assert "<entml:max_thinking_length>25600</entml:max_thinking_length>" in section
|
|
167
167
|
assert "<thinking_behavior>" in section
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
def test_thinking_prompt_on_by_level() -> None:
|
|
171
171
|
section = build_entml_thinking_section({"thinking_level": "low"})
|
|
172
|
-
assert "<entml:thinking_mode>
|
|
173
|
-
assert "<entml:max_thinking_length>
|
|
172
|
+
assert "<entml:thinking_mode>low</entml:thinking_mode>" in section
|
|
173
|
+
assert "<entml:max_thinking_length>12800</entml:max_thinking_length>" in section
|
|
174
174
|
assert "Never skip the thinking block" in section
|
|
175
175
|
assert "Your default is to think before it answers" in section
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
def test_thinking_prompt_medium_by_level() -> None:
|
|
179
|
+
section = build_entml_thinking_section({"thinking_level": "medium"})
|
|
180
|
+
assert "<entml:thinking_mode>medium</entml:thinking_mode>" in section
|
|
181
|
+
assert "<entml:max_thinking_length>25600</entml:max_thinking_length>" in section
|
|
182
|
+
|
|
183
|
+
|
|
178
184
|
def test_thinking_prompt_on() -> None:
|
|
179
185
|
section = build_entml_thinking_section({"thinking_mode": "on"})
|
|
180
186
|
assert "<entml:thinking_mode>on</entml:thinking_mode>" in section
|
|
@@ -223,6 +229,33 @@ def test_inject_no_tools_with_thinking_on() -> None:
|
|
|
223
229
|
assert "<entml:thinking_mode>on</entml:thinking_mode>" in result
|
|
224
230
|
assert "<thinking_behavior>" in result
|
|
225
231
|
assert "Never skip the thinking block" in result
|
|
232
|
+
# thinking 块必须在 current_user_message 之后
|
|
233
|
+
assert result.index("</current_user_message>") < result.index("<entml:thinking_mode>")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_render_prompt_thinking_after_current_user() -> None:
|
|
237
|
+
proto = get_protocol("entml")
|
|
238
|
+
prompt = proto.render_prompt(
|
|
239
|
+
tool_descs=proto.format_tool_descs([
|
|
240
|
+
{
|
|
241
|
+
"type": "function",
|
|
242
|
+
"function": {
|
|
243
|
+
"name": "search",
|
|
244
|
+
"description": "Search",
|
|
245
|
+
"parameters": {"type": "object", "properties": {}},
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
]),
|
|
249
|
+
lang="en",
|
|
250
|
+
history_text="<user>\nold\n</user>",
|
|
251
|
+
current_user_message="new",
|
|
252
|
+
protocol_options={"thinking_mode": "on", "max_thinking_length": 1000},
|
|
253
|
+
)
|
|
254
|
+
assert prompt.index("In this environment") < prompt.index("<entml:conversation_history>")
|
|
255
|
+
assert prompt.index("<entml:conversation_history>") < prompt.index("<current_user_message>")
|
|
256
|
+
assert prompt.index("</current_user_message>") < prompt.index("<entml:thinking_mode>")
|
|
257
|
+
assert prompt.index("<entml:thinking_mode>") < prompt.index("<thinking_behavior>")
|
|
258
|
+
assert prompt.rstrip().endswith("</thinking_behavior>")
|
|
226
259
|
|
|
227
260
|
|
|
228
261
|
def test_inject_with_history_entml_tags() -> None:
|
|
@@ -316,7 +349,7 @@ def test_entml_history_tool_invoke_reminder_when_tools_in_history() -> None:
|
|
|
316
349
|
|
|
317
350
|
|
|
318
351
|
def test_strip_entml_from_user_content() -> None:
|
|
319
|
-
from echotools.exec.fncall.protocols.
|
|
352
|
+
from echotools.exec.fncall.protocols.entml import strip_entml_from_content
|
|
320
353
|
|
|
321
354
|
raw = (
|
|
322
355
|
"请查天气\n"
|