Skip to content

feat(agent-service): add stateless /recommend endpoint (V1 hardcoded) - #6436

Open
gupta-sahil01 wants to merge 4 commits into
apache:mainfrom
gupta-sahil01:feat/operator-recommender-backend
Open

gupta-sahil01 wants to merge 4 commits into
apache:mainfrom
gupta-sahil01:feat/operator-recommender-backend

Conversation

@gupta-sahil01

@gupta-sahil01 gupta-sahil01 commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

Backend half (PR 1 of 2) of the ambient operator recommender, Version 1 — see #6293 (design discussion #5240).

Adds a stateless POST /api/recommend endpoint to agent-service: given the operator a user just added to the canvas, it returns a short ranked list of likely next operators for the frontend to render as suggestion chips.

  • Stateless, zero API cost: V1 ranks from a small hardcoded rule table — no LLM call, no persistent state, no user token required.
  • Catalog-validated: suggestions are filtered against the live operator catalog (WorkflowSystemMetadata) when available, so a stale rule degrades to "not suggested" rather than a broken suggestion.
  • Forward-compatible: the response carries a strategy discriminator ("hardcoded" / "llm") so V2 can swap in a small-LLM ranker behind the same request/response shape without a breaking change.

The frontend that consumes this endpoint (suggestion rendering, opt-in flag, click-to-materialize) follows in PR 2.

Any related issues, documentation, discussions?

How was this PR tested?

bun test in agent-service/ — 131 passed, including new src/recommender/recommender.spec.ts and src/server.recommend.spec.ts covering ranking order, catalog validation, terminal sinks (no suggestions), limit clamping, and 400s on malformed/empty input. Also verified live: POST /api/recommend with {"operatorType":"CSVFileScan"} returns the expected ranked JSON.

Was this PR authored or co-authored using generative AI tooling?

Generated-by: Anthropic Claude (Claude Code)

@github-actions

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • Contributors with relevant context: @bobbai00
    You can notify them by mentioning @bobbai00 in a comment.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Thanks for your first contribution to Texera, @gupta-sahil01!

If you're looking for a good place to start, browse issues labeled starter-task; they're scoped to be approachable for newcomers.

You can drive common housekeeping yourself by commenting one of these commands on its own line:

  • Issues. Comment /take to assign an open issue to yourself, or /untake to release it. You can find unclaimed work with the search filter is:issue is:open no:assignee.
  • Sub-issues. To link issues into a parent/child hierarchy, comment /sub-issue #5166 #5222 on the parent to attach those children (or /unsub-issue #5166 #5222 to detach them). From a child issue, comment /parent-issue #5166 to set its parent, or /unparent-issue to clear it (the current parent is detected automatically). References may be written as #5166 or as a bare 5166; cross-repository references are not supported.
  • Pull requests (author only). Comment /request-review @user to request a review from someone, or /unrequest-review @user to withdraw that request.

Each command must match exactly: /take this will not work, only /take does. For the full contribution flow, see CONTRIBUTING.md.

@kz930

kz930 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Really clean V1 — the hardcoded/strategy-discriminator split and the catalog validation are a nice way to ship the whole pipeline at zero API cost. Two forward-looking notes, both about what the endpoint
knows before V2 lands:

  1. Validation checks existence, but not connectability.
    operatorTypeExists guarantees the ghost is a real operator, which is great for not suggesting broken types. It doesn't yet check that the suggested operator can actually be wired to the just-added operator's
    output. Two levels here:
  • Arity is checkable in V1 today: catalog metadata knows a target's input-port count, so an operator that needs two inputs (e.g. a join) can't be single-wired as a "next" step. Right now this is avoided by
    hand-curating the successor lists (no joins/unions appear as successors), but nothing enforces it — a future rule edit could reintroduce an un-connectable suggestion.
  • Schema/type compatibility can't be done in V1, since the request only carries the operator type, not the output schema. Worth flagging as a V2 need (and it ties into the next point).
  1. The reserved context field may be thinner than its stated purpose.
    Nice call reserving existingOperatorTypes so V2 can add graph context without a request-shape change. One thing to reconsider before V2 builds on it: a flat list of operator types loses the topology (what's
    actually upstream of the added operator) and the output-port schema — which is most of what an LLM would need to "bias with graph context." When V2 lands, capturing the upstream path / operator id + the
    output schema (rather than just the set of types present) would likely serve the ranker better — and it's the same information point 1's schema check would want, so the two converge.

Neither blocks V1 — just wanted to surface them while the request shape is still easy to evolve.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment thread agent-service/src/server.ts Resolved
Comment thread agent-service/src/server.ts Resolved
@codecov-commenter

codecov-commenter commented Jul 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.26590% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.67%. Comparing base (39db110) to head (b3a6f15).
⚠️ Report is 142 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
agent-service/src/server.ts 85.00% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6436      +/-   ##
============================================
+ Coverage     70.57%   70.67%   +0.10%     
  Complexity     3425     3425              
