Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ REACT_APP_GOOGLE_TAG_MANAGER_ID=''
REACT_APP_API_ENDPOINT='http://localhost:3009'
REACT_APP_ALLOWED_IFRAME_ORIGINS='http://localhost:3011,http://localhost:3012,http://classroom.localhost:3013'
CSP_API_MULTIPLE_ORIGINS='http://localhost:3011,http://localhost:3009'
# Escalating error help (see docs/ErrorHelpEscalation.md). Leave the URL empty to
# hide the model-backed rungs; the pre-written ones are unaffected. Requires a
# local `ollama serve`. Override the model per tab with ?ollama_model=gemma2:2b
REACT_APP_OLLAMA_URL=''
REACT_APP_OLLAMA_MODEL='llama3.2:3b'
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ yarn exec cypress open
- `docs/host-integration/AutosaveFlush.md`
- `docs/HTML.md`
- `docs/PythonDependencies.md`
- `docs/ErrorHelpEscalation.md`
- `docs/Deployment.md`
- `.github/workflows/ci-cd.yml`

Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,41 @@ This allows the host page to query the current code in the editor and to control

Deeper host guides live under [docs/host-integration](./docs/host-integration/README.md).

### Experimental SLM features

Enable support for the AI-assisted error message functionality (using a locally-running SLM):

```bash
brew install ollama
ollama serve
```

This starts up the Ollama server, which is then used to fetch a model - in a new terminal, run:

```bash
ollama pull qwen3.5:9b
```

Then ensure you have the following set in `.env`:

```bash
REACT_APP_OLLAMA_MODEL='qwen3.5:9b'
REACT_APP_OLLAMA_URL='http://localhost:11434'
```

And, with `ollama serve` still running, in a new terminal start up the web component with `yarn start`, visit http://localhost:3011/web-component.html and run some code that produces an error, eg:

```python
age = "30"
print(age + 1)
```

(this will generate an error that the friendly / simple error message library will first handle, and then escalate to the SLM for a more detailed explanation).

You can also install different models (using `ollama pull <model>`), and change the model used dynamically by setting the `ollama_model` query parameter, for example:

http://localhost:3011/web-component.html?ollama_model=llama3.2:3b

### Offline support

The web component displays an offline indicator when the host page's service worker broadcasts connectivity changes. The web component itself does not register a service worker - caching and offline detection are the host app's responsibility.
Expand Down
78 changes: 78 additions & 0 deletions cypress/e2e/spec-wc-pyodide.cy.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
getAiErrorHelp,
getAiErrorHelpButton,
getEditorShadow,
getErrorMessage,
getFriendlyErrorMessage,
Expand Down Expand Up @@ -321,4 +323,80 @@ describe("When friendly errors enabled with pyodide", () => {
"This variable doesn't exist here",
);
});

it("escalates through the levels of support when asked", () => {
// Ollama is stubbed so this stays deterministic and does not need a model
// pulled. The response is newline-delimited JSON, as Ollama streams it.
cy.intercept("POST", "**/api/chat", (req) => {
req.reply({
statusCode: 200,
headers: { "content-type": "application/x-ndjson" },
body: `${JSON.stringify({
message: { content: "Look at line 1 and check what kitten is." },
})}\n${JSON.stringify({ done: true, done_reason: "stop" })}\n`,
});
}).as("chat");

runCode("print(kitten)");
getFriendlyErrorMessage().should(
"contain",
"This variable doesn't exist here",
);

// Rung 1: the pre-written explanation, with no request to the model.
getAiErrorHelpButton().should("contain", "Explain this to me").click();
getAiErrorHelp().should(
"contain",
"A variable created in another place might not be available here",
);
getAiErrorHelp().should("contain", "If it is meant to be text");
cy.get("@chat.all").should("have.length", 0);

// Rung 2: escalate to the model.
getAiErrorHelpButton().should("contain", "I'm still stuck").click();
cy.wait("@chat")
.its("request.body")
.then((body) => {
expect(body.stream).to.eq(true);
expect(body.think).to.eq(false);
expect(body.options).to.deep.eq({ temperature: 0.7, num_predict: 400 });
expect(body.messages[0].content).to.contain("Do not repeat");
expect(body.messages[1].content).to.contain("print(kitten)");
expect(body.messages[1].content).to.contain("NameError");
});
getAiErrorHelp().should(
"contain",
"Look at line 1 and check what kitten is.",
);
getAiErrorHelp().should("contain", "Written by AI");

// Rung 3: the fix, with the previous answer replayed to the model.
getAiErrorHelpButton().should("contain", "Show me the fix").click();
cy.wait("@chat")
.its("request.body.messages")
.then((messages) => {
expect(messages.map((m) => m.role)).to.deep.eq([
"system",
"user",
"assistant",
"user",
]);
expect(messages[2].content).to.contain("Look at line 1");
});

// Ladder exhausted.
getAiErrorHelpButton().should("not.exist");
});

it("leaves the pre-written help working when Ollama is unreachable", () => {
cy.intercept("POST", "**/api/chat", { forceNetworkError: true }).as("chat");

runCode("print(kitten)");
getAiErrorHelpButton().click();
getAiErrorHelp().should("contain", "If it is meant to be text");

getAiErrorHelpButton().click();
getAiErrorHelp().should("contain", "The help service is not running");
getAiErrorHelp().should("contain", "If it is meant to be text");
});
});
5 changes: 5 additions & 0 deletions cypress/helpers/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ export const getErrorMessage = () =>
export const getFriendlyErrorMessage = () =>
getEditorShadow().find(".friendly-error-message");

export const getAiErrorHelp = () => getEditorShadow().find(".ai-error-help");

export const getAiErrorHelpButton = () =>
getEditorShadow().find(".ai-error-help__button");

export const getTextOutputTab = () =>
getPyodideOutput().findByLabelText("Text output");

Expand Down
Loading
Loading