Skip to content

feat: Add opt-in AI Chat Assistant dock panel (OpenAI-compatible API)#580

Open
Vickyrrrrrr wants to merge 13 commits into
CadQuery:masterfrom
Vickyrrrrrr:master
Open

feat: Add opt-in AI Chat Assistant dock panel (OpenAI-compatible API)#580
Vickyrrrrrr wants to merge 13 commits into
CadQuery:masterfrom
Vickyrrrrrr:master

Conversation

@Vickyrrrrrr
Copy link
Copy Markdown

What this PR does

Adds an AI Chat Assistant as an optional, dockable panel inside CQ-editor.
Users can describe what they want to model in plain English, receive valid
CadQuery code from any OpenAI-compatible LLM, and insert it directly into
the editor — all without leaving the app.

Motivation

CadQuery is powerful but has a steep learning curve for new users. An
in-editor AI assistant lowers that barrier significantly: instead of
looking up API docs, users can describe intent ("make a box with filleted
edges") and get runnable code instantly. Power users benefit too — they
can ask the AI to iterate on an existing model using the current script
as context.

Changes

cq_editor/widgets/ai_chat.py (new file)

  • AIChatWidget — dockable QWidget, integrates like any existing panel
  • Chat history with colour-coded roles (You / AI / Error)
  • Context injection: current editor script is prepended to every prompt
    so the LLM edits the existing model rather than starting from scratch
  • Async LLM calls via QThread — UI never freezes
  • Insert & Run button: extracts code from reply, pushes to editor,
    optionally triggers an immediate re-render
  • preferences Parameter tree: Enabled, Provider URL, Model, API Key,
    Auto-run — appears under Edit → Preferences → AI Assistant

cq_editor/main_window.py (minimal, additive changes only)

  • Register AIChatWidget as a dock panel (right side, default)
  • Wire insert_code signal → editor.set_text
  • Add AI Assistant toggle under Tools menu

How to use

  1. pip install openai
  2. Edit → Preferences → AI Assistant → enter API Key + Model
  3. Tools → AI Assistant
  4. Type a prompt, click Insert & Run

Works with OpenAI, Anthropic (via OpenRouter), local Ollama, or any
OpenAI-compatible endpoint via the Provider URL field.

Compatibility

  • openai is optional — guarded by try/except ImportError; CQ-editor
    starts normally without it
  • Zero changes to existing behaviour or existing widgets
  • All new logic is self-contained in ai_chat.py

Testing

  • CQ-editor launches normally with and without openai installed
  • Panel docks, undocks, and toggles via View menu
  • Preferences save and restore across sessions
  • Context injection sends current script to LLM
  • Insert & Run loads code into editor and triggers re-render
  • Tested with OpenAI (gpt-4o, o3) and OpenRouter

Future work (follow-up PRs)

  • Streaming token-by-token response display
  • Diff-based code patching instead of full replace
  • Auto-fix: feed traceback back to LLM on render error

…port

- Add cq_editor/widgets/ai_chat.py: new QDockWidget with chat UI,
  async LLM calls via QThread, context injection (sends current editor
  code to LLM), and one-click code insertion + auto-run
- Edit cq_editor/main_window.py: register AIChatWidget as a dockable
  panel, wire insert_code signal to editor, add View menu toggle
- Edit cq_editor/preferences.py: add AI Settings preference group
  (provider, model, API key, base URL, enabled toggle)

All AI features are optional and guarded; existing behavior unchanged.
feat: Add AI Chat Assistant dock panel (OpenAI-compatible, opt-in)
docs: update README with AI Chat Assistant feature and usage guide
@jmwright
Copy link
Copy Markdown
Member

Claude Code Review


Correctness / Bugs

  1. Enabled preference does nothing. preferences has an "Enabled": bool child, but nowhere in the code is it ever read. The panel is unconditionally registered and instantiated regardless of that setting. Either wire it up or
    remove it — right now it's misleading UI.

  2. _toggle_ai_panel calls raise_() after hide(). Calling raise_() on a widget that was just hidden makes it visible again. The hide branch should not call raise_().

  3. Bare code fallback in _on_response is dangerous. When no fenced code block is found, the entire raw reply is treated as code and loaded into the editor on "Insert & Run". An explanatory error message from the LLM gets inserted
    verbatim into the script.

  4. import re inside _extract_code. This import belongs at the top of the module.

  5. QAction keyword arguments. The QAction("🤖 AI Assistant", self, triggered=..., toolTip=...) form may not work in all PyQt5 versions. Connect the signal and set the tooltip explicitly after construction.


Safety / Privacy

  1. API key stored in plaintext preferences. There is no use of keyring or any secure storage. The tooltip reassures users the key is "not sent anywhere else," but it will be written to a plaintext config file on disk. At minimum
    this should be documented prominently; ideally use the system keychain.

  2. User code is silently sent to an external API. Every prompt includes the full current script via _inject_context. There is no confirmation dialog, no opt-in warning on first use, and no indication in the UI that code is leaving
    the machine. This is a meaningful privacy/data concern for commercial or proprietary models.


Resource / Thread Safety

  1. No closeEvent cleanup. If the widget is destroyed while _thread is running (e.g., the user closes CQ-editor mid-request), the thread and worker are not stopped. The worker's signals will fire into a deleted widget. Add a
    closeEvent (or __del__) that calls _thread.quit() / _thread.wait().

  2. Context grows unbounded. _history accumulates the full conversation, and _inject_context prepends the entire current script on every user turn. Long sessions with large scripts will send very large payloads and incur
    unnecessary token cost.


Architecture

  1. from .widgets.ai_chat import AIChatWidget is unconditional. The PR description says the feature is "guarded by try/except ImportError," but the module is always imported by main_window.py. The openai package import is lazy
    (inside _LLMWorker.run), which is correct — but the whole widget class is always loaded. The description is misleading.

  2. preferences is a class-level variable. This works fine for a singleton, but it's an unusual pattern that will silently misbehave if two instances are ever created (shared state). The registration pattern used by other widgets
    should be followed.

  3. Direct _editor / _debugger attribute assignment post-construction. Passing editor=None, debugger=None then immediately setting them via attribute access in prepare_actions is fragile. Pass them properly through the
    constructor or a dedicated set_dependencies() method.


Missing

  • No tests of any kind.
  • No entry in requirements.txt or setup.cfg for the optional openai dependency (even as an extras group).

@Vickyrrrrrr
Copy link
Copy Markdown
Author

Thanks , agreed on the main points. I’ll fix the enabled toggle, remove the unsafe raw-reply fallback, move the regex import, make QAction creation explicit, and clean up dock visibility handling.
I also agree the privacy story is too weak: I’ll add a clear disclosure that editor code is sent to the configured API, and I’ll switch API key storage to system keyring if possible, or at minimum document the plaintext behavior prominently.
I’ll also add thread cleanup in closeEvent, bound the history/context size, and include tests for the AI widget behavior.

Vickyrrrrrr and others added 5 commits May 18, 2026 18:58
Correctness / Bugs:
- Wire Enabled preference to actually show/hide the dock panel via
  preferences.sigTreeStateChanged; panel starts hidden when Enabled=False
- Fix _toggle_ai_panel: remove raise_() from the hide branch (it
  re-showed the widget immediately after hiding it)
- Remove dangerous bare-code fallback in _on_response: if no fenced
  code block is found, show an informational message and disable Insert
  button instead of pasting raw LLM text into the editor
- Move `import re` to module level (was inside _extract_code staticmethod)
- Fix QAction construction: set toolTip via explicit .setToolTip() call
  after construction for full PyQt5 compatibility

Safety / Privacy:
- Replace plaintext API key in preferences with system keyring storage
  (keyring package, falls back to plaintext with a warning if unavailable)
- Add first-use privacy consent dialog: warns user that their script is
  sent to the configured API endpoint; stores consent in preferences so
  dialog only shows once
- Update API Key tooltip to accurately state storage mechanism

Resource / Thread Safety:
- Add closeEvent to AIChatWidget that calls _thread.quit()/_thread.wait()
  so in-flight requests are cleanly stopped when the app closes
- Bound conversation history: keep at most MAX_HISTORY_TURNS (10) turns;
  older messages are pruned (system prompt always retained) to prevent
  unbounded token growth

Architecture:
- Pass editor and debugger properly via set_dependencies() method instead
  of post-construction attribute assignment; main_window.py updated to call
  set_dependencies() in prepare_actions()
- Move preferences from class-level to instance-level Parameter to avoid
  shared-state bugs if multiple instances are ever created
- Make AIChatWidget import conditional in main_window.py with try/except
  so the claim of optional dependency is accurate end-to-end
…, and add model-specific configuration support.
@Vickyrrrrrr
Copy link
Copy Markdown
Author

Vickyrrrrrr commented May 18, 2026

Screenshot 2026-05-19 002716 Screenshot 2026-05-19 002705

do give a try sir use good models . just imagine : )

