fix(chart): gate the chart instruction + render varied chart-JSON shapes#27
Merged
Conversation
Tiny models (e.g. Qwen3 0.6B) parrot the in-prompt chart examples into
unrelated answers instead of honoring the "only when it genuinely helps"
guardrail — e.g. "Help me draft an email" came back full of "Sales Trends:
[Chart data here]" and a placeholder {"chart":{...}} block (which then can't
render, so it falls back to a code block). The chart instruction was added
unconditionally to every chat's system prompt.
- Add ModelDescriptor.supportsCharts (default false), mirroring supportsVision.
- Opt in only the >=1.5B-class models: DeepSeek-R1 1.5B, Gemma 4 E2B/E4B,
Phi-4 mini, Qwen3 4B (app catalog) and the Gemma bundles (engine sample
catalog). Qwen3 0.6B and Gemma 3 1B stay off.
- Build the chat system instruction per active model: append
ChartInstruction.SYSTEM only when the active model supportsCharts; otherwise
use the plain base instruction.
- Test: chartsGatedToCapableModelsOnly.
Verified on-device (Redmi Pad SE, Qwen3 0.6B, release): "Help me draft an
email" now returns a clean, chart-free reply. Capable models (e.g. Gemma 4
E2B) keep charts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capable non-Gemma models (e.g. DeepSeek-R1 1.5B) emit chart-shaped JSON in a
shape ChartParser didn't accept — nested under a "chart" wrapper and with no
top-level "type" (e.g. {"chart":{"name":"…","title":"…","data":[{label,value}]}}).
That fell through to a code block, so the chart never rendered.
ChartParser now resolves the chart tolerantly while keeping the strict path
FIRST (so Gemma's {"type":"donut",…} is byte-for-byte unchanged):
- accept `chartType` / `kind` as `type` synonyms;
- unwrap a single chart-wrapper key (chart / graph / donut / pie / bar / line /
progress / …), taking the type from the wrapper name when it names one,
else the inner type, else inferring from the payload (labeled data → donut,
series → line, value → progress);
- a bare untyped, unwrapped object still returns null (ordinary JSON stays a
code block — conservative, content never lost).
Tests: wrapper-without-type → donut, type-from-wrapper-key, chartType synonym,
bare-untyped stays null.
Verified on-device (release): DeepSeek-R1 1.5B's wrapped JSON now renders a
donut on the tablet, and Gemma 4 E2B still renders its donut on the phone
(no regression).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes both halves of the chart problem surfaced on-device: tiny models forcing charts into unrelated answers, and capable models' chart JSON failing to render because of shape differences. Branched off
main, independent of the UI PR #22.1 — Gate the chart instruction by model capability (
566d5c3)The chart instruction was appended to every chat's system prompt. Tiny models can't follow its "only when it genuinely helps" guardrail — observed with Qwen3 0.6B: "Help me draft an email" came back full of
Sales Trends: [Chart data here]and a placeholder{"chart":{…}}block.ModelDescriptor.supportsCharts(defaultfalse), mirroringsupportsVision.2 — Render chart JSON that models wrap or emit without a type (
c5fa222)DeepSeek-R1 1.5B emits
{"chart":{"name":"…","title":"…","data":[{label,value}]}}— wrapped, no top-level"type"— whichChartParserrejected → code-block fallback.ChartParsernow resolves tolerantly, stricttypepath first so Gemma is unchanged: acceptchartType/kindsynonyms; unwrap a single chart-wrapper key (type from the wrapper name → inner type → inferred from payload); bare untyped/unwrapped JSON still returns null (ordinary code stays code). NewChartParserTestcases (CI:lib).On-device verification (release)
{"chart":{…}}now renders a donut.{"type":"donut"}path).