============================================
  Files          1146     1149       +3     
  Lines         45062    45235     +173     
  Branches       4960     4960              
============================================
+ Hits          31802    31972     +170     
- Misses        11613    11616       +3     
  Partials       1647     1647              
Flag Coverage Δ *Carryforward flag
access-control-service 70.00% <ø> (ø) Carriedforward from 39db110
agent-service 77.91% <98.26%> (+1.15%) ⬆️
amber 66.72% <ø> (ø) Carriedforward from 39db110
computing-unit-managing-service 17.72% <ø> (ø) Carriedforward from 39db110
config-service 52.30% <ø> (ø) Carriedforward from 39db110
file-service 66.80% <ø> (ø) Carriedforward from 39db110
frontend 69.36% <ø> (ø) Carriedforward from 39db110
notebook-migration-service 78.94% <ø> (ø) Carriedforward from 39db110
pyamber 91.74% <ø> (ø) Carriedforward from 39db110
workflow-compiling-service 55.14% <ø> (ø) Carriedforward from 39db110

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chenlica
chenlica requested review from xuang7 and zuozhiw July 18, 2026 04:05
@chenlica

Copy link
Copy Markdown
Contributor

@xuang7 and @zuozhiw : can you review it?

@chenlica

Copy link
Copy Markdown
Contributor

@gupta-sahil01 Can you change the word "ghost" to a better name?

@gupta-sahil01

Copy link
Copy Markdown
Contributor Author

@gupta-sahil01 Can you change the word "ghost" to a better name?

Yes professor, will do

