echotools 2.3.38__tar.gz → 2.3.40__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.38 → echotools-2.3.40}/PKG-INFO +1 -1
- {echotools-2.3.38 → echotools-2.3.40}/pyproject.toml +5 -1
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/stream.py +55 -4
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/inject.py +4 -1
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/prompt_helpers.py +6 -6
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml.py +0 -1
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -1
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_thinking_parse.py +39 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/console/spinner.py +4 -4
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/console/ui.py +28 -29
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools.egg-info/SOURCES.txt +1 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_console.py +0 -2
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_entml_thinking_parse.py +6 -0
- echotools-2.3.40/src/tests/test_fncall_stream_thinking.py +110 -0
- {echotools-2.3.38 → echotools-2.3.40}/README.md +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/docs/README.md +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/setup.cfg +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/compat.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_sanitize.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_thinking.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_thinking_history.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/console/__init__.py +1 -1
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/tailwind.css +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools/version.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/bench_import.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/conftest.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_completion.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_config.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_entml_protocol.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_errors.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_events.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_facade.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_ids.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_imports.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_io.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_keys.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_logger.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_merge.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_usage.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_web.py +0 -0
- {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_web_broker.py +0 -0
|
@@ -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.40"
|
|
8
8
|
description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
|
|
9
9
|
readme = "docs/README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -97,6 +97,10 @@ disable_error_code = [
|
|
|
97
97
|
module = "echotools.exec.terminal.*"
|
|
98
98
|
ignore_errors = true
|
|
99
99
|
|
|
100
|
+
[[tool.mypy.overrides]]
|
|
101
|
+
module = "echotools.media.console.ui"
|
|
102
|
+
ignore_errors = true
|
|
103
|
+
|
|
100
104
|
[tool.pytest.ini_options]
|
|
101
105
|
testpaths = ["src/tests"]
|
|
102
106
|
asyncio_mode = "auto"
|
|
@@ -9,10 +9,13 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
9
9
|
|
|
10
10
|
from echotools.exec.protocol.base import ToolProtocol
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
# 懒导入:仅 entml 协议需要
|
|
13
14
|
def _make_thinking_filter(protocol: ToolProtocol):
|
|
14
15
|
if getattr(protocol, "id", None) == "entml":
|
|
15
|
-
from echotools.exec.fncall.protocols.entml_thinking_parse import
|
|
16
|
+
from echotools.exec.fncall.protocols.entml_thinking_parse import (
|
|
17
|
+
EntmlThinkingStreamFilter,
|
|
18
|
+
)
|
|
16
19
|
return EntmlThinkingStreamFilter()
|
|
17
20
|
return None
|
|
18
21
|
|
|
@@ -133,12 +136,59 @@ class FncallStreamParser:
|
|
|
133
136
|
|
|
134
137
|
return buffer, ""
|
|
135
138
|
|
|
139
|
+
def _append_content_text(self, text: str) -> None:
|
|
140
|
+
if text:
|
|
141
|
+
self._text_parts.append(text)
|
|
142
|
+
|
|
143
|
+
def _feed_content_waiting(self, text: str) -> None:
|
|
144
|
+
"""thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
|
|
145
|
+
if not text:
|
|
146
|
+
return
|
|
147
|
+
trigger_tags = self._protocol.get_trigger_tags()
|
|
148
|
+
found, pos = self._protocol.detect_start(text)
|
|
149
|
+
if not found:
|
|
150
|
+
safe, remain = self._split_safe_text(text, trigger_tags)
|
|
151
|
+
if safe:
|
|
152
|
+
self._append_content_text(safe)
|
|
153
|
+
self._waiting_tail = remain
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if pos > 0:
|
|
157
|
+
self._append_content_text(text[:pos])
|
|
158
|
+
|
|
159
|
+
self._fncall_buf = text[pos:]
|
|
160
|
+
self._detected = True
|
|
161
|
+
self._state = self.IN_FUNCTION_CALLS
|
|
162
|
+
if self._is_call_closed():
|
|
163
|
+
self._state = self.DONE
|
|
164
|
+
|
|
165
|
+
def _feed_waiting_thinking_plain(self, combined: str) -> None:
|
|
166
|
+
"""未闭合 thinking 阶段:块内一律按纯文本进 thinking,不检测 invoke。"""
|
|
167
|
+
assert self._thinking_filter is not None
|
|
168
|
+
for kind, part in self._thinking_filter.feed(combined):
|
|
169
|
+
if kind == "thinking":
|
|
170
|
+
self._thinking_parts.append(part)
|
|
171
|
+
elif part:
|
|
172
|
+
self._feed_content_waiting(part)
|
|
173
|
+
|
|
136
174
|
def _feed_waiting(self, chunk: str) -> None:
|
|
137
175
|
"""在 WAITING_FOR_TAG 状态下处理新块。"""
|
|
138
176
|
combined = self._waiting_tail + chunk
|
|
139
|
-
|
|
177
|
+
self._waiting_tail = ""
|
|
178
|
+
|
|
179
|
+
if self._thinking_filter is not None:
|
|
180
|
+
from echotools.exec.fncall.protocols.entml_thinking_parse import (
|
|
181
|
+
has_unclosed_entml_thinking,
|
|
182
|
+
)
|
|
183
|
+
if (
|
|
184
|
+
self._thinking_filter.in_open_thinking()
|
|
185
|
+
or has_unclosed_entml_thinking(combined)
|
|
186
|
+
):
|
|
187
|
+
self._feed_waiting_thinking_plain(combined)
|
|
188
|
+
return
|
|
140
189
|
|
|
141
190
|
trigger_tags = self._protocol.get_trigger_tags()
|
|
191
|
+
found, pos = self._protocol.detect_start(combined)
|
|
142
192
|
|
|
143
193
|
if not found:
|
|
144
194
|
safe, remain = self._split_safe_text(combined, trigger_tags)
|
|
@@ -151,7 +201,6 @@ class FncallStreamParser:
|
|
|
151
201
|
self._emit_text(combined[:pos])
|
|
152
202
|
|
|
153
203
|
self._fncall_buf = combined[pos:]
|
|
154
|
-
self._waiting_tail = ""
|
|
155
204
|
self._detected = True
|
|
156
205
|
self._state = self.IN_FUNCTION_CALLS
|
|
157
206
|
|
|
@@ -213,7 +262,9 @@ class FncallStreamParser:
|
|
|
213
262
|
|
|
214
263
|
# 兜底:剥离残留 <entml:thinking>(holdback 边界/分片异常时)
|
|
215
264
|
if self._thinking_filter is not None and clean_text:
|
|
216
|
-
from echotools.exec.fncall.protocols.entml_thinking_parse import
|
|
265
|
+
from echotools.exec.fncall.protocols.entml_thinking_parse import (
|
|
266
|
+
split_entml_thinking,
|
|
267
|
+
)
|
|
217
268
|
clean_text, more_thinking = split_entml_thinking(clean_text)
|
|
218
269
|
if more_thinking:
|
|
219
270
|
self._thinking_parts.append(more_thinking)
|
|
@@ -28,7 +28,10 @@ from echotools.exec.fncall.protocols.entml_thinking_history import (
|
|
|
28
28
|
parse_include_thinking_in_history,
|
|
29
29
|
)
|
|
30
30
|
from echotools.exec.fncall.shared.loop_detect import detect_tool_loop
|
|
31
|
-
from echotools.exec.fncall.shared.normalization import
|
|
31
|
+
from echotools.exec.fncall.shared.normalization import (
|
|
32
|
+
format_tool_descs,
|
|
33
|
+
normalize_content,
|
|
34
|
+
)
|
|
32
35
|
from echotools.exec.protocol.base import ToolProtocol
|
|
33
36
|
|
|
34
37
|
__all__ = ["inject_fncall"]
|
|
@@ -3,6 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
4
4
|
|
|
5
5
|
from echotools.base.logger.manager import get_logger
|
|
6
|
+
from echotools.exec.fncall.protocols.entml_thinking_history import (
|
|
7
|
+
extract_reasoning_text,
|
|
8
|
+
format_entml_thinking_history_block,
|
|
9
|
+
)
|
|
6
10
|
from echotools.exec.fncall.shared.normalization import normalize_content
|
|
7
11
|
|
|
8
12
|
from .history import (
|
|
@@ -15,11 +19,6 @@ from .history import (
|
|
|
15
19
|
_try_convert_user_to_tool,
|
|
16
20
|
)
|
|
17
21
|
|
|
18
|
-
from echotools.exec.fncall.protocols.entml_thinking_history import (
|
|
19
|
-
extract_reasoning_text,
|
|
20
|
-
format_entml_thinking_history_block,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
22
|
logger = get_logger(__name__)
|
|
24
23
|
|
|
25
24
|
|
|
@@ -86,7 +85,8 @@ def _format_assistant_text_block(
|
|
|
86
85
|
)
|
|
87
86
|
if not blocks:
|
|
88
87
|
return None
|
|
89
|
-
|
|
88
|
+
body = "\n\n".join(blocks)
|
|
89
|
+
return f"<assistant>\n{body}\n</assistant>"
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def _register_tool_call_names(
|
|
@@ -15,7 +15,6 @@ from echotools.exec.fncall.prompt.templates import (
|
|
|
15
15
|
_HISTORY_TOOL_INVOKE_REMINDER_EN,
|
|
16
16
|
)
|
|
17
17
|
from echotools.exec.fncall.protocols.entml_invoke import (
|
|
18
|
-
format_entml_tool_calls,
|
|
19
18
|
parse_entml_tool_calls,
|
|
20
19
|
)
|
|
21
20
|
from echotools.exec.fncall.protocols.entml_patterns import BLOCK_RE
|
{echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_thinking_parse.py
RENAMED
|
@@ -12,6 +12,31 @@ _THINKING_OPEN_PREFIX = "<entml:thinking"
|
|
|
12
12
|
_THINKING_CLOSE = "</entml:thinking>"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def has_unclosed_entml_thinking(text: str) -> bool:
|
|
16
|
+
"""buffer 中是否存在尚未闭合的 <entml:thinking> 块(含开标签未收齐)。"""
|
|
17
|
+
if not text:
|
|
18
|
+
return False
|
|
19
|
+
i = 0
|
|
20
|
+
while i < len(text):
|
|
21
|
+
open_at = text.find(_THINKING_OPEN_PREFIX, i)
|
|
22
|
+
if open_at < 0:
|
|
23
|
+
break
|
|
24
|
+
gt = text.find(">", open_at)
|
|
25
|
+
if gt < 0:
|
|
26
|
+
return True
|
|
27
|
+
close_at = text.find(_THINKING_CLOSE, gt + 1)
|
|
28
|
+
if close_at < 0:
|
|
29
|
+
return True
|
|
30
|
+
i = close_at + len(_THINKING_CLOSE)
|
|
31
|
+
max_keep = len(_THINKING_OPEN_PREFIX) - 1
|
|
32
|
+
check_len = min(len(text), max_keep)
|
|
33
|
+
for length in range(check_len, 0, -1):
|
|
34
|
+
suffix = text[-length:]
|
|
35
|
+
if _THINKING_OPEN_PREFIX.startswith(suffix) and suffix != _THINKING_OPEN_PREFIX:
|
|
36
|
+
return True
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
15
40
|
def split_entml_thinking(text: str) -> Tuple[str, str]:
|
|
16
41
|
"""从文本中剥离 <entml:thinking> 块,返回 (正文, 思考链拼接)。"""
|
|
17
42
|
if not text:
|
|
@@ -53,6 +78,20 @@ class EntmlThinkingStreamFilter:
|
|
|
53
78
|
# 当前 thinking 块是否已输出过任何内容(用于首片去前导空白)
|
|
54
79
|
self._thinking_started = False
|
|
55
80
|
|
|
81
|
+
def in_open_thinking(self) -> bool:
|
|
82
|
+
"""思考块已开始且尚未收到 </entml:thinking>(含开/闭标签分片 hold)。"""
|
|
83
|
+
if self._in_block:
|
|
84
|
+
return True
|
|
85
|
+
open_at = self._pending.find(_THINKING_OPEN_PREFIX)
|
|
86
|
+
if open_at >= 0:
|
|
87
|
+
gt = self._pending.find(">", open_at)
|
|
88
|
+
if gt < 0:
|
|
89
|
+
return True
|
|
90
|
+
_, hold = _hold_prefix(self._pending, _THINKING_OPEN_PREFIX)
|
|
91
|
+
if hold:
|
|
92
|
+
return True
|
|
93
|
+
return False
|
|
94
|
+
|
|
56
95
|
def feed(self, chunk: str) -> List[Tuple[str, str]]:
|
|
57
96
|
"""返回 [(kind, text), ...],kind 为 content 或 thinking。"""
|
|
58
97
|
if not chunk:
|
|
@@ -6,13 +6,13 @@ Spinner 动画引擎 - 绿→蓝单向渐变版
|
|
|
6
6
|
- stop() 后恢复光标并清除 spinner 行
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import
|
|
10
|
-
import time
|
|
9
|
+
import math
|
|
11
10
|
import os
|
|
12
11
|
import platform
|
|
13
|
-
import
|
|
12
|
+
import sys
|
|
14
13
|
import threading
|
|
15
|
-
|
|
14
|
+
import time
|
|
15
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
16
16
|
|
|
17
17
|
try:
|
|
18
18
|
from wcwidth import wcswidth as _wcswidth
|
|
@@ -22,12 +22,11 @@ import math
|
|
|
22
22
|
import os
|
|
23
23
|
import re
|
|
24
24
|
import sys
|
|
25
|
-
from pathlib import Path
|
|
26
25
|
import threading
|
|
27
26
|
import time
|
|
28
|
-
from contextlib import contextmanager
|
|
29
27
|
from dataclasses import dataclass, field
|
|
30
28
|
from enum import Enum, auto
|
|
29
|
+
from pathlib import Path
|
|
31
30
|
from typing import (
|
|
32
31
|
Any,
|
|
33
32
|
AsyncIterator,
|
|
@@ -51,7 +50,7 @@ from typing import (
|
|
|
51
50
|
|
|
52
51
|
from rich.console import Console
|
|
53
52
|
from rich.text import Text
|
|
54
|
-
from wcwidth import
|
|
53
|
+
from wcwidth import wcswidth, wcwidth
|
|
55
54
|
|
|
56
55
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
57
56
|
# 类型定义与平台常量
|
|
@@ -133,39 +132,39 @@ class _WindowsBackend:
|
|
|
133
132
|
|
|
134
133
|
def _define_structures(self) -> None:
|
|
135
134
|
"""定义 Windows 控制台输入记录结构体"""
|
|
136
|
-
|
|
135
|
+
ct = self._ctypes
|
|
137
136
|
|
|
138
|
-
class COORD(
|
|
139
|
-
_fields_ = [("X",
|
|
137
|
+
class COORD(ct.Structure):
|
|
138
|
+
_fields_ = [("X", ct.c_short), ("Y", ct.c_short)]
|
|
140
139
|
|
|
141
|
-
class KEY_EVENT_RECORD(
|
|
140
|
+
class KEY_EVENT_RECORD(ct.Structure):
|
|
142
141
|
_fields_ = [
|
|
143
|
-
("bKeyDown",
|
|
144
|
-
("wRepeatCount",
|
|
145
|
-
("wVirtualKeyCode",
|
|
146
|
-
("wVirtualScanCode",
|
|
147
|
-
("uChar",
|
|
148
|
-
("dwControlKeyState",
|
|
142
|
+
("bKeyDown", ct.wintypes.BOOL),
|
|
143
|
+
("wRepeatCount", ct.wintypes.WORD),
|
|
144
|
+
("wVirtualKeyCode", ct.wintypes.WORD),
|
|
145
|
+
("wVirtualScanCode", ct.wintypes.WORD),
|
|
146
|
+
("uChar", ct.wintypes.WCHAR),
|
|
147
|
+
("dwControlKeyState", ct.wintypes.DWORD),
|
|
149
148
|
]
|
|
150
149
|
|
|
151
|
-
class MOUSE_EVENT_RECORD(
|
|
150
|
+
class MOUSE_EVENT_RECORD(ct.Structure):
|
|
152
151
|
_fields_ = [
|
|
153
152
|
("dwMousePosition", COORD),
|
|
154
|
-
("dwButtonState",
|
|
155
|
-
("dwControlKeyState",
|
|
156
|
-
("dwEventFlags",
|
|
153
|
+
("dwButtonState", ct.wintypes.DWORD),
|
|
154
|
+
("dwControlKeyState", ct.wintypes.DWORD),
|
|
155
|
+
("dwEventFlags", ct.wintypes.DWORD),
|
|
157
156
|
]
|
|
158
157
|
|
|
159
|
-
class WINDOW_BUFFER_SIZE_RECORD(
|
|
158
|
+
class WINDOW_BUFFER_SIZE_RECORD(ct.Structure):
|
|
160
159
|
_fields_ = [("dwSize", COORD)]
|
|
161
160
|
|
|
162
|
-
class MENU_EVENT_RECORD(
|
|
163
|
-
_fields_ = [("dwCommandId",
|
|
161
|
+
class MENU_EVENT_RECORD(ct.Structure):
|
|
162
|
+
_fields_ = [("dwCommandId", ct.wintypes.UINT)]
|
|
164
163
|
|
|
165
|
-
class FOCUS_EVENT_RECORD(
|
|
166
|
-
_fields_ = [("bSetFocus",
|
|
164
|
+
class FOCUS_EVENT_RECORD(ct.Structure):
|
|
165
|
+
_fields_ = [("bSetFocus", ct.wintypes.BOOL)]
|
|
167
166
|
|
|
168
|
-
class INPUT_RECORD_EVENT(
|
|
167
|
+
class INPUT_RECORD_EVENT(ct.Union):
|
|
169
168
|
_fields_ = [
|
|
170
169
|
("KeyEvent", KEY_EVENT_RECORD),
|
|
171
170
|
("MouseEvent", MOUSE_EVENT_RECORD),
|
|
@@ -174,9 +173,9 @@ class _WindowsBackend:
|
|
|
174
173
|
("FocusEvent", FOCUS_EVENT_RECORD),
|
|
175
174
|
]
|
|
176
175
|
|
|
177
|
-
class INPUT_RECORD(
|
|
176
|
+
class INPUT_RECORD(ct.Structure):
|
|
178
177
|
_fields_ = [
|
|
179
|
-
("EventType",
|
|
178
|
+
("EventType", ct.wintypes.WORD),
|
|
180
179
|
("Event", INPUT_RECORD_EVENT),
|
|
181
180
|
]
|
|
182
181
|
|
|
@@ -304,8 +303,8 @@ class _UnixBackend:
|
|
|
304
303
|
vk = arrow_map.get(ch3, "unknown")
|
|
305
304
|
# 消费 Delete 键的尾部 '~'
|
|
306
305
|
if ch3 == "3":
|
|
307
|
-
|
|
308
|
-
if
|
|
306
|
+
pending_del, _, _ = select.select([fd], [], [], 0.02)
|
|
307
|
+
if pending_del:
|
|
309
308
|
os.read(fd, 1)
|
|
310
309
|
results.append({
|
|
311
310
|
"type": "key", "vk": vk,
|
|
@@ -389,10 +388,10 @@ class _UnixBackend:
|
|
|
389
388
|
# 尝试读取更多字节以组成完整 UTF-8 字符
|
|
390
389
|
more, _, _ = select.select([fd], [], [], 0.02)
|
|
391
390
|
if more:
|
|
392
|
-
|
|
391
|
+
utf_tail = os.read(fd, 3).decode(
|
|
393
392
|
"utf-8", errors="replace",
|
|
394
393
|
)
|
|
395
|
-
full_char +=
|
|
394
|
+
full_char += utf_tail
|
|
396
395
|
results.append({
|
|
397
396
|
"type": "key", "vk": "char",
|
|
398
397
|
"char": full_char, "ctrl": False,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from echotools.exec.fncall.protocols.entml_thinking_parse import (
|
|
2
2
|
EntmlThinkingStreamFilter,
|
|
3
|
+
has_unclosed_entml_thinking,
|
|
3
4
|
split_entml_thinking,
|
|
4
5
|
)
|
|
5
6
|
|
|
@@ -16,6 +17,11 @@ def test_split_entml_thinking() -> None:
|
|
|
16
17
|
assert "<entml:thinking>" not in content
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
def test_has_unclosed_entml_thinking_streaming_prefix() -> None:
|
|
21
|
+
assert has_unclosed_entml_thinking("<entml:think")
|
|
22
|
+
assert not has_unclosed_entml_thinking("<entml:thinking>done</entml:thinking>")
|
|
23
|
+
|
|
24
|
+
|
|
19
25
|
def test_entml_thinking_stream_filter() -> None:
|
|
20
26
|
filt = EntmlThinkingStreamFilter()
|
|
21
27
|
parts = []
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from echotools.exec.fncall import get_protocol
|
|
4
|
+
from echotools.exec.fncall.parsers.stream import FncallStreamParser
|
|
5
|
+
from echotools.exec.fncall.protocols.entml_thinking_parse import (
|
|
6
|
+
EntmlThinkingStreamFilter,
|
|
7
|
+
has_unclosed_entml_thinking,
|
|
8
|
+
split_entml_thinking,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
TOOLS = [
|
|
12
|
+
{
|
|
13
|
+
"type": "function",
|
|
14
|
+
"function": {
|
|
15
|
+
"name": "get_weather",
|
|
16
|
+
"parameters": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {"city": {"type": "string"}},
|
|
19
|
+
"required": ["city"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
INVOKE = (
|
|
26
|
+
'<entml:invoke name="get_weather">'
|
|
27
|
+
'<entml:parameter name="city">Hangzhou</entml:parameter>'
|
|
28
|
+
"</entml:invoke>"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_has_unclosed_entml_thinking() -> None:
|
|
33
|
+
assert not has_unclosed_entml_thinking("")
|
|
34
|
+
assert not has_unclosed_entml_thinking("hello")
|
|
35
|
+
assert has_unclosed_entml_thinking("<entml:thinking>\nstep")
|
|
36
|
+
assert has_unclosed_entml_thinking("<entml:thinking>\nstep</entml:think")
|
|
37
|
+
assert not has_unclosed_entml_thinking(
|
|
38
|
+
"<entml:thinking>\nstep\n</entml:thinking>\nanswer"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_in_open_thinking_hold_open_tag() -> None:
|
|
43
|
+
filt = EntmlThinkingStreamFilter()
|
|
44
|
+
assert not filt.in_open_thinking()
|
|
45
|
+
filt.feed("prefix <entml:think")
|
|
46
|
+
assert filt.in_open_thinking()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_split_entml_thinking_ignores_unclosed_block() -> None:
|
|
50
|
+
text = "<entml:thinking>still going\n" + INVOKE
|
|
51
|
+
content, thinking = split_entml_thinking(text)
|
|
52
|
+
assert thinking == ""
|
|
53
|
+
assert INVOKE in content
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_invoke_inside_unclosed_thinking_not_parsed_as_tool() -> None:
|
|
57
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
58
|
+
stream = f"<entml:thinking>\nplan {INVOKE}\n"
|
|
59
|
+
for i in range(0, len(stream), 7):
|
|
60
|
+
parser.feed(stream[i : i + 7])
|
|
61
|
+
assert not parser.has_calls
|
|
62
|
+
assert INVOKE in parser.partial_thinking
|
|
63
|
+
clean, calls = parser.finalize()
|
|
64
|
+
assert not calls
|
|
65
|
+
assert INVOKE not in clean
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_invoke_after_thinking_close_is_parsed() -> None:
|
|
69
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
70
|
+
text = f"<entml:thinking>ok</entml:thinking>\n\n{INVOKE}"
|
|
71
|
+
parser.feed(text)
|
|
72
|
+
assert parser.has_calls
|
|
73
|
+
clean, calls = parser.finalize()
|
|
74
|
+
assert len(calls) == 1
|
|
75
|
+
assert calls[0]["function"]["name"] == "get_weather"
|
|
76
|
+
assert "Hangzhou" in calls[0]["function"]["arguments"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_thinking_close_and_invoke_in_one_chunk() -> None:
|
|
80
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
81
|
+
chunks = ["<entml:thinking>reason</entml:thinking>", INVOKE]
|
|
82
|
+
for chunk in chunks:
|
|
83
|
+
parser.feed(chunk)
|
|
84
|
+
clean, calls = parser.finalize()
|
|
85
|
+
assert parser.partial_thinking.strip() == "reason"
|
|
86
|
+
assert len(calls) == 1
|
|
87
|
+
assert "reason" not in clean or clean.strip() == ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_no_thinking_invoke_still_works() -> None:
|
|
91
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
92
|
+
parser.feed(f"Sure.\n{INVOKE}")
|
|
93
|
+
clean, calls = parser.finalize()
|
|
94
|
+
assert len(calls) == 1
|
|
95
|
+
assert "Sure" in clean
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_stream_thinking_incremental_before_close() -> None:
|
|
99
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
100
|
+
parser.feed("<entml:thinking>\n")
|
|
101
|
+
parser.feed("line one\n")
|
|
102
|
+
assert "line one" in parser.partial_thinking
|
|
103
|
+
assert not parser.has_calls
|
|
104
|
+
parser.feed(f"mention {INVOKE}\n")
|
|
105
|
+
assert INVOKE in parser.partial_thinking
|
|
106
|
+
assert not parser.has_calls
|
|
107
|
+
parser.feed("</entml:thinking>\nanswer")
|
|
108
|
+
clean, calls = parser.finalize()
|
|
109
|
+
assert not calls
|
|
110
|
+
assert "answer" in clean
|
|
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
|
|
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
|