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]

Reply via email to