echotools 2.4.1__tar.gz → 2.4.2__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.4.1 → echotools-2.4.2}/PKG-INFO +2 -2
- {echotools-2.4.1 → echotools-2.4.2}/README.md +1 -1
- {echotools-2.4.1 → echotools-2.4.2}/pyproject.toml +1 -1
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/prompt_helpers.py +14 -1
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml.py +12 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/params.py +1 -1
- echotools-2.4.2/src/echotools/exec/fncall/protocols/entml_schema/validate.py +218 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_stream/body.py +6 -6
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/fakemarkup.py +30 -2
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/invoke.py +7 -4
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/values.py +17 -8
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/coercion.py +27 -6
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/entml_format.py +16 -1
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools.egg-info/PKG-INFO +2 -2
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools.egg-info/SOURCES.txt +3 -1
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/simulated_llm_tool_responses.py +1 -1
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_fake_structure_markup.py +41 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_stream_json_exhaustive.py +1 -1
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_tool_pipeline.py +4 -5
- echotools-2.4.2/src/tests/test_multiline_parameter_comprehensive.py +346 -0
- echotools-2.4.2/src/tests/test_schema_validate.py +103 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_stream_partial_json_parity.py +2 -2
- echotools-2.4.1/src/echotools/exec/fncall/prompt/templates.py +0 -168
- {echotools-2.4.1 → echotools-2.4.2}/setup.cfg +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/base.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/center.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/parsers/stream.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/parsers/stream_delta.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/parsers/stream_feed.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/parsers/stream_final.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/behavior_blocks.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/invoke.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/regex.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/strip.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_schema/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_schema/mangled.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_stream/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_stream/snap.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/filter.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/hold.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/parse.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/comment.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/tools.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/history_markup.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/keys.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_bridge.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_richcli.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_themes.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/events/event.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/translate.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/application.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools/version.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/_check_parity.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/bench_import.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/conftest.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/entml_golden.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/entml_golden_update.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_completion.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_config.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_config_extended.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_console.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_dispatch.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_agent_output_formats.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_golden.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_invoke_known_tool_gate.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_parser_comprehensive.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_protocol.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_errors.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_errors_common.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_events.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_events_event.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_facade.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fake_history_markup_batch.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fake_history_markup_stream.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fault_thinking_close_batch.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fault_thinking_close_stream.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fncall.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fncall_stream_thinking.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_history_markup_filter.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_ids.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_imports.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_io.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_keys.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_logger.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_merge.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_model_branch_matrix.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_plugin.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_printstream.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_protocols.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_proxy.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_runtime.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_scheduler.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_stream_realtime_parse.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_tracing.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_ui_bridge.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_usage.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_web.py +0 -0
- {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_web_broker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: echotools
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
|
|
5
5
|
Home-page: https://github.com/nichengfuben/echotools
|
|
6
6
|
Author: nichengfuben
|
|
@@ -58,7 +58,7 @@ Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
|
58
58
|
|
|
59
59
|
通用基础设施 SDK:配置、日志、事件、调度、插件、调用链,以及 **entml** LLM 工具调用协议与终端 Console UI。
|
|
60
60
|
|
|
61
|
-
完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.
|
|
61
|
+
完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.2**(详见 [CHANGELOG](docs/CHANGELOG.md))。
|
|
62
62
|
|
|
63
63
|
## 安装
|
|
64
64
|
|
|
@@ -18,6 +18,18 @@ from .history import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _clean_assistant_history_content(
|
|
22
|
+
content_str: str,
|
|
23
|
+
protocol: Optional[Any],
|
|
24
|
+
) -> str:
|
|
25
|
+
if not content_str or protocol is None:
|
|
26
|
+
return content_str
|
|
27
|
+
clean_fn = getattr(protocol, "clean_history_assistant_content", None)
|
|
28
|
+
if clean_fn is None:
|
|
29
|
+
return content_str
|
|
30
|
+
return clean_fn(content_str)
|
|
31
|
+
|
|
32
|
+
|
|
21
33
|
def _assistant_history_content_blocks(
|
|
22
34
|
m: Dict[str, Any],
|
|
23
35
|
content_str: str,
|
|
@@ -33,11 +45,12 @@ def _assistant_history_content_blocks(
|
|
|
33
45
|
):
|
|
34
46
|
reasoning = extract_reasoning_text(m)
|
|
35
47
|
if reasoning:
|
|
48
|
+
reasoning = _clean_assistant_history_content(reasoning, protocol)
|
|
36
49
|
thinking_block = format_entml_thinking_history_block(reasoning)
|
|
37
50
|
if thinking_block:
|
|
38
51
|
blocks.append(thinking_block)
|
|
39
52
|
if content_str:
|
|
40
|
-
blocks.append(content_str)
|
|
53
|
+
blocks.append(_clean_assistant_history_content(content_str, protocol))
|
|
41
54
|
return blocks
|
|
42
55
|
|
|
43
56
|
|
|
@@ -18,6 +18,7 @@ from echotools.exec.fncall.protocols.entml_patterns import (
|
|
|
18
18
|
)
|
|
19
19
|
from echotools.exec.fncall.protocols.entml_schema import format_entml_tool_descs
|
|
20
20
|
from echotools.exec.fncall.protocols.entml_tool.fakemarkup import (
|
|
21
|
+
strip_fake_entml_structure_markup,
|
|
21
22
|
strip_orphan_entml_close_tags,
|
|
22
23
|
)
|
|
23
24
|
from echotools.exec.fncall.protocols.entml_tool.invoke import (
|
|
@@ -179,6 +180,7 @@ class EntmlProtocol(ToolProtocol):
|
|
|
179
180
|
if tool_calls:
|
|
180
181
|
clean = strip_actionable_entml_invoke_blocks(clean, known_names=known)
|
|
181
182
|
clean = strip_tool_entml_residue(clean, known_names=known)
|
|
183
|
+
clean, _ = strip_fake_entml_structure_markup(clean)
|
|
182
184
|
clean, _ = strip_orphan_entml_close_tags(clean)
|
|
183
185
|
return (clean, normalize_tool_calls(tool_calls, tools))
|
|
184
186
|
|
|
@@ -193,6 +195,14 @@ class EntmlProtocol(ToolProtocol):
|
|
|
193
195
|
def clean_tags(self, content: str) -> str:
|
|
194
196
|
return strip_entml_from_content(content)
|
|
195
197
|
|
|
198
|
+
def clean_history_assistant_content(self, content: str) -> str:
|
|
199
|
+
"""构建 conversation_history 时清理 assistant 正文(伪 history 块 + fakemarkup)。"""
|
|
200
|
+
if not content:
|
|
201
|
+
return content
|
|
202
|
+
cleaned, _ = strip_fake_history_markup(content)
|
|
203
|
+
cleaned, _ = strip_fake_entml_structure_markup(cleaned)
|
|
204
|
+
return cleaned
|
|
205
|
+
|
|
196
206
|
def clean_tool_tags(
|
|
197
207
|
self,
|
|
198
208
|
content: str,
|
|
@@ -202,6 +212,8 @@ class EntmlProtocol(ToolProtocol):
|
|
|
202
212
|
known = resolve_known_tool_names(tools, schema_index)
|
|
203
213
|
cleaned = strip_tool_entml_residue(content, known_names=known)
|
|
204
214
|
cleaned, _ = strip_fake_history_markup(cleaned)
|
|
215
|
+
cleaned, _ = strip_fake_entml_structure_markup(cleaned)
|
|
216
|
+
cleaned, _ = strip_orphan_entml_close_tags(cleaned)
|
|
205
217
|
return cleaned
|
|
206
218
|
|
|
207
219
|
def format_assistant_tool_calls(
|
{echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/params.py
RENAMED
|
@@ -192,7 +192,7 @@ def parse_sub_tags(
|
|
|
192
192
|
args: Dict[str, Any] = {}
|
|
193
193
|
for m in SUB_TAG_RE.finditer(content):
|
|
194
194
|
pname = m.group(1).strip()
|
|
195
|
-
pval = m.group(2)
|
|
195
|
+
pval = m.group(2) or ""
|
|
196
196
|
pschema = schema_index.get(func_name, {}).get(pname, {}) if schema_index else {}
|
|
197
197
|
args[pname] = coerce_entml_parameter_value(pval, pschema or None)
|
|
198
198
|
return args
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""JSON Schema 边界校验(coerce 之后):enum、null union、基础类型匹配。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
7
|
+
|
|
8
|
+
_SCALAR_CHECK = {
|
|
9
|
+
"string": lambda v: isinstance(v, str),
|
|
10
|
+
"integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
|
|
11
|
+
"number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
|
|
12
|
+
"boolean": lambda v: isinstance(v, bool),
|
|
13
|
+
"null": lambda v: v is None,
|
|
14
|
+
"array": lambda v: isinstance(v, list),
|
|
15
|
+
"object": lambda v: isinstance(v, dict),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ParamValidationIssue:
|
|
21
|
+
path: str
|
|
22
|
+
message: str
|
|
23
|
+
|
|
24
|
+
def to_llm_feedback(self) -> str:
|
|
25
|
+
loc = self.path or "(root)"
|
|
26
|
+
return f"Parameter validation failed at {loc}: {self.message}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ToolArgValidationError(Exception):
|
|
30
|
+
"""工具参数未通过 schema 校验(可回灌模型自修正)。"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
issues: Sequence[ParamValidationIssue],
|
|
35
|
+
*,
|
|
36
|
+
tool_name: str = "",
|
|
37
|
+
) -> None:
|
|
38
|
+
self.tool_name = tool_name
|
|
39
|
+
self.issues = list(issues)
|
|
40
|
+
detail = "; ".join(i.message for i in self.issues)
|
|
41
|
+
super().__init__(detail or "tool argument validation failed")
|
|
42
|
+
|
|
43
|
+
def to_llm_feedback(self) -> str:
|
|
44
|
+
lines = [i.to_llm_feedback() for i in self.issues]
|
|
45
|
+
prefix = f"Tool {self.tool_name!r} arguments invalid." if self.tool_name else ""
|
|
46
|
+
body = "\n".join(lines)
|
|
47
|
+
return f"{prefix}\n{body}".strip() if prefix else body
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def schema_allows_null(schema: Dict[str, Any]) -> bool:
|
|
51
|
+
if not schema:
|
|
52
|
+
return False
|
|
53
|
+
raw_type = schema.get("type")
|
|
54
|
+
if raw_type == "null":
|
|
55
|
+
return True
|
|
56
|
+
if isinstance(raw_type, list) and "null" in raw_type:
|
|
57
|
+
return True
|
|
58
|
+
for key in ("anyOf", "oneOf"):
|
|
59
|
+
combiner = schema.get(key)
|
|
60
|
+
if not isinstance(combiner, list):
|
|
61
|
+
continue
|
|
62
|
+
for sub in combiner:
|
|
63
|
+
if isinstance(sub, dict) and sub.get("type") == "null":
|
|
64
|
+
return True
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_null_literal(raw: str) -> bool:
|
|
69
|
+
if raw is None:
|
|
70
|
+
return True
|
|
71
|
+
return raw.strip().lower() in ("", "null", "none")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _enum_values(schema: Dict[str, Any]) -> Optional[List[Any]]:
|
|
75
|
+
enum_vals = schema.get("enum")
|
|
76
|
+
if isinstance(enum_vals, list) and enum_vals:
|
|
77
|
+
return list(enum_vals)
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _type_names(schema: Dict[str, Any]) -> List[str]:
|
|
82
|
+
raw_type = schema.get("type")
|
|
83
|
+
if isinstance(raw_type, str) and raw_type:
|
|
84
|
+
return [raw_type]
|
|
85
|
+
if isinstance(raw_type, list):
|
|
86
|
+
return [t for t in raw_type if isinstance(t, str)]
|
|
87
|
+
enum_vals = schema.get("enum")
|
|
88
|
+
if isinstance(enum_vals, list) and enum_vals:
|
|
89
|
+
first = enum_vals[0]
|
|
90
|
+
if isinstance(first, bool):
|
|
91
|
+
return ["boolean"]
|
|
92
|
+
if isinstance(first, int):
|
|
93
|
+
return ["integer"]
|
|
94
|
+
if isinstance(first, float):
|
|
95
|
+
return ["number"]
|
|
96
|
+
if isinstance(first, str):
|
|
97
|
+
return ["string"]
|
|
98
|
+
if isinstance(first, list):
|
|
99
|
+
return ["array"]
|
|
100
|
+
if isinstance(first, dict):
|
|
101
|
+
return ["object"]
|
|
102
|
+
for combiner_key in ("anyOf", "oneOf"):
|
|
103
|
+
combiner = schema.get(combiner_key)
|
|
104
|
+
if not isinstance(combiner, list):
|
|
105
|
+
continue
|
|
106
|
+
names: List[str] = []
|
|
107
|
+
for sub in combiner:
|
|
108
|
+
if isinstance(sub, dict):
|
|
109
|
+
names.extend(_type_names(sub))
|
|
110
|
+
if names:
|
|
111
|
+
return list(dict.fromkeys(names))
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _value_matches_types(value: Any, type_names: Sequence[str]) -> bool:
|
|
116
|
+
if not type_names:
|
|
117
|
+
return True
|
|
118
|
+
if value is None:
|
|
119
|
+
return "null" in type_names
|
|
120
|
+
for name in type_names:
|
|
121
|
+
if name == "null":
|
|
122
|
+
continue
|
|
123
|
+
checker = _SCALAR_CHECK.get(name)
|
|
124
|
+
if checker and checker(value):
|
|
125
|
+
return True
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_param_value(
|
|
130
|
+
value: Any,
|
|
131
|
+
schema: Dict[str, Any],
|
|
132
|
+
*,
|
|
133
|
+
path: str = "",
|
|
134
|
+
) -> List[ParamValidationIssue]:
|
|
135
|
+
if not schema:
|
|
136
|
+
return []
|
|
137
|
+
issues: List[ParamValidationIssue] = []
|
|
138
|
+
enum_vals = _enum_values(schema)
|
|
139
|
+
if enum_vals is not None and value not in enum_vals:
|
|
140
|
+
issues.append(
|
|
141
|
+
ParamValidationIssue(
|
|
142
|
+
path,
|
|
143
|
+
f"value {value!r} is not one of {enum_vals!r}",
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
type_names = _type_names(schema)
|
|
147
|
+
if type_names and not _value_matches_types(value, type_names):
|
|
148
|
+
issues.append(
|
|
149
|
+
ParamValidationIssue(
|
|
150
|
+
path,
|
|
151
|
+
f"value {value!r} (type {type(value).__name__}) "
|
|
152
|
+
f"does not match schema types {list(type_names)!r}",
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
if isinstance(value, dict):
|
|
156
|
+
props = schema.get("properties") or {}
|
|
157
|
+
if isinstance(props, dict):
|
|
158
|
+
for key, sub_schema in props.items():
|
|
159
|
+
if not isinstance(sub_schema, dict):
|
|
160
|
+
continue
|
|
161
|
+
sub_path = f"{path}.{key}" if path else key
|
|
162
|
+
if key in value:
|
|
163
|
+
issues.extend(
|
|
164
|
+
validate_param_value(value[key], sub_schema, path=sub_path)
|
|
165
|
+
)
|
|
166
|
+
if isinstance(value, list):
|
|
167
|
+
item_schema = schema.get("items")
|
|
168
|
+
if isinstance(item_schema, dict) and item_schema:
|
|
169
|
+
for idx, item in enumerate(value):
|
|
170
|
+
sub_path = f"{path}[{idx}]" if path else f"[{idx}]"
|
|
171
|
+
issues.extend(
|
|
172
|
+
validate_param_value(item, item_schema, path=sub_path)
|
|
173
|
+
)
|
|
174
|
+
return issues
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def validate_tool_arguments(
|
|
178
|
+
args: Dict[str, Any],
|
|
179
|
+
func_name: str,
|
|
180
|
+
schema_index: Dict[str, Dict[str, Dict[str, Any]]],
|
|
181
|
+
*,
|
|
182
|
+
required: Optional[Sequence[str]] = None,
|
|
183
|
+
) -> List[ParamValidationIssue]:
|
|
184
|
+
func_props = schema_index.get(func_name) or {}
|
|
185
|
+
issues: List[ParamValidationIssue] = []
|
|
186
|
+
if required:
|
|
187
|
+
for key in required:
|
|
188
|
+
if key not in args:
|
|
189
|
+
issues.append(
|
|
190
|
+
ParamValidationIssue(key, "required parameter is missing")
|
|
191
|
+
)
|
|
192
|
+
for key, value in args.items():
|
|
193
|
+
pschema = func_props.get(key) or {}
|
|
194
|
+
if pschema:
|
|
195
|
+
issues.extend(validate_param_value(value, pschema, path=key))
|
|
196
|
+
return issues
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def format_issues_for_llm(
|
|
200
|
+
issues: Sequence[ParamValidationIssue],
|
|
201
|
+
*,
|
|
202
|
+
tool_name: str = "",
|
|
203
|
+
) -> str:
|
|
204
|
+
return ToolArgValidationError(list(issues), tool_name=tool_name).to_llm_feedback()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def assert_valid_tool_arguments(
|
|
208
|
+
args: Dict[str, Any],
|
|
209
|
+
func_name: str,
|
|
210
|
+
schema_index: Dict[str, Dict[str, Dict[str, Any]]],
|
|
211
|
+
*,
|
|
212
|
+
required: Optional[Sequence[str]] = None,
|
|
213
|
+
) -> None:
|
|
214
|
+
issues = validate_tool_arguments(
|
|
215
|
+
args, func_name, schema_index, required=required
|
|
216
|
+
)
|
|
217
|
+
if issues:
|
|
218
|
+
raise ToolArgValidationError(issues, tool_name=func_name)
|
{echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_stream/body.py
RENAMED
|
@@ -127,10 +127,10 @@ def _final_invoke_arguments_json(
|
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
def _streaming_string_value(raw: str, *, is_complete: bool) -> str:
|
|
130
|
-
"""
|
|
130
|
+
"""流式 string 参数:保留模型原文;未完成时仅去掉尾部未收齐的标签前缀。"""
|
|
131
131
|
if is_complete:
|
|
132
|
-
return raw
|
|
133
|
-
return raw
|
|
132
|
+
return raw
|
|
133
|
+
return _strip_incomplete_markup_suffix(raw)
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
def _parameter_json_fragment(
|
|
@@ -197,7 +197,7 @@ def _parse_bare_invoke_entries(body: str) -> List[Tuple[str, str, bool, Optional
|
|
|
197
197
|
key = normalize_entml_name(match.group(1))
|
|
198
198
|
if not key:
|
|
199
199
|
continue
|
|
200
|
-
tagged.append((match.start(), key,
|
|
200
|
+
tagged.append((match.start(), key, match.group(2) or "", True, None))
|
|
201
201
|
for match in BARE_INVOKE_CHILD_OPEN_RE.finditer(body):
|
|
202
202
|
key = normalize_entml_name(match.group(1))
|
|
203
203
|
if not key:
|
|
@@ -229,7 +229,7 @@ def _parse_direct_child_entries(body: str) -> List[Tuple[str, str, bool, Optiona
|
|
|
229
229
|
key = normalize_entml_name(match.group(1))
|
|
230
230
|
if not key or key.lower() in INVOKE_DIRECT_CHILD_SKIP:
|
|
231
231
|
continue
|
|
232
|
-
tagged.append((match.start(), key,
|
|
232
|
+
tagged.append((match.start(), key, match.group(2) or "", True, None))
|
|
233
233
|
for match in INVOKE_DIRECT_CHILD_OPEN_RE.finditer(gap_text):
|
|
234
234
|
key = normalize_entml_name(match.group(1))
|
|
235
235
|
if not key or key.lower() in INVOKE_DIRECT_CHILD_SKIP:
|
|
@@ -284,7 +284,7 @@ def _parse_parameter_entries(body: str) -> List[Tuple[str, str, bool, Optional[s
|
|
|
284
284
|
for extra_key, extra_val in extra.items():
|
|
285
285
|
entries.append((extra_key, str(extra_val), True, None))
|
|
286
286
|
break
|
|
287
|
-
raw = body[val_start:close]
|
|
287
|
+
raw = body[val_start:close]
|
|
288
288
|
raw, extra = split_mangled_json_param_tail(raw, param_name=key)
|
|
289
289
|
entries.append((key, raw, True, type_hint))
|
|
290
290
|
for extra_key, extra_val in extra.items():
|
{echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/fakemarkup.py
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
|
-
from typing import Tuple
|
|
6
|
+
from typing import List, Tuple
|
|
7
7
|
|
|
8
8
|
from .comment import (
|
|
9
9
|
leading_partial_tool_result_id_comment_len,
|
|
@@ -83,6 +83,10 @@ _TRAILING_ORPHAN_ENTML_CLOSE_RE = re.compile(
|
|
|
83
83
|
r"(?:\n|\A)\s*</entml:?[a-z0-9_-]*>\s*$",
|
|
84
84
|
re.IGNORECASE,
|
|
85
85
|
)
|
|
86
|
+
_ENTML_INVOKE_BLOCK_RE = re.compile(
|
|
87
|
+
r"<entml:invoke\b[^>]*>[\s\S]*?</entml:invoke>",
|
|
88
|
+
re.IGNORECASE,
|
|
89
|
+
)
|
|
86
90
|
|
|
87
91
|
|
|
88
92
|
def _strip_fake_entml_result_id_blocks(text: str) -> Tuple[str, bool]:
|
|
@@ -184,6 +188,30 @@ def _truncate_unclosed_fake_result_id_tail(text: str) -> Tuple[str, bool]:
|
|
|
184
188
|
return text, found
|
|
185
189
|
|
|
186
190
|
|
|
191
|
+
def _collapse_excess_blank_lines(text: str) -> str:
|
|
192
|
+
"""压缩连续空行;``<entml:invoke>`` 块内原样保留(含 parameter 多行值)。"""
|
|
193
|
+
if not text:
|
|
194
|
+
return text
|
|
195
|
+
parts: List[Tuple[str, bool]] = []
|
|
196
|
+
i = 0
|
|
197
|
+
while i < len(text):
|
|
198
|
+
match = _ENTML_INVOKE_BLOCK_RE.search(text, i)
|
|
199
|
+
if not match:
|
|
200
|
+
parts.append((text[i:], False))
|
|
201
|
+
break
|
|
202
|
+
if match.start() > i:
|
|
203
|
+
parts.append((text[i : match.start()], False))
|
|
204
|
+
parts.append((match.group(0), True))
|
|
205
|
+
i = match.end()
|
|
206
|
+
out: List[str] = []
|
|
207
|
+
for chunk, protected in parts:
|
|
208
|
+
if protected:
|
|
209
|
+
out.append(chunk)
|
|
210
|
+
else:
|
|
211
|
+
out.append(re.sub(r"\n{3,}", "\n\n", chunk))
|
|
212
|
+
return "".join(out)
|
|
213
|
+
|
|
214
|
+
|
|
187
215
|
def strip_fake_entml_structure_markup(content: str) -> Tuple[str, bool]:
|
|
188
216
|
"""batch:id result 整块 → 仅标签 → Tool Result ID 注释。"""
|
|
189
217
|
if not content:
|
|
@@ -200,7 +228,7 @@ def strip_fake_entml_structure_markup(content: str) -> Tuple[str, bool]:
|
|
|
200
228
|
found = found or hit
|
|
201
229
|
text, hit = _strip_orphan_entml_close_leaks(text)
|
|
202
230
|
found = found or hit
|
|
203
|
-
text =
|
|
231
|
+
text = _collapse_excess_blank_lines(text)
|
|
204
232
|
return text, found
|
|
205
233
|
|
|
206
234
|
|
{echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/invoke.py
RENAMED
|
@@ -23,6 +23,7 @@ from ..entml_patterns.regex import (
|
|
|
23
23
|
PARAMETERS_RE,
|
|
24
24
|
)
|
|
25
25
|
from ..entml_schema.mangled import split_mangled_json_param_tail
|
|
26
|
+
from .fakemarkup import strip_fake_entml_structure_markup
|
|
26
27
|
from .values import (
|
|
27
28
|
_coerce_entml_arg_value,
|
|
28
29
|
coerce_entml_arguments,
|
|
@@ -41,7 +42,7 @@ def _parse_direct_child_tags(
|
|
|
41
42
|
key = normalize_entml_name(match.group(1))
|
|
42
43
|
if not key or key.lower() in INVOKE_DIRECT_CHILD_SKIP or key in args:
|
|
43
44
|
continue
|
|
44
|
-
raw =
|
|
45
|
+
raw = match.group(2) or ""
|
|
45
46
|
args[key] = coerce_entml_parameter_value(
|
|
46
47
|
raw,
|
|
47
48
|
func_props.get(key) or None,
|
|
@@ -58,7 +59,7 @@ def _parse_bare_invoke_children(
|
|
|
58
59
|
key = normalize_entml_name(match.group(1))
|
|
59
60
|
if not key or key in args:
|
|
60
61
|
continue
|
|
61
|
-
raw =
|
|
62
|
+
raw = match.group(2) or ""
|
|
62
63
|
args[key] = coerce_entml_parameter_value(
|
|
63
64
|
raw,
|
|
64
65
|
func_props.get(key) or None,
|
|
@@ -93,7 +94,7 @@ def _fill_parameter_tag_args(
|
|
|
93
94
|
if not pname:
|
|
94
95
|
continue
|
|
95
96
|
pname = normalize_entml_name(pname)
|
|
96
|
-
pval =
|
|
97
|
+
pval = param_m.group(2) or ""
|
|
97
98
|
pval, extra = split_mangled_json_param_tail(pval, param_name=pname)
|
|
98
99
|
type_hint = extract_parameter_type_attr(attrs)
|
|
99
100
|
pschema = func_props.get(pname) or {}
|
|
@@ -124,7 +125,7 @@ def parse_invoke_args(
|
|
|
124
125
|
params_m = PARAMETERS_RE.search(body)
|
|
125
126
|
if params_m:
|
|
126
127
|
return _parse_parameters_block_args(
|
|
127
|
-
params_m.group(1)
|
|
128
|
+
params_m.group(1), name, schema_index
|
|
128
129
|
)
|
|
129
130
|
args: Dict[str, Any] = {}
|
|
130
131
|
_fill_parameter_tag_args(body, args, func_props)
|
|
@@ -138,6 +139,8 @@ def parse_entml_tool_calls(
|
|
|
138
139
|
tools: Optional[List[Dict[str, Any]]],
|
|
139
140
|
schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
|
|
140
141
|
) -> List[Dict[str, Any]]:
|
|
142
|
+
if text:
|
|
143
|
+
text, _ = strip_fake_entml_structure_markup(text)
|
|
141
144
|
tool_calls: List[Dict[str, Any]] = []
|
|
142
145
|
known = resolve_known_tool_names(tools, schema_index)
|
|
143
146
|
for _start, _end, attrs, body in iter_actionable_entml_invoke_blocks(
|
{echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/values.py
RENAMED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import Any, Dict, Optional
|
|
4
|
+
from typing import Any, Dict, Optional, Sequence
|
|
5
5
|
|
|
6
6
|
from echotools.exec.fncall.shared.coercion import (
|
|
7
7
|
_coerce_param_value,
|
|
8
8
|
_resolve_effective_type,
|
|
9
9
|
)
|
|
10
|
+
from echotools.exec.fncall.protocols.entml_schema.validate import (
|
|
11
|
+
assert_valid_tool_arguments,
|
|
12
|
+
)
|
|
10
13
|
|
|
11
14
|
_TYPE_HINT_TO_JSON_TYPE = {
|
|
12
15
|
"str": "string",
|
|
@@ -61,17 +64,16 @@ def coerce_entml_parameter_value(
|
|
|
61
64
|
if effective:
|
|
62
65
|
return _coerce_param_value(raw, effective)
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
if not stripped:
|
|
67
|
+
if raw is None or raw == "":
|
|
66
68
|
return ""
|
|
67
69
|
|
|
68
|
-
if
|
|
70
|
+
if raw.lstrip().startswith(("{", "[")):
|
|
69
71
|
try:
|
|
70
|
-
return json.loads(
|
|
72
|
+
return json.loads(raw)
|
|
71
73
|
except json.JSONDecodeError:
|
|
72
|
-
return
|
|
74
|
+
return raw
|
|
73
75
|
|
|
74
|
-
return
|
|
76
|
+
return raw
|
|
75
77
|
|
|
76
78
|
|
|
77
79
|
def effective_entml_param_json_type(
|
|
@@ -116,8 +118,11 @@ def coerce_entml_arguments(
|
|
|
116
118
|
args: Dict[str, Any],
|
|
117
119
|
func_name: str,
|
|
118
120
|
schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
|
|
121
|
+
*,
|
|
122
|
+
strict: bool = False,
|
|
123
|
+
required: Optional[Sequence[str]] = None,
|
|
119
124
|
) -> Dict[str, Any]:
|
|
120
|
-
"""对已解析参数字典按工具 schema
|
|
125
|
+
"""对已解析参数字典按工具 schema 做逐项类型转换,可选 strict 校验。"""
|
|
121
126
|
if not schema_index or not func_name:
|
|
122
127
|
return args
|
|
123
128
|
func_schema = schema_index.get(func_name) or {}
|
|
@@ -131,4 +136,8 @@ def coerce_entml_arguments(
|
|
|
131
136
|
out[key] = _coerce_entml_arg_value(value, pschema)
|
|
132
137
|
else:
|
|
133
138
|
out[key] = value
|
|
139
|
+
if strict:
|
|
140
|
+
assert_valid_tool_arguments(
|
|
141
|
+
out, func_name, schema_index, required=required
|
|
142
|
+
)
|
|
134
143
|
return out
|
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
|
-
from typing import Any, Dict, List, Optional
|
|
10
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
11
11
|
|
|
12
12
|
from echotools.base.logger.manager import get_logger
|
|
13
|
+
from echotools.exec.fncall.protocols.entml_schema.validate import (
|
|
14
|
+
is_null_literal,
|
|
15
|
+
schema_allows_null,
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
logger = get_logger(__name__)
|
|
15
19
|
|
|
@@ -51,6 +55,24 @@ def _build_param_schema_index(
|
|
|
51
55
|
return index
|
|
52
56
|
|
|
53
57
|
|
|
58
|
+
def required_params_for_func(
|
|
59
|
+
tools: Optional[List[Dict[str, Any]]],
|
|
60
|
+
func_name: str,
|
|
61
|
+
) -> List[str]:
|
|
62
|
+
if not tools or not func_name:
|
|
63
|
+
return []
|
|
64
|
+
for tool in tools:
|
|
65
|
+
fn: Dict[str, Any] = tool.get("function", tool) # type: ignore[arg-type]
|
|
66
|
+
if (fn.get("name") or "") != func_name:
|
|
67
|
+
continue
|
|
68
|
+
params = fn.get("parameters") or {}
|
|
69
|
+
required = params.get("required")
|
|
70
|
+
if isinstance(required, list):
|
|
71
|
+
return [str(k) for k in required if k]
|
|
72
|
+
return []
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
|
|
54
76
|
def _resolve_effective_type(schema: Dict[str, Any]) -> Optional[str]:
|
|
55
77
|
"""从 JSON Schema 中解析出有效的单一类型字符串。"""
|
|
56
78
|
if not schema:
|
|
@@ -254,17 +276,16 @@ def _coerce_param_value(raw: str, schema: Dict[str, Any]) -> Any:
|
|
|
254
276
|
if not schema:
|
|
255
277
|
return parsed
|
|
256
278
|
|
|
279
|
+
if schema_allows_null(schema) and is_null_literal(raw):
|
|
280
|
+
return None
|
|
281
|
+
|
|
257
282
|
effective_type = _resolve_effective_type(schema)
|
|
258
283
|
|
|
259
284
|
if effective_type is None:
|
|
260
285
|
return parsed
|
|
261
286
|
|
|
262
287
|
if effective_type == "string":
|
|
263
|
-
|
|
264
|
-
if len(stripped) >= 2 and stripped[0] == '"' and stripped[-1] == '"':
|
|
265
|
-
if isinstance(parsed, str):
|
|
266
|
-
return parsed
|
|
267
|
-
return stripped
|
|
288
|
+
return raw
|
|
268
289
|
|
|
269
290
|
if effective_type == "integer":
|
|
270
291
|
return _coerce_to_integer(stripped, parsed)
|
|
@@ -10,7 +10,6 @@ from echotools.exec.fncall.prompt.behavior_blocks import (
|
|
|
10
10
|
format_function_calling_behavior,
|
|
11
11
|
format_hard_constraint_restatement,
|
|
12
12
|
)
|
|
13
|
-
from echotools.exec.fncall.prompt.templates import _HISTORY_CLARIFY_EN
|
|
14
13
|
from echotools.exec.fncall.protocols.entml_think.core import (
|
|
15
14
|
build_entml_thinking_behavior_section,
|
|
16
15
|
build_entml_thinking_meta_section,
|
|
@@ -18,6 +17,22 @@ from echotools.exec.fncall.protocols.entml_think.core import (
|
|
|
18
17
|
|
|
19
18
|
_ENTML_NAMESPACE_RE = re.compile(r"(</?)entml:", re.IGNORECASE)
|
|
20
19
|
|
|
20
|
+
_HISTORY_CLARIFY_EN = (
|
|
21
|
+
"The following is a transcript of completed interactions. Each assistant turn "
|
|
22
|
+
"that called a tool ends at its last <entml:invoke> block; the comment "
|
|
23
|
+
"immediately following it states the environment-generated result id and was "
|
|
24
|
+
"appended by the environment when this turn was logged, not by the assistant "
|
|
25
|
+
"at generation time. The actual result content for every id shown here lives "
|
|
26
|
+
"exclusively in the separate, top-level <entml:funtions_results> block that "
|
|
27
|
+
"follows this history block.\n"
|
|
28
|
+
"IMPORTANT: The agent must never regenerate, continue, or imitate the "
|
|
29
|
+
"id-comment format shown below as part of its own current-turn output.\n"
|
|
30
|
+
"IMPORTANT: The agent must not repeat a tool call using the same tool and "
|
|
31
|
+
"the same parameters as one already recorded below, unless the user "
|
|
32
|
+
"explicitly requests a fresh or updated value.\n"
|
|
33
|
+
"The user's latest message follows below."
|
|
34
|
+
)
|
|
35
|
+
|
|
21
36
|
_ENTML_INSTRUCTION = """\
|
|
22
37
|
In this environment you have access to a set of tools you can use to answer the user's question.
|
|
23
38
|
You can invoke functions by writing a "<entml:invoke>" block like the following as part of your reply to the user:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: echotools
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
|
|
5
5
|
Home-page: https://github.com/nichengfuben/echotools
|
|
6
6
|
Author: nichengfuben
|
|
@@ -58,7 +58,7 @@ Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
|
58
58
|
|
|
59
59
|
通用基础设施 SDK:配置、日志、事件、调度、插件、调用链,以及 **entml** LLM 工具调用协议与终端 Console UI。
|
|
60
60
|
|
|
61
|
-
完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.
|
|
61
|
+
完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.2**(详见 [CHANGELOG](docs/CHANGELOG.md))。
|
|
62
62
|
|
|
63
63
|
## 安装
|
|
64
64
|
|