echotools 2.3.45__tar.gz → 2.3.47__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.45 → echotools-2.3.47}/PKG-INFO +2 -1
- {echotools-2.3.45 → echotools-2.3.47}/pyproject.toml +2 -2
- echotools-2.3.47/setup.cfg +8 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/parsers/stream.py +69 -7
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/inject.py +7 -1
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml.py +48 -16
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_invoke.py +1 -1
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_patterns.py +18 -0
- echotools-2.3.47/src/echotools/exec/fncall/protocols/entml_stream_json.py +141 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_think/core.py +18 -5
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_think/parse.py +122 -4
- echotools-2.3.47/src/echotools/media/console/ui.py +128 -0
- echotools-2.3.47/src/echotools/media/console/uicore/mixins/__init__.py +15 -0
- echotools-2.3.47/src/echotools/media/console/uicore/mixins/uibase.py +195 -0
- echotools-2.3.47/src/echotools/media/console/uicore/mixins/uicmds.py +224 -0
- echotools-2.3.47/src/echotools/media/console/uicore/mixins/uiinteract.py +199 -0
- echotools-2.3.47/src/echotools/media/console/uicore/mixins/uioutput.py +304 -0
- echotools-2.3.47/src/echotools/media/console/uicore/mixins/uistream.py +144 -0
- echotools-2.3.47/src/echotools/media/console/uicore/ui_console.py +20 -0
- echotools-2.3.47/src/echotools/media/console/uicore/ui_io.py +27 -0
- echotools-2.3.47/src/echotools/media/console/uicore/ui_log.py +67 -0
- echotools-2.3.47/src/echotools/media/console/uicore/ui_platform.py +389 -0
- echotools-2.3.47/src/echotools/media/console/uicore/ui_text.py +368 -0
- echotools-2.3.47/src/echotools/media/console/uicore/ui_types.py +246 -0
- echotools-2.3.47/src/echotools/media/console/uilayout/ui_countdown.py +62 -0
- echotools-2.3.47/src/echotools/media/console/uilayout/ui_layout.py +50 -0
- echotools-2.3.47/src/echotools/media/console/uilayout/ui_misc.py +336 -0
- echotools-2.3.47/src/echotools/media/console/uilayout/ui_panel.py +137 -0
- echotools-2.3.47/src/echotools/media/console/uilayout/ui_table.py +205 -0
- echotools-2.3.47/src/echotools/media/console/uilayout/ui_tree.py +146 -0
- echotools-2.3.47/src/echotools/media/console/uiwidgets/ui_box.py +197 -0
- echotools-2.3.47/src/echotools/media/console/uiwidgets/ui_editor.py +360 -0
- echotools-2.3.47/src/echotools/media/console/uiwidgets/ui_progress.py +296 -0
- echotools-2.3.47/src/echotools/media/console/uiwidgets/ui_select.py +199 -0
- echotools-2.3.47/src/echotools/media/console/uiwidgets/ui_stream.py +85 -0
- echotools-2.3.47/src/echotools/media/web/input_box/components/sortablelist.js +104 -0
- echotools-2.3.47/src/echotools/media/web/input_box/components/textinput.js +185 -0
- echotools-2.3.47/src/echotools/media/web/input_box/inputbox.js +277 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools.egg-info/PKG-INFO +2 -1
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools.egg-info/SOURCES.txt +27 -1
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/fixtures/simulated_llm_tool_responses.py +44 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_adversarial_stream_splits.py +4 -5
- echotools-2.3.47/src/tests/test_entml_bare_invoke_format.py +375 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_protocol.py +56 -0
- echotools-2.3.47/src/tests/test_entml_stream_json.py +76 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_tool_parse_leak.py +0 -1
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_tool_pipeline.py +30 -2
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_fncall_stream_thinking.py +11 -8
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_simulated_llm_tool_parse.py +7 -5
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_stream_realtime_parse.py +12 -15
- echotools-2.3.45/setup.cfg +0 -4
- echotools-2.3.45/src/echotools/media/console/ui.py +0 -4242
- echotools-2.3.45/src/echotools/media/web/input_box/components/sortablelist.js +0 -104
- echotools-2.3.45/src/echotools/media/web/input_box/components/textinput.js +0 -186
- echotools-2.3.45/src/echotools/media/web/input_box/fonts/tailwind.css +0 -1
- echotools-2.3.45/src/echotools/media/web/input_box/inputbox.js +0 -278
- {echotools-2.3.45 → echotools-2.3.47}/README.md +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/docs/README.md +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/compat.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools/version.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/bench_import.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/conftest.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_completion.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_config.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_console.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_errors.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_events.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_facade.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_ids.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_imports.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_io.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_keys.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_logger.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_merge.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_usage.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_web.py +0 -0
- {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_web_broker.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: echotools
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.47
|
|
4
4
|
Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
|
|
5
|
+
Home-page: https://github.com/nichengfuben/echotools
|
|
5
6
|
Author: nichengfuben
|
|
6
7
|
License: MIT
|
|
7
8
|
Project-URL: Homepage, https://github.com/nichengfuben/echotools
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "echotools"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.47"
|
|
8
8
|
description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
|
|
9
9
|
readme = "docs/README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -98,7 +98,7 @@ module = "echotools.exec.terminal.*"
|
|
|
98
98
|
ignore_errors = true
|
|
99
99
|
|
|
100
100
|
[[tool.mypy.overrides]]
|
|
101
|
-
module = "echotools.media.console
|
|
101
|
+
module = "echotools.media.console.*"
|
|
102
102
|
ignore_errors = true
|
|
103
103
|
|
|
104
104
|
[tool.pytest.ini_options]
|
|
@@ -52,6 +52,8 @@ class FncallStreamParser:
|
|
|
52
52
|
self._thinking_parts: List[str] = []
|
|
53
53
|
self._thinking_filter = _make_thinking_filter(protocol)
|
|
54
54
|
self._emitted_invoke_count: int = 0
|
|
55
|
+
self._json_stream_encoder = None
|
|
56
|
+
self._pending_stream_delta: Optional[Tuple[str, str]] = None
|
|
55
57
|
|
|
56
58
|
# 三种情况:
|
|
57
59
|
# 1. 协议实现了 get_stream_end_tags() 且返回非空列表 → 用声明的结束标记
|
|
@@ -140,13 +142,28 @@ class FncallStreamParser:
|
|
|
140
142
|
if text:
|
|
141
143
|
self._text_parts.append(text)
|
|
142
144
|
|
|
145
|
+
def _normalize_stream_chunk(self, text: str) -> str:
|
|
146
|
+
fn = getattr(self._protocol, "normalize_stream_buffer", None)
|
|
147
|
+
if callable(fn):
|
|
148
|
+
return fn(text)
|
|
149
|
+
return text
|
|
150
|
+
|
|
143
151
|
def _feed_content_waiting(self, text: str) -> None:
|
|
144
152
|
"""thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
|
|
145
153
|
if not text:
|
|
146
154
|
return
|
|
155
|
+
text = self._normalize_stream_chunk(text)
|
|
147
156
|
trigger_tags = self._protocol.get_trigger_tags()
|
|
148
157
|
found, pos = self._protocol.detect_start(text)
|
|
149
158
|
if not found:
|
|
159
|
+
hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
|
|
160
|
+
if hold_from_fn is not None:
|
|
161
|
+
hold_from = hold_from_fn(text)
|
|
162
|
+
if hold_from is not None:
|
|
163
|
+
if hold_from > 0:
|
|
164
|
+
self._append_content_text(text[:hold_from])
|
|
165
|
+
self._waiting_tail = text[hold_from:]
|
|
166
|
+
return
|
|
150
167
|
safe, remain = self._split_safe_text(text, trigger_tags)
|
|
151
168
|
if safe:
|
|
152
169
|
self._append_content_text(safe)
|
|
@@ -173,7 +190,7 @@ class FncallStreamParser:
|
|
|
173
190
|
|
|
174
191
|
def _feed_waiting(self, chunk: str) -> None:
|
|
175
192
|
"""在 WAITING_FOR_TAG 状态下处理新块。"""
|
|
176
|
-
combined = self._waiting_tail + chunk
|
|
193
|
+
combined = self._normalize_stream_chunk(self._waiting_tail + chunk)
|
|
177
194
|
self._waiting_tail = ""
|
|
178
195
|
|
|
179
196
|
# 已进入 thinking 块时,块内不做 invoke 检测。
|
|
@@ -196,8 +213,8 @@ class FncallStreamParser:
|
|
|
196
213
|
return
|
|
197
214
|
|
|
198
215
|
# 无完整工具开标签时,再处理未闭合 thinking / 思考开标签 holdback。
|
|
199
|
-
# 必须放在 detect_start 之后:否则 `<
|
|
200
|
-
#
|
|
216
|
+
# 必须放在 detect_start 之后:否则 legacy 外壳后的 `<en` 会被
|
|
217
|
+
# 尾部误判为 thinking 前缀,导致 invoke 内容被截断。
|
|
201
218
|
if self._thinking_filter is not None:
|
|
202
219
|
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
203
220
|
has_unclosed_entml_thinking,
|
|
@@ -206,6 +223,15 @@ class FncallStreamParser:
|
|
|
206
223
|
self._feed_waiting_thinking_plain(combined)
|
|
207
224
|
return
|
|
208
225
|
|
|
226
|
+
hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
|
|
227
|
+
if hold_from_fn is not None:
|
|
228
|
+
hold_from = hold_from_fn(combined)
|
|
229
|
+
if hold_from is not None:
|
|
230
|
+
if hold_from > 0:
|
|
231
|
+
self._emit_text(combined[:hold_from])
|
|
232
|
+
self._waiting_tail = combined[hold_from:]
|
|
233
|
+
return
|
|
234
|
+
|
|
209
235
|
safe, remain = self._split_safe_text(combined, trigger_tags)
|
|
210
236
|
if safe:
|
|
211
237
|
self._emit_text(safe)
|
|
@@ -222,9 +248,11 @@ class FncallStreamParser:
|
|
|
222
248
|
return False
|
|
223
249
|
|
|
224
250
|
def feed(self, chunk: str) -> List[Dict[str, Any]]:
|
|
225
|
-
"""
|
|
251
|
+
"""喂入新的流式文本块。
|
|
226
252
|
|
|
227
|
-
|
|
253
|
+
返回本轮新完成的 tool_calls(与 ``get_ready_tool_calls()`` 相同语义:
|
|
254
|
+
调用后计数前进,勿再对本轮结果重复调用 ``get_ready_tool_calls``)。
|
|
255
|
+
DONE 或 finalize 后调用返回 ``[]``。
|
|
228
256
|
"""
|
|
229
257
|
if not chunk or self._state == self.DONE:
|
|
230
258
|
return []
|
|
@@ -236,11 +264,45 @@ class FncallStreamParser:
|
|
|
236
264
|
if self._state == self.WAITING_FOR_TAG:
|
|
237
265
|
self._feed_waiting(chunk)
|
|
238
266
|
else:
|
|
239
|
-
self._fncall_buf += chunk
|
|
267
|
+
self._fncall_buf += self._normalize_stream_chunk(chunk)
|
|
240
268
|
if self._is_call_closed():
|
|
241
269
|
self._state = self.DONE
|
|
242
270
|
|
|
243
|
-
|
|
271
|
+
self._pending_stream_delta = self._poll_streaming_tool_input_delta()
|
|
272
|
+
ready = self.get_ready_tool_calls()
|
|
273
|
+
if ready:
|
|
274
|
+
if self._json_stream_encoder is not None:
|
|
275
|
+
self._json_stream_encoder.reset()
|
|
276
|
+
return ready
|
|
277
|
+
|
|
278
|
+
def _poll_streaming_tool_input_delta(self) -> Optional[Tuple[str, str]]:
|
|
279
|
+
"""invoke 开标签完整匹配后,返回 (tool_name, partial_json_delta)。"""
|
|
280
|
+
if not self._detected or getattr(self._protocol, "id", None) != "entml":
|
|
281
|
+
return None
|
|
282
|
+
if self._state not in (self.IN_FUNCTION_CALLS, self.DONE):
|
|
283
|
+
return None
|
|
284
|
+
from echotools.exec.fncall.protocols.entml_stream_json import (
|
|
285
|
+
EntmlInvokeJsonStreamEncoder,
|
|
286
|
+
split_invoke_open,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
parsed = split_invoke_open(self._fncall_buf)
|
|
290
|
+
if not parsed:
|
|
291
|
+
return None
|
|
292
|
+
name, body_start = parsed
|
|
293
|
+
body = self._fncall_buf[body_start:]
|
|
294
|
+
if self._json_stream_encoder is None:
|
|
295
|
+
self._json_stream_encoder = EntmlInvokeJsonStreamEncoder()
|
|
296
|
+
delta = self._json_stream_encoder.poll(body)
|
|
297
|
+
if not delta:
|
|
298
|
+
return None
|
|
299
|
+
return (name, delta)
|
|
300
|
+
|
|
301
|
+
def consume_stream_delta(self) -> Optional[Tuple[str, str]]:
|
|
302
|
+
"""取出本轮 ``feed`` 产生的 streaming partial_json 增量。"""
|
|
303
|
+
delta = self._pending_stream_delta
|
|
304
|
+
self._pending_stream_delta = None
|
|
305
|
+
return delta
|
|
244
306
|
|
|
245
307
|
def _assembly_for_tool_parse(self) -> str:
|
|
246
308
|
"""可供工具解析的已缓冲文本(不含 thinking 过滤器内部 pending)。"""
|
|
@@ -147,11 +147,17 @@ def inject_fncall(
|
|
|
147
147
|
if not tools:
|
|
148
148
|
if protocol.id == "entml":
|
|
149
149
|
sections: List[str] = []
|
|
150
|
+
if user_system_prompt and user_system_prompt.strip():
|
|
151
|
+
sections.append(
|
|
152
|
+
f"<user_system_prompt>\n{user_system_prompt.strip()}\n</user_system_prompt>"
|
|
153
|
+
)
|
|
150
154
|
if history_text.strip():
|
|
151
155
|
sections.append(format_entml_conversation_history(history_text))
|
|
152
156
|
sections.append(format_entml_current_user_message(current_user_message))
|
|
153
157
|
# thinking 放在最后,超限截断时优先保留在 send_text 尾部
|
|
154
|
-
thinking_section = build_entml_thinking_section(
|
|
158
|
+
thinking_section = build_entml_thinking_section(
|
|
159
|
+
protocol_options, has_tools=False
|
|
160
|
+
)
|
|
155
161
|
if thinking_section:
|
|
156
162
|
sections.append(thinking_section)
|
|
157
163
|
prompt = "\n\n".join(sections)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Entropy ML (entml) 协议实现。
|
|
2
2
|
|
|
3
|
-
- 工具调用:<entml:
|
|
3
|
+
- 工具调用:<entml:invoke> / <entml:parameter>(legacy ``function_calls`` 外壳仅解析兼容)
|
|
4
4
|
- 对话历史:<entml:conversation_history>
|
|
5
5
|
- 当前用户消息:<current_user_message>
|
|
6
6
|
"""
|
|
@@ -20,6 +20,9 @@ from echotools.exec.fncall.protocols.entml_invoke import (
|
|
|
20
20
|
)
|
|
21
21
|
from echotools.exec.fncall.protocols.entml_patterns import (
|
|
22
22
|
BLOCK_RE,
|
|
23
|
+
extract_attr_value,
|
|
24
|
+
normalize_entml_name,
|
|
25
|
+
strip_legacy_function_calls_wrapper,
|
|
23
26
|
strip_tool_entml_residue,
|
|
24
27
|
)
|
|
25
28
|
from echotools.exec.fncall.protocols.entml_think.core import (
|
|
@@ -136,20 +139,22 @@ class EntmlProtocol(ToolProtocol):
|
|
|
136
139
|
|
|
137
140
|
_TRIGGER = "<entml:invoke>"
|
|
138
141
|
_TRIGGER_PREFIX = "<entml:invoke"
|
|
139
|
-
|
|
142
|
+
_LEGACY_WRAPPER_PREFIX = "<entml:function_calls"
|
|
140
143
|
_THINKING_PREFIX = "<entml:thinking"
|
|
141
144
|
|
|
142
145
|
def get_trigger_tags(self) -> List[str]:
|
|
143
|
-
# invoke /
|
|
146
|
+
# invoke / thinking 前缀 holdback,避免 `<e` 歧义被提前吐出
|
|
144
147
|
return [
|
|
145
148
|
self._TRIGGER,
|
|
146
149
|
self._TRIGGER_PREFIX,
|
|
147
|
-
self._WRAPPER_PREFIX,
|
|
148
|
-
f"{self._WRAPPER_PREFIX}>",
|
|
149
150
|
self._THINKING_PREFIX,
|
|
150
151
|
f"{self._THINKING_PREFIX}>",
|
|
151
152
|
]
|
|
152
153
|
|
|
154
|
+
def normalize_stream_buffer(self, buffer: str) -> str:
|
|
155
|
+
"""流式处理前剥离 legacy function_calls 外壳(提示词已不再要求)。"""
|
|
156
|
+
return strip_legacy_function_calls_wrapper(buffer)
|
|
157
|
+
|
|
153
158
|
def get_stream_end_tags(self) -> List[str]:
|
|
154
159
|
"""新格式无外层 wrapper,不自动关闭流,由 finalize() 统一解析。"""
|
|
155
160
|
return []
|
|
@@ -194,25 +199,52 @@ class EntmlProtocol(ToolProtocol):
|
|
|
194
199
|
sections.append(format_entml_current_user_message(current_user_message))
|
|
195
200
|
|
|
196
201
|
# thinking 放在最后,超限截断时优先保留在 send_text 尾部
|
|
197
|
-
thinking_section = build_entml_thinking_section(
|
|
202
|
+
thinking_section = build_entml_thinking_section(
|
|
203
|
+
protocol_options, has_tools=bool(tool_descs)
|
|
204
|
+
)
|
|
198
205
|
if thinking_section:
|
|
199
206
|
sections.append(thinking_section)
|
|
200
207
|
|
|
201
208
|
return "\n\n".join(sections)
|
|
202
209
|
|
|
203
210
|
def detect_start(self, buffer: str) -> Tuple[bool, int]:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
"""invoke 开标签(含 name 且闭合 ``>``)稳定后才视为工具流开始。"""
|
|
212
|
+
invoke_pos = self._find_complete_invoke_open(buffer)
|
|
213
|
+
if invoke_pos < 0:
|
|
214
|
+
return (False, -1)
|
|
215
|
+
return (True, invoke_pos)
|
|
216
|
+
|
|
217
|
+
def find_fncall_hold_from(self, buffer: str) -> Optional[int]:
|
|
218
|
+
"""invoke 未稳定前 hold 前缀;legacy 外壳开标签未闭合时同样 hold。"""
|
|
219
|
+
if self._find_complete_invoke_open(buffer) >= 0:
|
|
220
|
+
return None
|
|
221
|
+
hold: Optional[int] = None
|
|
222
|
+
legacy_pos = buffer.find(self._LEGACY_WRAPPER_PREFIX)
|
|
223
|
+
if legacy_pos >= 0:
|
|
224
|
+
close = buffer.find(">", legacy_pos)
|
|
225
|
+
if close < 0 and (hold is None or legacy_pos < hold):
|
|
226
|
+
hold = legacy_pos
|
|
227
|
+
invoke_pos = buffer.find(self._TRIGGER_PREFIX)
|
|
228
|
+
if invoke_pos >= 0 and (hold is None or invoke_pos < hold):
|
|
229
|
+
hold = invoke_pos
|
|
230
|
+
return hold
|
|
231
|
+
|
|
232
|
+
def _find_complete_invoke_open(self, buffer: str) -> int:
|
|
233
|
+
"""返回首个含 name 且已闭合 ``>`` 的 ``<entml:invoke`` 起始下标;否则 -1。"""
|
|
234
|
+
search_from = 0
|
|
235
|
+
prefix_len = len(self._TRIGGER_PREFIX)
|
|
236
|
+
while True:
|
|
237
|
+
pos = buffer.find(self._TRIGGER_PREFIX, search_from)
|
|
207
238
|
if pos < 0:
|
|
208
|
-
|
|
209
|
-
close = buffer.find(">", pos +
|
|
239
|
+
return -1
|
|
240
|
+
close = buffer.find(">", pos + prefix_len)
|
|
210
241
|
if close < 0:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
242
|
+
return -1
|
|
243
|
+
attrs = buffer[pos + prefix_len : close]
|
|
244
|
+
name = extract_attr_value(attrs, "name")
|
|
245
|
+
if name and normalize_entml_name(name):
|
|
246
|
+
return pos
|
|
247
|
+
search_from = close + 1
|
|
216
248
|
|
|
217
249
|
def parse(
|
|
218
250
|
self,
|
|
@@ -93,7 +93,7 @@ def format_entml_parameter_value(value: Any) -> str:
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def format_entml_tool_calls(tool_calls: List[Dict[str, Any]]) -> str:
|
|
96
|
-
"""将 tool_call
|
|
96
|
+
"""将 tool_call 列表渲染为裸 ``<entml:invoke>`` 块(无 function_calls 外壳)。"""
|
|
97
97
|
if not tool_calls:
|
|
98
98
|
return ""
|
|
99
99
|
|
|
@@ -39,6 +39,15 @@ _TOOL_WRAPPER_PAIR_RE = re.compile(
|
|
|
39
39
|
r"<entml:function_calls\b[^>]*>[\s\S]*?</entml:function_calls>",
|
|
40
40
|
re.DOTALL,
|
|
41
41
|
)
|
|
42
|
+
# 旧版 function_calls 外壳(提示词已不再要求;流式/解析前静默剥离完整开闭标签)。
|
|
43
|
+
_LEGACY_WRAPPER_OPEN_RE = re.compile(
|
|
44
|
+
r"<entml:function_calls\b[^>]*>\s*",
|
|
45
|
+
re.IGNORECASE,
|
|
46
|
+
)
|
|
47
|
+
_LEGACY_WRAPPER_CLOSE_RE = re.compile(
|
|
48
|
+
r"\s*</entml:function_calls\s*>",
|
|
49
|
+
re.IGNORECASE,
|
|
50
|
+
)
|
|
42
51
|
_TOOL_ORPHAN_TAG_RE = re.compile(
|
|
43
52
|
r"</?entml:(?:function_calls|invoke|parameter|parameters)\b[^>]*/?>",
|
|
44
53
|
re.DOTALL,
|
|
@@ -98,6 +107,15 @@ def parse_sub_tags(
|
|
|
98
107
|
return args
|
|
99
108
|
|
|
100
109
|
|
|
110
|
+
def strip_legacy_function_calls_wrapper(text: str) -> str:
|
|
111
|
+
"""移除完整 legacy ``<entml:function_calls>`` 开/闭标签(裸 invoke 为一等格式)。"""
|
|
112
|
+
if not text:
|
|
113
|
+
return text
|
|
114
|
+
out = _LEGACY_WRAPPER_OPEN_RE.sub("", text)
|
|
115
|
+
out = _LEGACY_WRAPPER_CLOSE_RE.sub("", out)
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
101
119
|
def strip_tool_entml_residue(content: str) -> str:
|
|
102
120
|
"""剥离工具相关 entml 标签残留,保留 thinking 等非工具标签。"""
|
|
103
121
|
if not content:
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from .entml_patterns import extract_attr_value, normalize_entml_name
|
|
8
|
+
|
|
9
|
+
_PARAM_OPEN_RE = re.compile(r"<entml:parameter\b([^>]*)>", re.IGNORECASE)
|
|
10
|
+
_PARAM_CLOSE = "</entml:parameter>"
|
|
11
|
+
_INVOKE_CLOSE = "</entml:invoke>"
|
|
12
|
+
_INVOKE_OPEN_PREFIX = "<entml:invoke"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _strip_incomplete_markup_suffix(value: str) -> str:
|
|
16
|
+
lt = value.rfind("<")
|
|
17
|
+
if lt < 0:
|
|
18
|
+
return value
|
|
19
|
+
tail = value[lt:]
|
|
20
|
+
for tag in (_PARAM_CLOSE, _INVOKE_CLOSE):
|
|
21
|
+
if tag.startswith(tail) and tail != tag:
|
|
22
|
+
return value[:lt]
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def split_invoke_open(buffer: str) -> Optional[Tuple[str, int]]:
|
|
27
|
+
search_from = 0
|
|
28
|
+
prefix_len = len(_INVOKE_OPEN_PREFIX)
|
|
29
|
+
while True:
|
|
30
|
+
pos = buffer.find(_INVOKE_OPEN_PREFIX, search_from)
|
|
31
|
+
if pos < 0:
|
|
32
|
+
return None
|
|
33
|
+
gt = buffer.find(">", pos + prefix_len)
|
|
34
|
+
if gt < 0:
|
|
35
|
+
return None
|
|
36
|
+
attrs = buffer[pos + prefix_len : gt]
|
|
37
|
+
name = extract_attr_value(attrs, "name")
|
|
38
|
+
if name and normalize_entml_name(name):
|
|
39
|
+
return normalize_entml_name(name), gt + 1
|
|
40
|
+
search_from = gt + 1
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _json_string_body(value: str) -> str:
|
|
44
|
+
"""JSON 字符串内部片段(不含外围引号)。"""
|
|
45
|
+
return json.dumps(value, ensure_ascii=False)[1:-1]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _parse_parameter_entries(body: str) -> List[Tuple[str, str, bool]]:
|
|
49
|
+
"""返回 [(key, value, is_complete), ...]。"""
|
|
50
|
+
if not body:
|
|
51
|
+
return []
|
|
52
|
+
entries: List[Tuple[str, str, bool]] = []
|
|
53
|
+
i = 0
|
|
54
|
+
while i < len(body):
|
|
55
|
+
match = _PARAM_OPEN_RE.search(body, i)
|
|
56
|
+
if not match:
|
|
57
|
+
break
|
|
58
|
+
attrs = match.group(1) or ""
|
|
59
|
+
pname = extract_attr_value(attrs, "name")
|
|
60
|
+
if not pname:
|
|
61
|
+
i = match.end()
|
|
62
|
+
continue
|
|
63
|
+
key = normalize_entml_name(pname)
|
|
64
|
+
val_start = match.end()
|
|
65
|
+
close = body.find(_PARAM_CLOSE, val_start)
|
|
66
|
+
if close < 0:
|
|
67
|
+
entries.append((key, _strip_incomplete_markup_suffix(body[val_start:]), False))
|
|
68
|
+
break
|
|
69
|
+
entries.append((key, body[val_start:close], True))
|
|
70
|
+
i = close + len(_PARAM_CLOSE)
|
|
71
|
+
return entries
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_streaming_json_snapshot(body: str) -> str:
|
|
75
|
+
"""构造当前应已发出的 partial_json 累积串(可未完成)。"""
|
|
76
|
+
invoke_closed = _INVOKE_CLOSE in body
|
|
77
|
+
inner = body[: body.index(_INVOKE_CLOSE)] if invoke_closed else body
|
|
78
|
+
entries = _parse_parameter_entries(inner)
|
|
79
|
+
if not entries:
|
|
80
|
+
return ""
|
|
81
|
+
|
|
82
|
+
parts: List[str] = ["{"]
|
|
83
|
+
for idx, (key, value, is_complete) in enumerate(entries):
|
|
84
|
+
if idx > 0:
|
|
85
|
+
parts.append(", ")
|
|
86
|
+
parts.append(json.dumps(key, ensure_ascii=False))
|
|
87
|
+
parts.append(": ")
|
|
88
|
+
parts.append('"')
|
|
89
|
+
parts.append(_json_string_body(value))
|
|
90
|
+
if is_complete:
|
|
91
|
+
parts.append('"')
|
|
92
|
+
else:
|
|
93
|
+
return "".join(parts)
|
|
94
|
+
|
|
95
|
+
if entries and all(entry[2] for entry in entries):
|
|
96
|
+
parts.append("}")
|
|
97
|
+
elif invoke_closed:
|
|
98
|
+
parts.append("}")
|
|
99
|
+
return "".join(parts)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _parse_partial_parameter_body(body: str) -> Dict[str, Any]:
|
|
103
|
+
obj: Dict[str, Any] = {}
|
|
104
|
+
for key, value, is_complete in _parse_parameter_entries(
|
|
105
|
+
body[: body.index(_INVOKE_CLOSE)] if _INVOKE_CLOSE in body else body
|
|
106
|
+
):
|
|
107
|
+
if is_complete or value:
|
|
108
|
+
obj[key] = value
|
|
109
|
+
return obj
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def encode_streaming_invoke_json(body: str) -> str:
|
|
113
|
+
obj = _parse_partial_parameter_body(body)
|
|
114
|
+
if not obj:
|
|
115
|
+
return ""
|
|
116
|
+
return json.dumps(obj, ensure_ascii=False)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class EntmlInvokeJsonStreamEncoder:
|
|
120
|
+
"""invoke 开标签之后,将 growing body 编码为可拼接的 partial_json 增量。"""
|
|
121
|
+
|
|
122
|
+
def __init__(self) -> None:
|
|
123
|
+
self._emitted = ""
|
|
124
|
+
|
|
125
|
+
def poll(self, body: str) -> str:
|
|
126
|
+
snapshot = build_streaming_json_snapshot(body)
|
|
127
|
+
if not snapshot.startswith(self._emitted):
|
|
128
|
+
common = 0
|
|
129
|
+
for a, b in zip(self._emitted, snapshot):
|
|
130
|
+
if a != b:
|
|
131
|
+
break
|
|
132
|
+
common += 1
|
|
133
|
+
delta = snapshot[common:]
|
|
134
|
+
self._emitted = snapshot
|
|
135
|
+
return delta
|
|
136
|
+
delta = snapshot[len(self._emitted) :]
|
|
137
|
+
self._emitted = snapshot
|
|
138
|
+
return delta
|
|
139
|
+
|
|
140
|
+
def reset(self) -> None:
|
|
141
|
+
self._emitted = ""
|
{echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_think/core.py
RENAMED
|
@@ -62,16 +62,24 @@ _DEFAULT_MAX_BY_LEVEL: Dict[str, int] = {
|
|
|
62
62
|
|
|
63
63
|
_LEGACY_ON_DEFAULT_MAX = _DEFAULT_MAX_BY_LEVEL["medium"]
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
_THINKING_BEHAVIOR_ON_WITH_TOOLS = """\
|
|
66
66
|
Your default is to think before it answers to give the person the best possible answer. Even for questions that might seem obvious, if there are any signs of lurking complexity, You takes the time to open up an extended thinking block and dig in to make sure it's got the details figured out and isn't just pattern-matching to the familiar. At the end of its thinking, You restates which language it should respond in.
|
|
67
67
|
|
|
68
68
|
You MUST output a <entml:thinking> block before any other content in every reply. Only after that block, output your visible reply and/or <entml:invoke> tool call(s). Never skip the thinking block."""
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
_THINKING_BEHAVIOR_ON_NO_TOOLS = """\
|
|
71
|
+
Your default is to think before it answers to give the person the best possible answer. Even for questions that might seem obvious, if there are any signs of lurking complexity, You takes the time to open up an extended thinking block and dig in to make sure it's got the details figured out and isn't just pattern-matching to the familiar. At the end of its thinking, You restates which language it should respond in.
|
|
72
|
+
|
|
73
|
+
You MUST output a <entml:thinking> block before any other content in every reply. Only after that block, output your visible reply. Never skip the thinking block."""
|
|
74
|
+
|
|
75
|
+
_THINKING_BEHAVIOR_AUTO_WITH_TOOLS = """\
|
|
71
76
|
You decide whether extended thinking helps for each reply. When the question has hidden complexity, when tool results need interpretation, or when you are uncertain, open a <entml:thinking> block before continuing and strongly prefer to do so rather than guessing.
|
|
72
77
|
|
|
73
78
|
After completed tool turns appear in conversation history inside <tool> blocks (for example a line like [tool_name: value] followed by its result), strongly consider outputting a <entml:thinking> block before your next visible reply or tool call."""
|
|
74
79
|
|
|
80
|
+
_THINKING_BEHAVIOR_AUTO_NO_TOOLS = """\
|
|
81
|
+
You decide whether extended thinking helps for each reply. When the question has hidden complexity or when you are uncertain, open a <entml:thinking> block before continuing and strongly prefer to do so rather than guessing."""
|
|
82
|
+
|
|
75
83
|
|
|
76
84
|
def normalize_thinking_level(level: Any) -> Optional[str]:
|
|
77
85
|
"""将 echotools 思考挡位归一化为 none | low | medium | high | xhigh | max | auto。"""
|
|
@@ -155,13 +163,18 @@ def _uses_forced_thinking_behavior(injection_mode: str) -> bool:
|
|
|
155
163
|
return injection_mode != "auto"
|
|
156
164
|
|
|
157
165
|
|
|
158
|
-
def _format_thinking_behavior(injection_mode: str) -> str:
|
|
159
|
-
|
|
166
|
+
def _format_thinking_behavior(injection_mode: str, *, has_tools: bool) -> str:
|
|
167
|
+
if _uses_forced_thinking_behavior(injection_mode):
|
|
168
|
+
body = _THINKING_BEHAVIOR_ON_WITH_TOOLS if has_tools else _THINKING_BEHAVIOR_ON_NO_TOOLS
|
|
169
|
+
else:
|
|
170
|
+
body = _THINKING_BEHAVIOR_AUTO_WITH_TOOLS if has_tools else _THINKING_BEHAVIOR_AUTO_NO_TOOLS
|
|
160
171
|
return f"<thinking_behavior>\n{body}\n</thinking_behavior>"
|
|
161
172
|
|
|
162
173
|
|
|
163
174
|
def build_entml_thinking_section(
|
|
164
175
|
protocol_options: Optional[Dict[str, Any]] = None,
|
|
176
|
+
*,
|
|
177
|
+
has_tools: bool = True,
|
|
165
178
|
) -> str:
|
|
166
179
|
"""按思考挡位构建注入块:thinking_mode + max_thinking_length + thinking_behavior。"""
|
|
167
180
|
resolved = resolve_thinking_injection(protocol_options)
|
|
@@ -173,5 +186,5 @@ def build_entml_thinking_section(
|
|
|
173
186
|
if max_length is not None:
|
|
174
187
|
lines.append(f"<entml:max_thinking_length>{max_length}</entml:max_thinking_length>")
|
|
175
188
|
lines.append("")
|
|
176
|
-
lines.append(_format_thinking_behavior(injection_mode))
|
|
189
|
+
lines.append(_format_thinking_behavior(injection_mode, has_tools=has_tools))
|
|
177
190
|
return "\n".join(lines)
|