diff --git a/src/vtk_prompt/client.py b/src/vtk_prompt/client.py
index 3177d6c..e395801 100644
--- a/src/vtk_prompt/client.py
+++ b/src/vtk_prompt/client.py
@@ -33,6 +33,40 @@
logger = get_logger(__name__)
+def _parse_text_tool_calls(content: str | None, tool_names: set[str]) -> list[dict] | None:
+ """Extract tool calls a backend emitted as text instead of structured tool_calls.
+
+ Some local OpenAI-compatible backends (e.g. Ollama with a quantized model) return
+ a tool call as plain content rather than populating ``tool_calls``. Handle both a
+ ``{...}`` block and a bare JSON object with ``name`` and
+ ``arguments``. Only objects whose name matches a known tool are treated as calls,
+ so normal tagged answers are never misread. Returns a list of {name, arguments}.
+ """
+ if not content:
+ return None
+ candidates = re.findall(r"\s*(\{.*?\})\s*", content, re.DOTALL)
+ if not candidates:
+ stripped = content.strip()
+ if stripped.startswith("{") and stripped.endswith("}"):
+ candidates = [stripped]
+ calls: list[dict] = []
+ for raw in candidates:
+ try:
+ obj = json.loads(raw)
+ except (ValueError, TypeError):
+ continue
+ name = obj.get("name")
+ args = obj.get("arguments", {})
+ if isinstance(args, str):
+ try:
+ args = json.loads(args)
+ except (ValueError, TypeError):
+ args = {}
+ if name in tool_names and isinstance(args, dict):
+ calls.append({"name": name, "arguments": args})
+ return calls or None
+
+
@dataclass
class VTKPromptClient:
"""OpenAI client for VTK code generation."""
@@ -296,6 +330,8 @@ def query(
custom_prompt: dict | None = None,
ui_mode: bool = False,
execution_error: str | None = None,
+ log_tool_calls: bool = False,
+ agentic_retrieval: bool = False,
) -> tuple[str, str, Any] | tuple[str, str, Any, list[str]] | str:
"""Generate VTK code using vtk-mcp tools when available.
@@ -359,7 +395,8 @@ def query(
else:
# Normal path: build context and prompt
context_snippets = None
- if mcp_client:
+ # Agentic mode: skip pre-injected context so the model must use tools.
+ if mcp_client and not agentic_retrieval:
mcp_context = mcp_client.get_enriched_context(message, top_k=top_k)
if mcp_context:
context_snippets = mcp_context
@@ -417,6 +454,11 @@ def query(
# Fetch vtk-mcp tools for LLM tool calling
tools = mcp_client.list_tools() if mcp_client else []
+ tool_names: set[str] = {
+ str(name)
+ for t in tools
+ if (name := (t.get("function") or {}).get("name")) is not None
+ }
# Retry loop for AST validation
for attempt in range(retry_attempts):
@@ -468,18 +510,64 @@ def query(
except Exception:
args = {}
result = mcp_client.call_tool(tc.function.name, args) # type: ignore
- logger.debug("Tool %s -> %s...", tc.function.name, result[:80])
+ if log_tool_calls:
+ logger.info(
+ "vtk-mcp: %s(%s) -> %s",
+ tc.function.name,
+ tc.function.arguments,
+ result[:120],
+ )
+ else:
+ logger.debug("Tool %s -> %s...", tc.function.name, result[:80])
self.conversation.append(
{"role": "tool", "tool_call_id": tc.id, "content": result}
)
continue # let LLM decide what to do next
+ # Fallback: the backend returned a tool call as plain text rather than
+ # in tool_calls (common with quantized local models). Run it anyway.
+ text_calls = _parse_text_tool_calls(choice.message.content, tool_names)
+ if tools and text_calls:
+ text_msg: dict = {"role": "assistant", "content": ""}
+ text_msg["tool_calls"] = [
+ {
+ "id": f"call_{i}",
+ "type": "function",
+ "function": {
+ "name": c["name"],
+ "arguments": json.dumps(c["arguments"]),
+ },
+ }
+ for i, c in enumerate(text_calls)
+ ]
+ self.conversation.append(text_msg)
+ for i, c in enumerate(text_calls):
+ result = mcp_client.call_tool(c["name"], c["arguments"]) # type: ignore
+ if log_tool_calls:
+ logger.info(
+ "vtk-mcp (text): %s(%s) -> %s",
+ c["name"],
+ json.dumps(c["arguments"]),
+ result[:120],
+ )
+ else:
+ logger.debug("Tool %s (text) -> %s...", c["name"], result[:80])
+ self.conversation.append(
+ {"role": "tool", "tool_call_id": f"call_{i}", "content": result}
+ )
+ continue
+
# LLM generated a response (not a tool call)
content = choice.message.content or "No content in response"
break
if content is None:
# Tool loop exhausted without a text response
+ if log_tool_calls:
+ logger.info(
+ "vtk-mcp: tool loop hit the %d-round cap without a final response",
+ MAX_TOOL_ROUNDS,
+ )
if attempt == retry_attempts - 1:
return ("No response generated", "", getattr(response, "usage", None) or {})
continue
diff --git a/src/vtk_prompt/controllers/configuration.py b/src/vtk_prompt/controllers/configuration.py
index 601841b..a4e4433 100644
--- a/src/vtk_prompt/controllers/configuration.py
+++ b/src/vtk_prompt/controllers/configuration.py
@@ -46,6 +46,8 @@ def save_config(app: Any) -> str:
mcp_url = getattr(app.state, "mcp_url", "").strip()
data_root = getattr(app.state, "data_root", "").strip()
top_k = int(getattr(app.state, "top_k", 5))
+ log_tool_calls = bool(getattr(app.state, "log_tool_calls", False))
+ agentic_retrieval = bool(getattr(app.state, "agentic_retrieval", False))
base_url = getattr(app.state, "local_base_url", "").strip() if not use_cloud else ""
content = {
@@ -56,6 +58,8 @@ def save_config(app: Any) -> str:
"mcp_url": mcp_url,
"data_root": data_root,
"top_k": top_k,
+ "log_tool_calls": log_tool_calls,
+ "agentic_retrieval": agentic_retrieval,
"retries": retries,
"modelParameters": {
"temperature": temperature,
diff --git a/src/vtk_prompt/controllers/generation.py b/src/vtk_prompt/controllers/generation.py
index 576f0ed..72e728f 100644
--- a/src/vtk_prompt/controllers/generation.py
+++ b/src/vtk_prompt/controllers/generation.py
@@ -103,6 +103,8 @@ async def generate_and_execute_code(app: Any) -> None:
temperature=float(app.state.temperature),
top_k=int(app.state.top_k),
retry_attempts=int(app.state.retry_attempts),
+ log_tool_calls=bool(app.state.log_tool_calls),
+ agentic_retrieval=bool(app.state.agentic_retrieval),
provider=app.state.provider,
custom_prompt=app.custom_prompt_data,
ui_mode=True, # This tells the client to use UI-specific components
@@ -176,6 +178,8 @@ async def generate_and_execute_code(app: Any) -> None:
temperature=float(app.state.temperature),
top_k=int(app.state.top_k),
retry_attempts=1,
+ log_tool_calls=bool(app.state.log_tool_calls),
+ agentic_retrieval=bool(app.state.agentic_retrieval),
provider=app.state.provider,
custom_prompt=app.custom_prompt_data,
ui_mode=True,
diff --git a/src/vtk_prompt/state/initializer.py b/src/vtk_prompt/state/initializer.py
index 74ae8b7..be26236 100644
--- a/src/vtk_prompt/state/initializer.py
+++ b/src/vtk_prompt/state/initializer.py
@@ -51,6 +51,8 @@ def initialize_state(app: Any) -> None:
app.state.code_history_pos = -1
app.state.is_loading = False
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.error_message = ""
app.state.input_tokens = 0
app.state.output_tokens = 0
diff --git a/src/vtk_prompt/ui/layout/settings_dialog.py b/src/vtk_prompt/ui/layout/settings_dialog.py
index 48cbdf0..f7b1c88 100644
--- a/src/vtk_prompt/ui/layout/settings_dialog.py
+++ b/src/vtk_prompt/ui/layout/settings_dialog.py
@@ -190,6 +190,23 @@ def _advanced_tab() -> None:
hint="Context snippets retrieved per request",
persistent_hint=True,
)
+ vuetify.VCheckbox(
+ label="Log tool calls to the server console",
+ v_model=("log_tool_calls", False),
+ density="compact",
+ color="primary",
+ disabled=("!mcp_url",),
+ hide_details=True,
+ classes="mt-2",
+ )
+ vuetify.VCheckbox(
+ label="Agentic retrieval (use tools instead of pre-injected context)",
+ v_model=("agentic_retrieval", False),
+ density="compact",
+ color="primary",
+ disabled=("!mcp_url",),
+ hide_details=True,
+ )
vuetify.VDivider(classes="my-5")
_section("Generation")
diff --git a/src/vtk_prompt/utils/prompt_loader.py b/src/vtk_prompt/utils/prompt_loader.py
index 668d89a..8ab2480 100644
--- a/src/vtk_prompt/utils/prompt_loader.py
+++ b/src/vtk_prompt/utils/prompt_loader.py
@@ -120,6 +120,14 @@ def _process_rag_and_generation_settings(app: Any) -> None:
_mcp = app.custom_prompt_data.get("mcp_url")
if isinstance(_mcp, str):
app.state.mcp_url = _mcp.strip()
+ if "log_tool_calls" in app.custom_prompt_data:
+ _ltc = app.custom_prompt_data.get("log_tool_calls")
+ if isinstance(_ltc, bool):
+ app.state.log_tool_calls = _ltc
+ if "agentic_retrieval" in app.custom_prompt_data:
+ _ag = app.custom_prompt_data.get("agentic_retrieval")
+ if isinstance(_ag, bool):
+ app.state.agentic_retrieval = _ag
if "base_url" in app.custom_prompt_data:
_base = app.custom_prompt_data.get("base_url")
if isinstance(_base, str) and _base.strip():