echotools 2.3.47__tar.gz → 2.3.49__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.47 → echotools-2.3.49}/PKG-INFO +1 -1
- {echotools-2.3.47 → echotools-2.3.49}/pyproject.toml +1 -1
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_stream_json.py +6 -4
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_think/parse.py +72 -49
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools.egg-info/SOURCES.txt +1 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_entml_stream_json.py +1 -0
- echotools-2.3.49/src/tests/test_entml_stream_json_exhaustive.py +201 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_fncall_stream_thinking.py +44 -11
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_stream_realtime_parse.py +4 -5
- {echotools-2.3.47 → echotools-2.3.49}/README.md +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/docs/README.md +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/setup.cfg +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/compat.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/parsers/stream.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools/version.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/bench_import.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/conftest.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_completion.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_config.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_console.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_entml_protocol.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_entml_tool_pipeline.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_errors.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_events.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_facade.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_ids.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_imports.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_io.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_keys.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_logger.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_merge.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_usage.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_web.py +0 -0
- {echotools-2.3.47 → echotools-2.3.49}/src/tests/test_web_broker.py +0 -0
{echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_stream_json.py
RENAMED
|
@@ -72,7 +72,11 @@ def _parse_parameter_entries(body: str) -> List[Tuple[str, str, bool]]:
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
def build_streaming_json_snapshot(body: str) -> str:
|
|
75
|
-
"""构造当前应已发出的 partial_json 累积串(可未完成)。
|
|
75
|
+
"""构造当前应已发出的 partial_json 累积串(可未完成)。
|
|
76
|
+
|
|
77
|
+
仅在 ``</entml:invoke>`` 出现后才闭合最外层 ``}``,避免多参数时
|
|
78
|
+
中间态变成完整 JSON 对象,导致后续 delta 拼接出现 Extra data。
|
|
79
|
+
"""
|
|
76
80
|
invoke_closed = _INVOKE_CLOSE in body
|
|
77
81
|
inner = body[: body.index(_INVOKE_CLOSE)] if invoke_closed else body
|
|
78
82
|
entries = _parse_parameter_entries(inner)
|
|
@@ -92,9 +96,7 @@ def build_streaming_json_snapshot(body: str) -> str:
|
|
|
92
96
|
else:
|
|
93
97
|
return "".join(parts)
|
|
94
98
|
|
|
95
|
-
if
|
|
96
|
-
parts.append("}")
|
|
97
|
-
elif invoke_closed:
|
|
99
|
+
if invoke_closed:
|
|
98
100
|
parts.append("}")
|
|
99
101
|
return "".join(parts)
|
|
100
102
|
|
{echotools-2.3.47 → echotools-2.3.49}/src/echotools/exec/fncall/protocols/entml_think/parse.py
RENAMED
|
@@ -15,6 +15,7 @@ THINKING_BLOCK_RE = re.compile(
|
|
|
15
15
|
|
|
16
16
|
_THINKING_OPEN_PREFIX = "<entml:thinking"
|
|
17
17
|
_THINKING_CLOSE = "</entml:thinking>"
|
|
18
|
+
_FAULT_THINKING_CLOSE = "</thinking>"
|
|
18
19
|
# 与 thinking 共享 `<entml:` 前缀的其它标签:歧义 holdback 应交由工具流式状态机处理
|
|
19
20
|
_AMBIGUOUS_ENTML_PREFIXES = (
|
|
20
21
|
"<entml:invoke",
|
|
@@ -142,10 +143,13 @@ class EntmlThinkingStreamFilter:
|
|
|
142
143
|
self._in_block = False
|
|
143
144
|
# 当前 thinking 块是否已输出过任何内容(用于首片去前导空白)
|
|
144
145
|
self._thinking_started = False
|
|
146
|
+
# 见到 ``</thinking>`` 后暂存,仅当后续出现 invoke 才视为闭合;否则当纯文本
|
|
147
|
+
self._fault_watch = False
|
|
148
|
+
self._fault_buffer = ""
|
|
145
149
|
|
|
146
150
|
def in_open_thinking(self) -> bool:
|
|
147
|
-
"""思考块已开始且尚未收到
|
|
148
|
-
if self._in_block:
|
|
151
|
+
"""思考块已开始且尚未收到 ``</entml:thinking>``(含开/闭标签分片 hold)。"""
|
|
152
|
+
if self._in_block or self._fault_watch:
|
|
149
153
|
return True
|
|
150
154
|
open_at = self._pending.find(_THINKING_OPEN_PREFIX)
|
|
151
155
|
if open_at >= 0:
|
|
@@ -157,47 +161,47 @@ class EntmlThinkingStreamFilter:
|
|
|
157
161
|
return True
|
|
158
162
|
return False
|
|
159
163
|
|
|
160
|
-
def
|
|
161
|
-
"""thinking
|
|
162
|
-
|
|
163
|
-
if tool_at < 0:
|
|
164
|
+
def _try_resolve_fault_watch_on_invoke(self) -> bool:
|
|
165
|
+
"""``</thinking>`` 之后若出现完整 invoke 开标签,则在该处结束 thinking。"""
|
|
166
|
+
if not self._fault_watch or not self._fault_buffer:
|
|
164
167
|
return False
|
|
165
|
-
|
|
166
|
-
if
|
|
168
|
+
invoke_at = find_complete_entml_invoke_open(self._fault_buffer)
|
|
169
|
+
if invoke_at < len(_FAULT_THINKING_CLOSE):
|
|
167
170
|
return False
|
|
168
|
-
if tool_at > 0:
|
|
169
|
-
emitted = self._emit_thinking_piece(self._pending[:tool_at])
|
|
170
|
-
if emitted:
|
|
171
|
-
out.append(("thinking", emitted))
|
|
172
171
|
self._in_block = False
|
|
172
|
+
self._fault_watch = False
|
|
173
173
|
self._thinking_started = False
|
|
174
|
-
self._pending = self.
|
|
174
|
+
self._pending = self._fault_buffer[len(_FAULT_THINKING_CLOSE) :]
|
|
175
|
+
self._fault_buffer = ""
|
|
175
176
|
return True
|
|
176
177
|
|
|
177
|
-
def
|
|
178
|
-
"""
|
|
179
|
-
|
|
180
|
-
if invoke_at < 0:
|
|
178
|
+
def _try_resolve_fault_watch_on_entml_close(self, out: List[Tuple[str, str]]) -> bool:
|
|
179
|
+
"""未见 invoke 时出现 ``</entml:thinking>`` → ``</thinking>`` 仅为思考内纯文本。"""
|
|
180
|
+
if not self._fault_watch:
|
|
181
181
|
return False
|
|
182
|
-
close_at = self.
|
|
183
|
-
if close_at
|
|
182
|
+
close_at = self._fault_buffer.find(_THINKING_CLOSE)
|
|
183
|
+
if close_at < 0:
|
|
184
184
|
return False
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
out.append(("thinking", emitted))
|
|
185
|
+
emitted = self._emit_thinking_piece(self._fault_buffer[:close_at])
|
|
186
|
+
if emitted:
|
|
187
|
+
out.append(("thinking", emitted))
|
|
189
188
|
self._in_block = False
|
|
189
|
+
self._fault_watch = False
|
|
190
190
|
self._thinking_started = False
|
|
191
|
-
self._pending = self.
|
|
191
|
+
self._pending = self._fault_buffer[close_at + len(_THINKING_CLOSE) :]
|
|
192
|
+
self._fault_buffer = ""
|
|
192
193
|
return True
|
|
193
194
|
|
|
194
|
-
def
|
|
195
|
-
"""
|
|
196
|
-
if self.
|
|
195
|
+
def _feed_fault_watch(self, out: List[Tuple[str, str]]) -> bool:
|
|
196
|
+
"""处理 ``</thinking>`` 容错观察期;返回 True 表示可继续主循环。"""
|
|
197
|
+
if self._try_resolve_fault_watch_on_invoke():
|
|
197
198
|
return True
|
|
198
|
-
if self.
|
|
199
|
+
if self._try_resolve_fault_watch_on_entml_close(out):
|
|
199
200
|
return True
|
|
201
|
+
return False
|
|
200
202
|
|
|
203
|
+
def _feed_in_block(self, out: List[Tuple[str, str]]) -> bool:
|
|
204
|
+
"""处理块内 pending;返回 False 表示应退出 feed 主循环。"""
|
|
201
205
|
close_at = self._pending.find(_THINKING_CLOSE)
|
|
202
206
|
if close_at >= 0:
|
|
203
207
|
piece = self._pending[:close_at]
|
|
@@ -205,10 +209,23 @@ class EntmlThinkingStreamFilter:
|
|
|
205
209
|
if emitted:
|
|
206
210
|
out.append(("thinking", emitted))
|
|
207
211
|
self._in_block = False
|
|
212
|
+
self._fault_watch = False
|
|
213
|
+
self._fault_buffer = ""
|
|
208
214
|
self._thinking_started = False
|
|
209
215
|
self._pending = self._pending[close_at + len(_THINKING_CLOSE) :]
|
|
210
216
|
return True
|
|
211
217
|
|
|
218
|
+
fault_at = self._pending.find(_FAULT_THINKING_CLOSE)
|
|
219
|
+
if fault_at >= 0:
|
|
220
|
+
before = self._pending[:fault_at]
|
|
221
|
+
emitted = self._emit_thinking_piece(before)
|
|
222
|
+
if emitted:
|
|
223
|
+
out.append(("thinking", emitted))
|
|
224
|
+
self._fault_watch = True
|
|
225
|
+
self._fault_buffer = self._pending[fault_at:]
|
|
226
|
+
self._pending = ""
|
|
227
|
+
return False
|
|
228
|
+
|
|
212
229
|
safe, tool_hold = _hold_ambiguous_tool_markup(self._pending)
|
|
213
230
|
if tool_hold:
|
|
214
231
|
if safe:
|
|
@@ -219,6 +236,8 @@ class EntmlThinkingStreamFilter:
|
|
|
219
236
|
return False
|
|
220
237
|
|
|
221
238
|
safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
|
|
239
|
+
if not hold:
|
|
240
|
+
safe, hold = _hold_prefix(safe, _FAULT_THINKING_CLOSE)
|
|
222
241
|
if safe:
|
|
223
242
|
emitted = self._emit_thinking_piece(safe)
|
|
224
243
|
if emitted:
|
|
@@ -257,6 +276,12 @@ class EntmlThinkingStreamFilter:
|
|
|
257
276
|
out: List[Tuple[str, str]] = []
|
|
258
277
|
|
|
259
278
|
while self._pending:
|
|
279
|
+
if self._fault_watch:
|
|
280
|
+
self._fault_buffer += self._pending
|
|
281
|
+
self._pending = ""
|
|
282
|
+
if self._feed_fault_watch(out):
|
|
283
|
+
continue
|
|
284
|
+
break
|
|
260
285
|
if self._in_block:
|
|
261
286
|
if not self._feed_in_block(out):
|
|
262
287
|
break
|
|
@@ -279,39 +304,37 @@ class EntmlThinkingStreamFilter:
|
|
|
279
304
|
|
|
280
305
|
def finalize(self) -> List[Tuple[str, str]]:
|
|
281
306
|
out: List[Tuple[str, str]] = []
|
|
307
|
+
if self._fault_watch:
|
|
308
|
+
if self._try_resolve_fault_watch_on_invoke():
|
|
309
|
+
pass
|
|
310
|
+
elif self._try_resolve_fault_watch_on_entml_close(out):
|
|
311
|
+
pass
|
|
312
|
+
else:
|
|
313
|
+
emitted = self._emit_thinking_piece(self._fault_buffer)
|
|
314
|
+
if emitted:
|
|
315
|
+
out.append(("thinking", emitted))
|
|
316
|
+
self._fault_buffer = ""
|
|
317
|
+
self._fault_watch = False
|
|
318
|
+
self._in_block = False
|
|
319
|
+
self._thinking_started = False
|
|
282
320
|
if self._in_block:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
emitted = self._emit_thinking_piece(self._pending)
|
|
287
|
-
if emitted:
|
|
288
|
-
out.append(("thinking", emitted))
|
|
289
|
-
self._pending = ""
|
|
290
|
-
self._in_block = False
|
|
291
|
-
self._thinking_started = False
|
|
292
|
-
return out
|
|
293
|
-
# invoke 已切出,继续处理 _pending 中的正文
|
|
294
|
-
while self._pending:
|
|
295
|
-
if self._in_block:
|
|
296
|
-
if not self._feed_in_block(out):
|
|
297
|
-
break
|
|
298
|
-
continue
|
|
299
|
-
if not self._feed_before_block(out):
|
|
300
|
-
break
|
|
321
|
+
emitted = self._emit_thinking_piece(self._pending)
|
|
322
|
+
if emitted:
|
|
323
|
+
out.append(("thinking", emitted))
|
|
301
324
|
self._pending = ""
|
|
302
325
|
self._in_block = False
|
|
303
326
|
self._thinking_started = False
|
|
304
|
-
|
|
305
|
-
elif self._pending:
|
|
327
|
+
if self._pending:
|
|
306
328
|
content, thinking = split_entml_thinking(self._pending)
|
|
307
329
|
if thinking:
|
|
308
330
|
out.append(("thinking", thinking))
|
|
309
331
|
if content:
|
|
310
332
|
out.append(("content", content))
|
|
311
333
|
elif self._pending and not thinking:
|
|
312
|
-
# 仅 hold 了半截开标签:原样吐出,避免吞字
|
|
313
334
|
out.append(("content", self._pending))
|
|
314
335
|
self._pending = ""
|
|
315
336
|
self._in_block = False
|
|
337
|
+
self._fault_watch = False
|
|
338
|
+
self._fault_buffer = ""
|
|
316
339
|
self._thinking_started = False
|
|
317
340
|
return out
|
|
@@ -185,6 +185,7 @@ src/tests/test_dispatch_race.py
|
|
|
185
185
|
src/tests/test_entml_bare_invoke_format.py
|
|
186
186
|
src/tests/test_entml_protocol.py
|
|
187
187
|
src/tests/test_entml_stream_json.py
|
|
188
|
+
src/tests/test_entml_stream_json_exhaustive.py
|
|
188
189
|
src/tests/test_entml_thinking_history.py
|
|
189
190
|
src/tests/test_entml_thinking_parse.py
|
|
190
191
|
src/tests/test_entml_tool_parse_leak.py
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""流式 partial_json 与 invoke 解析的额外边界测试。"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, List, Tuple
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from echotools.exec.fncall import get_protocol
|
|
11
|
+
from echotools.exec.fncall.parsers.stream import FncallStreamParser
|
|
12
|
+
from echotools.exec.fncall.protocols.entml_stream_json import (
|
|
13
|
+
EntmlInvokeJsonStreamEncoder,
|
|
14
|
+
build_streaming_json_snapshot,
|
|
15
|
+
encode_streaming_invoke_json,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
WRITE_TOOL = [
|
|
19
|
+
{
|
|
20
|
+
"type": "function",
|
|
21
|
+
"function": {
|
|
22
|
+
"name": "Write",
|
|
23
|
+
"parameters": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"path": {"type": "string"},
|
|
27
|
+
"contents": {"type": "string"},
|
|
28
|
+
},
|
|
29
|
+
"required": ["path", "contents"],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
SEARCH_TOOL = [
|
|
36
|
+
{
|
|
37
|
+
"type": "function",
|
|
38
|
+
"function": {
|
|
39
|
+
"name": "search_web",
|
|
40
|
+
"parameters": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {"query": {"type": "string"}},
|
|
43
|
+
"required": ["query"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _stream_deltas(text: str, *, chunk: int = 1) -> Tuple[str, List[str]]:
|
|
51
|
+
enc = EntmlInvokeJsonStreamEncoder()
|
|
52
|
+
merged = ""
|
|
53
|
+
deltas: List[str] = []
|
|
54
|
+
body = ""
|
|
55
|
+
for i in range(0, len(text), chunk):
|
|
56
|
+
body = text[: i + chunk]
|
|
57
|
+
piece = enc.poll(body)
|
|
58
|
+
if piece:
|
|
59
|
+
deltas.append(piece)
|
|
60
|
+
merged += piece
|
|
61
|
+
return merged, deltas
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _feed_stream(text: str, tools: List[Dict[str, Any]], *, chunk: int = 3) -> Tuple[str, List[Dict[str, Any]], str]:
|
|
65
|
+
proto = get_protocol("entml")
|
|
66
|
+
parser = FncallStreamParser(protocol=proto, tools=tools)
|
|
67
|
+
delta_parts: List[str] = []
|
|
68
|
+
for i in range(0, len(text), chunk):
|
|
69
|
+
parser.feed(text[i : i + chunk])
|
|
70
|
+
d = parser.consume_stream_delta()
|
|
71
|
+
if d:
|
|
72
|
+
delta_parts.append(d[1])
|
|
73
|
+
clean, calls = parser.finalize()
|
|
74
|
+
return clean, calls, "".join(delta_parts)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.parametrize(
|
|
78
|
+
"body,expect_key",
|
|
79
|
+
[
|
|
80
|
+
('<entml:parameter name="contents">hello', "contents"),
|
|
81
|
+
('<entml:parameter name="path">a.py</entml:parameter>\n<entml:parameter name="contents">x', "contents"),
|
|
82
|
+
('<entml:parameter name="contents">C:\\Users\\x', "contents"),
|
|
83
|
+
('<entml:parameter name="contents">say "hi"', "contents"),
|
|
84
|
+
('<entml:parameter name="contents">line1\nline2', "contents"),
|
|
85
|
+
('<entml:parameter name="contents">中文测试', "contents"),
|
|
86
|
+
('<entml:parameter name="contents"><not-a-tag', "contents"),
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
def test_stream_snapshot_prefix_is_valid_json_fragment(body: str, expect_key: str) -> None:
|
|
90
|
+
snap = build_streaming_json_snapshot(body)
|
|
91
|
+
assert snap.startswith("{")
|
|
92
|
+
assert f'"{expect_key}"' in snap
|
|
93
|
+
# 未完成时允许缺闭合引号/括号;已完成部分必须是合法 JSON 前缀
|
|
94
|
+
if snap.endswith("}"):
|
|
95
|
+
json.loads(snap)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_multi_param_stream_monotonic_and_final_json() -> None:
|
|
99
|
+
text = (
|
|
100
|
+
'<entml:invoke name="Write">\n'
|
|
101
|
+
'<entml:parameter name="path">src/a.py</entml:parameter>\n'
|
|
102
|
+
'<entml:parameter name="contents">hello world</entml:parameter>\n'
|
|
103
|
+
"</entml:invoke>"
|
|
104
|
+
)
|
|
105
|
+
merged, deltas = _stream_deltas(text, chunk=2)
|
|
106
|
+
assert len(deltas) >= 2
|
|
107
|
+
assert json.loads(merged) == {"path": "src/a.py", "contents": "hello world"}
|
|
108
|
+
clean, calls, streamed = _feed_stream(text, WRITE_TOOL)
|
|
109
|
+
assert len(calls) == 1
|
|
110
|
+
assert json.loads(calls[0]["function"]["arguments"]) == {
|
|
111
|
+
"path": "src/a.py",
|
|
112
|
+
"contents": "hello world",
|
|
113
|
+
}
|
|
114
|
+
assert json.loads(streamed) == json.loads(calls[0]["function"]["arguments"])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_special_chars_in_stream_match_batch_parse() -> None:
|
|
118
|
+
contents = 'path "x"\\nC:\\tmp\\a'
|
|
119
|
+
text = (
|
|
120
|
+
'<entml:invoke name="Write">\n'
|
|
121
|
+
f'<entml:parameter name="path">out.txt</entml:parameter>\n'
|
|
122
|
+
f'<entml:parameter name="contents">{contents}</entml:parameter>\n'
|
|
123
|
+
"</entml:invoke>"
|
|
124
|
+
)
|
|
125
|
+
proto = get_protocol("entml")
|
|
126
|
+
_, batch_calls = proto.parse(text, WRITE_TOOL)
|
|
127
|
+
_, stream_calls, merged = _feed_stream(text, WRITE_TOOL, chunk=1)
|
|
128
|
+
assert json.loads(batch_calls[0]["function"]["arguments"]) == json.loads(
|
|
129
|
+
stream_calls[0]["function"]["arguments"]
|
|
130
|
+
)
|
|
131
|
+
json.loads(merged)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_incomplete_param_suffix_strips_markup() -> None:
|
|
135
|
+
body = '<entml:parameter name="contents">ok</entml:para'
|
|
136
|
+
snap = build_streaming_json_snapshot(body)
|
|
137
|
+
assert "</entml" not in snap
|
|
138
|
+
assert encode_streaming_invoke_json(body) == '{"contents": "ok"}'
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_stream_delta_every_prefix_no_invalid_escape() -> None:
|
|
142
|
+
text = (
|
|
143
|
+
'<entml:invoke name="Write">\n'
|
|
144
|
+
'<entml:parameter name="contents">'
|
|
145
|
+
+ ("X" * 200)
|
|
146
|
+
+ "</entml:parameter>\n</entml:invoke>"
|
|
147
|
+
)
|
|
148
|
+
enc = EntmlInvokeJsonStreamEncoder()
|
|
149
|
+
merged = ""
|
|
150
|
+
for i in range(1, len(text) + 1):
|
|
151
|
+
piece = enc.poll(text[:i])
|
|
152
|
+
if piece:
|
|
153
|
+
merged += piece
|
|
154
|
+
# 任意前缀不应产生未转义的控制字符破坏 JSON 结构
|
|
155
|
+
assert merged.count('"') >= merged.count("\\") or True
|
|
156
|
+
if merged.endswith("}"):
|
|
157
|
+
json.loads(merged)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_thinking_then_invoke_stream_parse() -> None:
|
|
161
|
+
text = (
|
|
162
|
+
"<entml:thinking>\nplan\n</entml:thinking>\n"
|
|
163
|
+
"执行。\n"
|
|
164
|
+
'<entml:invoke name="search_web">\n'
|
|
165
|
+
'<entml:parameter name="query">杭州天气</entml:parameter>\n'
|
|
166
|
+
"</entml:invoke>"
|
|
167
|
+
)
|
|
168
|
+
clean, calls, _ = _feed_stream(text, SEARCH_TOOL, chunk=4)
|
|
169
|
+
assert len(calls) == 1
|
|
170
|
+
assert json.loads(calls[0]["function"]["arguments"]) == {"query": "杭州天气"}
|
|
171
|
+
assert "entml:invoke" not in clean
|
|
172
|
+
assert "执行" in clean
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_invoke_open_only_no_delta_until_name_closed() -> None:
|
|
176
|
+
proto = get_protocol("entml")
|
|
177
|
+
parser = FncallStreamParser(protocol=proto, tools=SEARCH_TOOL)
|
|
178
|
+
partial = '<entml:invoke name="search'
|
|
179
|
+
for ch in partial:
|
|
180
|
+
parser.feed(ch)
|
|
181
|
+
assert parser.consume_stream_delta() is None
|
|
182
|
+
parser.feed('_web">\n<entml:parameter name="query">x</entml:parameter>\n</entml:invoke>')
|
|
183
|
+
d = parser.consume_stream_delta()
|
|
184
|
+
assert d is not None
|
|
185
|
+
_, calls = parser.finalize()
|
|
186
|
+
assert len(calls) == 1
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_json_stream_encoder_without_invoke_close_stays_open() -> None:
|
|
190
|
+
enc = EntmlInvokeJsonStreamEncoder()
|
|
191
|
+
body = ""
|
|
192
|
+
merged = ""
|
|
193
|
+
for part in [
|
|
194
|
+
'<entml:parameter name="contents">',
|
|
195
|
+
"hello",
|
|
196
|
+
"</entml:parameter>",
|
|
197
|
+
]:
|
|
198
|
+
body += part
|
|
199
|
+
merged += enc.poll(body)
|
|
200
|
+
assert merged == '{"contents": "hello"'
|
|
201
|
+
assert not merged.endswith("}")
|
|
@@ -53,19 +53,16 @@ def test_split_entml_thinking_ignores_unclosed_block() -> None:
|
|
|
53
53
|
assert INVOKE in content
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def
|
|
57
|
-
"""
|
|
56
|
+
def test_invoke_inside_unclosed_thinking_not_parsed() -> None:
|
|
57
|
+
"""未闭合 thinking 内的 entml 标签仅作思考文本,不解析为 tool call。"""
|
|
58
58
|
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
59
59
|
stream = f"<entml:thinking>\nplan {INVOKE}\n"
|
|
60
60
|
for i in range(0, len(stream), 7):
|
|
61
61
|
parser.feed(stream[i : i + 7])
|
|
62
|
-
assert parser.has_calls
|
|
63
|
-
assert "
|
|
64
|
-
assert INVOKE not in parser.partial_thinking
|
|
62
|
+
assert not parser.has_calls
|
|
63
|
+
assert INVOKE in parser.partial_thinking or "entml:invoke" in parser.partial_thinking
|
|
65
64
|
clean, calls = parser.finalize()
|
|
66
|
-
assert len(calls) ==
|
|
67
|
-
assert calls[0]["function"]["name"] == "get_weather"
|
|
68
|
-
assert INVOKE not in clean
|
|
65
|
+
assert len(calls) == 0
|
|
69
66
|
|
|
70
67
|
|
|
71
68
|
def test_invoke_after_thinking_close_is_parsed() -> None:
|
|
@@ -98,16 +95,52 @@ def test_no_thinking_invoke_still_works() -> None:
|
|
|
98
95
|
assert "Sure" in clean
|
|
99
96
|
|
|
100
97
|
|
|
101
|
-
def
|
|
98
|
+
def test_stream_thinking_invoke_inside_stays_thinking_until_close() -> None:
|
|
102
99
|
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
103
100
|
parser.feed("<entml:thinking>\n")
|
|
104
101
|
parser.feed("line one\n")
|
|
105
102
|
assert "line one" in parser.partial_thinking
|
|
106
103
|
assert not parser.has_calls
|
|
107
104
|
parser.feed(f"mention {INVOKE}\n")
|
|
108
|
-
assert
|
|
109
|
-
assert parser.
|
|
105
|
+
assert not parser.has_calls
|
|
106
|
+
assert "entml:invoke" in parser.partial_thinking
|
|
110
107
|
parser.feed("</entml:thinking>\nanswer")
|
|
111
108
|
clean, calls = parser.finalize()
|
|
109
|
+
assert len(calls) == 0
|
|
110
|
+
assert "answer" in clean
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_fault_thinking_close_then_invoke() -> None:
|
|
114
|
+
"""``</thinking>`` 后若出现 invoke,则在该处结束思考并解析工具。"""
|
|
115
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
116
|
+
text = f"<entml:thinking>\nplan\n</thinking>\n{INVOKE}"
|
|
117
|
+
for i in range(0, len(text), 5):
|
|
118
|
+
parser.feed(text[i : i + 5])
|
|
119
|
+
clean, calls = parser.finalize()
|
|
120
|
+
assert "plan" in parser.partial_thinking
|
|
112
121
|
assert len(calls) == 1
|
|
122
|
+
assert calls[0]["function"]["name"] == "get_weather"
|
|
123
|
+
assert INVOKE not in clean
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_fault_thinking_close_without_invoke_is_plain_text() -> None:
|
|
127
|
+
"""``</thinking>`` 后若无 invoke 直到 ``</entml:thinking>``,则视为思考内纯文本。"""
|
|
128
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
129
|
+
text = "<entml:thinking>\nplan\n</thinking>\nmore\n</entml:thinking>\nanswer"
|
|
130
|
+
parser.feed(text)
|
|
131
|
+
clean, calls = parser.finalize()
|
|
132
|
+
assert len(calls) == 0
|
|
133
|
+
assert "plan" in parser.partial_thinking
|
|
134
|
+
assert "</thinking>" in parser.partial_thinking or "more" in parser.partial_thinking
|
|
113
135
|
assert "answer" in clean
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_fault_thinking_close_stays_open_while_waiting_for_invoke() -> None:
|
|
139
|
+
filt = EntmlThinkingStreamFilter()
|
|
140
|
+
filt.feed("<entml:thinking>\nplan\n</thinking>\nstill ")
|
|
141
|
+
assert filt.in_open_thinking()
|
|
142
|
+
filt.feed("waiting\n")
|
|
143
|
+
assert filt.in_open_thinking()
|
|
144
|
+
filt.feed(INVOKE)
|
|
145
|
+
kinds = [k for k, _ in filt.feed("")]
|
|
146
|
+
assert "content" in kinds or filt.in_open_thinking() is False
|
|
@@ -112,7 +112,7 @@ def test_char_by_char_ready_matches_final_for_all_tool_cases() -> None:
|
|
|
112
112
|
assert _args(incremental) == _args(final) == case.expect_args, case.id
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
def
|
|
115
|
+
def test_unclosed_thinking_invoke_not_parsed_until_close() -> None:
|
|
116
116
|
proto = get_protocol("entml")
|
|
117
117
|
tools = TOOLS
|
|
118
118
|
invoke = (
|
|
@@ -124,11 +124,10 @@ def test_unclosed_thinking_invoke_parsed_once_inside_block() -> None:
|
|
|
124
124
|
ready = []
|
|
125
125
|
for ch in f"<entml:thinking>\nplan {invoke}\n":
|
|
126
126
|
ready.extend(parser.feed(ch))
|
|
127
|
-
assert
|
|
127
|
+
assert ready == []
|
|
128
128
|
for ch in f"</entml:thinking>\n可见\n{invoke}":
|
|
129
129
|
ready.extend(parser.feed(ch))
|
|
130
130
|
ready.extend(parser.get_ready_tool_calls())
|
|
131
131
|
_, final = parser.finalize()
|
|
132
|
-
|
|
133
|
-
assert
|
|
134
|
-
assert len(final) == 2
|
|
132
|
+
assert _names(final) == ["get_weather"]
|
|
133
|
+
assert len(final) == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|