Ambient operator recommender, Version 1 (apache#5240). Adds a
stateless POST /api/recommend that, given the operator a user just added,
returns a short ranked list of likely next operators for the canvas to
render as ghost suggestions.

V1 ranks from a hardcoded rule table (no LLM call, no persistent state),
so it ships and validates the full pipeline at zero API cost, per the
discussion's V1/V2 split. Suggestions are validated against the live
operator catalog (WorkflowSystemMetadata) when available, so a stale rule
degrades to "not suggested" rather than a broken ghost. The response
carries a `strategy` discriminator ("hardcoded") so V2 can swap in a small
LLM behind the same request/response shape without a breaking change.
@gupta-sahil01
gupta-sahil01 force-pushed the feat/operator-recommender-backend branch from bda9360 to b95928d Compare July 18, 2026 04:27

@xuang7 xuang7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. Generally LGTM. For the hardcoded rules, consider adding RATIONALE entries for the operators that currently fall back to the generic reason. Also, please resolve the Copilot comments where helpful.

@xuang7

xuang7 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Thanks for addressing the comments. I saw that Copilot left a few comments. Please address them if they are helpful.

…dling

- Reject whitespace-only operatorType (schema now requires a non-whitespace
  character) instead of returning 200 with an empty list; add a regression test.
- Return a generic message on 500 responses (the real error is still logged)
  to avoid leaking internal details.
- Relax the OperatorRecommendation docstring to match actual behavior
  (suggestions are validated against the catalog only when it is available).
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has
not had any activity for 30 days. It will be closed in 7 days if no
further activity occurs.

If this PR is still relevant, please:

  • Push new commits or rebase onto the latest main, or
  • Comment with a status update, or
  • Address any open review feedback.

To exempt this PR from auto-close, a maintainer can add the pinned label.
Thank you for your contribution to Apache Texera (incubating)!

@github-actions github-actions Bot added the stale label Aug 31, 2026
@gupta-sahil01

Copy link
Copy Markdown
Contributor Author

/request-review @xuang7

@github-actions
github-actions Bot requested a review from xuang7 September 3, 2026 00:19
mengw15 added a commit to mengw15/texeraFork that referenced this pull request Sep 3, 2026
The head was stale, and so is `base.sha`: it is main's tip at the pull
request's last synchronize, not now. For any PR nobody has pushed to
since apache#6941 landed, it predates release-branches.yml and its parser, so
the `run:` step exits 2 under `bash -eo pipefail` and this required check
goes red with a file-not-found that no approval or unlabel can clear.
Measured: 33 of the 221 open pull requests into main have such a base —
apache#6009's is dated 2026-07-10, apache#6436's 2026-07-18, both still active today.

This is not a prediction. backport-auto-label.yml pins the same ref and
has been failing exactly this way in production for a month (run
31542186218, 2026-08-11), survivable only because that check is not
required.

Read the default branch instead, which always has the config and is what
Direct Backport Push already reads — so both consumers judge a backport by
the same file. It also means a PR retargeted onto a release branch, where
the config does not exist, reaches the report instead of dying before it.

Also corrects the header comment: since the context moved into its own
`~DEFAULT_BRANCH`-only ruleset, a PR into a release branch is no longer
waiting on it. The reason not to condition on the base branch is simply
that a backport is not itself backported onward.
@github-actions github-actions Bot removed the stale label Sep 7, 2026
@gupta-sahil01

Copy link
Copy Markdown
Contributor Author

Any updates here?

@Yicong-Huang

Copy link
Copy Markdown
Contributor

@Xiao-zhen-Liu can you help on review this PR? it is a good feature

@Xiao-zhen-Liu Xiao-zhen-Liu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together! Starting with a deterministic V1 rule table and live catalog validation is a clean way to validate canvas suggestions without inference cost.

Across the PR, the endpoint path, module folder, filenames, and request and response types all claim generic recommendation names, whereas this feature only suggests next operators. Because Texera and agent-service may later recommend datasets, workflows, links, or operator settings, could we scope the route, directory, files, and types specifically to operator recommendation (matching #6437)? Merging main will also trigger CI, which did not run on the latest commit.

// Never suggest the operator itself, and never suggest the same type twice.
if (candidate === operatorType || seen.has(candidate)) continue;
// Drop suggestions the running system can't actually instantiate.
if (catalogReady && !catalog!.operatorTypeExists(candidate)) continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main has removed this existence helper from the operator catalog (#9239). Once merged with main, typechecking fails and every request returns 500 after startup loads the catalog (for example, requesting suggestions after adding CSV File Scan throws because the method is missing). Could we merge main and check existence through what the catalog still exposes, such as looking up the operator's schema?

// hardcoded rule table with no LLM call, so it needs no user token and no
// per-request model configuration; it only reads the process-wide operator
// catalog to validate and label its suggestions.
const recommendRouter = new Elysia({ prefix: "/recommend" })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On its own, this PR leaves the new endpoint unreachable from the browser: the frontend dev proxy, single-node web server config, and Kubernetes gateway only forward the existing agents route to the agent service (#6437 adds the first two, while the gateway route was dropped in a merge conflict). Could we wire all three routes here, using a separate Kubernetes route so stateless calls bypass workflow-header consistent hashing?

// Stateless ambient operator recommender (apache/texera#5240). Given the
// operator a user just added, returns a short ranked list of likely next
// operators for the canvas to render as suggestions. Version 1 uses a
// hardcoded rule table with no LLM call, so it needs no user token and no

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this endpoint unauthenticated will force a breaking change in V2: the agent service holds no LLM key of its own and forwards the signed-in user's Bearer token to the LLM gateway (#5421), while the Kubernetes gateway does not enforce auth on agent-service routes. Because the workflow editor already attaches the user's Bearer token on HTTP requests, could we validate it here just like agent creation does so V2 needs no auth contract change?

set.status = 400;
return { error: "operatorType must be a non-empty string" };
}
return recommendOperators({ ...req, operatorType }, WorkflowSystemMetadata.getInstance());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the agent service starts before the backend is ready, startup logs a warning and leaves the catalog uninitialized. Chat agents retry loading the catalog on creation, whereas this handler keeps reading the uninitialized catalog on every request, returning unvalidated operator types with no display names until someone creates a chat agent. Could we lazily retry initializing the catalog here when it is not ready yet?

* The operator type that was just added and whose output port we are
* suggesting successors for (e.g. `"CSVFileScan"`). Primary ranking signal.
*/
operatorType: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither the request nor each returned suggestion identifies a port, which forces the canvas to hardcode the first output port and first input port. That breaks multi-output operators like Split and multi-input targets like Sklearn Prediction, whose first input port expects a trained model while its second expects data rows. Could we include the source output port in the request and the target input port on each suggestion so multi-port operators work without a future contract break?

import type { RecommendationResponse } from "./recommender/recommender-types";

const API = env.API_PREFIX;
const app = buildApp();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These route tests never initialize the real operator catalog, so catalog validation and display-name lookup are only exercised against the unit-test stub (which hid the removed catalog method on main). Could we add a route test with an initialized catalog fixture so mismatches with the real catalog class are caught?

return { error: errorMessage || "Internal server error" };
})
.post(
"/",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Prettier fails on the 5-space indentation in this handler and in the whitespace test in the route spec. Could we run the formatter before pushing?

PostgreSQLSource: "source",
MySQLSource: "source",
AsterixDBSource: "source",
SQLSource: "source",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This entry refers to an abstract base descriptor rather than a registered operator (and Parallel CSV File Scan above is disabled in the catalog). Could we keep only concrete registered operators in this table?

/** Whether an operator is a terminal visualization/sink with no useful successor. */
export function isSink(operatorType: string): boolean {
if (EXTRA_SINKS.has(operatorType)) return true;
if (OPERATOR_CATEGORY[operatorType] === "sink") return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: None of the entries in the category map above use the sink label, so this check never matches. Could we drop it?

* heuristic. Operators absent here fall back to {@link inferCategory}.
*/
const OPERATOR_CATEGORY: Record<string, OperatorCategory> = {
// Sources — read data into the workflow.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could we replace the em-dashes in the comments across these files with colons or parentheses to match project style?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants