echotools 2.3.40__tar.gz → 2.3.42__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.42}/PKG-INFO +1 -1
- {echotools-2.3.40 → echotools-2.3.42}/pyproject.toml +1 -1
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/parsers/stream.py +3 -3
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/prompt/history.py +39 -0
- echotools-2.3.42/src/echotools/exec/fncall/prompt/history_format.py +116 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/prompt/inject.py +6 -5
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/prompt/prompt_helpers.py +1 -1
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/protocols/entml.py +29 -6
- echotools-2.3.42/src/echotools/exec/fncall/protocols/entml_think/__init__.py +31 -0
- echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_thinking_parse.py → echotools-2.3.42/src/echotools/exec/fncall/protocols/entml_think/parse.py +47 -38
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools.egg-info/SOURCES.txt +4 -4
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_entml_protocol.py +30 -3
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_entml_thinking_history.py +1 -1
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_entml_thinking_parse.py +1 -1
- {echotools-2.3.40 → echotools-2.3.42}/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.42}/README.md +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/docs/README.md +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/setup.cfg +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/compat.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
- /echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_thinking.py → /echotools-2.3.42/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- /echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_thinking_history.py → /echotools-2.3.42/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/fonts/tailwind.css +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools/version.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/bench_import.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/conftest.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_completion.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_config.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_console.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_errors.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_events.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_facade.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_ids.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_imports.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_io.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_keys.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_logger.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_merge.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_usage.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/src/tests/test_web.py +0 -0
- {echotools-2.3.40 → echotools-2.3.42}/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
|
+
]
|
|
@@ -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
|
|
@@ -223,6 +223,33 @@ def test_inject_no_tools_with_thinking_on() -> None:
|
|
|
223
223
|
assert "<entml:thinking_mode>on</entml:thinking_mode>" in result
|
|
224
224
|
assert "<thinking_behavior>" in result
|
|
225
225
|
assert "Never skip the thinking block" in result
|
|
226
|
+
# thinking 块必须在 current_user_message 之后
|
|
227
|
+
assert result.index("</current_user_message>") < result.index("<entml:thinking_mode>")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_render_prompt_thinking_after_current_user() -> None:
|
|
231
|
+
proto = get_protocol("entml")
|
|
232
|
+
prompt = proto.render_prompt(
|
|
233
|
+
tool_descs=proto.format_tool_descs([
|
|
234
|
+
{
|
|
235
|
+
"type": "function",
|
|
236
|
+
"function": {
|
|
237
|
+
"name": "search",
|
|
238
|
+
"description": "Search",
|
|
239
|
+
"parameters": {"type": "object", "properties": {}},
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
]),
|
|
243
|
+
lang="en",
|
|
244
|
+
history_text="<user>\nold\n</user>",
|
|
245
|
+
current_user_message="new",
|
|
246
|
+
protocol_options={"thinking_mode": "on", "max_thinking_length": 1000},
|
|
247
|
+
)
|
|
248
|
+
assert prompt.index("In this environment") < prompt.index("<entml:conversation_history>")
|
|
249
|
+
assert prompt.index("<entml:conversation_history>") < prompt.index("<current_user_message>")
|
|
250
|
+
assert prompt.index("</current_user_message>") < prompt.index("<entml:thinking_mode>")
|
|
251
|
+
assert prompt.index("<entml:thinking_mode>") < prompt.index("<thinking_behavior>")
|
|
252
|
+
assert prompt.rstrip().endswith("</thinking_behavior>")
|
|
226
253
|
|
|
227
254
|
|
|
228
255
|
def test_inject_with_history_entml_tags() -> None:
|
|
@@ -316,7 +343,7 @@ def test_entml_history_tool_invoke_reminder_when_tools_in_history() -> None:
|
|
|
316
343
|
|
|
317
344
|
|
|
318
345
|
def test_strip_entml_from_user_content() -> None:
|
|
319
|
-
from echotools.exec.fncall.protocols.
|
|
346
|
+
from echotools.exec.fncall.protocols.entml import strip_entml_from_content
|
|
320
347
|
|
|
321
348
|
raw = (
|
|
322
349
|
"请查天气\n"
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from echotools.exec.fncall import get_protocol
|
|
4
4
|
from echotools.exec.fncall.parsers.stream import FncallStreamParser
|
|
5
|
-
from echotools.exec.fncall.protocols.
|
|
5
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
6
6
|
EntmlThinkingStreamFilter,
|
|
7
7
|
has_unclosed_entml_thinking,
|
|
8
8
|
split_entml_thinking,
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
"""Conversation history block formatting."""
|
|
4
|
-
|
|
5
|
-
from typing import Any, Dict, List, Optional
|
|
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 _format_conversation_history(
|
|
30
|
-
messages: List[Dict[str, Any]],
|
|
31
|
-
protocol: Optional[Any] = None,
|
|
32
|
-
*,
|
|
33
|
-
include_thinking_in_history: bool = False,
|
|
34
|
-
) -> str:
|
|
35
|
-
if not messages:
|
|
36
|
-
return ""
|
|
37
|
-
|
|
38
|
-
call_id_to_name: Dict[str, str] = {}
|
|
39
|
-
seen_assistant_keys = set()
|
|
40
|
-
parts: List[tuple] = []
|
|
41
|
-
clean_fn = protocol.clean_tags if protocol and hasattr(protocol, "clean_tags") else None
|
|
42
|
-
|
|
43
|
-
i = 0
|
|
44
|
-
while i < len(messages):
|
|
45
|
-
m = messages[i]
|
|
46
|
-
role: str = m.get("role") or "user"
|
|
47
|
-
content_str = normalize_content(m.get("content", ""))
|
|
48
|
-
|
|
49
|
-
if role == "user":
|
|
50
|
-
parts.append((format_user_block(content_str, clean_fn), False))
|
|
51
|
-
i += 1
|
|
52
|
-
continue
|
|
53
|
-
|
|
54
|
-
if role == "assistant":
|
|
55
|
-
tcs = m.get("tool_calls") or []
|
|
56
|
-
if tcs:
|
|
57
|
-
# collect immediately following tool messages
|
|
58
|
-
tool_msgs = []
|
|
59
|
-
j = i + 1
|
|
60
|
-
while j < len(messages) and (messages[j].get("role") or "user") == "tool":
|
|
61
|
-
tool_msgs.append(messages[j])
|
|
62
|
-
j += 1
|
|
63
|
-
blocks = format_assistant_block_with_results(
|
|
64
|
-
m, tool_msgs, protocol, call_id_to_name, seen_assistant_keys,
|
|
65
|
-
include_thinking_in_history=include_thinking_in_history,
|
|
66
|
-
)
|
|
67
|
-
for block in blocks:
|
|
68
|
-
parts.append((block, False))
|
|
69
|
-
i = j
|
|
70
|
-
else:
|
|
71
|
-
blocks = format_assistant_block(
|
|
72
|
-
m, content_str, protocol, call_id_to_name, seen_assistant_keys,
|
|
73
|
-
include_thinking_in_history=include_thinking_in_history,
|
|
74
|
-
)
|
|
75
|
-
for block in blocks:
|
|
76
|
-
parts.append((block, False))
|
|
77
|
-
i += 1
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
if role == "tool":
|
|
81
|
-
# orphan tool message (not consumed above)
|
|
82
|
-
parts.append((format_tool_block(m, content_str, call_id_to_name), True))
|
|
83
|
-
i += 1
|
|
84
|
-
continue
|
|
85
|
-
|
|
86
|
-
parts.append((f"<{role}>\n{content_str}\n</{role}>", False))
|
|
87
|
-
i += 1
|
|
88
|
-
|
|
89
|
-
return join_history_parts(parts)
|