Yicong-Huang commented on code in PR #6437:
URL: https://github.com/apache/texera/pull/6437#discussion_r3741382850
##########
bin/k8s/templates/base/gateway/gateway-routes.yaml:
##########
@@ -154,6 +154,11 @@ spec:
- path:
type: PathPrefix
value: /api/agents
+ # Ambient operator recommender endpoint (apache/texera#5240) is served
Review Comment:
Worth settling the auth posture now rather than after V2. For V1 leaving it
open is defensible: it matches the `/api/agents` sibling, #6293 specifies no
user token, and the payload is operator-type strings with no user-owned entity.
But #6293's V2 puts a LiteLLM-backed model behind this same path, and an
anonymous LLM endpoint is a cost and abuse surface. Is the plan to add auth
when V2 lands?
##########
common/config/src/main/resources/gui.conf:
##########
@@ -108,6 +108,10 @@ gui {
copilot-enabled = false
copilot-enabled = ${?GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED}
+ # whether the ambient operator recommender (ghost next-operator
suggestions) is enabled
+ operator-recommendation-enabled = false
+ operator-recommendation-enabled =
${?GUI_WORKFLOW_WORKSPACE_OPERATOR_RECOMMENDATION_ENABLED}
Review Comment:
`GUI_WORKFLOW_WORKSPACE_OPERATOR_RECOMMENDATION_ENABLED` is absent from
`bin/k8s/values.yaml`, `values-development.yaml`, and `bin/single-node/.env`.
config-service's pod env renders strictly from `.Values.texeraEnvVars`
(`config-service-deployment.yaml:41-52`), so an unlisted var never reaches the
container and the flag keeps its `false` default.
Every sibling GUI flag is listed — `COPILOT_ENABLED` at `values.yaml:344`,
`values-development.yaml:341`, `.env:88` — so this reads as a gap rather than a
deliberate omission.
##########
frontend/proxy.config.json:
##########
@@ -10,6 +10,11 @@
"secure": false,
"changeOrigin": true
},
+ "/api/recommend": {
Review Comment:
The dev proxy is covered, but `bin/single-node/nginx.conf` is not. It routes
`/api/agents` to `agent-service:3001` (line 73) and has no `/api/recommend`
location, so longest-prefix matching hands the call to `location /api/` and
`dashboard-service:8080` (line 91).
With `catchError(() => of([]))` swallowing the result, the feature is
silently inert in single-node docker with the flag on — no console error to
explain it.
##########
bin/k8s/templates/base/gateway/gateway-routes.yaml:
##########
@@ -154,6 +154,11 @@ spec:
- path:
type: PathPrefix
value: /api/agents
+ # Ambient operator recommender endpoint (apache/texera#5240) is served
+ # by the agent-service alongside /api/agents.
+ - path:
+ type: PathPrefix
+ value: /api/recommend
Review Comment:
This route is not a neutral container. Its own comment says it is split out
so the `BackendTrafficPolicy` can target it, and
`gateway-agent-traffic-policy.yaml:28-37` pins the whole route to
`ConsistentHash` on `X-Agent-Workflow-Id`. That policy's comment states the
client stamps the header on every agent request. `AgentService` does
(`agent.service.ts:238`); `OperatorRecommendationService` sends none.
#6293 specifies the recommender as stateless, so it should not want affinity
at all. Its own HTTPRoute, outside the policy's `targetRefs`, sidesteps this.
Could you confirm what Envoy does when the hash header is absent?
##########
frontend/src/app/workspace/service/operator-recommendation/operator-recommendation.service.ts:
##########
@@ -0,0 +1,152 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from "@angular/core";
+import { HttpClient } from "@angular/common/http";
+import { Observable, catchError, map, of } from "rxjs";
+import { OperatorLink, OperatorPredicate, Point } from
"../../types/workflow-common.interface";
+import { WorkflowActionService } from
"../workflow-graph/model/workflow-action.service";
+import { WorkflowUtilService } from
"../workflow-graph/util/workflow-util.service";
+import { JointUIService } from "../joint-ui/joint-ui.service";
+import { GuiConfigService } from "../../../common/service/gui-config.service";
+
+/**
+ * A single operator suggestion returned by the agent-service `/api/recommend`
+ * endpoint (apache/texera#5240). Mirrors the backend `OperatorRecommendation`.
+ */
+export interface OperatorRecommendation {
+ /** Recommended operator type; a real, catalog-known type. */
+ operatorType: string;
+ /** Confidence in `[0, 1]`, monotonically non-increasing down the list. */
+ score: number;
+ /** Short, human-readable rationale shown alongside the suggested operator.
*/
+ reason: string;
+ /** Display name from operator metadata, when available. */
+ userFriendlyName?: string;
+}
+
+interface RecommendationResponse {
+ recommendations: OperatorRecommendation[];
+ strategy: "hardcoded" | "llm";
+}
+
+/**
+ * Client for the ambient operator recommender. Asks the stateless
agent-service
+ * endpoint what operators are likely to follow the one just added, and turns a
+ * chosen suggestion into a real operator wired onto the source's output port.
+ *
+ * The service never fails loudly: the recommender is a non-essential, ambient
+ * aid, so a backend error or a disabled feature simply yields no suggestions
+ * and the canvas behaves exactly as before.
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class OperatorRecommendationService {
+ private static readonly RECOMMEND_API_URL = "/api/recommend";
+
+ // Horizontal gap between the source operator and a materialized suggestion.
+ private static readonly MATERIALIZE_GAP_X = 100;
+
+ constructor(
+ private http: HttpClient,
+ private config: GuiConfigService,
+ private workflowActionService: WorkflowActionService,
+ private workflowUtilService: WorkflowUtilService
+ ) {}
+
+ /** Whether the opt-in recommender feature is turned on for this deployment.
*/
+ public isEnabled(): boolean {
+ return this.config.env.operatorRecommendationEnabled === true;
+ }
+
+ /**
+ * Fetch ranked next-operator suggestions for the operator just added.
+ *
+ * Returns an empty list (never errors) when the feature is disabled, the
+ * operator has no output port to suggest from, or the backend call fails.
+ *
+ * @param operator the operator that was just added to the canvas
+ * @param limit maximum number of suggestions to request
+ */
+ public getRecommendations(operator: OperatorPredicate, limit?: number):
Observable<OperatorRecommendation[]> {
+ // A source-less/output-less operator (e.g. a chart sink) has no output
port
+ // to hang suggestions on, so we skip the backend call.
+ if (!this.isEnabled() || operator.outputPorts.length === 0) {
+ return of([]);
+ }
+
+ const existingOperatorTypes = this.workflowActionService
+ .getTexeraGraph()
+ .getAllOperators()
+ .map(op => op.operatorType);
+
+ return this.http
+
.post<RecommendationResponse>(OperatorRecommendationService.RECOMMEND_API_URL, {
+ operatorType: operator.operatorType,
+ existingOperatorTypes,
+ ...(limit !== undefined ? { limit } : {}),
+ })
+ .pipe(
+ map(response => response.recommendations ?? []),
+ // Ambient feature: swallow failures and fall back to "no suggestions".
+ catchError(() => of([]))
+ );
+ }
+
+ /**
+ * Turn a chosen suggestion into a real operator: create it just to the right
+ * of the source operator and link the source's output port to the new
+ * operator's first input port. Added as a single undoable action.
+ *
+ * @param sourceOperator the operator the suggestion was made from
+ * @param sourceOutputPortID the output port the suggestion was anchored on
+ * @param recommendedType the operator type the user clicked
+ * @returns the new operator's ID, or `undefined` if it could not be created
Review Comment:
Reinforcing Copilot's r3600302078 rather than re-raising it. The documented
`undefined` return is not merely unused, it is unreachable: the body has
exactly one return (`return newOperator.operatorID`, line 150) and no early
return.
The failure that can happen — `getNewOperatorPredicate` throwing on an
unknown operator type (`workflow-util.service.ts:116-119`) — is neither caught
nor documented, and the sole caller ignores the return value. So the fix is not
only to catch the throw: the declared `string | undefined` contract describes a
branch that does not exist.
##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1712,6 +1728,155 @@ export class WorkflowEditorComponent implements OnInit,
AfterViewInit, OnDestroy
return
this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"]
=== true;
}
+ /**
+ * Ambient operator recommender (apache/texera#5240). When an operator is
+ * added, ask the recommender for likely next operators and float them as
+ * suggestion chips on the operator's output port; clicking one materializes
+ * it. The whole feature is opt-in and self-effacing: if it is disabled or
the
+ * backend returns nothing, the canvas is untouched.
+ */
+
+ private readonly repositionSuggestion$ = new Subject<void>();
+
+ private handleOperatorRecommendation(): void {
+ this.repositionSuggestion$
+ .pipe(auditTime(100), untilDestroyed(this))
+ .subscribe(() => this.repositionRecommendations());
+
+ if (!this.operatorRecommendationService.isEnabled()) {
+ return;
+ }
+
+ // Trigger: an operator was just added to the canvas.
+ this.workflowActionService
+ .getTexeraGraph()
+ .getOperatorAddStream()
+ .pipe(untilDestroyed(this))
+ .subscribe(operator => this.showRecommendationsFor(operator));
+
+ // Dismiss when the user clicks on blank canvas.
+ fromJointPaperEvent(this.paper, "blank:pointerdown")
+ .pipe(untilDestroyed(this))
+ .subscribe(() => this.closeRecommendations());
+
+ // Dismiss if the anchor operator is deleted out from under the
suggestions.
+ this.workflowActionService
+ .getTexeraGraph()
+ .getOperatorDeleteStream()
+ .pipe(untilDestroyed(this))
+ .subscribe(({ deletedOperatorID }) => {
+ if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+ this.closeRecommendations();
+ }
+ });
+
+ // Keep the suggestions anchored to the operator's output port as it moves.
+ this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+ if (this.operatorSuggestion && cell.id.toString() ===
this.operatorSuggestion.operatorId) {
+ this.repositionSuggestion$.next();
+ }
+ });
+
+ // Keep the suggestions anchored on zoom / pan.
+ this.wrapper
+ .getWorkflowEditorZoomStream()
+ .pipe(untilDestroyed(this))
+ .subscribe(() => {
+ if (this.operatorSuggestion) {
+ this.repositionRecommendations();
+ }
+ });
+ }
+
+ private showRecommendationsFor(operator: OperatorPredicate): void {
+ this.closeRecommendations();
+ if (operator.outputPorts.length === 0) {
+ return;
+ }
+ const sourceOutputPortID = operator.outputPorts[0].portID;
+
+ this.operatorRecommendationService
+ .getRecommendations(operator)
+ .pipe(untilDestroyed(this))
+ .subscribe(recommendations => {
+ // The operator may have been deleted while the request was in flight.
+ if (
+ recommendations.length === 0 ||
+
!this.workflowActionService.getTexeraGraph().hasOperator(operator.operatorID)
+ ) {
+ return;
+ }
+ const position = this.getRecommendationPosition(operator.operatorID);
+ if (!position) {
+ return;
+ }
+ this.operatorSuggestion = {
+ operatorId: operator.operatorID,
+ sourceOutputPortID,
+ position,
+ recommendations,
+ };
+ this.changeDetectorRef.detectChanges();
+ });
+ }
+
+ /**
+ * Materialize a clicked suggestion into a real operator wired onto the
+ * source operator's output port.
+ */
+ materializeRecommendation(recommendation: OperatorRecommendation): void {
+ if (!this.operatorSuggestion) {
+ return;
+ }
+ const graph = this.workflowActionService.getTexeraGraph();
+ if (graph.hasOperator(this.operatorSuggestion.operatorId)) {
+ const sourceOperator =
graph.getOperator(this.operatorSuggestion.operatorId);
+ this.operatorRecommendationService.materialize(
+ sourceOperator,
+ this.operatorSuggestion.sourceOutputPortID,
+ recommendation.operatorType
+ );
+ }
+ this.closeRecommendations();
+ }
+
+ closeRecommendations(): void {
+ if (this.operatorSuggestion) {
+ this.operatorSuggestion = null;
+ this.changeDetectorRef.detectChanges();
+ }
+ }
+
+ private repositionRecommendations(): void {
+ if (!this.operatorSuggestion) {
+ return;
+ }
+ const position =
this.getRecommendationPosition(this.operatorSuggestion.operatorId);
+ const prev = this.operatorSuggestion.position;
+ if (!position || (position.x === prev.x && position.y === prev.y)) {
+ return;
+ }
+ this.operatorSuggestion = { ...this.operatorSuggestion, position };
+ this.changeDetectorRef.detectChanges();
+ }
+
+ /**
+ * Screen position for the suggestions: just off the right edge of the
+ * operator, vertically centered — the direction its output port faces.
+ */
+ private getRecommendationPosition(operatorId: string): { x: number; y:
number } | null {
Review Comment:
This is `getOperatorChatPopoverPosition` (line 1693) with different anchor
offsets: same `getModelById` to `getBBox()` to `scale()` to `translate()`
sequence, same null-cell guard, nothing shared, and neither helper mentions the
other.
Nothing is wrong today. The cost is the next fix to that transform landing
on one copy and silently mis-anchoring the other. Suggest extracting one helper
that takes the anchor offsets — the description's "reusing the chat-popover
anchoring pattern" makes the drift likelier, not less.
##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1712,6 +1728,155 @@ export class WorkflowEditorComponent implements OnInit,
AfterViewInit, OnDestroy
return
this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"]
=== true;
}
+ /**
+ * Ambient operator recommender (apache/texera#5240). When an operator is
+ * added, ask the recommender for likely next operators and float them as
+ * suggestion chips on the operator's output port; clicking one materializes
+ * it. The whole feature is opt-in and self-effacing: if it is disabled or
the
+ * backend returns nothing, the canvas is untouched.
+ */
+
+ private readonly repositionSuggestion$ = new Subject<void>();
Review Comment:
The feature-overview TSDoc block above this line binds to
`repositionSuggestion$`, so tooling shows the whole "Ambient operator
recommender..." description on a `Subject`.
The field also sits in the method-body region, while every other field in
the class — including its direct sibling `chatPopoverOperator` at line 109 — is
declared in the header block at 99-123.
##########
common/config/src/main/resources/gui.conf:
##########
@@ -108,6 +108,10 @@ gui {
copilot-enabled = false
copilot-enabled = ${?GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED}
+ # whether the ambient operator recommender (ghost next-operator
suggestions) is enabled
Review Comment:
Last PR-introduced occurrence of "ghost" in the tree — the rename reached
every other site.
```suggestion
# whether the ambient operator recommender (faded next-operator
suggestions) is enabled
```
##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -1712,6 +1728,155 @@ export class WorkflowEditorComponent implements OnInit,
AfterViewInit, OnDestroy
return
this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"]
=== true;
}
+ /**
+ * Ambient operator recommender (apache/texera#5240). When an operator is
+ * added, ask the recommender for likely next operators and float them as
+ * suggestion chips on the operator's output port; clicking one materializes
+ * it. The whole feature is opt-in and self-effacing: if it is disabled or
the
+ * backend returns nothing, the canvas is untouched.
+ */
+
+ private readonly repositionSuggestion$ = new Subject<void>();
+
+ private handleOperatorRecommendation(): void {
+ this.repositionSuggestion$
+ .pipe(auditTime(100), untilDestroyed(this))
+ .subscribe(() => this.repositionRecommendations());
+
+ if (!this.operatorRecommendationService.isEnabled()) {
+ return;
+ }
+
+ // Trigger: an operator was just added to the canvas.
+ this.workflowActionService
+ .getTexeraGraph()
+ .getOperatorAddStream()
+ .pipe(untilDestroyed(this))
+ .subscribe(operator => this.showRecommendationsFor(operator));
+
+ // Dismiss when the user clicks on blank canvas.
+ fromJointPaperEvent(this.paper, "blank:pointerdown")
+ .pipe(untilDestroyed(this))
+ .subscribe(() => this.closeRecommendations());
+
+ // Dismiss if the anchor operator is deleted out from under the
suggestions.
+ this.workflowActionService
+ .getTexeraGraph()
+ .getOperatorDeleteStream()
+ .pipe(untilDestroyed(this))
+ .subscribe(({ deletedOperatorID }) => {
+ if (this.operatorSuggestion?.operatorId === deletedOperatorID) {
+ this.closeRecommendations();
+ }
+ });
+
+ // Keep the suggestions anchored to the operator's output port as it moves.
+ this.paper.model.on("change:position", (cell: joint.dia.Cell) => {
+ if (this.operatorSuggestion && cell.id.toString() ===
this.operatorSuggestion.operatorId) {
+ this.repositionSuggestion$.next();
+ }
+ });
+
+ // Keep the suggestions anchored on zoom / pan.
Review Comment:
The subscribed stream is `getWorkflowEditorZoomStream()`, whose only
producer is `setZoomProperty` (`joint-graph-wrapper.ts:575`). Pan is
`handlePaperPan`'s `blank:pointerdown` plus `mousemove` to `paper.translate`
(lines 551-562) and emits nothing there. Harmless, since the same
`blank:pointerdown` dismisses the overlay, but the comment claims behaviour the
code does not implement.
```suggestion
// Keep the suggestions anchored on zoom.
```
##########
frontend/src/app/workspace/service/operator-recommendation/operator-recommendation.service.ts:
##########
@@ -0,0 +1,152 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from "@angular/core";
+import { HttpClient } from "@angular/common/http";
+import { Observable, catchError, map, of } from "rxjs";
+import { OperatorLink, OperatorPredicate, Point } from
"../../types/workflow-common.interface";
+import { WorkflowActionService } from
"../workflow-graph/model/workflow-action.service";
+import { WorkflowUtilService } from
"../workflow-graph/util/workflow-util.service";
+import { JointUIService } from "../joint-ui/joint-ui.service";
+import { GuiConfigService } from "../../../common/service/gui-config.service";
+
+/**
+ * A single operator suggestion returned by the agent-service `/api/recommend`
+ * endpoint (apache/texera#5240). Mirrors the backend `OperatorRecommendation`.
+ */
+export interface OperatorRecommendation {
+ /** Recommended operator type; a real, catalog-known type. */
+ operatorType: string;
+ /** Confidence in `[0, 1]`, monotonically non-increasing down the list. */
+ score: number;
+ /** Short, human-readable rationale shown alongside the suggested operator.
*/
+ reason: string;
+ /** Display name from operator metadata, when available. */
+ userFriendlyName?: string;
+}
+
+interface RecommendationResponse {
+ recommendations: OperatorRecommendation[];
+ strategy: "hardcoded" | "llm";
+}
+
+/**
+ * Client for the ambient operator recommender. Asks the stateless
agent-service
+ * endpoint what operators are likely to follow the one just added, and turns a
+ * chosen suggestion into a real operator wired onto the source's output port.
+ *
+ * The service never fails loudly: the recommender is a non-essential, ambient
+ * aid, so a backend error or a disabled feature simply yields no suggestions
+ * and the canvas behaves exactly as before.
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class OperatorRecommendationService {
+ private static readonly RECOMMEND_API_URL = "/api/recommend";
+
+ // Horizontal gap between the source operator and a materialized suggestion.
+ private static readonly MATERIALIZE_GAP_X = 100;
+
+ constructor(
+ private http: HttpClient,
+ private config: GuiConfigService,
+ private workflowActionService: WorkflowActionService,
+ private workflowUtilService: WorkflowUtilService
+ ) {}
+
+ /** Whether the opt-in recommender feature is turned on for this deployment.
*/
+ public isEnabled(): boolean {
+ return this.config.env.operatorRecommendationEnabled === true;
+ }
+
+ /**
+ * Fetch ranked next-operator suggestions for the operator just added.
+ *
+ * Returns an empty list (never errors) when the feature is disabled, the
+ * operator has no output port to suggest from, or the backend call fails.
+ *
+ * @param operator the operator that was just added to the canvas
+ * @param limit maximum number of suggestions to request
+ */
+ public getRecommendations(operator: OperatorPredicate, limit?: number):
Observable<OperatorRecommendation[]> {
+ // A source-less/output-less operator (e.g. a chart sink) has no output
port
Review Comment:
In Texera a *source* operator is defined by having no **input** ports, so
"source-less" reads as "has inputs" — the opposite axis from the guard on the
next line, which tests output ports.
```suggestion
// An operator with no output ports (e.g. a chart sink) has no port to
anchor on
```
##########
frontend/src/app/workspace/service/operator-recommendation/operator-recommendation.service.ts:
##########
@@ -0,0 +1,152 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from "@angular/core";
+import { HttpClient } from "@angular/common/http";
+import { Observable, catchError, map, of } from "rxjs";
+import { OperatorLink, OperatorPredicate, Point } from
"../../types/workflow-common.interface";
+import { WorkflowActionService } from
"../workflow-graph/model/workflow-action.service";
+import { WorkflowUtilService } from
"../workflow-graph/util/workflow-util.service";
+import { JointUIService } from "../joint-ui/joint-ui.service";
+import { GuiConfigService } from "../../../common/service/gui-config.service";
+
+/**
+ * A single operator suggestion returned by the agent-service `/api/recommend`
+ * endpoint (apache/texera#5240). Mirrors the backend `OperatorRecommendation`.
+ */
+export interface OperatorRecommendation {
+ /** Recommended operator type; a real, catalog-known type. */
+ operatorType: string;
+ /** Confidence in `[0, 1]`, monotonically non-increasing down the list. */
+ score: number;
+ /** Short, human-readable rationale shown alongside the suggested operator.
*/
+ reason: string;
+ /** Display name from operator metadata, when available. */
+ userFriendlyName?: string;
+}
+
+interface RecommendationResponse {
+ recommendations: OperatorRecommendation[];
+ strategy: "hardcoded" | "llm";
+}
+
+/**
+ * Client for the ambient operator recommender. Asks the stateless
agent-service
+ * endpoint what operators are likely to follow the one just added, and turns a
+ * chosen suggestion into a real operator wired onto the source's output port.
+ *
+ * The service never fails loudly: the recommender is a non-essential, ambient
+ * aid, so a backend error or a disabled feature simply yields no suggestions
+ * and the canvas behaves exactly as before.
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class OperatorRecommendationService {
+ private static readonly RECOMMEND_API_URL = "/api/recommend";
+
+ // Horizontal gap between the source operator and a materialized suggestion.
+ private static readonly MATERIALIZE_GAP_X = 100;
+
+ constructor(
+ private http: HttpClient,
+ private config: GuiConfigService,
+ private workflowActionService: WorkflowActionService,
+ private workflowUtilService: WorkflowUtilService
+ ) {}
+
+ /** Whether the opt-in recommender feature is turned on for this deployment.
*/
+ public isEnabled(): boolean {
+ return this.config.env.operatorRecommendationEnabled === true;
+ }
+
+ /**
+ * Fetch ranked next-operator suggestions for the operator just added.
+ *
+ * Returns an empty list (never errors) when the feature is disabled, the
+ * operator has no output port to suggest from, or the backend call fails.
+ *
+ * @param operator the operator that was just added to the canvas
+ * @param limit maximum number of suggestions to request
+ */
+ public getRecommendations(operator: OperatorPredicate, limit?: number):
Observable<OperatorRecommendation[]> {
Review Comment:
`limit` has no caller: the spread at line 104 omits it, and `editor.ts:1802`
plus all five spec calls pass nothing. Suggest dropping it until a second use
arrives. As it stands it is an untested parameter reaching a wire payload whose
backend contract is not on this branch yet.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]