Skip to content

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

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

feat: Add opt-in AI Chat Assistant dock panel (OpenAI-compatible API)#580
Vickyrrrrrr wants to merge 9 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 : )

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