code-muse 0.0.1__py3-none-any.whl
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.
- code_muse/__init__.py +26 -0
- code_muse/__main__.py +10 -0
- code_muse/agents/__init__.py +31 -0
- code_muse/agents/_builder.py +214 -0
- code_muse/agents/_compaction.py +506 -0
- code_muse/agents/_diagnostics.py +171 -0
- code_muse/agents/_history.py +382 -0
- code_muse/agents/_key_listeners.py +148 -0
- code_muse/agents/_non_streaming_render.py +148 -0
- code_muse/agents/_runtime.py +596 -0
- code_muse/agents/agent_creator_agent.py +603 -0
- code_muse/agents/agent_helios.py +47 -0
- code_muse/agents/agent_manager.py +740 -0
- code_muse/agents/agent_muse.py +78 -0
- code_muse/agents/agent_planning.py +44 -0
- code_muse/agents/agent_qa_melpomene.py +207 -0
- code_muse/agents/base_agent.py +194 -0
- code_muse/agents/event_stream_handler.py +361 -0
- code_muse/agents/json_agent.py +201 -0
- code_muse/agents/prompt_v3.py +521 -0
- code_muse/agents/subagent_stream_handler.py +273 -0
- code_muse/callbacks.py +941 -0
- code_muse/chatgpt_codex_client.py +333 -0
- code_muse/claude_cache_client.py +853 -0
- code_muse/cli_runner/__init__.py +319 -0
- code_muse/cli_runner/args.py +63 -0
- code_muse/cli_runner/loop.py +510 -0
- code_muse/cli_runner/resume.py +72 -0
- code_muse/cli_runner/runner.py +161 -0
- code_muse/command_line/__init__.py +1 -0
- code_muse/command_line/add_model_menu.py +1331 -0
- code_muse/command_line/agent_menu.py +674 -0
- code_muse/command_line/attachments.py +397 -0
- code_muse/command_line/autosave_menu.py +709 -0
- code_muse/command_line/clipboard.py +528 -0
- code_muse/command_line/colors_menu.py +530 -0
- code_muse/command_line/command_handler.py +262 -0
- code_muse/command_line/command_registry.py +150 -0
- code_muse/command_line/config_commands.py +711 -0
- code_muse/command_line/core_commands.py +740 -0
- code_muse/command_line/diff_menu.py +865 -0
- code_muse/command_line/file_path_completion.py +73 -0
- code_muse/command_line/load_context_completion.py +57 -0
- code_muse/command_line/model_picker_completion.py +512 -0
- code_muse/command_line/model_settings_menu.py +983 -0
- code_muse/command_line/onboarding_slides.py +162 -0
- code_muse/command_line/onboarding_wizard.py +337 -0
- code_muse/command_line/pagination.py +41 -0
- code_muse/command_line/pin_command_completion.py +329 -0
- code_muse/command_line/prompt_toolkit_completion.py +886 -0
- code_muse/command_line/session_commands.py +304 -0
- code_muse/command_line/shell_passthrough.py +145 -0
- code_muse/command_line/skills_completion.py +158 -0
- code_muse/command_line/types.py +18 -0
- code_muse/command_line/uc_menu.py +908 -0
- code_muse/command_line/utils.py +105 -0
- code_muse/command_line/wiggum_state.py +77 -0
- code_muse/config.py +1138 -0
- code_muse/config_agent.py +168 -0
- code_muse/config_appearance.py +241 -0
- code_muse/config_model.py +357 -0
- code_muse/config_security.py +73 -0
- code_muse/error_logging.py +132 -0
- code_muse/evals/__init__.py +35 -0
- code_muse/evals/eval_helpers.py +81 -0
- code_muse/evals/eval_runner.py +299 -0
- code_muse/evals/sample_evals/__init__.py +1 -0
- code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
- code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
- code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
- code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
- code_muse/fs_scan_cache/__init__.py +31 -0
- code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
- code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
- code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
- code_muse/fs_scan_cache/tool_integration.py +309 -0
- code_muse/fs_scan_cache/ttl_policy.py +44 -0
- code_muse/gemini_code_assist.py +383 -0
- code_muse/gemini_model.py +838 -0
- code_muse/hook_engine/README.md +105 -0
- code_muse/hook_engine/__init__.py +21 -0
- code_muse/hook_engine/aliases.py +153 -0
- code_muse/hook_engine/engine.py +221 -0
- code_muse/hook_engine/executor.py +347 -0
- code_muse/hook_engine/matcher.py +154 -0
- code_muse/hook_engine/models.py +245 -0
- code_muse/hook_engine/registry.py +114 -0
- code_muse/hook_engine/trust.py +268 -0
- code_muse/hook_engine/validator.py +144 -0
- code_muse/http_utils.py +360 -0
- code_muse/keymap.py +128 -0
- code_muse/list_filtering.py +26 -0
- code_muse/main.py +10 -0
- code_muse/messaging/__init__.py +259 -0
- code_muse/messaging/bus.py +621 -0
- code_muse/messaging/commands.py +166 -0
- code_muse/messaging/markdown_patches.py +57 -0
- code_muse/messaging/message_queue.py +397 -0
- code_muse/messaging/messages.py +591 -0
- code_muse/messaging/queue_console.py +269 -0
- code_muse/messaging/renderers.py +308 -0
- code_muse/messaging/rich_renderer.py +1158 -0
- code_muse/messaging/shimmer.py +154 -0
- code_muse/messaging/spinner/__init__.py +87 -0
- code_muse/messaging/spinner/console_spinner.py +250 -0
- code_muse/messaging/spinner/spinner_base.py +82 -0
- code_muse/messaging/subagent_console.py +458 -0
- code_muse/model_factory.py +1203 -0
- code_muse/model_switching.py +59 -0
- code_muse/model_utils.py +156 -0
- code_muse/models.json +66 -0
- code_muse/models_cache/__init__.py +26 -0
- code_muse/models_cache/blocking_lru_cache.py +98 -0
- code_muse/models_cache/cache_writer.py +86 -0
- code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
- code_muse/models_cache/sha256_hash.pyx +34 -0
- code_muse/models_cache/startup_integration.py +75 -0
- code_muse/models_dev_api.json +1 -0
- code_muse/models_dev_parser.py +590 -0
- code_muse/motion.py +126 -0
- code_muse/plugins/__init__.py +471 -0
- code_muse/plugins/agent_skills/__init__.py +32 -0
- code_muse/plugins/agent_skills/config.py +176 -0
- code_muse/plugins/agent_skills/discovery.py +309 -0
- code_muse/plugins/agent_skills/downloader.py +389 -0
- code_muse/plugins/agent_skills/installer.py +19 -0
- code_muse/plugins/agent_skills/metadata.py +293 -0
- code_muse/plugins/agent_skills/prompt_builder.py +66 -0
- code_muse/plugins/agent_skills/register_callbacks.py +298 -0
- code_muse/plugins/agent_skills/remote_catalog.py +320 -0
- code_muse/plugins/agent_skills/skill_catalog.py +254 -0
- code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
- code_muse/plugins/agent_skills/skills_menu.py +791 -0
- code_muse/plugins/autonomous_memory/__init__.py +39 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
- code_muse/plugins/autonomous_memory/consolidation.py +82 -0
- code_muse/plugins/autonomous_memory/extraction.py +382 -0
- code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
- code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
- code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
- code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
- code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
- code_muse/plugins/aws_bedrock/__init__.py +14 -0
- code_muse/plugins/aws_bedrock/config.py +99 -0
- code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
- code_muse/plugins/aws_bedrock/utils.py +153 -0
- code_muse/plugins/azure_foundry/README.md +238 -0
- code_muse/plugins/azure_foundry/__init__.py +15 -0
- code_muse/plugins/azure_foundry/config.py +125 -0
- code_muse/plugins/azure_foundry/discovery.py +187 -0
- code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
- code_muse/plugins/azure_foundry/token.py +180 -0
- code_muse/plugins/azure_foundry/utils.py +345 -0
- code_muse/plugins/build_filter/__init__.py +1 -0
- code_muse/plugins/build_filter/register_callbacks.py +201 -0
- code_muse/plugins/build_filter/strategies/__init__.py +1 -0
- code_muse/plugins/build_filter/strategies/build.py +397 -0
- code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
- code_muse/plugins/chatgpt_oauth/config.py +52 -0
- code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
- code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
- code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_muse/plugins/chatgpt_oauth/utils.py +538 -0
- code_muse/plugins/checkpointing/__init__.py +29 -0
- code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
- code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
- code_muse/plugins/checkpointing/register_callbacks.py +51 -0
- code_muse/plugins/checkpointing/restore_command.py +263 -0
- code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
- code_muse/plugins/checkpointing/shadow_git.py +90 -0
- code_muse/plugins/claude_code_hooks/__init__.py +1 -0
- code_muse/plugins/claude_code_hooks/config.py +188 -0
- code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
- code_muse/plugins/claude_code_oauth/README.md +167 -0
- code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
- code_muse/plugins/claude_code_oauth/__init__.py +25 -0
- code_muse/plugins/claude_code_oauth/config.py +52 -0
- code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
- code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
- code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
- code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
- code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
- code_muse/plugins/claude_code_oauth/utils.py +664 -0
- code_muse/plugins/copilot_auth/__init__.py +11 -0
- code_muse/plugins/copilot_auth/config.py +91 -0
- code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
- code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
- code_muse/plugins/copilot_auth/utils.py +584 -0
- code_muse/plugins/custom_commands/__init__.py +14 -0
- code_muse/plugins/custom_commands/args_injection.py +82 -0
- code_muse/plugins/custom_commands/command_discovery.py +89 -0
- code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
- code_muse/plugins/custom_commands/register_callbacks.py +176 -0
- code_muse/plugins/customizable_commands/__init__.py +0 -0
- code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
- code_muse/plugins/destructive_command_guard/__init__.py +14 -0
- code_muse/plugins/destructive_command_guard/detector.py +375 -0
- code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
- code_muse/plugins/example_custom_command/README.md +280 -0
- code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
- code_muse/plugins/file_permission_handler/__init__.py +4 -0
- code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
- code_muse/plugins/filter_engine/__init__.py +30 -0
- code_muse/plugins/filter_engine/classifier.py +153 -0
- code_muse/plugins/filter_engine/content_detector.py +184 -0
- code_muse/plugins/filter_engine/dispatcher.py +244 -0
- code_muse/plugins/filter_engine/register_callbacks.py +188 -0
- code_muse/plugins/filter_engine/registry.py +279 -0
- code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
- code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
- code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
- code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
- code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
- code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
- code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
- code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
- code_muse/plugins/filter_engine/verbosity.py +63 -0
- code_muse/plugins/force_push_guard/__init__.py +5 -0
- code_muse/plugins/force_push_guard/detector.py +96 -0
- code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
- code_muse/plugins/force_push_guard/test_detector.py +143 -0
- code_muse/plugins/frontend_emitter/__init__.py +25 -0
- code_muse/plugins/frontend_emitter/emitter.py +121 -0
- code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
- code_muse/plugins/gac/__init__.py +4 -0
- code_muse/plugins/gac/git_ops.py +136 -0
- code_muse/plugins/gac/prompt.py +191 -0
- code_muse/plugins/gac/register_callbacks.py +82 -0
- code_muse/plugins/hook_creator/__init__.py +1 -0
- code_muse/plugins/hook_creator/register_callbacks.py +34 -0
- code_muse/plugins/hook_manager/__init__.py +1 -0
- code_muse/plugins/hook_manager/config.py +289 -0
- code_muse/plugins/hook_manager/hooks_menu.py +563 -0
- code_muse/plugins/hook_manager/register_callbacks.py +227 -0
- code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
- code_muse/plugins/mindpack/__init__.py +0 -0
- code_muse/plugins/mindpack/factory.py +930 -0
- code_muse/plugins/mindpack/judge.py +573 -0
- code_muse/plugins/mindpack/memory.py +100 -0
- code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
- code_muse/plugins/mindpack/orchestration.py +605 -0
- code_muse/plugins/mindpack/register_callbacks.py +175 -0
- code_muse/plugins/mindpack/schemas.py +358 -0
- code_muse/plugins/mindpack/tools.py +387 -0
- code_muse/plugins/oauth_muse_html.py +226 -0
- code_muse/plugins/ollama_setup/__init__.py +5 -0
- code_muse/plugins/ollama_setup/completer.py +36 -0
- code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
- code_muse/plugins/plan_command/__init__.py +0 -0
- code_muse/plugins/plan_command/register_callbacks.py +206 -0
- code_muse/plugins/plan_mode/__init__.py +37 -0
- code_muse/plugins/plan_mode/mode_cycling.py +40 -0
- code_muse/plugins/plan_mode/plan_generation.py +68 -0
- code_muse/plugins/plan_mode/plan_hooks.py +74 -0
- code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
- code_muse/plugins/plan_mode/register_callbacks.py +121 -0
- code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
- code_muse/plugins/policy_engine/__init__.py +46 -0
- code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
- code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
- code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
- code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
- code_muse/plugins/policy_engine/register_callbacks.py +112 -0
- code_muse/plugins/pop_command/__init__.py +1 -0
- code_muse/plugins/pop_command/register_callbacks.py +189 -0
- code_muse/plugins/prompt_newline/__init__.py +13 -0
- code_muse/plugins/prompt_newline/config.py +19 -0
- code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
- code_muse/plugins/safety_status/__init__.py +0 -0
- code_muse/plugins/safety_status/register_callbacks.py +113 -0
- code_muse/plugins/semantic_compression/__init__.py +6 -0
- code_muse/plugins/semantic_compression/compressor.py +295 -0
- code_muse/plugins/semantic_compression/config.py +123 -0
- code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
- code_muse/plugins/shell_minimizer/__init__.py +50 -0
- code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
- code_muse/plugins/shell_minimizer/pipeline.py +556 -0
- code_muse/plugins/shell_minimizer/primitives.py +482 -0
- code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
- code_muse/plugins/shell_safety/__init__.py +6 -0
- code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_muse/plugins/shell_safety/command_cache.py +149 -0
- code_muse/plugins/shell_safety/register_callbacks.py +202 -0
- code_muse/plugins/synthetic_status/__init__.py +1 -0
- code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
- code_muse/plugins/synthetic_status/status_api.py +145 -0
- code_muse/plugins/token_caching/__init__.py +21 -0
- code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
- code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
- code_muse/plugins/token_caching/register_callbacks.py +54 -0
- code_muse/plugins/token_caching/stats_display.py +35 -0
- code_muse/plugins/token_tracking/__init__.py +26 -0
- code_muse/plugins/token_tracking/database.py +381 -0
- code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
- code_muse/plugins/token_tracking/record.py +55 -0
- code_muse/plugins/token_tracking/register_callbacks.py +277 -0
- code_muse/plugins/token_tracking/reports.py +329 -0
- code_muse/plugins/universal_constructor/__init__.py +13 -0
- code_muse/plugins/universal_constructor/models.py +136 -0
- code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
- code_muse/plugins/universal_constructor/registry.py +390 -0
- code_muse/plugins/universal_constructor/runner.py +474 -0
- code_muse/plugins/universal_constructor/safety.py +440 -0
- code_muse/plugins/universal_constructor/sandbox.py +584 -0
- code_muse/provider_identity.py +105 -0
- code_muse/pydantic_patches.py +410 -0
- code_muse/reopenable_async_client.py +233 -0
- code_muse/round_robin_model.py +151 -0
- code_muse/secret_storage.py +74 -0
- code_muse/security/__init__.py +1 -0
- code_muse/security/redaction.cpython-314-darwin.so +0 -0
- code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/security/redaction.pyx +135 -0
- code_muse/session_storage.py +565 -0
- code_muse/status_display.py +261 -0
- code_muse/stream_parser/__init__.py +76 -0
- code_muse/stream_parser/assistant_text_parser.py +90 -0
- code_muse/stream_parser/citation_parser.py +76 -0
- code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
- code_muse/stream_parser/proposed_plan_parser.py +158 -0
- code_muse/stream_parser/stream_text_chunk.py +23 -0
- code_muse/stream_parser/stream_text_parser.py +27 -0
- code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
- code_muse/stream_parser/tagged_line_parser.pyx +251 -0
- code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
- code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
- code_muse/summarization_agent.py +308 -0
- code_muse/terminal_utils.cpython-314-darwin.so +0 -0
- code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/terminal_utils.pyx +483 -0
- code_muse/tools/__init__.py +459 -0
- code_muse/tools/agent_tools.py +613 -0
- code_muse/tools/ask_user_question/__init__.py +26 -0
- code_muse/tools/ask_user_question/constants.py +73 -0
- code_muse/tools/ask_user_question/demo_tui.py +55 -0
- code_muse/tools/ask_user_question/handler.py +232 -0
- code_muse/tools/ask_user_question/models.py +302 -0
- code_muse/tools/ask_user_question/registration.py +37 -0
- code_muse/tools/ask_user_question/renderers.py +336 -0
- code_muse/tools/ask_user_question/terminal_ui.py +327 -0
- code_muse/tools/ask_user_question/theme.py +156 -0
- code_muse/tools/ask_user_question/tui_loop.py +422 -0
- code_muse/tools/background_jobs.py +99 -0
- code_muse/tools/browser/__init__.py +37 -0
- code_muse/tools/browser/browser_control.py +289 -0
- code_muse/tools/browser/browser_interactions.py +545 -0
- code_muse/tools/browser/browser_locators.py +640 -0
- code_muse/tools/browser/browser_manager.py +376 -0
- code_muse/tools/browser/browser_navigation.py +251 -0
- code_muse/tools/browser/browser_screenshot.py +180 -0
- code_muse/tools/browser/browser_scripts.py +462 -0
- code_muse/tools/browser/browser_workflows.py +222 -0
- code_muse/tools/chrome_cdp/__init__.py +1070 -0
- code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
- code_muse/tools/command_runner.py +1401 -0
- code_muse/tools/common.py +1407 -0
- code_muse/tools/display.py +87 -0
- code_muse/tools/file_modifications.py +1099 -0
- code_muse/tools/file_operations.py +860 -0
- code_muse/tools/image_tools.py +185 -0
- code_muse/tools/meetin_proxy/__init__.py +243 -0
- code_muse/tools/meetin_proxy/capture_addon.py +82 -0
- code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
- code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
- code_muse/tools/path_policy.py +219 -0
- code_muse/tools/skills_tools.py +586 -0
- code_muse/tools/subagent_context.py +158 -0
- code_muse/tools/tools_content.py +50 -0
- code_muse/tools/universal_constructor.py +965 -0
- code_muse/uvx_detection.py +241 -0
- code_muse/version_checker.py +86 -0
- code_muse-0.0.1.data/data/code_muse/models.json +66 -0
- code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
- code_muse-0.0.1.dist-info/METADATA +845 -0
- code_muse-0.0.1.dist-info/RECORD +394 -0
- code_muse-0.0.1.dist-info/WHEEL +4 -0
- code_muse-0.0.1.dist-info/entry_points.txt +2 -0
- code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Streaming parser for inline hidden tags.
|
|
2
|
+
|
|
3
|
+
Scans text for user-defined open/close delimiter pairs, extracts the content
|
|
4
|
+
between them as hidden payloads, and strips the delimiters from visible text.
|
|
5
|
+
Tags are matched literally and are not nested.
|
|
6
|
+
|
|
7
|
+
Partial delimiters that span chunk boundaries are buffered correctly using
|
|
8
|
+
suffix/prefix overlap checks so that a split ``<oai-mem-citation>`` is still
|
|
9
|
+
recognised when the chunks are ``'<oai-'`` and ``'mem-citation>'``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import TypeVar
|
|
14
|
+
|
|
15
|
+
from code_muse.stream_parser.stream_text_chunk import StreamTextChunk
|
|
16
|
+
from code_muse.stream_parser.stream_text_parser import StreamTextParser
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class InlineTagSpec[T]:
|
|
23
|
+
"""Specification for a single hidden inline tag type.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
tag: Payload type / identifier emitted in extracted results.
|
|
27
|
+
open: Literal opening delimiter (e.g. ``"<oai-mem-citation>"``).
|
|
28
|
+
close: Literal closing delimiter (e.g. ``"</oai-mem-citation>"``).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
tag: T
|
|
32
|
+
open: str
|
|
33
|
+
close: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ExtractedInlineTag[T]:
|
|
38
|
+
"""Payload produced when an inline tag is fully closed.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
tag: The tag identifier from the matching :class:`InlineTagSpec`.
|
|
42
|
+
content: Text that appeared between the open and close delimiters.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
tag: T
|
|
46
|
+
content: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _longest_suffix_prefix_len(s: str, needle: str) -> int:
|
|
50
|
+
"""Return the largest ``k > 0`` such that ``s`` ends with ``needle[:k]``.
|
|
51
|
+
|
|
52
|
+
This tells us how many characters at the end of ``s`` might be a partial
|
|
53
|
+
occurrence of ``needle`` when more input arrives in the next chunk.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
s: The string to inspect (usually the pending buffer).
|
|
57
|
+
needle: The delimiter we are searching for.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Length of the longest overlap, or ``0`` when there is none.
|
|
61
|
+
"""
|
|
62
|
+
if not needle:
|
|
63
|
+
return 0
|
|
64
|
+
max_k = min(len(s), len(needle))
|
|
65
|
+
for k in range(max_k, 0, -1):
|
|
66
|
+
if s.endswith(needle[:k]):
|
|
67
|
+
return k
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class InlineHiddenTagParser(StreamTextParser[T]):
|
|
72
|
+
"""Streaming parser that extracts hidden inline tags from text.
|
|
73
|
+
|
|
74
|
+
* Searches for the earliest open delimiter; when two open delimiters start
|
|
75
|
+
at the same position, the longer one wins (longest-match tiebreaker).
|
|
76
|
+
* Once inside a tag, everything up to the matching close delimiter is
|
|
77
|
+
treated as literal content—**nested tags are not parsed**.
|
|
78
|
+
* On chunk boundaries, characters that could be a partial delimiter are
|
|
79
|
+
kept in the pending buffer rather than being emitted as visible text.
|
|
80
|
+
* Unterminated tags are auto-closed at :meth:`finish`; their buffered
|
|
81
|
+
content becomes the extracted payload.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, specs: list[InlineTagSpec[T]]) -> None:
|
|
85
|
+
if not specs:
|
|
86
|
+
raise ValueError("InlineHiddenTagParser requires at least one tag spec")
|
|
87
|
+
for spec in specs:
|
|
88
|
+
if not spec.open:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"InlineHiddenTagParser requires non-empty open delimiters"
|
|
91
|
+
)
|
|
92
|
+
if not spec.close:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"InlineHiddenTagParser requires non-empty close delimiters"
|
|
95
|
+
)
|
|
96
|
+
self.specs = specs
|
|
97
|
+
self._pending: str = ""
|
|
98
|
+
self._active_spec: InlineTagSpec[T] | None = None
|
|
99
|
+
self._active_content: str = ""
|
|
100
|
+
|
|
101
|
+
def push_str(self, chunk: str) -> StreamTextChunk[T]:
|
|
102
|
+
"""Feed a new text chunk and scan for tag delimiters.
|
|
103
|
+
|
|
104
|
+
The chunk is appended to any buffered pending text, then the parser
|
|
105
|
+
loops until it can no longer make forward progress:
|
|
106
|
+
|
|
107
|
+
1. If a tag is open, search for its close delimiter.
|
|
108
|
+
* Found → emit the extracted tag and drain through the close.
|
|
109
|
+
* Not found → keep a suffix that might be a partial close,
|
|
110
|
+
drain the rest into the tag's content buffer.
|
|
111
|
+
2. Otherwise, search for the next open delimiter.
|
|
112
|
+
* Found → emit visible text before it, drain through the open,
|
|
113
|
+
start tracking tag content.
|
|
114
|
+
* Not found → keep a suffix that might be a partial open,
|
|
115
|
+
drain the rest as visible text.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
chunk: Incoming text delta.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Visible text and any fully-closed tags found in this delta.
|
|
122
|
+
"""
|
|
123
|
+
self._pending += chunk
|
|
124
|
+
visible_parts: list[str] = []
|
|
125
|
+
extracted_parts: list[ExtractedInlineTag[T]] = []
|
|
126
|
+
|
|
127
|
+
while True:
|
|
128
|
+
if self._active_spec is not None:
|
|
129
|
+
close_pos = self._pending.find(self._active_spec.close)
|
|
130
|
+
if close_pos != -1:
|
|
131
|
+
# Close delimiter found.
|
|
132
|
+
content = self._active_content + self._pending[:close_pos]
|
|
133
|
+
extracted_parts.append(
|
|
134
|
+
ExtractedInlineTag(self._active_spec.tag, content)
|
|
135
|
+
)
|
|
136
|
+
# Drain through the close delimiter.
|
|
137
|
+
close_len = len(self._active_spec.close)
|
|
138
|
+
self._pending = self._pending[close_pos + close_len :]
|
|
139
|
+
self._active_spec = None
|
|
140
|
+
self._active_content = ""
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# No close yet — keep characters that could be a partial
|
|
144
|
+
# close delimiter at the end of the buffer.
|
|
145
|
+
keep = _longest_suffix_prefix_len(
|
|
146
|
+
self._pending, self._active_spec.close
|
|
147
|
+
)
|
|
148
|
+
drain_len = len(self._pending) - keep
|
|
149
|
+
self._active_content += self._pending[:drain_len]
|
|
150
|
+
self._pending = self._pending[drain_len:]
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
# No active tag — look for the next open delimiter.
|
|
154
|
+
next_open = self._find_next_open()
|
|
155
|
+
if next_open is not None:
|
|
156
|
+
pos, spec_idx = next_open
|
|
157
|
+
spec = self.specs[spec_idx]
|
|
158
|
+
# Emit visible text before the open delimiter.
|
|
159
|
+
visible_parts.append(self._pending[:pos])
|
|
160
|
+
# Drain through the open delimiter.
|
|
161
|
+
open_len = len(spec.open)
|
|
162
|
+
self._pending = self._pending[pos + open_len :]
|
|
163
|
+
self._active_spec = spec
|
|
164
|
+
self._active_content = ""
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
# No open delimiter found — keep characters that could be a
|
|
168
|
+
# partial open delimiter at the end of the buffer.
|
|
169
|
+
keep = self._max_open_prefix_suffix_len()
|
|
170
|
+
drain_len = len(self._pending) - keep
|
|
171
|
+
visible_parts.append(self._pending[:drain_len])
|
|
172
|
+
self._pending = self._pending[drain_len:]
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
return StreamTextChunk(
|
|
176
|
+
visible_text="".join(visible_parts),
|
|
177
|
+
extracted=extracted_parts,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def finish(self) -> StreamTextChunk[T]:
|
|
181
|
+
"""Flush any remaining state.
|
|
182
|
+
|
|
183
|
+
If a tag is still open, its accumulated content is emitted as an
|
|
184
|
+
:class:`ExtractedInlineTag` with the tag's identifier. Any leftover
|
|
185
|
+
pending text (without an active tag) is emitted as visible text.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Final visible text and any auto-closed extracted tags.
|
|
189
|
+
"""
|
|
190
|
+
visible = ""
|
|
191
|
+
extracted: list[ExtractedInlineTag[T]] = []
|
|
192
|
+
|
|
193
|
+
if self._active_spec is not None:
|
|
194
|
+
content = self._active_content + self._pending
|
|
195
|
+
extracted.append(ExtractedInlineTag(self._active_spec.tag, content))
|
|
196
|
+
self._active_spec = None
|
|
197
|
+
self._active_content = ""
|
|
198
|
+
self._pending = ""
|
|
199
|
+
else:
|
|
200
|
+
visible = self._pending
|
|
201
|
+
self._pending = ""
|
|
202
|
+
|
|
203
|
+
return StreamTextChunk(visible_text=visible, extracted=extracted)
|
|
204
|
+
|
|
205
|
+
def _find_next_open(self) -> tuple[int, int] | None:
|
|
206
|
+
"""Find the earliest open delimiter in the pending buffer.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
``(position, spec_index)`` of the earliest open delimiter, or
|
|
210
|
+
``None`` when no open delimiter is present. Tie-breaking rules:
|
|
211
|
+
|
|
212
|
+
1. Smallest position (earliest in the buffer).
|
|
213
|
+
2. Longest open delimiter at that position.
|
|
214
|
+
3. Lowest spec index (stable ordering).
|
|
215
|
+
"""
|
|
216
|
+
candidates: list[tuple[int, int, int]] = []
|
|
217
|
+
for i, spec in enumerate(self.specs):
|
|
218
|
+
pos = self._pending.find(spec.open)
|
|
219
|
+
if pos != -1:
|
|
220
|
+
candidates.append((pos, len(spec.open), i))
|
|
221
|
+
if not candidates:
|
|
222
|
+
return None
|
|
223
|
+
best = min(candidates, key=lambda c: (c[0], -c[1], c[2]))
|
|
224
|
+
return best[0], best[2]
|
|
225
|
+
|
|
226
|
+
def _max_open_prefix_suffix_len(self) -> int:
|
|
227
|
+
"""Maximum overlap between the end of ``pending`` and any open delimiter.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Largest ``k > 0`` such that ``pending`` ends with the first ``k``
|
|
231
|
+
characters of at least one open delimiter.
|
|
232
|
+
"""
|
|
233
|
+
max_len = 0
|
|
234
|
+
for spec in self.specs:
|
|
235
|
+
max_len = max(max_len, _longest_suffix_prefix_len(self._pending, spec.open))
|
|
236
|
+
return max_len
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Parser for ``<proposed_plan>…</proposed_plan>`` line-based blocks.
|
|
2
|
+
|
|
3
|
+
Wraps :class:`TaggedLineParser` with a single tag spec and exposes a
|
|
4
|
+
:class:`StreamTextParser` interface that emits
|
|
5
|
+
:class:`ProposedPlanSegment` values.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import TypeVar
|
|
11
|
+
|
|
12
|
+
from code_muse.stream_parser.stream_text_chunk import StreamTextChunk
|
|
13
|
+
from code_muse.stream_parser.stream_text_parser import StreamTextParser
|
|
14
|
+
from code_muse.stream_parser.tagged_line_parser import (
|
|
15
|
+
TaggedLineParser,
|
|
16
|
+
TaggedLineSegment,
|
|
17
|
+
TaggedLineSegmentNormal,
|
|
18
|
+
TaggedLineSegmentTagDelta,
|
|
19
|
+
TaggedLineSegmentTagEnd,
|
|
20
|
+
TaggedLineSegmentTagStart,
|
|
21
|
+
TagSpec,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProposedPlanSegmentType(Enum):
|
|
28
|
+
"""Kind of segment produced by :class:`ProposedPlanParser`."""
|
|
29
|
+
|
|
30
|
+
NORMAL = "normal"
|
|
31
|
+
PLAN_START = "plan_start"
|
|
32
|
+
PLAN_DELTA = "plan_delta"
|
|
33
|
+
PLAN_END = "plan_end"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ProposedPlanSegment:
|
|
38
|
+
"""Single semantic piece of a parsed assistant response.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
type: Which kind of segment this is.
|
|
42
|
+
text: Literal text content. Only populated for ``NORMAL`` and
|
|
43
|
+
``PLAN_DELTA`` segments.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
type: ProposedPlanSegmentType
|
|
47
|
+
text: str = ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProposedPlanParser(StreamTextParser[ProposedPlanSegment]):
|
|
51
|
+
"""Streaming parser that identifies ``<proposed_plan>`` blocks.
|
|
52
|
+
|
|
53
|
+
Lines that exactly equal ``"<proposed_plan>"`` or ``"</proposed_plan>"``
|
|
54
|
+
(after trimming) are removed from visible text and replaced by
|
|
55
|
+
:class:`ProposedPlanSegment` boundary markers. Content lines between
|
|
56
|
+
those boundaries become ``PLAN_DELTA`` segments. Everything else is
|
|
57
|
+
``NORMAL``.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self._parser = TaggedLineParser(
|
|
62
|
+
[TagSpec(open="<proposed_plan>", close="</proposed_plan>", tag="plan")]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def push_str(self, chunk: str) -> StreamTextChunk[ProposedPlanSegment]:
|
|
66
|
+
"""Feed a new text chunk.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
chunk: Incoming text delta (may contain partial lines).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Visible text outside plan blocks, plus any plan segments
|
|
73
|
+
(``PLAN_START``, ``PLAN_DELTA``, ``PLAN_END``) extracted from
|
|
74
|
+
the chunk.
|
|
75
|
+
"""
|
|
76
|
+
segments = self._parser.parse(chunk)
|
|
77
|
+
return self._build_chunk(segments)
|
|
78
|
+
|
|
79
|
+
def finish(self) -> StreamTextChunk[ProposedPlanSegment]:
|
|
80
|
+
"""Flush any remaining buffered state.
|
|
81
|
+
|
|
82
|
+
Unterminated plan blocks are auto-closed with a ``PLAN_END`` segment.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Final visible text and any trailing plan segments.
|
|
86
|
+
"""
|
|
87
|
+
segments = self._parser.finish()
|
|
88
|
+
return self._build_chunk(segments)
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _map_segment(seg: TaggedLineSegment) -> ProposedPlanSegment:
|
|
92
|
+
"""Convert a raw :class:`TaggedLineSegment` to a plan segment."""
|
|
93
|
+
if isinstance(seg, TaggedLineSegmentNormal):
|
|
94
|
+
return ProposedPlanSegment(ProposedPlanSegmentType.NORMAL, seg.text)
|
|
95
|
+
if isinstance(seg, TaggedLineSegmentTagStart):
|
|
96
|
+
return ProposedPlanSegment(ProposedPlanSegmentType.PLAN_START)
|
|
97
|
+
if isinstance(seg, TaggedLineSegmentTagDelta):
|
|
98
|
+
return ProposedPlanSegment(ProposedPlanSegmentType.PLAN_DELTA, seg.text)
|
|
99
|
+
if isinstance(seg, TaggedLineSegmentTagEnd):
|
|
100
|
+
return ProposedPlanSegment(ProposedPlanSegmentType.PLAN_END)
|
|
101
|
+
# Exhaustive because TaggedLineSegment is a union of the four classes.
|
|
102
|
+
raise TypeError(f"unexpected segment type: {type(seg)}")
|
|
103
|
+
|
|
104
|
+
def _build_chunk(
|
|
105
|
+
self, segments: list[TaggedLineSegment]
|
|
106
|
+
) -> StreamTextChunk[ProposedPlanSegment]:
|
|
107
|
+
"""Turn raw line segments into a :class:`StreamTextChunk`."""
|
|
108
|
+
mapped = [self._map_segment(s) for s in segments]
|
|
109
|
+
visible = "".join(
|
|
110
|
+
s.text for s in mapped if s.type == ProposedPlanSegmentType.NORMAL
|
|
111
|
+
)
|
|
112
|
+
return StreamTextChunk(visible_text=visible, extracted=mapped)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def extract_proposed_plan_text(text: str) -> str | None:
|
|
116
|
+
"""Extract the raw plan text from a complete string.
|
|
117
|
+
|
|
118
|
+
Runs the parser over the full text and concatenates all ``PLAN_DELTA``
|
|
119
|
+
segments that appear inside ``PLAN_START`` / ``PLAN_END`` pairs.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
text: Full assistant response text.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
The concatenated plan text, or ``None`` when no plan block is
|
|
126
|
+
present.
|
|
127
|
+
"""
|
|
128
|
+
parser = ProposedPlanParser()
|
|
129
|
+
out = parser.push_str(text)
|
|
130
|
+
tail = parser.finish()
|
|
131
|
+
all_segments = out.extracted + tail.extracted
|
|
132
|
+
|
|
133
|
+
parts: list[str] = []
|
|
134
|
+
in_plan = False
|
|
135
|
+
for seg in all_segments:
|
|
136
|
+
if seg.type == ProposedPlanSegmentType.PLAN_START:
|
|
137
|
+
in_plan = True
|
|
138
|
+
elif seg.type == ProposedPlanSegmentType.PLAN_END:
|
|
139
|
+
in_plan = False
|
|
140
|
+
elif seg.type == ProposedPlanSegmentType.PLAN_DELTA and in_plan:
|
|
141
|
+
parts.append(seg.text)
|
|
142
|
+
|
|
143
|
+
return "".join(parts) if parts else None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def strip_proposed_plan_blocks(text: str) -> str:
|
|
147
|
+
"""Remove all ``<proposed_plan>…</proposed_plan>`` blocks from text.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
text: Full assistant response text.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Visible text with plan blocks and their content stripped.
|
|
154
|
+
"""
|
|
155
|
+
parser = ProposedPlanParser()
|
|
156
|
+
out = parser.push_str(text)
|
|
157
|
+
tail = parser.finish()
|
|
158
|
+
return out.visible_text + tail.visible_text
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Incremental parser result for one pushed chunk (or final flush)."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class StreamTextChunk[T]:
|
|
11
|
+
"""Result from feeding a text chunk to a StreamTextParser.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
visible_text: Text safe to render immediately.
|
|
15
|
+
extracted: Hidden payloads extracted from the chunk.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
visible_text: str = ""
|
|
19
|
+
extracted: list[T] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
def is_empty(self) -> bool:
|
|
22
|
+
"""Return True when no visible text or extracted payloads were produced."""
|
|
23
|
+
return not self.visible_text and not self.extracted
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Abstract base class for composable streaming text parsers."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
from code_muse.stream_parser.stream_text_chunk import StreamTextChunk
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StreamTextParser[T](ABC):
|
|
12
|
+
"""Base class for parsers that consume streamed text and emit visible text
|
|
13
|
+
plus extracted payloads.
|
|
14
|
+
|
|
15
|
+
Parsers are composable: one parser can wrap another, delegating and
|
|
16
|
+
merging output.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def push_str(self, chunk: str) -> StreamTextChunk[T]:
|
|
21
|
+
"""Feed a new text chunk. Returns visible text + extracted payloads."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def finish(self) -> StreamTextChunk[T]:
|
|
26
|
+
"""Flush any buffered state at end-of-stream (or end-of-item)."""
|
|
27
|
+
...
|
|
Binary file
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# cython: language_level=3
|
|
2
|
+
"""Line-based tag-block parser for streamed text.
|
|
3
|
+
|
|
4
|
+
A tag must appear alone on a line after trimming (e.g.
|
|
5
|
+
``"<proposed_plan>"`` or ``"</proposed_plan>"``). Lines inside a tag block
|
|
6
|
+
are emitted as :class:`TaggedLineSegmentTagDelta`; lines outside are emitted as
|
|
7
|
+
:class:`TaggedLineSegmentNormal`.
|
|
8
|
+
|
|
9
|
+
The parser buffers text until it can disprove that the current partial line is
|
|
10
|
+
a tag line—once the trimmed prefix is no longer a prefix of any known open or
|
|
11
|
+
close delimiter, the line is emitted immediately so that visible text is not
|
|
12
|
+
held back unnecessarily.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TagSpec:
|
|
21
|
+
"""Specification for a line-based tag block.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
open: Exact opening line text (e.g. ``"<proposed_plan>"``).
|
|
25
|
+
close: Exact closing line text (e.g. ``"</proposed_plan>"``).
|
|
26
|
+
tag: Tag identifier emitted in segment results.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
open: str
|
|
30
|
+
close: str
|
|
31
|
+
tag: Any
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class TaggedLineSegmentNormal:
|
|
36
|
+
"""Plain text that lives outside any tag block."""
|
|
37
|
+
|
|
38
|
+
text: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class TaggedLineSegmentTagStart:
|
|
43
|
+
"""Emitted when a line exactly matches a tag's ``open`` delimiter."""
|
|
44
|
+
|
|
45
|
+
tag: Any
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class TaggedLineSegmentTagDelta:
|
|
50
|
+
"""Text that belongs inside an open tag block.
|
|
51
|
+
|
|
52
|
+
Consecutive deltas with the same tag are coalesced by the parser.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
tag: Any
|
|
56
|
+
text: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class TaggedLineSegmentTagEnd:
|
|
61
|
+
"""Emitted when a line exactly matches a tag's ``close`` delimiter."""
|
|
62
|
+
|
|
63
|
+
tag: Any
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
TaggedLineSegment = (
|
|
67
|
+
TaggedLineSegmentNormal
|
|
68
|
+
| TaggedLineSegmentTagStart
|
|
69
|
+
| TaggedLineSegmentTagDelta
|
|
70
|
+
| TaggedLineSegmentTagEnd
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TaggedLineParser:
|
|
75
|
+
"""Streaming line-based tag-block parser.
|
|
76
|
+
|
|
77
|
+
* Buffers partial lines and emits them only once they can no longer be a
|
|
78
|
+
tag line (the trimmed prefix is not a prefix of any open or close).
|
|
79
|
+
* Complete lines (ending in ``\\n``) are classified immediately.
|
|
80
|
+
* Tag lines are stripped from visible output and replaced by
|
|
81
|
+
:class:`TaggedLineSegmentTagStart` / :class:`TaggedLineSegmentTagEnd`.
|
|
82
|
+
* Unterminated tag blocks are auto-closed at :meth:`finish`.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, specs: list[TagSpec]) -> None:
|
|
86
|
+
self.specs = specs
|
|
87
|
+
self._line_buffer: str = ""
|
|
88
|
+
self._active_tag: Any = None
|
|
89
|
+
|
|
90
|
+
def parse(self, delta: str) -> list[TaggedLineSegment]:
|
|
91
|
+
"""Process a text delta and emit any newly-resolved segments.
|
|
92
|
+
|
|
93
|
+
The delta is appended to the internal line buffer, then every complete
|
|
94
|
+
line (up to and including ``\\n``) is classified. If the remaining
|
|
95
|
+
partial line can no longer become a tag line, it is emitted as well.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
delta: Incoming text chunk (may contain zero or more newlines).
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of segments produced by this delta.
|
|
102
|
+
"""
|
|
103
|
+
cdef int newline_idx
|
|
104
|
+
cdef str line
|
|
105
|
+
cdef str buf
|
|
106
|
+
cdef list segments
|
|
107
|
+
|
|
108
|
+
self._line_buffer += delta
|
|
109
|
+
segments = []
|
|
110
|
+
buf = self._line_buffer
|
|
111
|
+
|
|
112
|
+
# Drain complete lines.
|
|
113
|
+
while True:
|
|
114
|
+
newline_idx = buf.find("\n")
|
|
115
|
+
if newline_idx == -1:
|
|
116
|
+
break
|
|
117
|
+
line = buf[: newline_idx + 1]
|
|
118
|
+
buf = buf[newline_idx + 1 :]
|
|
119
|
+
self._line_buffer = buf
|
|
120
|
+
self._finish_line(line, segments)
|
|
121
|
+
|
|
122
|
+
self._line_buffer = buf
|
|
123
|
+
|
|
124
|
+
# If the remaining partial line can never become a tag line,
|
|
125
|
+
# flush it immediately so it does not stall visible output.
|
|
126
|
+
if buf and not self._is_tag_prefix(buf.strip()):
|
|
127
|
+
self._push_text(buf, segments)
|
|
128
|
+
self._line_buffer = ""
|
|
129
|
+
|
|
130
|
+
return segments
|
|
131
|
+
|
|
132
|
+
def finish(self) -> list[TaggedLineSegment]:
|
|
133
|
+
"""Flush any remaining buffered state.
|
|
134
|
+
|
|
135
|
+
If the pending partial line exactly matches an ``open`` or ``close``
|
|
136
|
+
delimiter, the appropriate boundary segment is emitted. Otherwise the
|
|
137
|
+
text is emitted as Normal or TagDelta (depending on whether a tag is
|
|
138
|
+
currently open). Finally, any still-open tag block is auto-closed with
|
|
139
|
+
a :class:`TaggedLineSegmentTagEnd`.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
List of final segments.
|
|
143
|
+
"""
|
|
144
|
+
cdef list segments
|
|
145
|
+
cdef str slug
|
|
146
|
+
cdef object open_spec
|
|
147
|
+
cdef object close_spec
|
|
148
|
+
|
|
149
|
+
segments = []
|
|
150
|
+
|
|
151
|
+
if self._line_buffer:
|
|
152
|
+
slug = self._line_buffer.strip()
|
|
153
|
+
open_spec = self._match_open(slug)
|
|
154
|
+
if open_spec is not None:
|
|
155
|
+
segments.append(TaggedLineSegmentTagStart(open_spec.tag))
|
|
156
|
+
self._active_tag = open_spec.tag
|
|
157
|
+
else:
|
|
158
|
+
close_spec = self._match_close(slug)
|
|
159
|
+
if close_spec is not None:
|
|
160
|
+
segments.append(TaggedLineSegmentTagEnd(close_spec.tag))
|
|
161
|
+
self._active_tag = None
|
|
162
|
+
else:
|
|
163
|
+
self._push_text(self._line_buffer, segments)
|
|
164
|
+
self._line_buffer = ""
|
|
165
|
+
|
|
166
|
+
if self._active_tag is not None:
|
|
167
|
+
segments.append(TaggedLineSegmentTagEnd(self._active_tag))
|
|
168
|
+
self._active_tag = None
|
|
169
|
+
|
|
170
|
+
return segments
|
|
171
|
+
|
|
172
|
+
def _push_text(self, text: str, segments: list[TaggedLineSegment]) -> None:
|
|
173
|
+
"""Emit ``text`` as the appropriate segment type, coalescing when possible.
|
|
174
|
+
|
|
175
|
+
If a tag is currently active, ``text`` becomes a
|
|
176
|
+
:class:`TaggedLineSegmentTagDelta`; otherwise it becomes a
|
|
177
|
+
:class:`TaggedLineSegmentNormal`. Consecutive segments of the same
|
|
178
|
+
type (and same tag, for deltas) are merged into one segment so the
|
|
179
|
+
output stays compact.
|
|
180
|
+
"""
|
|
181
|
+
cdef object last
|
|
182
|
+
cdef object active = self._active_tag
|
|
183
|
+
|
|
184
|
+
if active is not None:
|
|
185
|
+
if segments:
|
|
186
|
+
last = segments[-1]
|
|
187
|
+
if isinstance(last, TaggedLineSegmentTagDelta) and last.tag == active:
|
|
188
|
+
last.text += text
|
|
189
|
+
return
|
|
190
|
+
segments.append(TaggedLineSegmentTagDelta(active, text))
|
|
191
|
+
else:
|
|
192
|
+
if segments:
|
|
193
|
+
last = segments[-1]
|
|
194
|
+
if isinstance(last, TaggedLineSegmentNormal):
|
|
195
|
+
last.text += text
|
|
196
|
+
return
|
|
197
|
+
segments.append(TaggedLineSegmentNormal(text))
|
|
198
|
+
|
|
199
|
+
def _finish_line(self, line: str, segments: list[TaggedLineSegment]) -> None:
|
|
200
|
+
"""Classify a complete line (including its trailing ``\\n``).
|
|
201
|
+
|
|
202
|
+
The line is trimmed and checked against ``open`` / ``close``
|
|
203
|
+
delimiters in order. Tag lines are consumed entirely; non-tag lines
|
|
204
|
+
are forwarded to :meth:`_push_text`.
|
|
205
|
+
"""
|
|
206
|
+
cdef str slug
|
|
207
|
+
cdef object open_spec
|
|
208
|
+
cdef object close_spec
|
|
209
|
+
|
|
210
|
+
slug = line.strip()
|
|
211
|
+
open_spec = self._match_open(slug)
|
|
212
|
+
if open_spec is not None:
|
|
213
|
+
segments.append(TaggedLineSegmentTagStart(open_spec.tag))
|
|
214
|
+
self._active_tag = open_spec.tag
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
close_spec = self._match_close(slug)
|
|
218
|
+
if close_spec is not None:
|
|
219
|
+
segments.append(TaggedLineSegmentTagEnd(close_spec.tag))
|
|
220
|
+
self._active_tag = None
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
self._push_text(line, segments)
|
|
224
|
+
|
|
225
|
+
def _is_tag_prefix(self, slug: str) -> bool:
|
|
226
|
+
"""Return ``True`` if ``slug`` is a prefix of any ``open`` or ``close``."""
|
|
227
|
+
cdef object spec
|
|
228
|
+
cdef str open_str
|
|
229
|
+
cdef str close_str
|
|
230
|
+
for spec in self.specs:
|
|
231
|
+
open_str = spec.open
|
|
232
|
+
close_str = spec.close
|
|
233
|
+
if open_str.startswith(slug) or close_str.startswith(slug):
|
|
234
|
+
return True
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
def _match_open(self, slug: str) -> TagSpec | None:
|
|
238
|
+
"""Return the first spec whose ``open`` exactly equals ``slug``."""
|
|
239
|
+
cdef object spec
|
|
240
|
+
for spec in self.specs:
|
|
241
|
+
if spec.open == slug:
|
|
242
|
+
return spec
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def _match_close(self, slug: str) -> TagSpec | None:
|
|
246
|
+
"""Return the first spec whose ``close`` exactly equals ``slug``."""
|
|
247
|
+
cdef object spec
|
|
248
|
+
for spec in self.specs:
|
|
249
|
+
if spec.close == slug:
|
|
250
|
+
return spec
|
|
251
|
+
return None
|
|
Binary file
|