Repository navigation
feat(frontend): add AI workflow fixer with LLM-powered diff panel - #8637
Conversation
- AiWorkflowFixerService: error classification, LLM call via LiteLLM proxy - AiFixFrameComponent: diff view with approve/discard actions - Wires into result-panel on explicit button trigger - Reads Python UDF errors from ConsoleUpdateEvent path Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PythonUDFV2 declares defaultEnv as a required property, so a workflow whose UDF omits it is rejected by the frontend's schema validation: the canvas marks the operator red and the Run button reads "Invalid Workflow". Every demo workflow hit this on import. Also adds the envName counterpart, which the operator reads when defaultEnv is false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Python UDF exception never becomes a WorkflowFatalError: the worker pauses on the exception and the traceback arrives as an ERROR console message while the execution stays Running. The tab was registered only from displayError(), which runs on the Failed and compilation-failed states, so it never appeared for the very case the panel exists to fix. Register it next to the console frame, where that case is already handled, still gated on the copilot feature flag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
👋 Thanks for your first contribution to Texera, @pedroreyesalavrado! If you're looking for a good place to start, browse issues labeled You can drive common housekeeping yourself by commenting one of these commands on its own line:
Each command must match exactly: |
Automated Reviewer SuggestionsBased on the
|
carloea2
left a comment
There was a problem hiding this comment.
Initial review: please apply the stale-suggestion check to configuration changes as well as code changes.
In AiWorkflowFixerService.applyFix(), the code-change branch checks that the original snippet is still present, but the property-change branch unconditionally assigns properties[String(fix.fieldName)] = fix.suggested. If a user or collaborator changes the model setting after analysis, applying the old suggestion silently overwrites that newer value and reruns the workflow. Re-reading the properties preserves unrelated fields but does not protect the field being replaced.
Please compare the live field with the original value captured for the suggestion and require re-analysis when they differ. Add a regression test that changes the target property between analysis and Apply and verifies that neither setOperatorProperty nor executeWorkflow is called; retain coverage for an unchanged field applying successfully.
Based on static inspection of commit c286bcc; I have not run the tests.
applyFix() only checked staleness on the code branch: the property branch wrote the suggested value unconditionally, so a model chosen after the analysis was silently overwritten and the workflow re-ran on it. Re-reading the operator's properties preserves unrelated fields but not the one being replaced. Compare the live field with the value captured when the suggestion was generated and refuse when they differ, exactly as the code branch does with its snippet: nothing is written, no re-run, and the panel asks for a fresh analysis. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Good catch, thanks. Fixed in
Added the regression test you described — the target property changes between analysis and Apply, and both calls are asserted absent — and strengthened the existing unchanged-field test, which checked the write but not the re-run or the resulting state. Tests pass locally on the rebased branch: |
|
Thanks for the update, @pedroreyesalavrado. I inspected Could you add side-by-side before/after screenshots and a short GIF or recording showing the error, Analyze, the suggested diff, Apply and re-run, and the resulting execution? Please also show Discard and an unsupported-error state. Include browser, viewport, commit, and reproduction steps using the supplied demo workflow. Two tracking items: please restore the PR template's Before merging, please also address the documented warehouse-selection case so the UI only reports "re-running" when execution actually starts. |
When a warehouse is required and none is selected, ExecuteWorkflowService refuses the run before it starts, so the AI fix panel reporting "re-running" contradicted the toast the user actually saw. The fix is still applied to the operator; the panel now reports that separately and asks for a warehouse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel re-rendered on execution-state, compilation, selection and result changes, but not on console messages. A Python UDF that raises while its own operator is already selected moves none of those: the worker pauses so the execution stays Running, and the highlight never moves. The tab therefore only appeared if the user clicked another operator and back -- which is not what the user does when they are already watching the operator that failed. Adds the console message stream to the re-render trigger, debounced because a UDF's prints share it. The existing tests called rerenderResultPanel() directly, so they exercised the branch but never the wiring; the new one drives the stream. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tab was registered whenever the operator reported an error but never removed once it stopped, so a re-run that cleared the console left a tab offering to fix an error that was no longer there. The Static Error frame beside it already tears itself down the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t ones pipeline_missing_column.json puts a Filter and a Sort after the failing UDF, so applying a fix visibly re-runs the whole workflow instead of only clearing the traceback -- it is the workflow used for the PR recording. Like missing_column_textinput.json it sources from TextInput, so neither needs the examples dataset. The README described five workflows and listed five; the TextInput one had been left undocumented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — the request for a recording paid for itself: setting it up surfaced two real bugs, both now fixed and pushed. Screenshots and the recording are in the PR description. The tab did not appear in the common case. The tab lingered after the error cleared. It was set when the operator reported an error but never removed when it stopped, so a re-run left a tab offering to fix an error that was gone. Fixed in b8926d2, matching the teardown the Both slipped through because my three existing tests called Recording details: Chrome, viewport 1568x653, commit 56889cc, model Steps to reproduce (no dataset needed — the workflow sources from
For the unsupported state in the second screenshot, edit that same UDF to raise something outside the four patterns ( The warehouse case you flagged is addressed in 19d21b6: when a warehouse is required and none is selected, Apply writes the fix but reports that separately instead of claiming a re-run Full suite: 205 passing across the result-panel specs. |
Ma77Ball
left a comment
There was a problem hiding this comment.
Review summary
The PR adds an AI Fix tab that classifies a failed Python-UDF error, asks the LiteLLM proxy for one minimal fix, and applies it through the undoable setOperatorProperty path. The core service and component are well structured and defensively coded: analysis fires only on an explicit click, apply re-reads the graph and guards against a stale suggestion on both the code and property branches, and the re-run is warehouse-aware. The specs are lean (small inline strings and a suggestion() helper), not JSON-heavy, so the mock test cases read fine. Two things should be settled before merge: the frame renders the shared singleton state without checking it belongs to the tab's own operator, so with two failed operators a suggestion can be shown and applied under the wrong one (P2); and the new top-level demo-workflows/ folder (README + 7 JSON files, ~800 lines referenced by nothing) is out of scope for the feature and, as it stands, its header-less README fails the license-header CI check (P1). No P0: the apply path is guarded and undoable, so nothing loses data.
Action items: Before merge: add the ASF header to demo-workflows/README.md or drop the folder, so check-header passes (P1); scope the AI Fix frame's state to its own operator so a suggestion cannot be applied to a different operator (P2); drop or relocate the demo-workflows/ folder (P2). The two P3s (over-broad 404 classification, hard-coded model alias) are optional.
Findings
5 total: 0 P0, 1 P1, 2 P2, 2 P3.
Blocking (P0/P1)
- Add the ASF license header to the new README (or drop the folder) -
demo-workflows/README.md:1- see inline.
Non-blocking (P2)
- Scope the AI Fix frame's state to its own operator -
frontend/src/app/workspace/component/result-panel/ai-fix/ai-fix-frame.component.ts:72- see inline. - Drop or relocate the
demo-workflows/folder -demo-workflows/combined.json:1- see inline.
Nit (P3)
- Tie the
404match to the model-lookup context -frontend/src/app/workspace/component/result-panel/ai-fix/ai-workflow-fixer.service.ts:71- see inline. - Source the model id from config instead of a constant -
frontend/src/app/workspace/component/result-panel/ai-fix/ai-workflow-fixer.service.ts:60- see inline.
Verification
- P1 header:
.github/workflows/check-header.ymlrunsapache/skywalking-eyeson everypull_request, driven by.licenserc.yaml.**/*.jsonis inpaths-ignore(so the 7 JSON files are fine), but**/*.mdis not, and every existing README.md in the repo carries the ASF<!-- ~ Licensed ... -->header. The newdemo-workflows/README.mdstarts at# Demo workflowswith no header, so that check fails on this PR. - P2 cross-operator:
ai-fix-frame.component.htmlbindsstate$ | asyncwith no comparison to the frame's@Input() operatorId;AiWorkflowFixerServiceisprovidedIn: "root"with a singlestateSubject;applyFix()(service:147) patchescurrent.operatorIdread from that shared state. Repro: operators A and B both raise an ERROR console message (both get the tab); analyze A, select B, B's tab shows A's diff, Apply patches A. Not covered by the specs (the "state survives re-subscription" test asserts the intended persistence, not a per-operator guard). - P2 demo-workflows:
git grep demo-workflowsover the head tree returns no reference outside the folder; the README itself says 5 of the 7 need the external movies dataset, so they are demo aids, not fixtures. - Not run: I did not execute the frontend specs or the live LLM/e2e path; correctness claims are from reading the changed files at 56889cc.
|
@aglinxinyuan Could you share your opinion on this UP hackathon proposal? Do you think the AI workflow fixer, which shows suggested changes for users to review before applying and re-running, is a direction we should pursue toward merging? Your input on the scope and integration with the existing workflow editor would help guide the next iteration alongside the review feedback already here. |
|
While we can use the agent to fix issues, it's very convenient to 1) highlight the places that have problems; and 2) have a button for the user to click to fix it. It's good that this feature uses the existing LiteLLM proxy. Overall, I like this new feature. |
|
This looks very polished. I agree with this change. |
They were a new top-level directory that nothing in the build or the tests references. They now sit under bin/single-node/examples/, whose workflow shape they already shared, and the README carries the ASF header the license check requires of every .md (**/*.json is in paths-ignore, **/*.md is not). They go beside examples/workflows/ rather than inside it on purpose: load-examples.sh creates a workflow for every *.json directly under that directory, so moving them in would add seven deliberately broken workflows to the account of anyone who brings the stack up with --with-examples. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AiWorkflowFixerService is a root singleton holding one suggestion, but a failed pipeline can put an AI Fix tab on several operators at once. The frame rendered that shared state without comparing it to its own operatorId, so analysing operator A and then selecting B showed A's diff under B -- and Apply, which reads the operator id from the shared state, patched A while the user was looking at B. The write was undoable, but it targeted the wrong operator. The frame now combines the state with its bound operator id and renders idle when they differ, so a suggestion is only ever shown and applied where it was analysed. Also ties the model-not-found classifier to a model reference: a bare 404 matched a line number in a traceback, an HTTP status from unrelated user code, or a literal in the data, and each of those sent off a request asking for a model-config fix. Adding the false-positive cases as tests reproduced it before the pattern changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks @Ma77Ball — all four actionable findings are addressed in e749a0c and 931a977. The fifth I did not do, and I say why below rather than leave it looking handled. P1, ASF header. You were right, and I verified it locally rather than from CI: of the 40 P2, cross-operator state. A real bug, and the omission was mine: I put the state in a root singleton deliberately (the panel destroys and rebuilds every frame on re-render) but never scoped it to an operator. The frame now combines the service state with its bound P2, demo-workflows/ folder. Moved out of the repo root — but to P3, the bare P3, model id from config — not done. Full suite: 209 passing. Thanks also @chenlica and @aglinxinyuan for weighing in on the direction. |
|
Hope this feature can be approved and merged into the main branch! |
|
@Ma77Ball gentle ping when you have a moment — the four actionable findings are addressed in e749a0c and 931a977, and two questions from my reply are still open:
I am happy with either answer; I just do not want to guess and churn the branch. One thing that may need a maintainer rather than me: the "Release Auditing" workflow has never run on this branch — only the seven lightweight checks execute, which I think is the first-time-contributor approval gate. That is probably why the PR reads as |
|
@kunwp1 Please review this PR. |
LIVE_STATES was inserted above TERMINAL_STATES and absorbed its comment, so one set carried a description of the other and the other carried none. TERMINAL_STATES also now says why `Uninitialized` is absent from it although updateWorkflowActionLock counts it as a run that is over: a kill lands there, and a kill is how Apply clears the lock itself, so resetting on it would throw away a suggestion that is still the right one for the error on screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by a full review of the branch, which is the pass Copilot's run did not finish. The occurrence count behind the "changes N places" badge is taken at the analysis, and Apply replaces every match while checking only that the snippet is still present. So a copy added or removed in between made the write touch a different number of lines than the panel promised -- and the badge is hidden at one occurrence, so gaining a second copy meant an extra edit with no warning at all. Reachable because a kill lifts the modification lock without resetting the panel: `Uninitialized` is deliberately not a terminal state, since that is where Apply's own kill lands. Stop the run with the suggestion on screen and the UDF is editable under it. It refuses in both directions. A count that dropped is not an oversight in the comparison: the badge said two places, so that is the edit the user approved, and patching whichever copy is left is a different change from the one on screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kunwp1
left a comment
There was a problem hiding this comment.
Thanks for working through the last round. I re-checked those comments and most are resolved. Three areas aren't fully there yet and they're the comments below.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #8637 +/- ##
============================================
- Coverage 91.96% 91.96% -0.01%
Complexity 5084 5084
============================================
Files 1280 1284 +4
Lines 55682 55965 +283
Branches 7063 7121 +58
============================================
+ Hits 51208 51468 +260
- Misses 2732 2742 +10
- Partials 1742 1755 +13
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
@kunwp1 on the last round: keying by name also keeps every other frame alive across re-renders and operator switches, because NgComponentOutlet only recreates a component when its type changes. That is broader than the problem. The config object is the right key. A re-render re-`set`s each entry with a fresh object, so those frames are recreated exactly as they were before this PR, while syncAiFixFrame touching only its own entry leaves the others' identity -- and so their components -- untouched. It also settles what I could not test. The DOM assertion was out of reach because rendering the frames needs the panel open, and that mounts components jsdom cannot draw. But "toggles only the AI Fix entry on a console update" already asserts that the Console config object survives by identity, which is exactly what the trackBy now keys on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kunwp1: every refusal in applyFix ran after ensureModifiable(), which stops the paused execution to clear the modification lock. So a fix that was never going to be written cost the user their run -- and the test for it shows the rest of the cost: fifteen seconds of unlock wait before the refusal even reached the panel. The checks and the patch move into patchedProperties(), which reads only the graph and returns a fresh object. It runs as a pre-flight before the kill, and again after it, since the operator can move during the wait and the pre-flight result describes the operator as it was before. The other half of the same comment: refusing a configuration change for anything but model_not_found was Apply-only, so the panel showed a diff and then turned it down -- which is what the comment in toFix says must not happen. The analysis refuses it now, and the check stays in patchedProperties as the second barrier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kunwp1 was right that JVM errors still did not classify, and the reason is worse than the pattern: on Java 9+ the JDK's own frames carry a module prefix (`java.base/java.lang.Thread.run(Thread.java:840)`) and native ones read `(Native Method)`. mkConsoleMessage sends `getStackTrace.mkString` with no `at ` prefix either, so a real worker trace ends on a frame nothing here recognised -- the backward scan returned that frame and `NullPointerException` classified as unsupported. The fix in 7ee6339 did not work in production. So stop deducing it. Both sources hand the exception over on its own: mkConsoleMessage puts `err.toString` in `title`, the Python worker puts `format_exception[-1]` there, and a fatal error carries `evt.e.toString` in `message` with the stack in `details`. analyzeError now takes both parts -- `header` to classify, `full` for the prompt -- and FRAME_LINE and finalExceptionLine are gone. That also retires the chained-traceback logic rather than reimplementing it: `format_exception[-1]` is already the exception that stopped the worker, which is what scanning backwards was trying to recover. The regression test moves to the frame spec, where the title is actually extracted, since that is where this can break again. Two service tests that fed a whole traceback to classifyError went with the heuristic -- passing a blob now only proves the pattern matches somewhere in it, not that the header was read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tation @kunwp1: the previous fix only covered a multi-line original. A single-line one whose suggestion already carried absolute indentation was still indented twice -- `return` landing at 20 spaces instead of 12 -- because the result depended on which convention the model happened to use and the prompt never stated one. So the prompt states one. original_snippet and suggested_snippet must be whole lines, copied from the start of a line with the leading indentation included, each already at the column it should have. startsAtLineBoundary refuses a reply that quotes from mid-line, checking every occurrence rather than the first, since one mid-line match is enough to make the result depend on a guess. replaceAllIndented is then just a replacement, and is named replaceAll. The last heuristic in the file is gone. Verified against claude-haiku-4.5 through the local LiteLLM proxy, on the three code-change shapes (KeyError, TypeError cast, NaN guard). The new prompt: 28 of 28 replies accepted. The old prompt, as a control: 11 of 12 -- and the one refusal is exactly a mid-line snippet with no indentation, the case this is about. The model's own multi-line guard applies to 0/4/8/12 nesting, valid Python, nothing doubled. The test fixture had to change with it, which says the same thing: its original_snippet was quoted mid-line, so the contract now turns it away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@kunwp1 — all four, one commit each: The JVM one was a regression of my own fix, not a gap in it. You were right about the module prefix, and I checked So I took your suggestion rather than patching the regex. I checked all three sources first: Validating after the kill was the worst of the four in practice, and the test makes the cost visible: it took 15 seconds to fail. The run was already stopped, and then the user waited out the unlock timeout to be told nothing had been written. The checks and the patch moved into On the indentation, you were right that the prompt never stated a convention, so I stated one and measured it rather than guessing. The prompt now requires whole lines copied from the start of a line with their indentation; I ran it against
The single refusal under the old prompt is exactly a mid-line snippet with no indentation — the case this is about. So the model was usually including the indentation on its own, which is why this looked fine end to end and still broke the way you found. Applying the model's real multi-line guard now yields 0/4/8/12 nesting, valid Python, nothing doubled. The test fixture had to change with it, which says the same thing from the other side: its
One more that nobody reported. Reviewing the branch myself I found that Apply checked only whether the snippet was still present, never whether it was still in the same number of places — so a copy added between the analysis and the click was patched too, with the "changes N places" badge hidden at one occurrence. It refuses in both directions now ( Suite: 6751 passing, One ask. The approved re-run on |
Found by running the stack, not by the tests.
The tab never came back when an operator failed twice. The console stream fed a
`distinctUntilChanged` keyed on operator + whether the tab is wanted, but
rerenderResultPanel writes to the same map without going through that pipe. So:
the operator fails (key `true`), a new run starts, the rerender drops the tab --
and the pipe still remembers `true`. The next traceback maps to `true` again, is
read as "no change", and the tab stays gone. That is the fix-and-re-run loop
this panel exists for.
The live trace, with the component instrumented in the browser:
rerenderResultPanel()
syncAiFixFrame(offer=false) <- run starts, tab dropped
STREAM emits -> key=op:true <- traceback arrives, key is right
<- syncAiFixFrame never called
syncAiFixFrame now compares against the entry that is actually in the map,
including which operator it points at, and reports whether it changed anything
so the caller can skip change detection. Nothing is remembered outside the map,
so it no longer matters which path wrote to it last.
The existing test passed because it drives the stream directly, with no rerender
interleaved -- distinctUntilChanged's first emission always gets through. The
new test follows the live trace instead, and fails without this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kunwp1
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround. All four comments from the last round look resolved.
The remaining comments are mostly about which errors can actually be fixed end to end.
…at cannot fail Two from @kunwp1's latest round. A match that begins a line but stops inside it still replaces cleanly, so ` n = 1` matched ` n = 10` once -- a count of 1, which hides the occurrence badge -- and Apply left the trailing `0` behind. The diff offered `n = 1` -> `n = 2` and the operator was left saying `n = 20`. Checking only the start was half the contract: starting a line is what makes the indentation unambiguous, ending one is what keeps the replacement honest. The function says what it does now and is called matchesWholeLines. The errorType check in the apply path is gone. I had kept it as a second barrier, the way the field checks are kept, but the two are not alike: a field can disappear between the analysis and the write, while errorType is fixed when the state is published and the analysis already refuses a property fix for anything but model_not_found. It was unreachable, so the comment now says why there is nothing there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kunwp1 asked whether the supported list could be narrowed to what works end to end. Checking the repo, two of the four shapes have no reachable producer: - **model_not_found.** The HuggingFace operator never raises for an unknown model. A 404 from the inference API is retried as a chat completion, and when every provider fails the text goes into the result rows via `_format_error` -- no exception, no ERROR console message, no tab. A malformed id does raise, but as `ValueError: Invalid Hugging Face model ID ...`, which these patterns do not match. The sklearn operators have a model field and no remote lookup to 404 on. - **NullPointerException.** Only a JVM operator raises it, and a JVM operator has no `code` for Apply to patch, so Analyze could only spend a model call to reach a refusal. So the panel now says what it is: a code fixer for Python UDFs. Three shapes, one kind of fix. The tab is offered only where `isPythonUdf` holds, and toFix turns away any reply that is not a code change. That left the configuration path with no way in, and dead code is what the last round was about, so it goes: the property branch of patchedProperties, coerceToFieldType, `type` and `fieldName` on SuggestedFix, `errorType` on FixState (its only reader was that branch), the field row in the diff, and the model rule in the prompt. Fourteen test blocks went with it. One of those tests was about to become tautological rather than fail: the credential test asked for a model error, which is now unsupported, so the model was never called and the assertion had nothing to inspect. It asks for a supported error now, so it still proves what it claims -- that the service redacts before building the prompt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@kunwp1 — all three, in The mid-line match was a real one and I had reasoned my way past it. When I made the prompt require whole lines I argued that checking the start was enough, because that is what makes the indentation unambiguous. It is not enough for the replacement: The errorType check was unreachable, and you are right that it is not like the field checks. I had kept it as a second barrier by analogy with those, but a field can disappear between the analysis and the write, while errorType is fixed when the state is published and the analysis already refuses a property fix for anything else. It is gone. On what can actually be fixed end to end, I checked the repo and it is worse than your two cases. So I took the narrowing. Three shapes, Python UDFs only, one kind of fix. That left the configuration path with no way in, and dead code is what your previous comment was about, so it went too: the property branch of One thing I want to flag rather than let it pass. Removing The PR body is updated, including three claims that had gone stale earlier and that I should have caught: it still described the final-exception-line heuristic, the tabs keyed by name, and the indentation inference, all of which are gone. Suite: 6959 passing. |
…reader of toFix still took the redacted properties, which it read to turn a field name into the field's current value for the diff. That branch went with the configuration path, so the parameter has been along for the ride since. Found by auditing what the deletion left behind rather than by a test or the linter, neither of which minds an unused parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kunwp1
left a comment
There was a problem hiding this comment.
LGTM! The rest are the small follow-ups.
… good replies @kunwp1's three follow-ups. **A credential could still reach the model.** withoutSecrets filters top-level field names, but a Python UDF's `uiParameters` is a list of {attribute, value} pairs, so the name says nothing about whether a value is an API key the user typed in -- and the whole list went into the configuration block. Reproduced before fixing: an `hf_live_...` value appears verbatim in the built prompt. Filtering harder was not the fix. None of the three remaining shapes reads configuration, so the block served nothing, and it is gone -- taking withoutSecrets, withoutCode, schemaProperties and the OperatorMetadataService dependency with it. analyzeError no longer takes the properties either. The PR body claimed credentials were kept out behind two barriers; now there is nothing to keep out. **The whole-line check refused valid replies.** A snippet quoted with its trailing newline left `code[end]` pointing at the next line's first character, so the same reply was refused in the middle of a file and accepted on its last line -- the verdict depended on where the line sat. One trailing newline comes off each snippet now, independently, so the replacement leaves the line's own newline in place and merges nothing. The prompt says the newline is optional rather than leaving it to chance. **`fix_type` is gone.** With one legal value it could only ever refuse a good answer; a reply naming a configuration field is already turned away by the occurrence count, which is what that test now says it is relying on. The `properties` parameter in the same comment went in 7873a6b, and the stale "four supported fixes" wording went with the configuration block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Ma77Ball, from the first round: AI_FIXER_MODEL was a compile-time constant holding `claude-haiku-4.5`, which is the alias this deployment's LiteLLM proxy publishes, not a provider id. A proxy serving a different alias could not be reached without rebuilding the bundle. It now travels the same route as every other workspace setting: `gui.workflow-workspace.ai-fix-model` in gui.conf, overridable by GUI_WORKFLOW_WORKSPACE_AI_FIX_MODEL, read by GuiConfig, served by ConfigResource beside copilotEnabled, and consumed from GuiConfigService. Deliberately not in llm.conf, which the frontend never sees: the panel is what names the model, and the proxy's base-url and master key stay backend-only. No unit test for the wiring. The one I wrote asserted the value I had just set and that callModel was called, which is true whether or not the id reaches `.chat()` -- callModel is the seam every other test stubs, so from a unit test there is nothing to see. Deleted rather than kept green, and verified against the running config endpoint instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@kunwp1 — all three follow-ups, plus @Ma77Ball's model-id comment from the first round. The uiParameters leak was real and I reproduced it before touching anything: an So the configuration block is gone rather than better filtered, and with it On the trailing newline, I had reasoned my way into exactly the mirror of the bug I fixed yesterday. I argued that checking the start of the match was enough, then added the end check for the Worth recording how I got there, since it took two tries: my first test called
And @Ma77Ball's point from the first round is addressed too. I deleted the unit test I wrote for that wiring rather than keep it green. It asserted the value I had just set and that Suite: 7011 passing. One ask. Every push puts the workflows back behind first-time-contributor approval, so none of these commits has been through CI — the run that passed was on |
|
@carloea2 Do you have any other comments for this PR? |
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 404 | 0.247 | 23,997/31,115/31,115 us | 🟢 -24.2% / 🔴 +125.9% |
| 🔴 | bs=100 sw=10 sl=64 | 851 | 0.519 | 115,262/133,608/133,608 us | 🔴 +11.1% / 🔴 +43.5% |
| 🔴 | bs=1000 sw=10 sl=64 | 967 | 0.59 | 1,017,263/1,161,385/1,161,385 us | 🔴 +6.0% / 🔴 +31.8% |
Baseline details
Latest main 8f30d82 from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 404 tuples/sec | 402 tuples/sec | 941.58 tuples/sec | +0.5% | -57.1% |
| bs=10 sw=10 sl=64 | MB/s | 0.247 MB/s | 0.246 MB/s | 0.575 MB/s | +0.4% | -57.0% |
| bs=10 sw=10 sl=64 | p50 | 23,997 us | 21,401 us | 10,991 us | +12.1% | +118.3% |
| bs=10 sw=10 sl=64 | p95 | 31,115 us | 41,063 us | 13,776 us | -24.2% | +125.9% |
| bs=10 sw=10 sl=64 | p99 | 31,115 us | 41,063 us | 16,872 us | -24.2% | +84.4% |
| bs=100 sw=10 sl=64 | throughput | 851 tuples/sec | 899 tuples/sec | 1,264 tuples/sec | -5.3% | -32.7% |
| bs=100 sw=10 sl=64 | MB/s | 0.519 MB/s | 0.549 MB/s | 0.772 MB/s | -5.5% | -32.7% |
| bs=100 sw=10 sl=64 | p50 | 115,262 us | 109,959 us | 86,073 us | +4.8% | +33.9% |
| bs=100 sw=10 sl=64 | p95 | 133,608 us | 120,264 us | 93,135 us | +11.1% | +43.5% |
| bs=100 sw=10 sl=64 | p99 | 133,608 us | 120,264 us | 111,124 us | +11.1% | +20.2% |
| bs=1000 sw=10 sl=64 | throughput | 967 tuples/sec | 984 tuples/sec | 1,304 tuples/sec | -1.7% | -25.9% |
| bs=1000 sw=10 sl=64 | MB/s | 0.59 MB/s | 0.601 MB/s | 0.796 MB/s | -1.8% | -25.9% |
| bs=1000 sw=10 sl=64 | p50 | 1,017,263 us | 1,013,672 us | 841,986 us | +0.4% | +20.8% |
| bs=1000 sw=10 sl=64 | p95 | 1,161,385 us | 1,095,955 us | 881,107 us | +6.0% | +31.8% |
| bs=1000 sw=10 sl=64 | p99 | 1,161,385 us | 1,095,955 us | 918,208 us | +6.0% | +26.5% |
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,494.70,200,128000,404,0.247,23996.87,31115.31,31115.31
1,100,10,64,20,2350.32,2000,1280000,851,0.519,115262.35,133607.88,133607.88
2,1000,10,64,20,20683.24,20000,12800000,967,0.590,1017262.62,1161384.90,1161384.90|
The two failing legs are flaky tests, and neither is in this PR's diff:
Different tests on different runners, both outside this PR, is the shape of flakiness rather than a regression — a regression fails in the same place everywhere. Windows passed. It also matches what Locally I ran everything CI runs on the frontend — Could you re-run the two failed jobs? I do not have permission to. If they fail on the same two tests again, that is worth raising with the owners of those specs rather than holding this PR. |
carloea2
left a comment
There was a problem hiding this comment.
Reviewed the latest changes at 7d3ccda. The earlier concerns are addressed through whole-line code replacements, removal of configuration edits and configuration data from the prompt, read-only/live-execution guards, and stable frame tracking. I also checked the model configuration wiring added after Chris's approval. Required CI checks are passing. No additional blocking comments from me.
What changes were proposed in this PR?
Adds an AI Fix tab to the result panel. When a Python UDF operator fails, the user clicks Analyze with AI; the tab sends the error, the operator's code, its input schemas and its configuration to an LLM through the existing LiteLLM proxy (
/api/chat/completions), and shows the proposed fix as a diff with a confidence badge. Apply and re-run patches the operator throughsetOperatorProperty(so it is undoable) and re-executes the workflow; Discard drops it.It handles three error shapes and says so plainly for anything else ("This error type is not yet supported for automatic fixing."):
KeyError: 'col'TypeError: unsupported operandValueError: ... NaN.dropna()or a null guardPython UDFs only, and one kind of fix: a change to
code. An operator without code would cost a model call to reach a refusal, so the tab is not offered there — a JVM operator raisingNullPointerExceptionclassifies, but has nothing Apply could patch. A configuration fix is not offered either: nothing in the repo raises an error one could repair, since the HuggingFace operator retries a 404 as a chat completion and writes "All inference providers failed" into its result rows rather than raising.Classification reads the exception header, which both sources already separate from the stack:
mkConsoleMessageputserr.toStringintitle, the Python worker putsformat_exception[-1]there, and a fatal error carries it inmessagewith the frames indetails. Nothing has to find the exception inside the joined text, which also means a chained traceback arrives already reduced to the one that stopped the worker.What leaves the browser. The error text, the operator's code and every input port's schema. Not the operator's configuration: no fix this panel writes reads it, and filtering it could never be made safe.
withoutSecretsmatched top-level field names, but a Python UDF'suiParametersis a list of attribute/value pairs, so the name says nothing about whether a value is an API key the user typed in — and the list went into the prompt whole. The block is gone rather than filtered harder, which takes the two sanitisers, the operator's JSON schema lookup and theOperatorMetadataServicedependency with it.The model the panel asks for is the alias the deployment's LiteLLM proxy publishes, read from
GuiConfig(gui.workflow-workspace.ai-fix-model, overridable byGUI_WORKFLOW_WORKSPACE_AI_FIX_MODEL), so a proxy serving a different alias does not need a rebuild to be reachable.When the tab appears. A Python UDF exception never becomes a
WorkflowFatalError: the worker pauses and the traceback arrives as an ERRORConsoleMessagewhile the execution staysRunning. The tab is driven by the console stream for that reason, with its own subscription that adds or removes only that entry — a full panel rerender on every console update would reset the Console frame's filter, scroll and debug input and the Result table whenever any operator printed. That decision is made against the frame map itself rather than anything remembered between emissions, becausererenderResultPanelwrites to the same map: a remembered key went stale the moment the rerender dropped the tab, and the next traceback was then read as "no change". The frame tabs are keyed by their config object, sincekeyvaluehands*ngFora fresh pair per entry andNgComponentOutletonly recreates a component when its type changes. It tears itself down when the operator stops reporting an error, and the whole panel resets when a run ends — or, since a UDF that raises leaves the runRunningand never reaches a terminal state, when a newer error arrives on the same operator.What Apply guarantees.
Running, and Texera locks workflow modification for as long as a run is live, so the panel kills the paused execution — which also releases the computing unit it still held. It refuses instead of killing when a live run is not what holds the lock: a read-only workflow locks through a pathenableWorkflowModification()cannot reopen, and running only needs write access to the computing unit, so a read-only user can reach a paused run. Every other refusal is checked before that kill as well as after it, so a fix that was never going to be written does not cost the user their run.ExecuteWorkflowServicewhether the re-run request was sent rather than predicting it, so it never reports "re-running" over a toast saying the run was refused. A deployment can still reject the request after it leaves the browser; that arrives on the execution state stream.Gated on
copilotEnabled, the flag the LiteLLM proxy already checks. The model is called only on an explicit click, never on render.Screenshots
Recorded on Chrome at a 1536x639 viewport, against the current code. The workflow is
TextInput→ Python UDF →Filter→Sort; the UDF readstuple_["lines"]whileTextInputemitsline. The JSON is in the reproduction comment below.The recording runs the whole loop, untouched: Run starts the workflow, the traceback arrives while the execution is still
Running, and the AI Fix tab appears beside Console on its own — the operator is already selected, so nothing but the console stream puts it there. Analyze returns the root cause and a diff with a confidence badge, quoting the whole line with its indentation. Apply and re-run stops the paused execution, writes the fix and re-runs; the pipeline then completes through all four operators and the tab removes itself, because the operator no longer reports an error.The same panel when the error falls outside the three patterns -- here a
ZeroDivisionErrorfrom the same operator. It is reported without calling the model at all, because the classification runs client-side before the request:Any related issues, documentation, discussions?
Closes #8662
Hackathon submission for #8520 (Universidad Panamericana – Apache Texera Hackathon).
How was this PR tested?
Unit tests: 295 across the result-panel specs — 57 for the service, 23 for the frame, 4 for the prompt builder, 69 for the result panel itself. Every fix in this PR was confirmed failing against the code before it, which caught eight tests of mine that passed either way and had to be rewritten or deleted.
Whole frontend suite: 7011 passing across 247 files (1 skipped). Everything CI runs on the frontend was run locally too:
yarn format:ci,yarn build:ci(clean production AOT build),yarn test:ci, andng run gui:test-browser(23 passing in Chromium).End to end on a local stack (Windows, native services, LiteLLM +
claude-haiku-4.5), as recorded above: the Python worker raisedKeyError: 'lines'with the execution stillRunning; Analyze returned- title = tuple_["lines"]/+ title = tuple_["line"]with high confidence, correctly citing the input schema; Apply patched the operator and the re-run reachedCompletedwith rows through both downstream operators.The prompt's whole-lines rule was measured rather than assumed: 28 of 28 replies from
claude-haiku-4.5were accepted across the three code-change shapes, against 11 of 12 for the previous prompt, whose single refusal is exactly the mid-line snippet the rule exists for.Was this PR authored or co-authored using generative AI tooling?
Yes. Generated-by: Claude Code (Claude Opus 5)