Comment thread cq_editor/widgets/ai_chat.py Outdated

resp = client.chat.completions.create(**kwargs)
self.finished.emit(resp.choices[0].message.content or "")
except Exception as exc: # noqa: BLE001
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this linter ignore needed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the linter ignore was not needed here. I changed the handler to catch openai.OpenAIError specifically and removed the noqa.

@jmwright
Copy link
Copy Markdown
Member

jmwright commented May 27, 2026

@Vickyrrrrrr The lint check is failing (tests are still running as I post this). I haven't tried forcing an LLM to be compliant with a specific linter yet, so I am interested how that will work for you.

I created an LLM skill to help overcome common errors LLMs make when trying to write idiomatic CadQuery code. The repo has a Modelfile with a streamlined system prompt that may be useful here.

A higher-level design decision that Claude Code flagged to be aware of:

The auto-fix path sets _auto_insert_flag = True, which means when triggered from the traceback pane, AI-generated code is automatically inserted and run without a confirmation step. That's the intended feature, but it's a prompt-injection surface — if someone could manipulate what goes into the error message or control the API endpoint's responses, they could cause arbitrary code to execute. Not a backdoor, just a design tradeoff you should be aware of.

I will take a closer look at this and try the feature out once CI is passing.

Thanks

@Vickyrrrrrr
Copy link
Copy Markdown
Author

@Vickyrrrrrr The lint check is failing (tests are still running as I post this). I haven't tried forcing an LLM to be compliant with a specific linter yet, so I am interested how that will work for you.

I created an LLM skill to help overcome common errors LLMs make when trying to write idiomatic CadQuery code. The repo has a Modelfile with a streamlined system prompt that may be useful here.

A higher-level design decision that Claude Code flagged to be aware of:

The auto-fix path sets _auto_insert_flag = True, which means when triggered from the traceback pane, AI-generated code is automatically inserted and run without a confirmation step. That's the intended feature, but it's a prompt-injection surface — if someone could manipulate what goes into the error message or control the API endpoint's responses, they could cause arbitrary code to execute. Not a backdoor, just a design tradeoff you should be aware of.

I will take a closer look at this and try the feature out once CI is passing.

Thanks

Damn , this will be very helpfulll . If the current pr merged i will try to implement more features . Me and Codex both won't trouble you much sir . : )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants