Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
16 changes: 15 additions & 1 deletion src/vtk_prompt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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)

Expand Down
63 changes: 62 additions & 1 deletion src/vtk_prompt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
)
Expand Down
5 changes: 4 additions & 1 deletion src/vtk_prompt/controllers/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/vtk_prompt/generate_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Expand Down
2 changes: 1 addition & 1 deletion src/vtk_prompt/prompts/components/model_defaults.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
model: anthropic/claude-sonnet-4-6
model: anthropic/claude-sonnet-5
modelParameters:
temperature: 0.5
max_tokens: 10000
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions src/vtk_prompt/prompts/prompt_component_assembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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")
Expand Down
12 changes: 6 additions & 6 deletions src/vtk_prompt/provider_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand All @@ -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:
Expand Down Expand Up @@ -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")
62 changes: 52 additions & 10 deletions src/vtk_prompt/rendering/code_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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.

Expand All @@ -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(
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/vtk_prompt/state/initializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading