Natural-language scheme editing #22

Closed
opened 2026-07-07 17:38:03 +00:00 by claude-bot · 1 comment
Contributor

Goal

Let users refine a scheme with plain-language instructions — "make it warmer and slower," "more Halloween, less pastel," "add a second accent color" — and get back a validated candidate scheme.

Why it's valuable

Iris already has the SuggestionProvider abstraction with Ollama/Anthropic/OpenAI-compatible providers, a prompt builder, and Pydantic response validation. Natural-language editing is a high-delight, low-cost extension of that existing pipeline rather than net-new infrastructure.

Sketch

  • New endpoint POST /schemes/{id}/refine { instruction } that feeds the current scheme JSON + the instruction to the provider, reusing the existing validator + retry + fallback.
  • Reuse the daily-limit counter (AI_DAILY_LIMIT).
  • UI: a text box on the scheme editor with the same before/after preview as "Generate Suggestion."

Acceptance criteria

  • Instruction returns a schema-valid modified candidate
  • Reuses validation/retry/fallback and the daily cap
  • Before/after preview before saving
  • Provider-mocked tests (happy path + invalid response)

Proposed enhancement (brainstorm follow-up); builds on the M7 AI pipeline.

## Goal Let users refine a scheme with plain-language instructions — "make it warmer and slower," "more Halloween, less pastel," "add a second accent color" — and get back a validated candidate scheme. ## Why it's valuable Iris already has the `SuggestionProvider` abstraction with Ollama/Anthropic/OpenAI-compatible providers, a prompt builder, and Pydantic response validation. Natural-language editing is a high-delight, low-cost extension of that existing pipeline rather than net-new infrastructure. ## Sketch - New endpoint `POST /schemes/{id}/refine { instruction }` that feeds the current scheme JSON + the instruction to the provider, reusing the existing validator + retry + fallback. - Reuse the daily-limit counter (`AI_DAILY_LIMIT`). - UI: a text box on the scheme editor with the same before/after preview as "Generate Suggestion." ## Acceptance criteria - [ ] Instruction returns a schema-valid modified candidate - [ ] Reuses validation/retry/fallback and the daily cap - [ ] Before/after preview before saving - [ ] Provider-mocked tests (happy path + invalid response) Proposed enhancement (brainstorm follow-up); builds on the M7 AI pipeline.
claude-bot added this to the v1.1.0 milestone 2026-07-15 15:32:56 +00:00
Author
Contributor

Landed in #130 (squash-merged to main). All four acceptance criteria met.

The design question was what happens on failure, and it splits refinement from generation cleanly.

Generation falls back to a built-in template when the model fails, which is right — it has nothing to lose, and a template beats no scheme. Refinement starts from the user's own work, so the same fallback would replace what they were editing with a generic template: losing work rather than declining to change it. So RefinementFailedError is a distinct exception surfacing as a 502 whose message says the scheme is unchanged, and it is.

It proposes; it never writes. The endpoint returns a before and an after; accepting one loads it into the editor's own controls, and the user saves with the ordinary Save. That keeps an AI edit on exactly the path a manual edit takes — promotion to source: user (#99), staleness cleared, audit entry. Persisting directly would have let an AI edit of an approved scheme reach the lights with nobody seeing it. It's also why refining an approved scheme is allowed at all, where generation refuses to touch one.

Smaller things worth recording:

  • A segment the model forgets to mention keeps its current values. "Make it warmer" is not an instruction to delete a segment, and an omission is far likelier to be the model forgetting than the user asking. on is likewise carried forward rather than defaulted to true, which would switch a deliberately-off segment back on.
  • The refine prompt deliberately drops generation's few-shot examples. Those are whole festive schemes; showing them during an edit invites the model to replace the user's scheme with something resembling an example rather than adjusting what it was handed.
  • The instruction is length-bounded and rejected when blank. The cap counts requests, not tokens — an unbounded instruction makes one pasted novel a single request and a very large bill. My own test caught the blank case: I expected a rejection and got a 503 from the provider path, which meant a stray space was reaching the model.
  • A failed call still counts against the cap. Charging only for successes would make a broken provider an unlimited retry loop.

Verified: 27 new provider-mocked tests, ruff/mypy clean, 749 backend tests, frontend tsc/eslint clean, CI green.

Landed in #130 (squash-merged to `main`). All four acceptance criteria met. **The design question was what happens on failure**, and it splits refinement from generation cleanly. Generation falls back to a built-in template when the model fails, which is right — it has nothing to lose, and a template beats no scheme. Refinement starts from the user's own work, so the *same* fallback would replace what they were editing with a generic template: losing work rather than declining to change it. So `RefinementFailedError` is a distinct exception surfacing as a 502 whose message says the scheme is unchanged, and it is. **It proposes; it never writes.** The endpoint returns a before and an after; accepting one loads it into the editor's own controls, and the user saves with the ordinary Save. That keeps an AI edit on exactly the path a manual edit takes — promotion to `source: user` (#99), staleness cleared, audit entry. Persisting directly would have let an AI edit of an *approved* scheme reach the lights with nobody seeing it. It's also why refining an approved scheme is allowed at all, where generation refuses to touch one. **Smaller things worth recording:** - A segment the model forgets to mention keeps its current values. "Make it warmer" is not an instruction to delete a segment, and an omission is far likelier to be the model forgetting than the user asking. `on` is likewise carried forward rather than defaulted to true, which would switch a deliberately-off segment back on. - The refine prompt deliberately drops generation's few-shot examples. Those are whole festive schemes; showing them during an *edit* invites the model to replace the user's scheme with something resembling an example rather than adjusting what it was handed. - The instruction is length-bounded and rejected when blank. The cap counts requests, not tokens — an unbounded instruction makes one pasted novel a single request and a very large bill. **My own test caught the blank case**: I expected a rejection and got a 503 from the provider path, which meant a stray space was reaching the model. - A failed call still counts against the cap. Charging only for successes would make a broken provider an unlimited retry loop. Verified: 27 new provider-mocked tests, `ruff`/`mypy` clean, 749 backend tests, frontend `tsc`/`eslint` clean, CI green.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Iris-WLED#22
No description provided.