From dd1b9095a05d8e204efed780a2d428443e88bbc8 Mon Sep 17 00:00:00 2001 From: Vicente Adolfo Bolea Sanchez Date: Thu, 4 Jun 2026 11:46:42 -0400 Subject: [PATCH] Auto-translate natural language prompts to DSL via vtk-mcp When a vtk-mcp server is available, the client now calls is_dsl_prompt() before code generation; if the input is plain natural language it calls translate_prompt_to_dsl() to convert it to the structured VTK pipeline DSL before RAG retrieval and generation. VTKMCPClient.translate_prompt() accepts optional model, base_url, and api_key overrides so callers can direct the translation to a specific model or Ollama endpoint without changing server config. --- README.md | 6 +- src/vtk_prompt/cli.py | 16 ++++- src/vtk_prompt/client.py | 63 ++++++++++++++++++- src/vtk_prompt/controllers/generation.py | 5 +- src/vtk_prompt/generate_files.py | 2 +- .../prompts/components/model_defaults.yml | 2 +- .../components/no_dsl_translation_tools.yml | 4 ++ .../prompts/prompt_component_assembler.py | 4 ++ src/vtk_prompt/provider_utils.py | 12 ++-- src/vtk_prompt/rendering/code_executor.py | 62 +++++++++++++++--- src/vtk_prompt/state/initializer.py | 2 + src/vtk_prompt/ui/layout/settings_dialog.py | 21 +++++++ src/vtk_prompt/vtk_mcp_client.py | 30 +++++++++ src/vtk_prompt/vtk_prompt_ui.py | 54 +++++++++++++++- tests/test_cli.py | 2 +- tests/test_config_roundtrip.py | 2 +- tests/test_executor_guarded_main.py | 6 +- tests/test_prompt_assembly.py | 2 +- 18 files changed, 265 insertions(+), 30 deletions(-) create mode 100644 src/vtk_prompt/prompts/components/no_dsl_translation_tools.yml diff --git a/README.md b/README.md index 04aaabf..f397b40 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ vtk-prompt "Create a red sphere" -t $API_KEY # Advanced options vtk-prompt "Create a textured cone with 32 resolution" \ --provider anthropic \ - --model claude-opus-4-7 \ + --model claude-opus-5 \ --max-tokens 4000 \ --mcp-url http://localhost:8000 \ --verbose \ @@ -156,7 +156,7 @@ print(code) ```yaml # Model and parameter configuration -model: anthropic/claude-opus-4-1-20250805 +model: anthropic/claude-opus-5 modelParameters: temperature: 0.2 max_tokens: 6000 @@ -215,7 +215,7 @@ uv pip install -e ".[test]" && pytest | Provider | Default Model | Base URL | | ------------- | ------------------------------ | ----------------------------------- | -| **anthropic** | claude-sonnet-4-6 | https://api.anthropic.com/v1 | +| **anthropic** | claude-sonnet-5 | https://api.anthropic.com/v1 | | **openai** | gpt-4.1 | https://api.openai.com/v1 | | **gemini** | gemini-2.5-pro | https://generativelanguage.googleapis.com/v1beta | | **nim** | meta/llama-3.3-70b-instruct | https://integrate.api.nvidia.com/v1 | diff --git a/src/vtk_prompt/cli.py b/src/vtk_prompt/cli.py index 7895d13..9d8d759 100644 --- a/src/vtk_prompt/cli.py +++ b/src/vtk_prompt/cli.py @@ -5,7 +5,7 @@ It handles argument parsing, validation, and orchestrates the VTKPromptClient. Example: - >>> vtk-prompt "create sphere" --mcp-url http://localhost:8000 --model claude-sonnet-4-6 + >>> vtk-prompt "create sphere" --mcp-url http://localhost:8000 --model claude-sonnet-5 """ import sys @@ -54,6 +54,16 @@ "--prompt-file", help="Path to custom YAML prompt file (overrides built-in prompts and defaults)", ) +@click.option( + "--dsl-translation/--no-dsl-translation", + default=True, + help="Auto-translate natural language prompts to the VTK pipeline DSL via vtk-mcp", +) +@click.option( + "--debug", + is_flag=True, + help="Dump the full LLM conversation (context and vtk-mcp tool calls included) to stdout", +) def main( input_string: str, provider: str, @@ -68,6 +78,8 @@ def main( retry_attempts: int, conversation: str | None, prompt_file: str | None, + dsl_translation: bool, + debug: bool, ) -> None: """ Generate and execute VTK code using LLMs. @@ -153,6 +165,8 @@ def main( retry_attempts=retry_attempts, provider=provider, custom_prompt=custom_prompt_data, + dsl_translation=dsl_translation, + debug=debug, ) save_conversation(conversation, messages) diff --git a/src/vtk_prompt/client.py b/src/vtk_prompt/client.py index 562a37a..76af372 100644 --- a/src/vtk_prompt/client.py +++ b/src/vtk_prompt/client.py @@ -305,6 +305,55 @@ def query( log_tool_calls: bool = False, agentic_retrieval: bool = False, conversation: list[dict[str, str]] | None = None, + dsl_translation: bool = True, + debug: bool = False, + ) -> tuple[str, str, Any] | tuple[str, str, Any, list[str]] | str: + """Generate VTK code, then dump the full conversation to stdout if debug is set. + + See _generate() for parameter docs. debug dumps the complete message + history sent to/from the LLM, including injected context snippets and + vtk-mcp tool calls/results, since those all live in the conversation list. + """ + result = self._generate( + message, + api_key=api_key, + model=model, + base_url=base_url, + max_tokens=max_tokens, + temperature=temperature, + top_k=top_k, + retry_attempts=retry_attempts, + provider=provider, + custom_prompt=custom_prompt, + ui_mode=ui_mode, + execution_error=execution_error, + log_tool_calls=log_tool_calls, + agentic_retrieval=agentic_retrieval, + conversation=conversation, + dsl_translation=dsl_translation, + ) + if debug: + print(json.dumps(conversation, indent=2, default=str)) + return result + + def _generate( + self, + message: str = "", + api_key: str | None = None, + model: str = DEFAULT_MODEL, + base_url: str | None = None, + max_tokens: int = 1000, + temperature: float = 0.1, + top_k: int = 5, + retry_attempts: int = 1, + provider: str | None = None, + custom_prompt: dict | None = None, + ui_mode: bool = False, + execution_error: str | None = None, + log_tool_calls: bool = False, + agentic_retrieval: bool = False, + conversation: list[dict[str, str]] | None = None, + dsl_translation: bool = True, ) -> tuple[str, str, Any] | tuple[str, str, Any, list[str]] | str: """Generate VTK code using vtk-mcp tools when available. @@ -320,6 +369,8 @@ def query( provider: LLM provider to use (overrides instance provider if provided) custom_prompt: Custom YAML prompt data (overrides built-in prompts) ui_mode: Whether the request is coming from UI (affects prompt selection) + dsl_translation: Whether to auto-translate natural language prompts to + the VTK pipeline DSL via vtk-mcp before code generation """ if not api_key: api_key = os.environ.get("OPENAI_API_KEY") @@ -366,7 +417,16 @@ def query( } ) else: - # Normal path: build context and prompt + # Normal path: translate to DSL if needed, then build context and prompt + if mcp_client and dsl_translation: + is_dsl = mcp_client._call_tool("is_dsl_prompt", {"text": message}) + if is_dsl not in ("true", "True", True): + translated = mcp_client.translate_prompt(message) + if translated: + if self.verbose: + logger.debug("DSL translation:\n%s", translated) + message = translated + context_snippets = None # Agentic mode: skip pre-injected context so the model must use tools. if mcp_client and not agentic_retrieval: @@ -411,6 +471,7 @@ def query( context_snippets=context_snippets, mcp_active=bool(mcp_client), uploaded_files=uploaded_names(), + dsl_translation=dsl_translation, VTK_VERSION=VTK_VERSION, PYTHON_VERSION=PYTHON_VERSION, ) diff --git a/src/vtk_prompt/controllers/generation.py b/src/vtk_prompt/controllers/generation.py index 0b7e791..374aa62 100644 --- a/src/vtk_prompt/controllers/generation.py +++ b/src/vtk_prompt/controllers/generation.py @@ -228,6 +228,8 @@ async def generate_and_execute_code(app: Any, origin_session_id: str = "") -> No provider=app.state.provider, custom_prompt=app.custom_prompt_data, ui_mode=True, # This tells the client to use UI-specific components + dsl_translation=bool(app.state.dsl_translation), + debug=getattr(app, "debug", False), ) if conversation_token(app, origin_session_id) != token: # The originating conversation was reset or re-generated; drop this. @@ -311,6 +313,7 @@ async def generate_and_execute_code(app: Any, origin_session_id: str = "") -> No provider=app.state.provider, custom_prompt=app.custom_prompt_data, ui_mode=True, + debug=getattr(app, "debug", False), ) app.state.conversation = retry_messages if isinstance(retry_result, tuple) and len(retry_result) >= 2: @@ -492,7 +495,7 @@ def execute_with_renderer(app: Any, code_string: str) -> tuple[bool, str | None] exec_code = stage_code(code_string) success, error_message, error_line_text = execute_vtk_code( - exec_code, app.renderer, app.render_window + exec_code, app.renderer, app.render_window, app.render_window_interactor ) # The formatted run error goes to the console (below), not a floating alert. diff --git a/src/vtk_prompt/generate_files.py b/src/vtk_prompt/generate_files.py index c052bd3..a97a065 100644 --- a/src/vtk_prompt/generate_files.py +++ b/src/vtk_prompt/generate_files.py @@ -146,7 +146,7 @@ def main( # Set default models based on provider if model == DEFAULT_MODEL: default_models = { - "anthropic": "claude-opus-4-1", + "anthropic": "claude-opus-5", "gemini": "gemini-2.5-pro", "nim": "meta/llama3-70b-instruct", } diff --git a/src/vtk_prompt/prompts/components/model_defaults.yml b/src/vtk_prompt/prompts/components/model_defaults.yml index 8070cf1..14b392a 100644 --- a/src/vtk_prompt/prompts/components/model_defaults.yml +++ b/src/vtk_prompt/prompts/components/model_defaults.yml @@ -1,4 +1,4 @@ -model: anthropic/claude-sonnet-4-6 +model: anthropic/claude-sonnet-5 modelParameters: temperature: 0.5 max_tokens: 10000 diff --git a/src/vtk_prompt/prompts/components/no_dsl_translation_tools.yml b/src/vtk_prompt/prompts/components/no_dsl_translation_tools.yml new file mode 100644 index 0000000..b70ec7f --- /dev/null +++ b/src/vtk_prompt/prompts/components/no_dsl_translation_tools.yml @@ -0,0 +1,4 @@ +role: assistant +content: | + DSL translation is disabled for this request. Do not call the + translate_prompt_to_dsl or is_dsl_prompt tools. diff --git a/src/vtk_prompt/prompts/prompt_component_assembler.py b/src/vtk_prompt/prompts/prompt_component_assembler.py index a0223d9..6db7a5d 100644 --- a/src/vtk_prompt/prompts/prompt_component_assembler.py +++ b/src/vtk_prompt/prompts/prompt_component_assembler.py @@ -199,6 +199,7 @@ def assemble_vtk_prompt( context_snippets: str | None = None, mcp_active: bool = False, uploaded_files: list[str] | None = None, + dsl_translation: bool = True, **variables: Any, ) -> PromptData: """Assemble VTK prompt from file-based components. @@ -209,6 +210,8 @@ def assemble_vtk_prompt( context_snippets: Optional context snippets from vtk-mcp (enables rag_context component) mcp_active: Whether a vtk-mcp server is reachable (enables tool_use guidance) uploaded_files: Names of user-uploaded data files (enables user_data component) + dsl_translation: Whether DSL auto-translation is enabled (when False, instructs + the LLM not to call the DSL translation tools itself) **variables: Additional variables for substitution Returns: @@ -223,6 +226,7 @@ def assemble_vtk_prompt( # Conditional components (order matters for message composition) assembler.add_if(mcp_active, "tool_use") + assembler.add_if(mcp_active and not dsl_translation, "no_dsl_translation_tools") assembler.add_if(bool(context_snippets), "rag_context") assembler.add_if(ui_mode, "ui_renderer") assembler.add_if(bool(uploaded_files), "user_data") diff --git a/src/vtk_prompt/provider_utils.py b/src/vtk_prompt/provider_utils.py index def29bd..7e7c6c6 100644 --- a/src/vtk_prompt/provider_utils.py +++ b/src/vtk_prompt/provider_utils.py @@ -13,8 +13,8 @@ OPENAI_MODELS = ["gpt-4.1", "gpt-4.1-mini", "o4-mini", "o3"] ANTHROPIC_MODELS = [ - "claude-opus-4-7", - "claude-sonnet-4-6", + "claude-opus-5", + "claude-sonnet-5", "claude-haiku-4-5-20251001", ] @@ -29,10 +29,10 @@ # Models that don't support temperature control (must use temperature=1.0) -TEMPERATURE_UNSUPPORTED_MODELS = ["o4-mini", "o3"] +TEMPERATURE_UNSUPPORTED_MODELS = ["o4-mini", "o3", "claude-opus-5", "claude-sonnet-5"] DEFAULT_PROVIDER = "anthropic" -DEFAULT_MODEL = "claude-sonnet-4-6" +DEFAULT_MODEL = "claude-sonnet-5" def supports_temperature(model: str) -> bool: @@ -82,8 +82,8 @@ def get_default_model(provider: str) -> str: """Get the default/recommended model for a provider.""" defaults = { "openai": "gpt-4.1", - "anthropic": "claude-sonnet-4-6", + "anthropic": "claude-sonnet-5", "gemini": "gemini-2.5-pro", "nim": "meta/llama-3.3-70b-instruct", } - return defaults.get(provider, "claude-sonnet-4-6") + return defaults.get(provider, "claude-sonnet-5") diff --git a/src/vtk_prompt/rendering/code_executor.py b/src/vtk_prompt/rendering/code_executor.py index 3ac46c0..9856321 100644 --- a/src/vtk_prompt/rendering/code_executor.py +++ b/src/vtk_prompt/rendering/code_executor.py @@ -5,6 +5,8 @@ import traceback import vtk +import vtkmodules.all as vtkmodules_all +import vtkmodules.vtkRenderingCore as vtkmodules_rendering_core from .. import get_logger from ..utils.helpers import ensure_vtk_importable @@ -49,6 +51,24 @@ def _noop(*args: object, **kwargs: object): return _noop +class _InjectedRendererFactory: + """Stand-in for vtkRenderer construction in generated code. + + Scripts routinely build their own vtkRenderer under an arbitrary variable + name (often even "renderer", clobbering the injected global) instead of + reusing the one handed to them, even when told not to. Rather than + absorbing those calls like the window/interactor stand-ins, this returns + the app's real, shared renderer, so whatever a script names it, the + actors it adds land in the scene that actually renders. + """ + + def __init__(self, renderer: object) -> None: + self._renderer = renderer + + def __call__(self, *args: object, **kwargs: object) -> object: + return self._renderer + + class _NoOpInteractor: """Stand-in for vtkRenderWindowInteractor used while running generated code. @@ -69,7 +89,10 @@ def _noop(*args: object, **kwargs: object) -> None: def execute_vtk_code( - code_string: str, renderer: vtk.vtkRenderer, render_window: vtk.vtkRenderWindow + code_string: str, + renderer: vtk.vtkRenderer, + render_window: vtk.vtkRenderWindow, + render_window_interactor: vtk.vtkRenderWindowInteractor, ) -> tuple[bool, str | None, str | None]: """Execute VTK code with renderer context. @@ -89,25 +112,42 @@ def execute_vtk_code( # `if __name__ == "__main__":` actually run. Without it, a bare # __name__ resolves via builtins to "builtins", the guard is False, # and the script body (e.g. a main()) never executes -> blank view. - # - render_window is injected alongside renderer for code that uses it. + # - render_window and render_window_interactor are injected alongside + # renderer for code that uses them, mirroring the app's own shared + # objects rather than the no-op stand-ins the classes are patched to. # - A single namespace (globals only) is used so top-level defs and the # guard share one scope and functions can see the injected names. exec_globals = { "vtk": vtk, "renderer": renderer, "render_window": render_window, + "render_window_interactor": render_window_interactor, "__name__": "__main__", } # Keep generated code inside the app (a script that builds its own window # or interactor would otherwise pop up a native window and block on - # Start()), and capture stdout/stderr separately so the console can - # colour output by stream. Restore vtk afterwards. + # Start()), and make sure a self-constructed renderer is actually the + # app's renderer (see _InjectedRendererFactory). Capture stdout/stderr + # separately so the console can colour output by stream. Restore the + # real classes afterwards. + # + # vtk.vtkRenderWindow and vtkmodules.vtkRenderingCore.vtkRenderWindow are + # the same class object, but `from vtkmodules.all import *` (common in + # VTK example code the model is trained on) copies its own reference to + # that class at vtkmodules.all's own import time, so patching one module + # does not affect the others' bindings. All three must be patched. global _last_stdout, _last_stderr - real_window_cls = vtk.vtkRenderWindow - real_interactor_cls = vtk.vtkRenderWindowInteractor - vtk.vtkRenderWindow = _NoOpRenderWindow # type: ignore[assignment,misc] - vtk.vtkRenderWindowInteractor = _NoOpInteractor # type: ignore[assignment,misc] + patched_modules = (vtk, vtkmodules_rendering_core, vtkmodules_all) + originals = [ + (mod, mod.vtkRenderWindow, mod.vtkRenderWindowInteractor, mod.vtkRenderer) + for mod in patched_modules + ] + renderer_factory = _InjectedRendererFactory(renderer) + for mod in patched_modules: + mod.vtkRenderWindow = _NoOpRenderWindow # type: ignore[assignment,misc] + mod.vtkRenderWindowInteractor = _NoOpInteractor # type: ignore[assignment,misc] + mod.vtkRenderer = renderer_factory # type: ignore[assignment,misc] out_buf, err_buf = io.StringIO(), io.StringIO() try: with contextlib.redirect_stdout(out_buf), contextlib.redirect_stderr( @@ -122,8 +162,10 @@ def execute_vtk_code( except Exception as render_error: logger.warning("Render error: %s", render_error) finally: - vtk.vtkRenderWindow = real_window_cls # type: ignore[assignment,misc] - vtk.vtkRenderWindowInteractor = real_interactor_cls # noqa: E501 # type: ignore[assignment,misc] + for mod, real_window_cls, real_interactor_cls, real_renderer_cls in originals: + mod.vtkRenderWindow = real_window_cls # type: ignore[assignment,misc] + mod.vtkRenderWindowInteractor = real_interactor_cls # type: ignore[assignment,misc] + mod.vtkRenderer = real_renderer_cls # type: ignore[assignment,misc] _last_stdout, _last_stderr = out_buf.getvalue(), err_buf.getvalue() return True, None, None diff --git a/src/vtk_prompt/state/initializer.py b/src/vtk_prompt/state/initializer.py index 56bc35a..fb487ae 100644 --- a/src/vtk_prompt/state/initializer.py +++ b/src/vtk_prompt/state/initializer.py @@ -53,6 +53,8 @@ def initialize_state(app: Any) -> None: app.state.mcp_url = "" app.state.log_tool_calls = False # log vtk-mcp tool calls to the console app.state.agentic_retrieval = False # skip pre-injected context; use tools + app.state.mcp_status = "idle" # "idle" | "checking" | "ok" | "error" + app.state.dsl_translation = True app.state.error_message = "" app.state.console_log = [] # per-run captured output groups app.state.info_tab = "conversation" # Conversation | Console tab in the info pane diff --git a/src/vtk_prompt/ui/layout/settings_dialog.py b/src/vtk_prompt/ui/layout/settings_dialog.py index f7b1c88..19d20fc 100644 --- a/src/vtk_prompt/ui/layout/settings_dialog.py +++ b/src/vtk_prompt/ui/layout/settings_dialog.py @@ -174,6 +174,15 @@ def _advanced_tab() -> None: clearable=True, density="compact", variant="outlined", + append_inner_icon=( + "mcp_status === 'ok' ? 'mdi-check-circle' : " + "mcp_status === 'error' ? 'mdi-alert-circle' : " + "mcp_status === 'checking' ? 'mdi-loading mdi-spin' : ''", + ), + color=( + "mcp_status === 'ok' ? 'success' : " + "mcp_status === 'error' ? 'error' : undefined", + ), hint="Leave blank for baseline generation without tools", persistent_hint=True, classes="mb-3", @@ -189,6 +198,18 @@ def _advanced_tab() -> None: disabled=("!mcp_url",), hint="Context snippets retrieved per request", persistent_hint=True, + classes="mb-3", + ) + vuetify.VCheckbox( + label="Auto-translate to DSL", + v_model=("dsl_translation", True), + density="compact", + color="primary", + disabled=("!mcp_url",), + hide_details="auto", + hint="Convert natural language prompts to the VTK " + "pipeline DSL before code generation", + persistent_hint=True, ) vuetify.VCheckbox( label="Log tool calls to the server console", diff --git a/src/vtk_prompt/vtk_mcp_client.py b/src/vtk_prompt/vtk_mcp_client.py index e31cf63..a80f2f4 100644 --- a/src/vtk_prompt/vtk_mcp_client.py +++ b/src/vtk_prompt/vtk_mcp_client.py @@ -180,6 +180,36 @@ def validate_code(self, code: str) -> str | None: except Exception: return None + def translate_prompt( + self, + query: str, + model: str | None = None, + base_url: str | None = None, + api_key: str | None = None, + ) -> str | None: + """Translate a natural language query into the VTK pipeline DSL. + + Args: + query: Natural language prompt. + model: LiteLLM model override (e.g. ``ollama/llama3``). + base_url: Base URL for OpenAI-compatible endpoints (e.g. Ollama). + api_key: API key for the endpoint. + + Returns the DSL string, or None if the tool call fails. + """ + args: dict = {"query": query} + if model: + args["model"] = model + if base_url: + args["base_url"] = base_url + if api_key: + args["api_key"] = api_key + result = self._call_tool("translate_prompt_to_dsl", args) + if not result or result.startswith("Error:"): + logger.warning("DSL translation failed: %s", result) + return None + return result + def get_enriched_context(self, query: str, top_k: int = 5) -> str: """Build context for the LLM combining code examples, docs, and VTK class hints.""" parts = [] diff --git a/src/vtk_prompt/vtk_prompt_ui.py b/src/vtk_prompt/vtk_prompt_ui.py index 5dd738f..f9d32f7 100644 --- a/src/vtk_prompt/vtk_prompt_ui.py +++ b/src/vtk_prompt/vtk_prompt_ui.py @@ -77,12 +77,20 @@ class VTKPromptApp(TrameApp): """VTK Prompt interactive application with 3D visualization and AI chat interface.""" - def __init__(self, server: Any | None = None, custom_prompt_file: str | None = None) -> None: + def __init__( + self, + server: Any | None = None, + custom_prompt_file: str | None = None, + debug: bool = False, + ) -> None: """Initialize VTK Prompt application. Args: server: Trame server instance custom_prompt_file: Path to custom YAML prompt file + debug: Dump the full LLM conversation (context and vtk-mcp tool + calls included) to stdout on every generation. Reuses wslink's + existing --debug flag rather than defining a new one. """ super().__init__(server=server, client_type="vue3") self.state.trame__title = "VTK Prompt" @@ -90,6 +98,7 @@ def __init__(self, server: Any | None = None, custom_prompt_file: str | None = N # Store custom prompt file path and data self.custom_prompt_file = custom_prompt_file self.custom_prompt_data = None + self.debug = debug # Add CLI argument for custom prompt file self.server.cli.add_argument( @@ -108,6 +117,7 @@ def __init__(self, server: Any | None = None, custom_prompt_file: str | None = N self.renderer, self.render_window, self.render_window_interactor = setup_vtk_renderer() self._conversation_loading = False self._snapshot_task: asyncio.Task | None = None + self._mcp_check_task: asyncio.Task | None = None add_default_scene(self.renderer) # Expose the live renderer/render_window to editor completion + hover, so @@ -452,6 +462,42 @@ async def _debounced_code_snapshot(self) -> None: with self.state: generation.push_code_snapshot(self, self.state.generated_code, label="Manual edit") + @change("mcp_url") + def _on_mcp_url_change(self, **_: Any) -> None: + """Debounce-check vtk-mcp server reachability after a typing pause.""" + if self._mcp_check_task is not None and not self._mcp_check_task.done(): + self._mcp_check_task.cancel() + try: + self._mcp_check_task = asyncio.ensure_future(self._debounced_mcp_check()) + except RuntimeError: + # No running event loop yet (e.g. during construction); nothing to do. + self._mcp_check_task = None + + async def _debounced_mcp_check(self) -> None: + """Probe the vtk-mcp server URL and reflect reachability in mcp_status.""" + try: + await asyncio.sleep(0.5) + except asyncio.CancelledError: + return + + url = (self.state.mcp_url or "").strip() + if not url: + self.state.mcp_status = "idle" + self.state.flush() + return + + self.state.mcp_status = "checking" + self.state.flush() + + from .vtk_mcp_client import check_mcp_available + + reachable = await asyncio.to_thread(check_mcp_available, url) + # The field may have changed again while the check was in flight. + if (self.state.mcp_url or "").strip() != url: + return + self.state.mcp_status = "ok" if reachable else "error" + self.state.flush() + def _build_ui(self) -> None: """Build a simplified Vuetify UI.""" # Show the Recents drawer by default; user can toggle it closed. @@ -510,8 +556,12 @@ def main() -> None: if custom_prompt_file: print(f"Using config: {custom_prompt_file}") + # wslink already defines --debug (its own debug logging); reuse it here to + # also dump the LLM conversation instead of registering a conflicting flag. + debug = "--debug" in sys.argv + # Create and start the app - app = VTKPromptApp(custom_prompt_file=custom_prompt_file) + app = VTKPromptApp(custom_prompt_file=custom_prompt_file, debug=debug) app.start() diff --git a/tests/test_cli.py b/tests/test_cli.py index ce8cc77..bbabdb4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -99,7 +99,7 @@ def test_max_tokens_error(self): "provider,expected_model", [ ("openai", "gpt-4.1"), - ("anthropic", "claude-sonnet-4-6"), + ("anthropic", "claude-sonnet-5"), ("gemini", "gemini-2.5-pro"), ("nim", "meta/llama-3.3-70b-instruct"), ], diff --git a/tests/test_config_roundtrip.py b/tests/test_config_roundtrip.py index 06547a0..f6787f9 100644 --- a/tests/test_config_roundtrip.py +++ b/tests/test_config_roundtrip.py @@ -37,7 +37,7 @@ def test_loader_restores_local_ollama_and_mcp(): def test_loader_sets_mcp_url_for_cloud_model(): - cfg = {"model": "anthropic/claude-sonnet-4-6", "mcp_url": "http://localhost:8000"} + cfg = {"model": "anthropic/claude-sonnet-5", "mcp_url": "http://localhost:8000"} app = _fake_app(cfg) prompt_loader._process_model_configuration(app) prompt_loader._process_rag_and_generation_settings(app) diff --git a/tests/test_executor_guarded_main.py b/tests/test_executor_guarded_main.py index 517198f..1d27248 100644 --- a/tests/test_executor_guarded_main.py +++ b/tests/test_executor_guarded_main.py @@ -43,9 +43,13 @@ def test_guarded_main_script_adds_actor_to_renderer(): render_window = vtk.vtkRenderWindow() render_window.SetOffScreenRendering(1) render_window.AddRenderer(renderer) + render_window_interactor = vtk.vtkRenderWindowInteractor() + render_window_interactor.SetRenderWindow(render_window) except Exception: # pragma: no cover - no VTK rendering backend available pytest.skip("VTK render window unavailable in this environment") - ok, err, _ = execute_vtk_code(GUARDED_SPHERE, renderer, render_window) + ok, err, _ = execute_vtk_code( + GUARDED_SPHERE, renderer, render_window, render_window_interactor + ) assert ok, err assert renderer.GetActors().GetNumberOfItems() == 1 diff --git a/tests/test_prompt_assembly.py b/tests/test_prompt_assembly.py index d0f82ff..0194e04 100644 --- a/tests/test_prompt_assembly.py +++ b/tests/test_prompt_assembly.py @@ -17,7 +17,7 @@ def _assert_basic_structure(result): assert isinstance(result["messages"], list) assert len(result["messages"]) >= 3 assert all("role" in msg and "content" in msg for msg in result["messages"]) - assert result.get("model") == "anthropic/claude-sonnet-4-6" + assert result.get("model") == "anthropic/claude-sonnet-5" def _get_content(result):