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
2 changes: 1 addition & 1 deletion claude/.claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "pywry",
"source": "./plugins/pywry/",
"description": "Native Claude Code integration for PyWry — MCP tools for generating and rendering HTML components, chat artifacts, and building native, web, or Jupyter applications with live preview. Built-in support for AgGrid, Plotly, TradingView, and more.",
"version": "0.1.0",
"version": "0.1.1",
"author": {
"name": "PyWry",
"email": "pywry2@gmail.com",
Expand Down
2 changes: 1 addition & 1 deletion claude/desktop-extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": "0.4",
"name": "pywry",
"display_name": "PyWry",
"version": "0.1.0",
"version": "0.1.1",
"description": "Native Claude Desktop integration for PyWry — MCP tools for generating and rendering HTML components, chat artifacts, and building native, web, or Jupyter applications with live preview. Built-in support for AgGrid, Plotly, TradingView, and more.",
"long_description": "PyWry is a cross-platform rendering engine and desktop UI toolkit for Python. This Desktop Extension bundles the PyWry MCP server (66+ tools) so Claude Desktop can scaffold widgets, render Plotly charts, drive AG Grid tables, build TradingView lightweight charts, and produce streaming chat artifacts — all delivered as embedded HTML resources viewable directly in the artifact pane. The `uv` runtime resolves `pywry[mcp]` from PyPI on first launch; no manual `pip install` is required.",
"author": {
Expand Down
2 changes: 1 addition & 1 deletion claude/plugins/pywry/.claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "pywry",
"source": "./",
"description": "Native Claude Code integration for PyWry — MCP tools for generating and rendering HTML components, chat artifacts, and building native, web, or Jupyter applications with live preview. Built-in support for AgGrid, Plotly, TradingView, and more.",
"version": "0.1.0",
"version": "0.1.1",
"author": {
"name": "PyWry",
"email": "pywry2@gmail.com",
Expand Down
2 changes: 1 addition & 1 deletion claude/plugins/pywry/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pywry",
"version": "0.1.0",
"version": "0.1.1",
"description": "Native Claude Code integration for PyWry — MCP tools for generating and rendering HTML components, chat artifacts, and building native, web, or Jupyter applications with live preview. Built-in support for AgGrid, Plotly, TradingView, and more.",
"author": {
"name": "PyWry",
Expand Down
19 changes: 19 additions & 0 deletions claude/plugins/pywry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@
Versioning follows [semver](https://semver.org/). Format is a feature
list per release — not a delta.

## 0.1.1 — chat-handler regression fix

**Chat widget reliability.** Fixed a regression where the desktop chat
panel rendered empty (no welcome message, dead buttons) after a
`set_content` IPC swap. Root cause: `chat-handlers.js` contained a
literal `'</head>'` string inside its iframe meta-tag injection
helper. When inlined into the host document's `<head>`, the desktop
subprocess's regex-based head-script extractor terminated early on
the stray tag, dropping the chat-handlers `<script>` and leaving
`window.initChatHandlers` undefined at re-init time. The literals are
now split (`'<' + 'head>'` / `'</' + 'head>'`) so any naive HTML
extractor sees a balanced document.

**Demo refresh.** `pywry_demo_deepagent_nvidia.py` model catalog
trimmed to a small curated set of currently-served NVIDIA NIM
tool-capable models; stale ids that 404 at inference removed. The
dropdown now only shows entries the live `ChatNVIDIA.
get_available_models()` lookup confirms.

## 0.1.0 — first public release

**MCP server.** `.mcp.json` launches `pywry mcp --transport stdio` and
Expand Down
2 changes: 1 addition & 1 deletion pywry/docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ mkdocs-literate-nav>=0.6.3
mkdocs-section-index>=0.3.12

# Needed for building docs (dependencies of pywry)
pydantic>=2.13.3
pydantic>=2.13.4
pydantic-settings>=2.14.0
55 changes: 27 additions & 28 deletions pywry/examples/pywry_demo_deepagent_nvidia.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,22 +84,19 @@ def new_event_loop(self) -> asyncio.AbstractEventLoop:
from pywry.tvchart import build_tvchart_toolbars # noqa: E402


DEFAULT_MODEL = "qwen/qwen3-coder-480b-a35b-instruct"

# Preferred tool-capable chat models exposed through NVIDIA NIM, in
# priority order. The first model that the live ``ChatNVIDIA.
# get_available_models()`` lookup actually returns is picked as the
# default. Llama variants are intentionally excluded — they under-
# perform on strict-format tool-calling compared to the models below.
DEFAULT_MODEL = "deepseek-ai/deepseek-v4-pro"

# Small curated set of currently-hosted tool-capable chat models on
# NVIDIA NIM (May 2026). Kept intentionally short — every entry must
# (a) support tool/function calling, (b) be currently served (no 4xx
# at inference), and (c) be strong enough for the chart-agent loop.
# The first entry that ``ChatNVIDIA.get_available_models()`` confirms
# is live becomes the default; the rest populate the model dropdown.
PREFERRED_TOOL_MODELS = [
"deepseek-ai/deepseek-v4-pro",
"nvidia/nemotron-3-super-120b-a12b",
"qwen/qwen3-coder-480b-a35b-instruct",
"deepseek-ai/deepseek-v3.1",
"deepseek-ai/deepseek-r1",
"moonshotai/kimi-k2-instruct-0905",
"openai/gpt-oss-120b",
"qwen/qwen3-235b-a22b-instruct-2507",
"zai-org/glm-4.5-air",
"mistralai/mistral-nemotron",
]

CHART_SYSTEM_PROMPT = """\
Expand Down Expand Up @@ -304,26 +301,28 @@ def stop() -> None:


def _fetch_nvidia_models() -> list[str]:
"""Fetch available tool-capable chat models from NVIDIA NIM."""
"""Return the curated tool-capable models actually live on NIM.

Filters ``PREFERRED_TOOL_MODELS`` against the live
``ChatNVIDIA.get_available_models()`` catalog so the dropdown only
ever shows models we've vetted AND that NVIDIA is currently serving.
Falls back to the full curated list on lookup failure so the UI is
never empty.
"""
try:
from langchain_nvidia_ai_endpoints import ChatNVIDIA

available = ChatNVIDIA.get_available_models()
model_ids = sorted(
live_ids = {
m.id
for m in available
for m in ChatNVIDIA.get_available_models()
if getattr(m, "model_type", None) == "chat" and getattr(m, "supports_tools", False)
)
if not model_ids:
return [DEFAULT_MODEL]
for preferred in PREFERRED_TOOL_MODELS:
if preferred in model_ids:
model_ids = [preferred, *[m for m in model_ids if m != preferred]]
break
if model_ids:
return model_ids
except Exception:
return [DEFAULT_MODEL]
}
except Exception as exc:
print(f"[deepagent] NVIDIA model catalog lookup failed ({exc}); using curated list.")
return list(PREFERRED_TOOL_MODELS)

curated_live = [m for m in PREFERRED_TOOL_MODELS if m in live_ids]
return curated_live or list(PREFERRED_TOOL_MODELS)


def main() -> None:
Expand Down
2 changes: 1 addition & 1 deletion pywry/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pywry"
version = "2.0.1"
version = "2.0.2"
description = "A lightweight and blazingly fast, cross-platform, WebView rendering engine and desktop UI toolkit for Python. Batteries included."
authors = [{ name = "PyWry", email = "pywry2@gmail.com" }]
license = { text = "Apache 2.0" }
Expand Down
12 changes: 9 additions & 3 deletions pywry/pywry/frontend/src/chat-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -1878,12 +1878,18 @@ function initChatHandlers(container, pywry) {
// they try to reconnect, which is the intended behaviour.
var html = data.html || data.content || '';
if (data.widgetId && data.revision) {
// NOTE: tag literals are split so this script can be safely inlined
// into a host document's <head> without a regex-based head/script
// extractor (see pywry/__main__.py) terminating early on a stray
// closing tag inside this string.
var headOpen = '<' + 'head>';
var headClose = '</' + 'head>';
var meta = '<meta name="pywry-app-revision" content="' + data.revision + '">';
// Insert meta tag into <head> if present, else as first element.
if (html.indexOf('<head>') !== -1) {
html = html.replace('<head>', '<head>' + meta);
if (html.indexOf(headOpen) !== -1) {
html = html.replace(headOpen, headOpen + meta);
} else if (html.indexOf('<html>') !== -1) {
html = html.replace('<html>', '<html><head>' + meta + '</head>');
html = html.replace('<html>', '<html>' + headOpen + meta + headClose);
} else {
html = meta + html;
}
Expand Down
12 changes: 12 additions & 0 deletions pywry/pywry/frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ window.pywry = {
htmlEl.classList.add('dark', 'pywry-theme-dark');
}
document.getElementById('app').innerHTML = html;
if (typeof initToolbarHandlers === 'function') {
initToolbarHandlers(document, window.pywry);
}
if (typeof initChatHandlers === 'function') {
initChatHandlers(document, window.pywry);
}
window.pywry.sendEvent('content:ready', { timestamp: Date.now() });
},

Expand Down Expand Up @@ -231,6 +237,12 @@ function registerBuiltinHandlers() {
} else {
document.body.innerHTML = data.html;
}
if (typeof initToolbarHandlers === 'function') {
initToolbarHandlers(document, window.pywry);
}
if (typeof initChatHandlers === 'function') {
initChatHandlers(document, window.pywry);
}
}
});

Expand Down
9 changes: 9 additions & 0 deletions pywry/pywry/frontend/src/system-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@

window.pywry.on('pywry:set-content', function(data) {
window.pywry.setContent(data);
if (typeof initChatHandlers === 'function') {
initChatHandlers(document, window.pywry);
}
});

window.pywry.on('pywry:refresh', function() {
Expand Down Expand Up @@ -224,6 +227,12 @@
} else {
document.body.innerHTML = data.html;
}
if (typeof initToolbarHandlers === 'function') {
initToolbarHandlers(document, window.pywry);
}
if (typeof initChatHandlers === 'function') {
initChatHandlers(document, window.pywry);
}
}
});

Expand Down
6 changes: 6 additions & 0 deletions pywry/pywry/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,5 +834,11 @@ def build_content_update_script(html_content: str) -> str:
}} else {{
document.body.innerHTML = '<div class="pywry-container">' + {escaped_html} + '</div>';
}}
if (typeof initToolbarHandlers === 'function') {{
initToolbarHandlers(document, window.pywry);
}}
if (typeof initChatHandlers === 'function') {{
initChatHandlers(document, window.pywry);
}}
}})();
"""
6 changes: 6 additions & 0 deletions pywry/pywry/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,9 @@ def _get_aggrid_widget_esm() -> str:
if (event.type === 'pywry:update-html' && event.data && event.data.html) {
container.innerHTML = event.data.html;
initToolbarHandlers(container, pywry);
if (typeof initChatHandlers === 'function') {
initChatHandlers(container, pywry);
}
}

const inlineHandledEvents = ['pywry:update-theme', 'pywry:inject-css', 'pywry:remove-css',
Expand Down Expand Up @@ -1256,6 +1259,9 @@ def _get_widget_esm() -> str:
if (event.type === 'pywry:update-html' && event.data && event.data.html) {
container.innerHTML = event.data.html;
initToolbarHandlers(container, pywry);
if (typeof initChatHandlers === 'function') {
initChatHandlers(container, pywry);
}
}

const inlineHandledEvents = ['pywry:update-theme', 'pywry:inject-css', 'pywry:remove-css',
Expand Down
Loading
Loading