Repository navigation
feat(frontend): add AI workflow fixer with LLM-powered diff panel - #8637
pedroreyesalvarado wants to merge 38 commits into
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.
| private workflowCompilingService: WorkflowCompilingService, | ||
| private workflowConsoleService: WorkflowConsoleService | ||
| ) { | ||
| this.state$ = aiWorkflowFixerService.getState$(); |
There was a problem hiding this comment.
The frame binds the shared service state without checking it belongs to this frame's operator, so a suggestion analyzed for one operator can be shown and applied under another.
Why it matters: AiWorkflowFixerService is a root singleton with one stateSubject, and the template renders state$ | async with no comparison to @Input() operatorId. Concrete sequence: two Python UDFs in a pipeline both raise an ERROR console message, so both get an AI Fix tab. Analyze operator A -> state = {operatorId: A, status: ready, suggestedFix: fixA}. Select operator B: the panel rebuilds the frame with operatorId = B, but state$ still holds A's ready suggestion, so B's tab shows A's root cause and diff. Clicking Apply calls applyFix(), which reads current.operatorId (= A) and patches operator A while the user is looking at B, then re-runs. The write is undoable but targets the wrong operator.
Suggested fix: gate the ready/diff UI and the Apply/Discard actions on state.operatorId === operatorId (expose a guarded observable, or reset the service to idle when the bound operatorId differs from state.operatorId). This is a multi-spot change across the component and template, not a one-click edit. Worth a spec that analyzes one operator and asserts a second operator's frame does not show or apply it.
| @@ -0,0 +1,92 @@ | |||
| { | |||
There was a problem hiding this comment.
Please drop this demo-workflows folder; it doubles the PR's line count, and I don't see any references to it. This also adds another level of complexity to maintaining the codebase and this feature.
| [/KeyError:\s*['"][^'"]+['"]/, "missing_column"], | ||
| [/TypeError:\s*unsupported operand/i, "type_error"], | ||
| [/ValueError:[^\n]*\bNaN\b|NullPointerException/i, "null_error"], | ||
| [/ModelNotFound|\b404\b/i, "model_not_found"], |
There was a problem hiding this comment.
The model_not_found pattern matches a bare 404 anywhere in the traceback via \b404\b.
Why it matters: a traceback that merely contains 404 (a line number, an HTTP status from unrelated user code, a literal in the data) is classified as model_not_found and sent to the model asking it to correct the model config field, producing an irrelevant property_change suggestion. Low harm (one wrong call), but the false-positive surface is wide.
Suggested fix: tie 404 to the model-lookup context rather than matching it standalone (require it alongside a model / not-found token, or match the specific message shape LiteLLM returns). Prose only: I do not know the exact upstream error strings to give a precise regex.
| } | ||
|
|
||
| // Model id as LiteLLM exposes it (bin/single-node/litellm-config.yaml), not the provider's id. | ||
| export const AI_FIXER_MODEL = "claude-haiku-4.5"; |
There was a problem hiding this comment.
AI_FIXER_MODEL is hard-coded to the LiteLLM alias claude-haiku-4.5. I recommend sourcing the model ID from GuiConfig/env rather than using a compile-time constant.
|
@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. |
The sequence guard only covered starting another analysis, but the state is replaced in four places. Clearing the panel published IDLE without bumping it, so a reply that was already in flight still matched and repainted a suggestion the panel had cleared -- reachable from the toolbar by analysing a failed operator and then stopping the run before the model answers. Both clearing paths now go through one reset that supersedes whatever is in flight. Discard is not reachable mid-analysis today, since that button only exists once a suggestion is on screen, but it is the same contract and it is cheaper to hold it in one place than to rely on the template keeping it true. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@kunwp1 Hi Chris, a quick reminder to revisit this PR when you have a chance. The author posted follow-up fixes and a response to your review on October 9, and the current head is e7c0d7b. Could you take another look and let us know whether your concerns are resolved or anything still blocks merging? Thanks! |
|
Thanks for the latest fixes. Could you check that suggestions and pending Apply operations are invalidated when the user switches workflows? At This still needs browser verification; I have not confirmed the full navigation sequence end to end. Could you try two imports of the same workflow JSON and, if confirmed, bind analysis/Apply to the workflow session, invalidate them on a switch, and recheck the session after the asynchronous unlock wait? A regression test should verify that a response or Apply from A cannot modify or rerun B. |
|
Could you check the retry path after an Apply failure? At I reproduced this with the actual service code and mocked dependencies: analyze a code fix, change the target snippet so Apply fails, restore the original snippet, then click Apply again. The second call silently returns without writing or rerunning. Could you either accept |
|
Could you check structured configuration values in the property-fix path? The new field-name validation and number/boolean handling help, but at In an isolated service reproduction using a mocked model response, Could you restrict property fixes to supported fields/types, or parse and validate structured replacements against the operator's JSON schema before writing? Please cover an invalid array/object replacement leaving the graph unchanged and not rerunning, alongside a valid supported property fix. |
Three findings from @carloea2, each reproduced against the service before the fix and confirmed failing without it. A suggestion outlived the workflow it was made for. The service is a root singleton and only tracked the operator id, but two imports of the same JSON carry the same ids, so a reply that landed after the user switched workflows could be applied to the wrong graph. The state now carries the workflow it belongs to, checked when the reply lands, before the write, and again after the unlock wait -- that wait can take seconds, and the user is free to navigate during it. Apply did nothing after a failed apply. The panel keeps the suggestion and its Apply button in `apply_failed`, but applyFix returned unless the status was `ready`, so the button was there and silently did nothing. It is retryable now; every check below it re-reads the operator and runs again, so a retry is not a shortcut. A structured setting became a string. coerceToFieldType covered numbers and booleans, and anything else fell through to the raw string -- naming an array field replaced it with the text "[]". Arrays and objects are refused: validating a replacement against the operator's schema is a larger change than the four error shapes here need. Also stubs OperatorMetadataService in the service spec. It fetches operator metadata from its constructor, so injecting it for the credential filter left every TestBed issuing a live request; with enough tests the suite stopped reporting at all rather than failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ng late Two findings from auditing what the previous commit touched rather than from the review. The frame still scoped by operator id alone. Refusing the write in applyFix was half the fix: the tab went on rendering a suggestion that belonged to another workflow, so the user saw a diff for a graph they had left and only found out by clicking Apply and getting an error. It now renders nothing unless the state names both this operator and the open workflow. An apply published over whatever replaced it. The unlock wait can take seconds, and any other failed operator's frame shows idle with a live Analyze button during it, so a fix applied on one operator announced itself on top of an analysis the user had since started elsewhere. The apply now reads the analysis counter when it begins and stays quiet if the panel has moved on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks @carloea2 — all three are fixed in 55f5ccd and 88b22d9, each reproduced against the service before the fix and confirmed failing without it. Suggestion bound to its workflow. You were right that the operator id alone does not identify the context, and the reproduction you described holds. Retry after a failed apply. This was the worst of the three and entirely my doing: I added Structured configuration values. Arrays and objects are refused rather than parsed. Validating a replacement against the operator's JSON schema is a larger change than the four error shapes here need, and none of them touches a structured field, so refusing with a clear message beats guessing. Your review also prompted me to audit what my own fixes touched, which turned up two more: The frame still scoped by operator id alone. Refusing the write was only half of your first finding — the tab went on rendering a suggestion from another workflow, so the user saw a diff for a graph they had left and only learned otherwise by clicking Apply. Fixed in 88b22d9; the frame now renders nothing unless the state names both this operator and the open workflow. An apply reported over whatever replaced it. During the unlock wait, any other failed operator's frame shows idle with a live Analyze button. Start an analysis there and the earlier apply announced "Fix applied" on top of it. The apply now reads the analysis counter when it begins and stays quiet if the panel has moved on. One more worth mentioning because it explains a gap in what I could tell you before: the service spec had been injecting the real I also chased three further suspicions and they are all unreachable: the occurrence count going stale, the panel being destroyed mid-apply, and the operator being deleted during an analysis. All three are protected by the same thing — workflow modification is locked for as long as a run is live, which is exactly the window between analysing a traceback and applying the fix. I mention it in case it saves you reproducing them. 144 specs across the result panel, 6701 across the frontend, lint and production build clean. The branch is current with main. |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
5 open issues
applyFix()is async; calling it withoutawait/voidcan violate commonno-floating-promises… · New The docstring says “run actually started”, but the implementation returnstrueimmediately after… · New The docstring says “run actually started”, but the implementation returnstrueimmediately after… · Newserifis a poor fallback for code/diff rendering and can change alignment;monospaceis the… · New The comment is duplicated verbatim, which makes the block look like an accidental copy/paste.… · New
What changed in this PR
Adds an AI Fix tab to the Result Panel that can classify certain Python UDF failures, request a single minimal fix from an LLM, present it as a diff with confidence, and apply it back to the operator (undoable) with an optional re-run signal from ExecuteWorkflowService.
Changes:
- Add
AiWorkflowFixerService+ prompt builder + AI Fix frame UI, plus Result Panel wiring to show/hide the tab based on console ERROR messages while execution remainsRunning. - Change
ExecuteWorkflowService.executeWorkflow*entry points to return a boolean indicating whether local preconditions allowed the run request to be sent. - Add extensive unit tests around classification, prompt redaction, diff display, apply semantics, and Result Panel tab toggling.
| File | Description |
|---|---|
| frontend/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts | Update mocks to match executeWorkflow() now returning boolean. |
| frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.ts | Change executeWorkflow* to return boolean and document semantics. |
| frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts | Update delegate spy return type for the new boolean signature. |
| frontend/src/app/workspace/component/result-panel/result-panel.component.ts | Add AI Fix frame registration + console-driven toggling; fix display-name subscription accumulation. |
| frontend/src/app/workspace/component/result-panel/result-panel.component.spec.ts | Add coverage for console-driven AI Fix tab behavior and display-name handler wiring. |
| frontend/src/app/workspace/component/result-panel/ai-fix/ai-workflow-fixer.service.ts | Implement classify/analyze/apply workflow with schema-aware secret stripping, staleness guards, and indentation-safe replacements. |
| frontend/src/app/workspace/component/result-panel/ai-fix/ai-workflow-fixer.service.spec.ts | Add thorough unit tests for classifier, prompt safety, concurrency/staleness, apply behavior, and indentation logic. |
| frontend/src/app/workspace/component/result-panel/ai-fix/ai-fix-prompt.ts | Build prompt and redact secrets; format multi-port schemas. |
| frontend/src/app/workspace/component/result-panel/ai-fix/ai-fix-prompt.spec.ts | Add tests for redaction and schema formatting. |
| frontend/src/app/workspace/component/result-panel/ai-fix/ai-fix-frame.component.ts | Add UI controller for Analyze/Apply/Discard and operator-scoped rendering. |
| frontend/src/app/workspace/component/result-panel/ai-fix/ai-fix-frame.component.spec.ts | Add component tests for rendering, operator/workflow scoping, and button behavior. |
| frontend/src/app/workspace/component/result-panel/ai-fix/ai-fix-frame.component.scss | Style diff/panel UI for the new tab. |
| frontend/src/app/workspace/component/result-panel/ai-fix/ai-fix-frame.component.html | Add AI Fix tab UI (diff view, status alerts, confidence badge). |
| frontend/src/app/workspace/component/menu/menu.component.spec.ts | Update mocks for executeWorkflowWithEmailNotification() now returning boolean. |
Review effort: Lite
…p a stale result Three findings from @kunwp1's latest review, each one wider than it first looked. Credentials could still reach the panel. withoutSecrets kept them out of the prompt, but toFix read the field's current value from the raw properties, so a guessed `hfApiToken` put the secret on screen in the diff before anyone clicked Apply, and the apply path validated against those same raw properties. Both now work from one redacted view, and a property change is only accepted for the model-not-found case, which is the only error the prompt asks one for. JVM errors never reached the model. ErrorUtils.mkConsoleMessage puts `err.toString()` in `title` and only the stack frames in `message`, and the frame read `message` first -- so for a JVM operator the model saw a bare stack trace and `NullPointerException` could not match anything. Both parts are sent now, title first. Fixing that exposed a second half: the frame filter only knew Python's `File "..."` shape, so a Java frame passed for the exception line and the classifier read it instead. It now recognises both. A stale "Fix applied" survived a failed re-run. Resetting on terminal execution states misses the case this panel exists for: a UDF that raises leaves the run `Running`, so a re-run that fails again never reaches a terminal state and the tab came back announcing a re-run over a run stuck on an error. The state now remembers which ERROR it was made for, and a newer one on that operator clears it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rest of @kunwp1's review, plus @Copilot's five. Two suggestions were offered that Apply could never honour. A read-only workflow locks modification through a path enableWorkflowModification() cannot reopen, and Running only needs write access to the computing unit -- so a read-only user could reach a paused run, have it killed on their behalf, and wait fifteen seconds to be told it would not stop. The lock is now refused up front unless a live execution is what holds it, since killing one is the only lock this can clear. Separately, a snippet the model invented counted zero occurrences, and the panel only surfaces that count when it is above one: the suggestion looked ordinary and Apply turned it away as a stale snippet, blaming an edit that never happened. The analysis now reports it instead. replaceAllIndented indented twice. A match that spans lines already carries the absolute indentation of the region it came from, and the model's reply mirrors it, so prefixing the context indentation on top doubled it. Only a single-line match needs the adjustment, which is the case the function was written for. The UDF source reached the model twice -- fenced under "Operator code" and JSON-escaped inside the configuration, because `code` is a property like any other. The escaped copy is the one no snippet can be quoted from. `unsupported` is a status rather than a status plus an errorType, so the template reads one field to decide one thing and IDLE_STATE carries no placeholder classification. The frame tabs are keyed by name: keyvalue hands *ngFor a fresh pair object per entry, so without trackBy every tab rebuilt whenever one was added -- which is the rerender the narrowed console subscription exists to avoid. From @Copilot: onApply drops its promise explicitly, the diff falls back to monospace rather than serif (the +/- columns stop lining up without it), executeWorkflow's contract says the request was sent rather than that the run started, and a comment block appeared twice. Comments that narrated what the code used to do now state what it does and which hazard the shape avoids. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by self-audit, same class as the invented-snippet finding. `original` for a property change is the field's CURRENT value, read so the diff has a "before". Naming a field the operator does not have rendered an empty one, and naming `code` rendered the entire UDF source as a single unwrapped line in a `white-space: pre` box. Apply refused both afterwards, which is too late: the panel had already shown it. The analysis rejects both now, so no suggestion that cannot be written reaches the screen. The apply-side check stays as the second barrier, and is now reachable only when a field present at the analysis is gone by the time Apply re-reads the properties. Its test asserted exactly that -- and passed with the check deleted, because the staleness guard covered the case on its own: an absent field reads as "" and no longer matched the captured original. With the field empty at the analysis both read "" and only the presence check can refuse, so the test now fails without it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@kunwp1 — all ten, plus the five from @Copilot. Three were in Read-only workflows. You were right that this was worse than a missed check. Indentation was applied twice. A match that spans lines already carries the absolute indentation of the region it came from, and the model's reply mirrors it, so prefixing the context indentation on top doubled it. Only a single-line match needs the adjustment, which is the case the function was written for. Zero occurrences. The panel only surfaces that count when it is above one, so a snippet the model invented was indistinguishable from any other suggestion — and Apply then turned it away as a stale snippet, blaming an edit that never happened. The analysis reports it now instead of offering an Apply that cannot work. That one made me look at the rest of the apply path for the same shape, and there was a second instance. That also turned up a tautological test of mine. The apply-side field check had a test that passed with the check deleted, because the staleness guard covered the case on its own: an absent field reads as
The state model.
I should be straight about one thing: this one has no test. I tried to assert in the DOM that the tabs survive, but the loop only renders with the panel open, and that mounts the real frames — jsdom dies in Comments that narrated history now state what the code does and which hazard the shape avoids, and the From @Copilot: On the font: Also worth flagging: Copilot's review did not finish — "couldn't run its full agentic review because it didn't start before the timeout", then "the job was not acquired by a runner even after multiple attempts". So those five are a partial pass, not a clean one. Re-request it if you want the full sweep; I would rather it find something now than after a merge. Frontend suite: 6745 passing, 290 of them across the result-panel specs (57 service, 24 frame, 8 prompt builder, 67 result panel). |
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 92.33% 92.33% -0.01%
Complexity 5045 5045
============================================
Files 1261 1265 +4
Lines 54120 54440 +320
Branches 6768 6838 +70
============================================
+ Hits 49973 50266 +293
- Misses 2486 2498 +12
- Partials 1661 1676 +15
*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.
| export function startsAtLineBoundary(code: string, original: string): boolean { | ||
| let from = 0; | ||
| for (;;) { | ||
| const at = code.indexOf(original, from); | ||
| if (at === -1) { | ||
| return true; | ||
| } | ||
| if (at !== 0 && code[at - 1] !== "\n") { | ||
| return false; |
There was a problem hiding this comment.
This checks that each match starts a line but not that it ends one. So a whole line can still match the beginning of a longer line. For example, an original of n = 1 in code containing n = 10 passes the check with a count of 1 so no badge appears. "Apply" then turns that line into n = 20 while the diff shows only n = 1. Requiring each match to also end at a newline (or the end of the code) could fix this issue.
| [/KeyError:\s*['"][^'"]+['"]/, "missing_column"], | ||
| [/TypeError:\s*unsupported operand/i, "type_error"], | ||
| [/ValueError:[^\n]*\bNaN\b|NullPointerException/i, "null_error"], | ||
| [/ModelNotFound|\bmodel[^\n]*\b404\b|\b404\b[^\n]*\bmodel/i, "model_not_found"], |
There was a problem hiding this comment.
I couldn't find an operator where two of these rows can finish end to end:
model_not_found. The HuggingFace operator never raises for an unknown model. It writes "All inference providers failed" into its result rows, so there's no ERROR message and no tab. A malformed id raisesValueError: Invalid Hugging Face model ID ..., which none of these patterns match. In a Python UDF the model name lives incode, but the prompt tells the model to change a configuration field, which the new analysis check then refuses.NullPointerException. It now classifies, but a JVM operator other than a UDF has nocodeand no allowed configuration change, so Analyze can only end in an error after a model call.
Could the supported list (and when the tab is offered) be narrowed to what can actually be fixed today? Alternatively, the model rule could allow a code change when the operator has code.
| } else { | ||
| // Only the model-not-found case asks for a configuration change; a property fix for | ||
| // any other error is the model leaving the four shapes this panel supports. | ||
| if (current.errorType !== "model_not_found") { |
There was a problem hiding this comment.
Now that analysis refuses a configuration change for anything but model_not_found, this check can't pass.


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 four 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/NullPointerException.dropna()or a null guard404beside a model referenceClassification reads the final exception line of the traceback, not the whole message: the frame lines carry paths and line numbers that look like patterns on their own, and a chained traceback reports the handled exception before the one that stopped the worker.
What leaves the browser. The error text, the operator's code, every input port's schema, and its configuration minus any credential. Operators keep secrets beside ordinary settings —
hfApiTokenon HuggingFaceInference,passwordon the SQL, Reddit and Twitter sources — so those fields are dropped before the prompt is built, behind two independent barriers: the operator's own schema marks them with the password widget, and a name pattern catches a schema that cannot be read or an operator missing the annotation. The UDF source is sent once, fenced under its own heading:codeis a property like any other, so without dropping it from the configuration block the whole operator source travelled twice, and the JSON-escaped copy is the one no snippet can be quoted from.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. The frame tabs are keyed by name for the same reason, becausekeyvaluehands*ngFora fresh pair object per entry: without that the narrowed subscription still rebuilt every tab whenever one was added. 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.
codeitself are all refused at the analysis, not at the click. For a property change the diff's "before" is the field's current value, so those would otherwise render an empty diff — or, forcode, the whole UDF source as one unwrapped line.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.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 four 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: 297 across the result-panel specs — 63 for the service, 24 for the frame, 8 for the prompt builder, 68 for the result panel itself. Every fix in this PR was confirmed failing against the code before it, which caught six tests of mine that passed either way and had to be rewritten or deleted.
Whole frontend suite: 6752 passing across 240 files (1 skipped). No substantive CI job has run on this branch — only the lightweight checks execute, the rest are still waiting on first-time-contributor approval — so everything CI runs on the frontend was run locally instead:
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)