From 5a2ec608933705df52b57eb9c026d54b567c1d86 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Thu, 16 Oct 2025 11:01:44 -0700 Subject: [PATCH 001/158] make server compatible with java 17 --- common/workflow-core/build.sbt | 1 - .../org/apache/amber/core/tuple/Tuple.scala | 18 ++++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/common/workflow-core/build.sbt b/common/workflow-core/build.sbt index 82a79e8e04b..0cb1630ce18 100644 --- a/common/workflow-core/build.sbt +++ b/common/workflow-core/build.sbt @@ -176,7 +176,6 @@ libraryDependencies ++= Seq( libraryDependencies ++= Seq( "com.github.sisyphsu" % "dateparser" % "1.0.11", // DateParser "com.google.guava" % "guava" % "31.1-jre", // Guava - "org.ehcache" % "sizeof" % "0.4.3", // Ehcache SizeOf "org.jgrapht" % "jgrapht-core" % "1.4.0", // JGraphT Core "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", // Scala Logging "org.eclipse.jgit" % "org.eclipse.jgit" % "5.13.0.202109080827-r", // jgit diff --git a/common/workflow-core/src/main/scala/org/apache/amber/core/tuple/Tuple.scala b/common/workflow-core/src/main/scala/org/apache/amber/core/tuple/Tuple.scala index 7bee0a0fc53..e9960fbd668 100644 --- a/common/workflow-core/src/main/scala/org/apache/amber/core/tuple/Tuple.scala +++ b/common/workflow-core/src/main/scala/org/apache/amber/core/tuple/Tuple.scala @@ -22,7 +22,6 @@ package org.apache.amber.core.tuple import com.fasterxml.jackson.annotation.{JsonCreator, JsonIgnore, JsonProperty} import com.google.common.base.Preconditions.checkNotNull import org.apache.amber.core.tuple.Tuple.checkSchemaMatchesFields -import org.ehcache.sizeof.SizeOf import java.util import scala.collection.mutable @@ -52,7 +51,22 @@ case class Tuple @JsonCreator() ( checkNotNull(fieldVals) checkSchemaMatchesFields(schema.getAttributes, fieldVals) - override val inMemSize: Long = SizeOf.newInstance().deepSizeOf(this) + // Fast approximation of in-memory size for statistics tracking + // Avoids expensive reflection and Java 17 module system issues + override val inMemSize: Long = { + val fieldSize = fieldVals.map { + case s: String => 40 + (s.length * 2) // String overhead + UTF-16 chars + case _: Int | _: Float => 4 + case _: Long | _: Double => 8 + case _: Boolean | _: Byte => 1 + case _: Short => 2 + case arr: Array[Byte] => 24 + arr.length // Array overhead + data + case arr: Array[_] => 24 + (arr.length * 8) // Array overhead + references + case null => 4 // null reference + case _ => 16 // Generic object reference estimate + }.sum + fieldSize + 64 // Tuple object overhead (object header + schema reference + array reference) + } @JsonIgnore def length: Int = fieldVals.length From c050948a926e6c552492540d6232ddec083c0c24 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Thu, 16 Oct 2025 11:11:38 -0700 Subject: [PATCH 002/158] finish v1 of MCP server --- bin/build-services.sh | 3 + bin/mcp-service.sh | 25 ++ build.sbt | 33 +- common/config/src/main/resources/mcp.conf | 79 ++++ .../org/apache/texera/config/McpConfig.scala | 59 +++ mcp-service/build.sbt | 106 +++++ mcp-service/project/build.properties | 1 + .../mcp/server/TexeraMcpServerImpl.java | 382 ++++++++++++++++++ .../mcp/tools/OperatorToolProvider.java | 201 +++++++++ .../main/resources/mcp-service-config.yaml | 26 ++ .../org/apache/texera/mcp/McpService.scala | 121 ++++++ .../mcp/resource/HealthCheckResource.scala | 37 ++ .../mcp/server/TexeraMcpServerImplTest.java | 160 ++++++++ 13 files changed, 1232 insertions(+), 1 deletion(-) create mode 100755 bin/mcp-service.sh create mode 100644 common/config/src/main/resources/mcp.conf create mode 100644 common/config/src/main/scala/org/apache/texera/config/McpConfig.scala create mode 100644 mcp-service/build.sbt create mode 100644 mcp-service/project/build.properties create mode 100644 mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java create mode 100644 mcp-service/src/main/java/org/apache/texera/mcp/tools/OperatorToolProvider.java create mode 100644 mcp-service/src/main/resources/mcp-service-config.yaml create mode 100644 mcp-service/src/main/scala/org/apache/texera/mcp/McpService.scala create mode 100644 mcp-service/src/main/scala/org/apache/texera/mcp/resource/HealthCheckResource.scala create mode 100644 mcp-service/src/test/java/org/apache/texera/mcp/server/TexeraMcpServerImplTest.java diff --git a/bin/build-services.sh b/bin/build-services.sh index a28e7e2cb0a..f03ca6c8fea 100755 --- a/bin/build-services.sh +++ b/bin/build-services.sh @@ -28,5 +28,8 @@ rm config-service/target/universal/config-service-*.zip unzip computing-unit-managing-service/target/universal/computing-unit-managing-service-*.zip -d target/ rm computing-unit-managing-service/target/universal/computing-unit-managing-service-*.zip +unzip mcp-service/target/universal/mcp-service-*.zip -d target/ +rm mcp-service/target/universal/mcp-service-*.zip + unzip amber/target/universal/texera-*.zip -d amber/target/ rm amber/target/universal/texera-*.zip diff --git a/bin/mcp-service.sh b/bin/mcp-service.sh new file mode 100755 index 00000000000..7d52cd1bfbc --- /dev/null +++ b/bin/mcp-service.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# 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. + +# Start the Texera MCP Service +# Use TEXERA_HOME environment variable, or default to parent directory of script +TEXERA_HOME="${TEXERA_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + +# Start the MCP service +cd "$TEXERA_HOME/mcp-service" && target/universal/stage/bin/mcp-service server src/main/resources/mcp-service-config.yaml diff --git a/build.sbt b/build.sbt index 60efe697ce7..3a951522e12 100644 --- a/build.sbt +++ b/build.sbt @@ -74,6 +74,31 @@ lazy val WorkflowCompilingService = (project in file("workflow-compiling-service ) ) +lazy val McpService = (project in file("mcp-service")) + .dependsOn(WorkflowOperator, Auth, Config, DAO) + .settings( + dependencyOverrides ++= Seq( + // Force Jackson 2.17.0 for compatibility with jackson-module-scala 2.17.0 + // Dropwizard 4.0.7 may pull in newer versions that are incompatible + "com.fasterxml.jackson.core" % "jackson-core" % "2.17.0", + "com.fasterxml.jackson.core" % "jackson-databind" % "2.17.0", + "com.fasterxml.jackson.core" % "jackson-annotations" % "2.17.0", + "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.17.0", + "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % "2.17.0", + "com.fasterxml.jackson.module" % "jackson-module-blackbird" % "2.17.0", + "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % "2.17.0", + "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.17.0", + "com.fasterxml.jackson.datatype" % "jackson-datatype-guava" % "2.17.0", + "com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % "2.17.0", + "com.fasterxml.jackson.dataformat" % "jackson-dataformat-xml" % "2.17.0", + "com.fasterxml.jackson.jakarta.rs" % "jackson-jakarta-rs-base" % "2.17.0", + "com.fasterxml.jackson.jakarta.rs" % "jackson-jakarta-rs-json-provider" % "2.17.0", + "com.fasterxml.jackson.module" % "jackson-module-jakarta-xmlbind-annotations" % "2.17.0" + ) + ) + .configs(Test) + .dependsOn(DAO % "test->test", Auth % "test->test") + lazy val WorkflowExecutionService = (project in file("amber")) .dependsOn(WorkflowOperator, Auth, Config) .settings( @@ -88,6 +113,11 @@ lazy val WorkflowExecutionService = (project in file("amber")) ), libraryDependencies ++= Seq( "com.squareup.okhttp3" % "okhttp" % "4.10.0" force () // Force usage of OkHttp 4.10.0 + ), + // Java 17+ compatibility: Apache Arrow requires reflective access to java.nio internals + // See: https://arrow.apache.org/docs/java/install.html + Universal / javaOptions ++= Seq( + "--add-opens=java.base/java.nio=ALL-UNNAMED" ) ) .configs(Test) @@ -106,6 +136,7 @@ lazy val TexeraProject = (project in file(".")) FileService, WorkflowOperator, WorkflowCompilingService, + McpService, WorkflowExecutionService ) .settings( @@ -114,4 +145,4 @@ lazy val TexeraProject = (project in file(".")) organization := "org.apache", scalaVersion := "2.13.12", publishMavenStyle := true - ) + ) \ No newline at end of file diff --git a/common/config/src/main/resources/mcp.conf b/common/config/src/main/resources/mcp.conf new file mode 100644 index 00000000000..ce78ab341a3 --- /dev/null +++ b/common/config/src/main/resources/mcp.conf @@ -0,0 +1,79 @@ +# 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. + +# MCP (Model Context Protocol) Service Configuration +mcp { + # Server settings + server { + port = 9098 + port = ${?MCP_PORT} + name = "texera-mcp" + version = "1.0.0" + } + + # Transport protocol: stdio, http, or websocket + transport = "http" + transport = ${?MCP_TRANSPORT} + + # Authentication and database + auth { + enabled = false + enabled = ${?MCP_AUTH_ENABLED} + } + + database { + enabled = true + enabled = ${?MCP_DATABASE_ENABLED} + } + + # MCP Capabilities (following MCP specification) + capabilities { + tools = true + tools = ${?MCP_TOOLS_ENABLED} + + resources = true + resources = ${?MCP_RESOURCES_ENABLED} + + prompts = true + prompts = ${?MCP_PROMPTS_ENABLED} + + sampling = false + sampling = ${?MCP_SAMPLING_ENABLED} + + logging = true + logging = ${?MCP_LOGGING_ENABLED} + } + + # Performance settings + performance { + max-concurrent-requests = 100 + max-concurrent-requests = ${?MCP_MAX_CONCURRENT_REQUESTS} + + request-timeout-ms = 30000 + request-timeout-ms = ${?MCP_REQUEST_TIMEOUT_MS} + } + + # Enabled feature groups + enabled-tools = ["operators", "workflows", "datasets", "projects", "executions"] + enabled-tools = ${?MCP_ENABLED_TOOLS} + + enabled-resources = ["operator-schemas", "workflow-templates", "dataset-schemas"] + enabled-resources = ${?MCP_ENABLED_RESOURCES} + + enabled-prompts = ["create-workflow", "optimize-workflow", "explain-operator"] + enabled-prompts = ${?MCP_ENABLED_PROMPTS} +} diff --git a/common/config/src/main/scala/org/apache/texera/config/McpConfig.scala b/common/config/src/main/scala/org/apache/texera/config/McpConfig.scala new file mode 100644 index 00000000000..75d13f0bfce --- /dev/null +++ b/common/config/src/main/scala/org/apache/texera/config/McpConfig.scala @@ -0,0 +1,59 @@ +/* + * 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. + */ + +package org.apache.texera.config + +import com.typesafe.config.{Config, ConfigFactory} +import scala.jdk.CollectionConverters._ + +/** + * Configuration for the MCP (Model Context Protocol) Service. + * Settings are loaded from mcp.conf. + */ +object McpConfig { + + private val conf: Config = ConfigFactory.parseResources("mcp.conf").resolve() + + // Server settings + val serverPort: Int = conf.getInt("mcp.server.port") + val serverName: String = conf.getString("mcp.server.name") + val serverVersion: String = conf.getString("mcp.server.version") + + // Transport protocol + val transport: String = conf.getString("mcp.transport") + + // Authentication and database + val authEnabled: Boolean = conf.getBoolean("mcp.auth.enabled") + val databaseEnabled: Boolean = conf.getBoolean("mcp.database.enabled") + + // MCP Capabilities + val toolsEnabled: Boolean = conf.getBoolean("mcp.capabilities.tools") + val resourcesEnabled: Boolean = conf.getBoolean("mcp.capabilities.resources") + val promptsEnabled: Boolean = conf.getBoolean("mcp.capabilities.prompts") + val samplingEnabled: Boolean = conf.getBoolean("mcp.capabilities.sampling") + val loggingEnabled: Boolean = conf.getBoolean("mcp.capabilities.logging") + + // Performance settings + val maxConcurrentRequests: Int = conf.getInt("mcp.performance.max-concurrent-requests") + val requestTimeoutMs: Long = conf.getLong("mcp.performance.request-timeout-ms") + + // Enabled features + val enabledTools: List[String] = conf.getStringList("mcp.enabled-tools").asScala.toList + val enabledResources: List[String] = conf.getStringList("mcp.enabled-resources").asScala.toList + val enabledPrompts: List[String] = conf.getStringList("mcp.enabled-prompts").asScala.toList +} diff --git a/mcp-service/build.sbt b/mcp-service/build.sbt new file mode 100644 index 00000000000..667e3d95a99 --- /dev/null +++ b/mcp-service/build.sbt @@ -0,0 +1,106 @@ +/* + * 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. + */ + +///////////////////////////////////////////////////////////////////////////// +// Project Settings +///////////////////////////////////////////////////////////////////////////// +name := "mcp-service" +organization := "org.apache" +version := "1.0.0" +scalaVersion := "2.13.12" + +enablePlugins(JavaAppPackaging) + +// Enable semanticdb for Scalafix +ThisBuild / semanticdbEnabled := true +ThisBuild / semanticdbVersion := scalafixSemanticdb.revision + +// Manage dependency conflicts +ThisBuild / conflictManager := ConflictManager.latestRevision + +// Restrict parallel test execution +Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) + +///////////////////////////////////////////////////////////////////////////// +// Compiler Options +///////////////////////////////////////////////////////////////////////////// + +Compile / scalacOptions ++= Seq( + "-Xelide-below", "WARNING", + "-feature", + "-deprecation", + "-Ywarn-unused:imports", +) + +///////////////////////////////////////////////////////////////////////////// +// Version Variables +///////////////////////////////////////////////////////////////////////////// +val dropwizardVersion = "4.0.7" +val mockitoVersion = "5.4.0" +val assertjVersion = "3.24.2" +val jacksonVersion = "2.17.0" +val reactorVersion = "3.6.0" + +///////////////////////////////////////////////////////////////////////////// +// Test Dependencies +///////////////////////////////////////////////////////////////////////////// +libraryDependencies ++= Seq( + "org.scalamock" %% "scalamock" % "5.2.0" % Test, + "org.scalatest" %% "scalatest" % "3.2.17" % Test, + "io.dropwizard" % "dropwizard-testing" % dropwizardVersion % Test, + "org.mockito" % "mockito-core" % mockitoVersion % Test, + "org.assertj" % "assertj-core" % assertjVersion % Test, + // JUnit 5 (Jupiter) for Java tests + "org.junit.jupiter" % "junit-jupiter-api" % "5.10.1" % Test, + "org.junit.jupiter" % "junit-jupiter-engine" % "5.10.1" % Test, + "org.junit.jupiter" % "junit-jupiter-params" % "5.10.1" % Test, + "com.github.sbt" % "junit-interface" % "0.13.3" % Test +) + +///////////////////////////////////////////////////////////////////////////// +// Core Dependencies +///////////////////////////////////////////////////////////////////////////// +libraryDependencies ++= Seq( + // Dropwizard + "io.dropwizard" % "dropwizard-core" % dropwizardVersion, + "io.dropwizard" % "dropwizard-auth" % dropwizardVersion, + + // Jackson + "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion, + "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion, + "com.fasterxml.jackson.core" % "jackson-annotations" % jacksonVersion, + + // MCP SDK (Note: These are placeholder versions - update with actual versions when available) + // For now, we'll implement MCP protocol directly + "io.modelcontextprotocol.sdk" % "mcp" % "0.14.1", + "io.modelcontextprotocol.sdk" % "mcp-json-jackson2" % "0.14.1", + + // Reactive Streams (for async support) + "io.projectreactor" % "reactor-core" % reactorVersion, + + // Logging + "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", + "ch.qos.logback" % "logback-classic" % "1.4.14", + + // WebSocket support (for potential future MCP transport) + "org.java-websocket" % "Java-WebSocket" % "1.5.4", + + // JSON Schema generation (reuse from WorkflowOperator) + "com.kjetland" % "mbknor-jackson-jsonschema_2.13" % "1.0.39" +) \ No newline at end of file diff --git a/mcp-service/project/build.properties b/mcp-service/project/build.properties new file mode 100644 index 00000000000..f0c2ecc73e3 --- /dev/null +++ b/mcp-service/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.4 \ No newline at end of file diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java b/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java new file mode 100644 index 00000000000..c09e21d0946 --- /dev/null +++ b/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java @@ -0,0 +1,382 @@ +/* + * 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. + */ + +package org.apache.texera.mcp.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.scala.DefaultScalaModule; +import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; +import io.modelcontextprotocol.spec.McpSchema; +import jakarta.servlet.http.HttpServlet; +import lombok.Getter; +import org.apache.texera.config.McpConfig; +import org.apache.texera.mcp.tools.OperatorToolProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +/** + * MCP Server implementation using the official Model Context Protocol SDK. + * Provides Texera operator metadata and capabilities to AI agents via HTTP Streamable transport. + * Configuration is loaded from McpConfig (common/config/src/main/resources/mcp.conf). + */ +public class TexeraMcpServerImpl { + private static final Logger logger = LoggerFactory.getLogger(TexeraMcpServerImpl.class); + + private final ObjectMapper objectMapper; + private final OperatorToolProvider operatorToolProvider; + private McpSyncServer mcpServer; + private HttpServletStreamableServerTransportProvider transportProvider; + + /** + * -- GETTER -- + * Check if server is running + */ + @Getter + private boolean running = false; + + public TexeraMcpServerImpl() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.registerModule(new DefaultScalaModule()); + this.operatorToolProvider = new OperatorToolProvider(); + } + + /** + * Start the MCP server with configured capabilities using SDK + */ + public void start() { + logger.info("Starting Texera MCP Server: {} v{}", McpConfig.serverName(), McpConfig.serverVersion()); + + try { + // Create JSON mapper for MCP protocol + var jsonMapper = new JacksonMcpJsonMapper(objectMapper); + + // Build HTTP Streamable transport provider using SDK builder + transportProvider = HttpServletStreamableServerTransportProvider.builder() + .jsonMapper(jsonMapper) +// .mcpEndpoint("/") // Base endpoint - will be mounted at /api/mcp/ +// .keepAliveInterval(Duration.ofSeconds(30)) + .build(); + + // Build server capabilities based on configuration + var capabilitiesBuilder = McpSchema.ServerCapabilities.builder(); + + if (McpConfig.toolsEnabled()) { + capabilitiesBuilder.tools(true); + } + if (McpConfig.loggingEnabled()) { + capabilitiesBuilder.logging(); + } + if (McpConfig.resourcesEnabled()) { + capabilitiesBuilder.resources(true, true); + } + if (McpConfig.promptsEnabled()) { + capabilitiesBuilder.prompts(true); + } + + var capabilities = capabilitiesBuilder.build(); + + // Build the MCP server with transport and capabilities using SDK + var serverBuilder = McpServer + .sync(transportProvider) + .serverInfo(McpConfig.serverName(), McpConfig.serverVersion()) + .capabilities(capabilities); + + // Register operator tools if enabled + if (McpConfig.toolsEnabled() && McpConfig.enabledTools().contains("operators")) { + registerOperatorTools(serverBuilder); + } + + mcpServer = serverBuilder.build(); + + running = true; + logger.info("MCP Server started successfully with 8 operator tools on HTTP Streamable transport"); + + } catch (Exception e) { + logger.error("Failed to start MCP Server", e); + throw new RuntimeException("Failed to start MCP Server", e); + } + } + + /** + * Get the HTTP servlet for Dropwizard integration. + * The transport provider IS a servlet that handles MCP protocol requests. + */ + public HttpServlet getServlet() { + if (transportProvider == null) { + throw new IllegalStateException("Server not started - call start() first"); + } + return transportProvider; // Transport provider IS the servlet + } + + /** + * Stop the MCP server + */ + public void stop() { + logger.info("Stopping Texera MCP Server"); + running = false; + if (mcpServer != null) { + try { + mcpServer.close(); + } catch (Exception e) { + logger.error("Error closing MCP server", e); + } + } + if (transportProvider != null) { + transportProvider.destroy(); + } + logger.info("MCP Server stopped"); + } + + /** + * Register all operator tools using MCP SDK + */ + private void registerOperatorTools(McpServer.SyncSpecification serverBuilder) { + logger.info("Registering operator tools"); + + // Tool 1: List all operators + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("list_operators") + .description("List all available Texera operators with their metadata, groups, and schemas") + .build(), + (exchange, request) -> { + try { + var result = operatorToolProvider.listOperators(); + String jsonResult = objectMapper.writeValueAsString(result); + return new McpSchema.CallToolResult(jsonResult, false); + } catch (Exception e) { + logger.error("Error listing operators", e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 2: Get specific operator + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("get_operator") + .description("Get detailed metadata for a specific operator by its type identifier") + .inputSchema(createInputSchema("operatorType", "The operator type identifier (e.g., 'CSVScanSource')")) + .build(), + (exchange, request) -> { + try { + String operatorType = getStringArg(request, "operatorType"); + var result = operatorToolProvider.getOperator(operatorType); + + if (result.isDefined()) { + String jsonResult = objectMapper.writeValueAsString(result.get()); + return new McpSchema.CallToolResult(jsonResult, false); + } else { + return new McpSchema.CallToolResult("Operator not found: " + operatorType, true); + } + } catch (Exception e) { + logger.error("Error getting operator", e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 3: Get operator schema + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("get_operator_schema") + .description("Get the JSON schema for a specific operator's configuration") + .inputSchema(createInputSchema("operatorType", "The operator type identifier")) + .build(), + (exchange, request) -> { + try { + String operatorType = getStringArg(request, "operatorType"); + var result = operatorToolProvider.getOperatorSchema(operatorType); + + if (result.isDefined()) { + String jsonResult = objectMapper.writeValueAsString(result.get()); + return new McpSchema.CallToolResult(jsonResult, false); + } else { + return new McpSchema.CallToolResult("Operator schema not found: " + operatorType, true); + } + } catch (Exception e) { + logger.error("Error getting operator schema", e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 4: Search operators + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("search_operators") + .description("Search operators by name, description, or type using a query string") + .inputSchema(createInputSchema("query", "Search query string")) + .build(), + (exchange, request) -> { + try { + String query = getStringArg(request, "query"); + var result = operatorToolProvider.searchOperators(query); + String jsonResult = objectMapper.writeValueAsString(result); + return new McpSchema.CallToolResult(jsonResult, false); + } catch (Exception e) { + logger.error("Error searching operators", e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 5: Get operators by group + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("get_operators_by_group") + .description("Get all operators in a specific group") + .inputSchema(createInputSchema("groupName", "The operator group name (e.g., 'Data Input', 'Machine Learning')")) + .build(), + (exchange, request) -> { + try { + String groupName = getStringArg(request, "groupName"); + var result = operatorToolProvider.getOperatorsByGroup(groupName); + String jsonResult = objectMapper.writeValueAsString(result); + return new McpSchema.CallToolResult(jsonResult, false); + } catch (Exception e) { + logger.error("Error getting operators by group", e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 6: Get operator groups + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("get_operator_groups") + .description("Get all operator groups in their hierarchical structure") + .build(), + (exchange, request) -> { + try { + var result = operatorToolProvider.getOperatorGroups(); + String jsonResult = objectMapper.writeValueAsString(result); + return new McpSchema.CallToolResult(jsonResult, false); + } catch (Exception e) { + logger.error("Error getting operator groups", e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 7: Describe operator + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("describe_operator") + .description("Get a detailed human-readable description of an operator including ports, capabilities, and schema") + .inputSchema(createInputSchema("operatorType", "The operator type identifier")) + .build(), + (exchange, request) -> { + try { + String operatorType = getStringArg(request, "operatorType"); + var result = operatorToolProvider.describeOperator(operatorType); + + if (result.isDefined()) { + return new McpSchema.CallToolResult(result.get(), false); + } else { + return new McpSchema.CallToolResult("Operator not found: " + operatorType, true); + } + } catch (Exception e) { + logger.error("Error describing operator", e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 8: Get operators by capability + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("get_operators_by_capability") + .description("Get operators that support specific capabilities") + .inputSchema(createInputSchema("capability", "The capability to filter by (reconfiguration, dynamic_input, dynamic_output, port_customization)")) + .build(), + (exchange, request) -> { + try { + String capability = getStringArg(request, "capability"); + var result = operatorToolProvider.getOperatorsByCapability(capability); + String jsonResult = objectMapper.writeValueAsString(result); + return new McpSchema.CallToolResult(jsonResult, false); + } catch (Exception e) { + logger.error("Error getting operators by capability", e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + logger.info("Registered 8 operator tools"); + } + + /** + * Helper to create input schema for a single string parameter + */ + private McpSchema.JsonSchema createInputSchema(String paramName, String description) { + try { + String schemaJson = String.format( + "{\"type\":\"object\",\"properties\":{\"%s\":{\"type\":\"string\",\"description\":\"%s\"}},\"required\":[\"%s\"]}", + paramName, description, paramName + ); + return objectMapper.readValue(schemaJson, McpSchema.JsonSchema.class); + } catch (Exception e) { + logger.error("Error creating input schema", e); + return null; + } + } + + /** + * Helper to extract string argument from CallToolRequest + */ + private String getStringArg(McpSchema.CallToolRequest request, String key) { + if (request.arguments() == null) { + return ""; + } + Object value = request.arguments().get(key); + return value != null ? value.toString() : ""; + } + + /** + * Get server information + */ + public Map getServerInfo() { + Map info = new HashMap<>(); + info.put("name", McpConfig.serverName()); + info.put("version", McpConfig.serverVersion()); + info.put("status", running ? "running" : "stopped"); + return info; + } + + /** + * Get server capabilities + */ + public Map getCapabilities() { + Map caps = new HashMap<>(); + caps.put("tools", McpConfig.toolsEnabled()); + caps.put("resources", McpConfig.resourcesEnabled()); + caps.put("prompts", McpConfig.promptsEnabled()); + caps.put("sampling", McpConfig.samplingEnabled()); + caps.put("logging", McpConfig.loggingEnabled()); + return caps; + } +} diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/tools/OperatorToolProvider.java b/mcp-service/src/main/java/org/apache/texera/mcp/tools/OperatorToolProvider.java new file mode 100644 index 00000000000..61c770d436a --- /dev/null +++ b/mcp-service/src/main/java/org/apache/texera/mcp/tools/OperatorToolProvider.java @@ -0,0 +1,201 @@ +/* + * 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. + */ + +package org.apache.texera.mcp.tools; + +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.amber.operator.metadata.AllOperatorMetadata; +import org.apache.amber.operator.metadata.GroupInfo; +import org.apache.amber.operator.metadata.OperatorMetadata; +import org.apache.amber.operator.metadata.OperatorMetadataGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import scala.Option; +import scala.jdk.javaapi.CollectionConverters; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Java implementation of Operator Tool operations. + * Provides direct access to Texera operator metadata using the MCP SDK. + */ +public class OperatorToolProvider { + + private static final Logger logger = LoggerFactory.getLogger(OperatorToolProvider.class); + + /** + * List all available Texera operators + */ + public AllOperatorMetadata listOperators() { + logger.info("MCP Tool: Listing all operators"); + return OperatorMetadataGenerator.allOperatorMetadata(); + } + + /** + * Get specific operator by type + */ + public Option getOperator(String operatorType) { + logger.info("MCP Tool: Getting operator metadata for type: {}", operatorType); + List operators = CollectionConverters.asJava( + OperatorMetadataGenerator.allOperatorMetadata().operators() + ); + + return operators.stream() + .filter(op -> op.operatorType().equals(operatorType)) + .findFirst() + .map(Option::apply) + .orElse(Option.empty()); + } + + /** + * Get operator JSON schema + */ + public Option getOperatorSchema(String operatorType) { + logger.info("MCP Tool: Getting operator schema for type: {}", operatorType); + Option operator = getOperator(operatorType); + + if (operator.isDefined()) { + return Option.apply(operator.get().jsonSchema()); + } + return Option.empty(); + } + + /** + * Get operators by group name + */ + public List getOperatorsByGroup(String groupName) { + logger.info("MCP Tool: Getting operators for group: {}", groupName); + List operators = CollectionConverters.asJava( + OperatorMetadataGenerator.allOperatorMetadata().operators() + ); + + return operators.stream() + .filter(op -> op.additionalMetadata().operatorGroupName().equals(groupName)) + .collect(Collectors.toList()); + } + + /** + * Search operators by query string + */ + public List searchOperators(String query) { + logger.info("MCP Tool: Searching operators with query: {}", query); + String lowerQuery = query.toLowerCase(); + List operators = CollectionConverters.asJava( + OperatorMetadataGenerator.allOperatorMetadata().operators() + ); + + return operators.stream() + .filter(op -> + op.additionalMetadata().userFriendlyName().toLowerCase().contains(lowerQuery) || + op.additionalMetadata().operatorDescription().toLowerCase().contains(lowerQuery) || + op.operatorType().toLowerCase().contains(lowerQuery) + ) + .collect(Collectors.toList()); + } + + /** + * Get all operator groups + */ + public List getOperatorGroups() { + logger.info("MCP Tool: Getting operator groups"); + return CollectionConverters.asJava( + OperatorMetadataGenerator.allOperatorMetadata().groups() + ); + } + + /** + * Get detailed description of operator + */ + public Option describeOperator(String operatorType) { + logger.info("MCP Tool: Describing operator: {}", operatorType); + Option operatorOpt = getOperator(operatorType); + + if (operatorOpt.isDefined()) { + OperatorMetadata op = operatorOpt.get(); + var info = op.additionalMetadata(); + + StringBuilder description = new StringBuilder(); + description.append("\nOperator: ").append(info.userFriendlyName()).append("\n"); + description.append("Type: ").append(op.operatorType()).append("\n"); + description.append("Version: ").append(op.operatorVersion()).append("\n"); + description.append("Group: ").append(info.operatorGroupName()).append("\n"); + description.append("Description: ").append(info.operatorDescription()).append("\n\n"); + + description.append("Input Ports: ").append(info.inputPorts().size()).append("\n"); + CollectionConverters.asJava(info.inputPorts()).forEach(port -> + description.append(" - ").append(port.displayName()) + .append(" (multi-links: ").append(port.allowMultiLinks()).append(")").append("\n") + ); + + description.append("\nOutput Ports: ").append(info.outputPorts().size()).append("\n"); + CollectionConverters.asJava(info.outputPorts()).forEach(port -> + description.append(" - ").append(port.displayName()).append("\n") + ); + + description.append("\nDynamic Input Ports: ").append(info.dynamicInputPorts()).append("\n"); + description.append("Dynamic Output Ports: ").append(info.dynamicOutputPorts()).append("\n"); + description.append("Supports Reconfiguration: ").append(info.supportReconfiguration()).append("\n"); + description.append("Allow Port Customization: ").append(info.allowPortCustomization()).append("\n\n"); + + description.append("Configuration Schema:\n"); + description.append(op.jsonSchema().toPrettyString()).append("\n"); + + return Option.apply(description.toString()); + } + + return Option.empty(); + } + + /** + * Get operators by capability + */ + public List getOperatorsByCapability(String capability) { + logger.info("MCP Tool: Getting operators by capability: {}", capability); + List operators = CollectionConverters.asJava( + OperatorMetadataGenerator.allOperatorMetadata().operators() + ); + + switch (capability.toLowerCase()) { + case "reconfiguration": + return operators.stream() + .filter(op -> op.additionalMetadata().supportReconfiguration()) + .collect(Collectors.toList()); + + case "dynamic_input": + return operators.stream() + .filter(op -> op.additionalMetadata().dynamicInputPorts()) + .collect(Collectors.toList()); + + case "dynamic_output": + return operators.stream() + .filter(op -> op.additionalMetadata().dynamicOutputPorts()) + .collect(Collectors.toList()); + + case "port_customization": + return operators.stream() + .filter(op -> op.additionalMetadata().allowPortCustomization()) + .collect(Collectors.toList()); + + default: + logger.warn("Unknown capability: {}", capability); + return List.of(); + } + } +} diff --git a/mcp-service/src/main/resources/mcp-service-config.yaml b/mcp-service/src/main/resources/mcp-service-config.yaml new file mode 100644 index 00000000000..6e22e9cad33 --- /dev/null +++ b/mcp-service/src/main/resources/mcp-service-config.yaml @@ -0,0 +1,26 @@ +# Dropwizard Configuration for MCP Service +# MCP-specific settings are now in common/config/src/main/resources/mcp.conf + +server: + applicationConnectors: + - type: http + port: 9098 + adminConnectors: [] + +logging: + level: INFO + loggers: + "io.dropwizard": INFO + "org.apache.texera": DEBUG + appenders: + - type: console + - type: file + currentLogFilename: log/mcp-service.log + threshold: ALL + queueSize: 512 + discardingThreshold: 0 + archive: true + archivedLogFilenamePattern: log/mcp-service-%d{yyyy-MM-dd}.log.gz + archivedFileCount: 7 + bufferSize: 8KiB + immediateFlush: true diff --git a/mcp-service/src/main/scala/org/apache/texera/mcp/McpService.scala b/mcp-service/src/main/scala/org/apache/texera/mcp/McpService.scala new file mode 100644 index 00000000000..20083f3c402 --- /dev/null +++ b/mcp-service/src/main/scala/org/apache/texera/mcp/McpService.scala @@ -0,0 +1,121 @@ +/* + * 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. + */ + +package org.apache.texera.mcp + +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider} +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.{Bootstrap, Environment} +import org.apache.amber.config.StorageConfig +import org.apache.texera.auth.{JwtAuthFilter, SessionUser} +import org.apache.texera.config.McpConfig +import org.apache.texera.dao.SqlServer +import org.apache.texera.mcp.resource.HealthCheckResource +import org.apache.texera.mcp.server.TexeraMcpServerImpl +import org.eclipse.jetty.servlets.CrossOriginFilter + +import java.nio.file.Path + +/** + * Main MCP Service application for Texera. + * Exposes Texera metadata and capabilities through Model Context Protocol. + * Uses the official MCP SDK implementation in Java. + * Configuration is loaded from common/config/src/main/resources/mcp.conf via McpConfig. + */ +class McpService extends Application[Configuration] { + + override def initialize(bootstrap: Bootstrap[Configuration]): Unit = { + // Register Scala module for Jackson + bootstrap.getObjectMapper.registerModule(DefaultScalaModule) + } + + override def run(config: Configuration, env: Environment): Unit = { + // Set API URL pattern + env.jersey.setUrlPattern("/api/*") + + // Initialize database connection if needed + if (McpConfig.databaseEnabled) { + SqlServer.initConnection( + StorageConfig.jdbcUrl, + StorageConfig.jdbcUsername, + StorageConfig.jdbcPassword + ) + } + + // Initialize MCP server using Java SDK implementation + val mcpServer = new TexeraMcpServerImpl() + mcpServer.start() + + // Register the MCP SDK servlet at /api/mcp and /api/mcp/* + // The servlet handles all MCP protocol requests (GET for SSE, POST for messages) + // Both paths are needed: /api/mcp for base endpoint, /api/mcp/* for any subpaths + val mcpServletRegistration = env.servlets() + .addServlet("mcp-protocol", mcpServer.getServlet) + mcpServletRegistration.addMapping("/api/mcp") + mcpServletRegistration.addMapping("/api/mcp/*") + + // Register health check resource (for service monitoring only) + env.jersey.register(new HealthCheckResource) + + // Add authentication if enabled + if (McpConfig.authEnabled) { + env.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) + env.jersey.register( + new AuthValueFactoryProvider.Binder(classOf[SessionUser]) + ) + } + + val cors = new CrossOriginFilter + val holder = env.servlets().addFilter("cors", cors) + holder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*") + holder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,POST,OPTIONS") + holder.setInitParameter( + CrossOriginFilter.ALLOWED_HEADERS_PARAM, + "Content-Type,Accept,Origin,User-Agent,Mcp-Session-Id" + ) + holder.setInitParameter( + CrossOriginFilter.EXPOSED_HEADERS_PARAM, + "Mcp-Session-Id" + ) + holder.addMappingForUrlPatterns(null, false, "/*") + + // Add shutdown hook for MCP server + env.lifecycle.addServerLifecycleListener(server => { + Runtime.getRuntime.addShutdownHook(new Thread(() => { + mcpServer.stop() + })) + }) + } +} + +object McpService { + def main(args: Array[String]): Unit = { + val configPath = Path + .of(sys.env.getOrElse("TEXERA_HOME", ".")) + .resolve("mcp-service") + .resolve("src/main/resources") + .resolve("mcp-service-config.yaml") + .toAbsolutePath + .toString + + new McpService().run("server", configPath) + } +} diff --git a/mcp-service/src/main/scala/org/apache/texera/mcp/resource/HealthCheckResource.scala b/mcp-service/src/main/scala/org/apache/texera/mcp/resource/HealthCheckResource.scala new file mode 100644 index 00000000000..9467f5e0578 --- /dev/null +++ b/mcp-service/src/main/scala/org/apache/texera/mcp/resource/HealthCheckResource.scala @@ -0,0 +1,37 @@ +/* + * 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. + */ + +package org.apache.texera.mcp.resource + +import jakarta.ws.rs.{GET, Path, Produces} +import jakarta.ws.rs.core.MediaType + +@Path("/healthcheck") +@Produces(Array(MediaType.APPLICATION_JSON)) +class HealthCheckResource { + + @GET + def healthCheck: Map[String, String] = { + Map( + "status" -> "ok", + "service" -> "mcp-service", + "version" -> "1.0.0" + ) + } +} diff --git a/mcp-service/src/test/java/org/apache/texera/mcp/server/TexeraMcpServerImplTest.java b/mcp-service/src/test/java/org/apache/texera/mcp/server/TexeraMcpServerImplTest.java new file mode 100644 index 00000000000..d6073a14bf0 --- /dev/null +++ b/mcp-service/src/test/java/org/apache/texera/mcp/server/TexeraMcpServerImplTest.java @@ -0,0 +1,160 @@ +/* + * 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. + */ + +package org.apache.texera.mcp.server; + +import org.apache.texera.config.McpConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TexeraMcpServerImpl. + * Tests server lifecycle, configuration, and metadata access. + */ +class TexeraMcpServerImplTest { + + private TexeraMcpServerImpl mcpServer; + + @BeforeEach + void setUp() { + mcpServer = new TexeraMcpServerImpl(); + } + + @AfterEach + void tearDown() { + if (mcpServer != null && mcpServer.isRunning()) { + mcpServer.stop(); + } + } + + @Test + @DisplayName("Server should be created in stopped state") + void testServerInitialState() { + assertFalse(mcpServer.isRunning(), "Server should not be running initially"); + } + + @Test + @DisplayName("Server should start successfully") + void testServerStart() { + assertDoesNotThrow(() -> mcpServer.start(), "Server start should not throw"); + assertTrue(mcpServer.isRunning(), "Server should be running after start"); + } + + @Test + @DisplayName("Server should stop successfully") + void testServerStop() { + mcpServer.start(); + assertTrue(mcpServer.isRunning(), "Server should be running before stop"); + + mcpServer.stop(); + assertFalse(mcpServer.isRunning(), "Server should not be running after stop"); + } + + @Test + @DisplayName("Server should handle multiple stop calls gracefully") + void testMultipleStopCalls() { + mcpServer.start(); + mcpServer.stop(); + + assertDoesNotThrow(() -> mcpServer.stop(), "Multiple stop calls should not throw"); + assertFalse(mcpServer.isRunning(), "Server should remain stopped"); + } + + @Test + @DisplayName("Server info should contain correct metadata") + void testServerInfo() { + Map info = mcpServer.getServerInfo(); + + assertNotNull(info, "Server info should not be null"); + assertEquals(McpConfig.serverName(), info.get("name"), "Server name should match config"); + assertEquals(McpConfig.serverVersion(), info.get("version"), "Server version should match config"); + assertEquals("stopped", info.get("status"), "Initial status should be stopped"); + } + + @Test + @DisplayName("Server info should reflect running status after start") + void testServerInfoAfterStart() { + mcpServer.start(); + Map info = mcpServer.getServerInfo(); + + assertEquals("running", info.get("status"), "Status should be running after start"); + } + + @Test + @DisplayName("Server capabilities should match configuration") + void testServerCapabilities() { + Map caps = mcpServer.getCapabilities(); + + assertNotNull(caps, "Capabilities should not be null"); + assertEquals(McpConfig.toolsEnabled(), caps.get("tools"), "Tools capability should match config"); + assertEquals(McpConfig.resourcesEnabled(), caps.get("resources"), "Resources capability should match config"); + assertEquals(McpConfig.promptsEnabled(), caps.get("prompts"), "Prompts capability should match config"); + assertEquals(McpConfig.samplingEnabled(), caps.get("sampling"), "Sampling capability should match config"); + assertEquals(McpConfig.loggingEnabled(), caps.get("logging"), "Logging capability should match config"); + } + + @Test + @DisplayName("Capabilities should be accessible before server start") + void testCapabilitiesBeforeStart() { + assertDoesNotThrow(() -> mcpServer.getCapabilities(), + "Getting capabilities should not require server to be running"); + } + + @Test + @DisplayName("Server lifecycle: start, stop, restart") + void testServerLifecycle() { + // First start + mcpServer.start(); + assertTrue(mcpServer.isRunning(), "Server should be running after first start"); + + // Stop + mcpServer.stop(); + assertFalse(mcpServer.isRunning(), "Server should be stopped"); + + // Restart + mcpServer.start(); + assertTrue(mcpServer.isRunning(), "Server should be running after restart"); + } + + @Test + @DisplayName("Server info and capabilities should remain consistent across lifecycle") + void testConsistencyAcrossLifecycle() { + Map infoBefore = mcpServer.getServerInfo(); + Map capsBefore = mcpServer.getCapabilities(); + + mcpServer.start(); + mcpServer.stop(); + + Map infoAfter = mcpServer.getServerInfo(); + Map capsAfter = mcpServer.getCapabilities(); + + assertEquals(infoBefore.get("name"), infoAfter.get("name"), + "Server name should remain consistent"); + assertEquals(infoBefore.get("version"), infoAfter.get("version"), + "Server version should remain consistent"); + assertEquals(capsBefore, capsAfter, + "Capabilities should remain consistent"); + } +} From e65f5985c1dfd392d1a035ae970fd161d25f08d8 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Thu, 16 Oct 2025 11:12:01 -0700 Subject: [PATCH 003/158] finish v1 of MCP server --- mcp-service/README.md | 533 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100644 mcp-service/README.md diff --git a/mcp-service/README.md b/mcp-service/README.md new file mode 100644 index 00000000000..625448d6a23 --- /dev/null +++ b/mcp-service/README.md @@ -0,0 +1,533 @@ +# Texera MCP Service + +Model Context Protocol (MCP) server for Apache Texera, exposing operator metadata and workflow capabilities to AI agents using the **official MCP SDK v0.14.1**. + +## Overview + +The MCP Service provides a standardized interface for AI assistants (like Claude) to understand and interact with Texera's workflow system through the Model Context Protocol. It exposes operator metadata, schemas, and capabilities using the official Java SDK. + +## Architecture + +The service uses the **official MCP Java SDK v0.14.1** for protocol implementation: + +``` +mcp-service/ +├── src/main/ +│ ├── java/org/apache/texera/mcp/ +│ │ ├── server/ +│ │ │ └── TexeraMcpServerImpl.java # MCP SDK server implementation +│ │ └── tools/ +│ │ └── OperatorToolProvider.java # Java wrapper for tools +│ └── scala/org/apache/texera/mcp/ +│ ├── McpService.scala # Main Dropwizard application +│ └── resource/ +│ └── HealthCheckResource.scala # Health check endpoint +├── common/config/ +│ ├── src/main/resources/ +│ │ └── mcp.conf # MCP configuration +│ └── src/main/scala/org/apache/texera/config/ +│ └── McpConfig.scala # Configuration reader +``` + +### MCP Protocol Communication + +- **Protocol**: Model Context Protocol (MCP) v1.0 +- **SDK**: Official MCP Java SDK v0.14.1 +- **Transport**: HTTP Streamable (Server-Sent Events) +- **Servlet**: `HttpServletStreamableServerTransportProvider` from SDK +- **Endpoint**: All MCP messages at `http://localhost:9098/api/mcp/` + +## Building + +### Build MCP Service Only +```bash +sbt "project McpService" compile +sbt "project McpService" dist +``` + +### Build All Services +```bash +bin/build-services.sh +``` + +## Running + +### Start MCP Service +```bash +bin/mcp-service.sh +``` + +The service starts on port **9098** with: +- MCP server using HTTP Streamable transport at `http://localhost:9098/api/mcp/` +- Health check endpoint at `http://localhost:9098/api/healthcheck` + +### Verify Service +```bash +# Health check (service monitoring only) +curl http://localhost:9098/api/healthcheck +# Expected: {"status":"ok","service":"mcp-service","version":"1.0.0"} + +# MCP protocol - Initialize session +curl -X POST http://localhost:9098/api/mcp/ \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "test", "version": "1.0"} + } + }' +``` + +## MCP Integration + +### HTTP Streamable Transport + +The MCP server uses HTTP Streamable transport (Server-Sent Events) for communication. MCP clients connect to: + +**Base URL**: `http://localhost:9098/api/mcp` + +The SDK's `HttpServletStreamableServerTransportProvider` handles: +- **GET requests**: Establish Server-Sent Events (SSE) connection for receiving server messages +- **POST requests**: Send client messages (tool calls, initialize, etc.) +- **DELETE requests**: Close the session + +### Using with MCP Clients + +MCP clients should use the HTTP transport to connect: + +```typescript +// Example using MCP TypeScript SDK +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { HttpClientTransport } from "@modelcontextprotocol/sdk/client/http.js"; + +const transport = new HttpClientTransport( + new URL("http://localhost:9098/api/mcp/") +); +const client = new Client({ name: "my-client", version: "1.0.0" }, {}); +await client.connect(transport); + +// List available tools +const tools = await client.listTools(); + +// Call a tool +const result = await client.callTool({ + name: "list_operators", + arguments: {} +}); +``` + +### Testing with curl + +```bash +# Initialize session (POST) +curl -X POST http://localhost:9098/api/mcp/ \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "test-client", "version": "1.0"} + } + }' + +# List tools +curl -X POST http://localhost:9098/api/mcp/ \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list" + }' + +# Call a tool +curl -X POST http://localhost:9098/api/mcp/ \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "list_operators", + "arguments": {} + } + }' +``` + +## Available MCP Tools + +The MCP server exposes 8 tools for querying Texera operator metadata: + +### 1. `list_operators` +List all available Texera operators with metadata, groups, and schemas. + +**Parameters**: None + +**Returns**: Complete operator catalog with metadata + +**Example usage in Claude**: +``` +List all available Texera operators +``` + +### 2. `get_operator` +Get detailed metadata for a specific operator. + +**Parameters**: +- `operatorType` (string): Operator type identifier (e.g., "CSVScanSource") + +**Returns**: Full operator metadata including schema, ports, and capabilities + +**Example**: +``` +Get details for the CSVScanSource operator +``` + +### 3. `get_operator_schema` +Get JSON schema for operator configuration. + +**Parameters**: +- `operatorType` (string): Operator type identifier + +**Returns**: JSON schema defining configuration structure + +**Example**: +``` +Show me the configuration schema for CSVScanSource +``` + +### 4. `search_operators` +Search operators by name, description, or type. + +**Parameters**: +- `query` (string): Search query + +**Returns**: List of matching operators + +**Example**: +``` +Search for operators related to CSV +``` + +### 5. `get_operators_by_group` +Get all operators in a specific group. + +**Parameters**: +- `groupName` (string): Group name (e.g., "Data Input", "Machine Learning") + +**Returns**: List of operators in the group + +**Example**: +``` +Show me all operators in the Machine Learning group +``` + +### 6. `get_operator_groups` +Get all operator groups in hierarchical structure. + +**Parameters**: None + +**Returns**: Hierarchical list of operator groups + +**Example**: +``` +What are the operator groups in Texera? +``` + +### 7. `describe_operator` +Get detailed human-readable description of an operator. + +**Parameters**: +- `operatorType` (string): Operator type identifier + +**Returns**: Formatted description with ports, capabilities, and schema + +**Example**: +``` +Describe the KeywordSearch operator in detail +``` + +### 8. `get_operators_by_capability` +Get operators supporting specific capabilities. + +**Parameters**: +- `capability` (string): One of: `reconfiguration`, `dynamic_input`, `dynamic_output`, `port_customization` + +**Returns**: List of operators with the capability + +**Example**: +``` +Which operators support reconfiguration? +``` + +## Example Queries for Claude + +Once integrated with Claude Desktop, you can ask: + +- "List all available Texera operators" +- "Show me operators in the Machine Learning group" +- "What are the configuration options for the CSV scan operator?" +- "Find operators that support reconfiguration" +- "Describe the KeywordSearch operator in detail" +- "Search for operators that work with JSON data" +- "What visualization operators are available?" + +## Monitoring Endpoints + +The service provides a basic health check endpoint for service monitoring: + +- `GET /api/healthcheck` - Standard health check endpoint + +**Note**: All MCP functionality (server info, capabilities, tools) is accessed through the MCP protocol at `/api/mcp/` using JSON-RPC messages, not REST endpoints. + +## Configuration + +The MCP service uses two configuration files following Texera's standard pattern: + +### MCP-Specific Configuration + +Edit `common/config/src/main/resources/mcp.conf` for MCP-specific settings: + +```hocon +mcp { + server { + port = 9098 # HTTP port for REST API + name = "texera-mcp" # Server name + version = "1.0.0" # Server version + } + + transport = "http" # MCP transport: http, stdio, or websocket + + auth { + enabled = false # Enable JWT authentication + } + + database { + enabled = true # Enable database access + } + + capabilities { + tools = true # Enable MCP tools + resources = true # Enable MCP resources (future) + prompts = true # Enable MCP prompts (future) + sampling = false # Enable sampling capability + logging = true # Enable logging capability + } + + performance { + max-concurrent-requests = 100 # Max concurrent requests + request-timeout-ms = 30000 # Request timeout in milliseconds + } + + enabled-tools = ["operators", "workflows", "datasets", "projects", "executions"] + enabled-resources = ["operator-schemas", "workflow-templates", "dataset-schemas"] + enabled-prompts = ["create-workflow", "optimize-workflow", "explain-operator"] +} +``` + +Configuration values can be overridden with environment variables: +- `MCP_PORT` - Override server port +- `MCP_TRANSPORT` - Override transport type +- `MCP_AUTH_ENABLED` - Enable/disable authentication +- `MCP_TOOLS_ENABLED` - Enable/disable tools capability +- etc. + +### Dropwizard Configuration + +Edit `mcp-service/src/main/resources/mcp-service-config.yaml` for Dropwizard-specific settings: + +```yaml +server: + applicationConnectors: + - type: http + port: 9098 + adminConnectors: [] + +logging: + level: INFO + loggers: + "io.dropwizard": INFO + "org.apache.texera": DEBUG + appenders: + - type: console + - type: file + currentLogFilename: log/mcp-service.log +``` + +## Dependencies + +### MCP SDK Dependencies (in build.sbt) +```scala +"io.modelcontextprotocol.sdk" % "mcp" % "0.14.1", +"io.modelcontextprotocol.sdk" % "mcp-json-jackson2" % "0.14.1" +``` + +### Module Dependencies +- `WorkflowOperator` - Operator metadata generation +- `Auth` - Authentication (optional) +- `Config` - Configuration management +- `DAO` - Database access (optional) + +## Development + +### Project Structure + +- **Java classes** (`src/main/java`): MCP SDK integration, uses official SDK +- **Scala classes** (`src/main/scala`): Dropwizard app, REST API, business logic + +### Adding New Tools + +1. Implement tool logic in `OperatorTool.scala` or create new tool class +2. Add Java wrapper in `OperatorToolProvider.java` (if needed) +3. Register tool in `TexeraMcpServerImpl.java` following SDK patterns: + +```java +private void registerMyTool(McpSyncServerBuilder builder) { + // Create input schema + ObjectNode inputSchema = objectMapper.createObjectNode(); + // ... define schema + + // Create tool + Tool tool = new Tool( + "my_tool_name", + "Tool description", + inputSchema + ); + + // Create tool specification + McpServerFeatures.SyncToolSpecification toolSpec = + new McpServerFeatures.SyncToolSpecification( + tool, + (exchange, arguments) -> { + // Tool implementation + // Return CallToolResult with TextContent + } + ); + + builder.tool(toolSpec); +} +``` + +### Testing + +```bash +# Compile +sbt "project McpService" compile + +# Run tests +sbt "project McpService" test + +# Build distribution +sbt "project McpService" dist +``` + +## Logs + +Logs are written to: +- Console output +- `log/mcp-service.log` - Current log +- `log/mcp-service-YYYY-MM-DD.log.gz` - Archived (7 days retention) + +## Troubleshooting + +### Service won't start +- Check port 9098 availability: `lsof -i :9098` +- Verify config YAML is valid +- Check logs: `log/mcp-service.log` + +### MCP client can't connect +- Ensure service is running: `curl http://localhost:9098/api/healthcheck` +- Verify HTTP transport endpoint is accessible at `http://localhost:9098/api/mcp/` +- Check client is using HTTP Streamable transport, not STDIO +- Test with curl initialize command (see "Testing with curl" section) + +### "Tool not found" errors +- Verify tools are registered in `TexeraMcpServerImpl` +- Check `enableTools: true` in configuration +- Review `enabledTools` list in config + +### Compilation errors +- Ensure MCP SDK version 0.14.1 is available +- Run `sbt update` to fetch dependencies +- Check Java 17+ is installed + +## Future Extensions + +The architecture supports adding: + +### Workflow Tools (Planned) +- `list_workflows` - List user workflows +- `get_workflow` - Get workflow details +- `validate_workflow` - Validate workflow +- `create_workflow` - Create new workflows +- `update_workflow` - Modify workflows + +### Dataset Tools (Planned) +- `list_datasets` - List available datasets +- `get_dataset_schema` - Get dataset schema +- `query_dataset` - Query dataset contents + +### Execution Tools (Planned) +- `get_execution_status` - Monitor execution +- `get_execution_logs` - Retrieve logs +- `get_execution_results` - Get results + +### MCP Resources (Planned) +- Workflow templates +- Operator schemas +- Dataset schemas + +### MCP Prompts (Planned) +- Create workflow from description +- Optimize workflow +- Explain operator functionality + +## Technical Details + +### MCP SDK Integration + +The service uses the official MCP Java SDK (v0.14.1) which provides: +- **Protocol implementation**: Full MCP specification compliance +- **Transport providers**: STDIO, HTTP, WebSocket support +- **Type-safe APIs**: Strongly typed tool specifications +- **Async/Sync modes**: Flexible execution models + +### Transport: HTTP Streamable + +HTTP Streamable transport uses Server-Sent Events (SSE) for MCP communication: +- **GET** requests establish SSE connection for server→client messages +- **POST** requests send client→server JSON-RPC messages +- **DELETE** requests close the session +- JSON-RPC message format over HTTP +- Stateful sessions maintained via SSE connection + +### Why Java Implementation? + +While Texera is primarily Scala, the MCP server uses Java because: +1. Official MCP SDK is Java/Kotlin +2. Better SDK compatibility and stability +3. Easier to follow official documentation +4. Scala wrapper provides clean API for Texera code + +## Port Information + +- **9098** - MCP Service (HTTP Streamable transport at `/api/mcp/`, health check at `/api/healthcheck`) +- 8080 - Amber (Main Texera service) +- 9090 - Workflow Compiling Service +- 9092 - File Service + +## References + +- [Model Context Protocol Specification](https://modelcontextprotocol.io/) +- [MCP Java SDK Documentation](https://modelcontextprotocol.io/sdk/java/mcp-server) +- [Claude Desktop MCP Integration](https://www.anthropic.com/news/model-context-protocol) + +## License + +Licensed under the Apache License, Version 2.0. See LICENSE file for details. From 6aabeb52ab2b525cc26d5759137cbe966f90c8e9 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Thu, 16 Oct 2025 17:13:17 -0700 Subject: [PATCH 004/158] finish e2e workable version --- .../mcp/server/TexeraMcpServerImpl.java | 54 +++++++++++++------ .../mcp/tools/OperatorToolProvider.java | 4 -- .../main/resources/mcp-service-config.yaml | 6 ++- .../org/apache/texera/mcp/McpService.scala | 18 +------ 4 files changed, 44 insertions(+), 38 deletions(-) diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java b/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java index c09e21d0946..c16c77d51ea 100644 --- a/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java +++ b/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.module.scala.DefaultScalaModule; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; @@ -45,7 +44,7 @@ public class TexeraMcpServerImpl { private static final Logger logger = LoggerFactory.getLogger(TexeraMcpServerImpl.class); - private final ObjectMapper objectMapper; + private final ObjectMapper objectMapper; // For serializing our data (operator metadata) private final OperatorToolProvider operatorToolProvider; private McpSyncServer mcpServer; private HttpServletStreamableServerTransportProvider transportProvider; @@ -58,8 +57,10 @@ public class TexeraMcpServerImpl { private boolean running = false; public TexeraMcpServerImpl() { + // ObjectMapper for our data (operator metadata) - includes Scala module this.objectMapper = new ObjectMapper(); this.objectMapper.registerModule(new DefaultScalaModule()); + this.operatorToolProvider = new OperatorToolProvider(); } @@ -70,14 +71,13 @@ public void start() { logger.info("Starting Texera MCP Server: {} v{}", McpConfig.serverName(), McpConfig.serverVersion()); try { - // Create JSON mapper for MCP protocol - var jsonMapper = new JacksonMcpJsonMapper(objectMapper); + // Use the SDK's default JSON mapper supplier - this ensures proper MCP protocol deserialization + // The SDK provides JacksonMcpJsonMapperSupplier which creates a properly configured mapper + logger.info("Using SDK's default JacksonMcpJsonMapperSupplier for MCP protocol"); // Build HTTP Streamable transport provider using SDK builder + // Don't specify jsonMapper - let the SDK use its default transportProvider = HttpServletStreamableServerTransportProvider.builder() - .jsonMapper(jsonMapper) -// .mcpEndpoint("/") // Base endpoint - will be mounted at /api/mcp/ -// .keepAliveInterval(Duration.ofSeconds(30)) .build(); // Build server capabilities based on configuration @@ -113,9 +113,10 @@ public void start() { running = true; logger.info("MCP Server started successfully with 8 operator tools on HTTP Streamable transport"); + logger.info("Server listening for MCP protocol messages - ready to handle requests"); } catch (Exception e) { - logger.error("Failed to start MCP Server", e); + logger.error("FATAL: Failed to start MCP Server - {}", e.getMessage(), e); throw new RuntimeException("Failed to start MCP Server", e); } } @@ -161,6 +162,7 @@ private void registerOperatorTools(McpServer.SyncSpecification { try { @@ -168,7 +170,7 @@ private void registerOperatorTools(McpServer.SyncSpecification { try { @@ -276,7 +279,7 @@ private void registerOperatorTools(McpServer.SyncSpecification getOperator(String operatorType) { - logger.info("MCP Tool: Getting operator metadata for type: {}", operatorType); List operators = CollectionConverters.asJava( OperatorMetadataGenerator.allOperatorMetadata().operators() ); @@ -68,7 +66,6 @@ public Option getOperator(String operatorType) { * Get operator JSON schema */ public Option getOperatorSchema(String operatorType) { - logger.info("MCP Tool: Getting operator schema for type: {}", operatorType); Option operator = getOperator(operatorType); if (operator.isDefined()) { @@ -81,7 +78,6 @@ public Option getOperatorSchema(String operatorType) { * Get operators by group name */ public List getOperatorsByGroup(String groupName) { - logger.info("MCP Tool: Getting operators for group: {}", groupName); List operators = CollectionConverters.asJava( OperatorMetadataGenerator.allOperatorMetadata().operators() ); diff --git a/mcp-service/src/main/resources/mcp-service-config.yaml b/mcp-service/src/main/resources/mcp-service-config.yaml index 6e22e9cad33..54727ef8143 100644 --- a/mcp-service/src/main/resources/mcp-service-config.yaml +++ b/mcp-service/src/main/resources/mcp-service-config.yaml @@ -11,7 +11,10 @@ logging: level: INFO loggers: "io.dropwizard": INFO - "org.apache.texera": DEBUG + "org.apache.texera": INFO +# "org.eclipse.jetty": DEBUG +# "jakarta.servlet": DEBUG + "io.modelcontextprotocol": DEBUG appenders: - type: console - type: file @@ -24,3 +27,4 @@ logging: archivedFileCount: 7 bufferSize: 8KiB immediateFlush: true + logFormat: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" diff --git a/mcp-service/src/main/scala/org/apache/texera/mcp/McpService.scala b/mcp-service/src/main/scala/org/apache/texera/mcp/McpService.scala index 20083f3c402..039192ef681 100644 --- a/mcp-service/src/main/scala/org/apache/texera/mcp/McpService.scala +++ b/mcp-service/src/main/scala/org/apache/texera/mcp/McpService.scala @@ -30,7 +30,6 @@ import org.apache.texera.config.McpConfig import org.apache.texera.dao.SqlServer import org.apache.texera.mcp.resource.HealthCheckResource import org.apache.texera.mcp.server.TexeraMcpServerImpl -import org.eclipse.jetty.servlets.CrossOriginFilter import java.nio.file.Path @@ -67,7 +66,8 @@ class McpService extends Application[Configuration] { // Register the MCP SDK servlet at /api/mcp and /api/mcp/* // The servlet handles all MCP protocol requests (GET for SSE, POST for messages) // Both paths are needed: /api/mcp for base endpoint, /api/mcp/* for any subpaths - val mcpServletRegistration = env.servlets() + val mcpServletRegistration = env + .servlets() .addServlet("mcp-protocol", mcpServer.getServlet) mcpServletRegistration.addMapping("/api/mcp") mcpServletRegistration.addMapping("/api/mcp/*") @@ -83,20 +83,6 @@ class McpService extends Application[Configuration] { ) } - val cors = new CrossOriginFilter - val holder = env.servlets().addFilter("cors", cors) - holder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*") - holder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,POST,OPTIONS") - holder.setInitParameter( - CrossOriginFilter.ALLOWED_HEADERS_PARAM, - "Content-Type,Accept,Origin,User-Agent,Mcp-Session-Id" - ) - holder.setInitParameter( - CrossOriginFilter.EXPOSED_HEADERS_PARAM, - "Mcp-Session-Id" - ) - holder.addMappingForUrlPatterns(null, false, "/*") - // Add shutdown hook for MCP server env.lifecycle.addServerLifecycleListener(server => { Runtime.getRuntime.addShutdownHook(new Thread(() => { From 680542bb26d07ab1a951ade7a5a4b6bc24e14e3c Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 17 Oct 2025 00:04:04 -0700 Subject: [PATCH 005/158] refactor to be more modulized --- .../mcp/server/TexeraMcpServerImpl.java | 230 +----------------- .../texera/mcp/tools/McpSchemaGenerator.java | 82 +++++++ .../mcp/tools/OperatorToolProvider.java | 204 ++++++++++++++++ .../mcp/tools/inputs/CapabilityInput.java | 42 ++++ .../texera/mcp/tools/inputs/EmptyInput.java | 28 +++ .../mcp/tools/inputs/GroupNameInput.java | 42 ++++ .../mcp/tools/inputs/OperatorTypeInput.java | 43 ++++ .../mcp/tools/inputs/SearchQueryInput.java | 42 ++++ 8 files changed, 486 insertions(+), 227 deletions(-) create mode 100644 mcp-service/src/main/java/org/apache/texera/mcp/tools/McpSchemaGenerator.java create mode 100644 mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/CapabilityInput.java create mode 100644 mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/EmptyInput.java create mode 100644 mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/GroupNameInput.java create mode 100644 mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/OperatorTypeInput.java create mode 100644 mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/SearchQueryInput.java diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java b/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java index c16c77d51ea..d8105a3fc63 100644 --- a/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java +++ b/mcp-service/src/main/java/org/apache/texera/mcp/server/TexeraMcpServerImpl.java @@ -19,8 +19,6 @@ package org.apache.texera.mcp.server; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.module.scala.DefaultScalaModule; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; @@ -44,7 +42,6 @@ public class TexeraMcpServerImpl { private static final Logger logger = LoggerFactory.getLogger(TexeraMcpServerImpl.class); - private final ObjectMapper objectMapper; // For serializing our data (operator metadata) private final OperatorToolProvider operatorToolProvider; private McpSyncServer mcpServer; private HttpServletStreamableServerTransportProvider transportProvider; @@ -57,10 +54,6 @@ public class TexeraMcpServerImpl { private boolean running = false; public TexeraMcpServerImpl() { - // ObjectMapper for our data (operator metadata) - includes Scala module - this.objectMapper = new ObjectMapper(); - this.objectMapper.registerModule(new DefaultScalaModule()); - this.operatorToolProvider = new OperatorToolProvider(); } @@ -152,228 +145,11 @@ public void stop() { } /** - * Register all operator tools using MCP SDK + * Register all operator tools using MCP SDK. + * Delegates to OperatorToolProvider for tool registration. */ private void registerOperatorTools(McpServer.SyncSpecification serverBuilder) { - logger.info("Registering operator tools"); - - // Tool 1: List all operators - serverBuilder.toolCall( - McpSchema.Tool.builder() - .name("list_operators") - .description("List all available Texera operators with their metadata, groups, and schemas") - .inputSchema(createEmptyInputSchema()) - .build(), - (exchange, request) -> { - try { - var result = operatorToolProvider.listOperators(); - String jsonResult = objectMapper.writeValueAsString(result); - return new McpSchema.CallToolResult(jsonResult, false); - } catch (Exception e) { - logger.error("Error in list_operators tool: {}", e.getMessage(), e); - return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); - } - } - ); - - // Tool 2: Get specific operator - serverBuilder.toolCall( - McpSchema.Tool.builder() - .name("get_operator") - .description("Get detailed metadata for a specific operator by its type identifier") - .inputSchema(createInputSchema("operatorType", "The operator type identifier (e.g., 'CSVScanSource')")) - .build(), - (exchange, request) -> { - try { - String operatorType = getStringArg(request, "operatorType"); - var result = operatorToolProvider.getOperator(operatorType); - - if (result.isDefined()) { - String jsonResult = objectMapper.writeValueAsString(result.get()); - return new McpSchema.CallToolResult(jsonResult, false); - } else { - return new McpSchema.CallToolResult("Operator not found: " + operatorType, true); - } - } catch (Exception e) { - logger.error("Error in get_operator tool: {}", e.getMessage(), e); - return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); - } - } - ); - - // Tool 3: Get operator schema - serverBuilder.toolCall( - McpSchema.Tool.builder() - .name("get_operator_schema") - .description("Get the JSON schema for a specific operator's configuration") - .inputSchema(createInputSchema("operatorType", "The operator type identifier")) - .build(), - (exchange, request) -> { - try { - String operatorType = getStringArg(request, "operatorType"); - var result = operatorToolProvider.getOperatorSchema(operatorType); - - if (result.isDefined()) { - String jsonResult = objectMapper.writeValueAsString(result.get()); - return new McpSchema.CallToolResult(jsonResult, false); - } else { - return new McpSchema.CallToolResult("Operator schema not found: " + operatorType, true); - } - } catch (Exception e) { - logger.error("Error in get_operator_schema tool: {}", e.getMessage(), e); - return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); - } - } - ); - - // Tool 4: Search operators - serverBuilder.toolCall( - McpSchema.Tool.builder() - .name("search_operators") - .description("Search operators by name, description, or type using a query string") - .inputSchema(createInputSchema("query", "Search query string")) - .build(), - (exchange, request) -> { - try { - String query = getStringArg(request, "query"); - var result = operatorToolProvider.searchOperators(query); - String jsonResult = objectMapper.writeValueAsString(result); - return new McpSchema.CallToolResult(jsonResult, false); - } catch (Exception e) { - logger.error("Error in search_operators tool: {}", e.getMessage(), e); - return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); - } - } - ); - - // Tool 5: Get operators by group - serverBuilder.toolCall( - McpSchema.Tool.builder() - .name("get_operators_by_group") - .description("Get all operators in a specific group") - .inputSchema(createInputSchema("groupName", "The operator group name (e.g., 'Data Input', 'Machine Learning')")) - .build(), - (exchange, request) -> { - try { - String groupName = getStringArg(request, "groupName"); - var result = operatorToolProvider.getOperatorsByGroup(groupName); - String jsonResult = objectMapper.writeValueAsString(result); - return new McpSchema.CallToolResult(jsonResult, false); - } catch (Exception e) { - logger.error("Error in get_operators_by_group tool: {}", e.getMessage(), e); - return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); - } - } - ); - - // Tool 6: Get operator groups - serverBuilder.toolCall( - McpSchema.Tool.builder() - .name("get_operator_groups") - .description("Get all operator groups in their hierarchical structure") - .inputSchema(createEmptyInputSchema()) - .build(), - (exchange, request) -> { - try { - var result = operatorToolProvider.getOperatorGroups(); - String jsonResult = objectMapper.writeValueAsString(result); - return new McpSchema.CallToolResult(jsonResult, false); - } catch (Exception e) { - logger.error("Error in get_operator_groups tool: {}", e.getMessage(), e); - return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); - } - } - ); - - // Tool 7: Describe operator - serverBuilder.toolCall( - McpSchema.Tool.builder() - .name("describe_operator") - .description("Get a detailed human-readable description of an operator including ports, capabilities, and schema") - .inputSchema(createInputSchema("operatorType", "The operator type identifier")) - .build(), - (exchange, request) -> { - try { - String operatorType = getStringArg(request, "operatorType"); - var result = operatorToolProvider.describeOperator(operatorType); - - if (result.isDefined()) { - return new McpSchema.CallToolResult(result.get(), false); - } else { - return new McpSchema.CallToolResult("Operator not found: " + operatorType, true); - } - } catch (Exception e) { - logger.error("Error in describe_operator tool: {}", e.getMessage(), e); - return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); - } - } - ); - - // Tool 8: Get operators by capability - serverBuilder.toolCall( - McpSchema.Tool.builder() - .name("get_operators_by_capability") - .description("Get operators that support specific capabilities") - .inputSchema(createInputSchema("capability", "The capability to filter by (reconfiguration, dynamic_input, dynamic_output, port_customization)")) - .build(), - (exchange, request) -> { - try { - String capability = getStringArg(request, "capability"); - var result = operatorToolProvider.getOperatorsByCapability(capability); - String jsonResult = objectMapper.writeValueAsString(result); - return new McpSchema.CallToolResult(jsonResult, false); - } catch (Exception e) { - logger.error("Error in get_operators_by_capability tool: {}", e.getMessage(), e); - return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); - } - } - ); - - logger.info("Registered 8 operator tools"); - } - - /** - * Helper to create empty input schema for tools with no parameters - */ - private McpSchema.JsonSchema createEmptyInputSchema() { - try { - // Create a plain ObjectMapper for schema parsing (no custom modules needed) - ObjectMapper schemaMapper = new ObjectMapper(); - String schemaJson = "{\"type\":\"object\",\"properties\":{},\"required\":[]}"; - return schemaMapper.readValue(schemaJson, McpSchema.JsonSchema.class); - } catch (Exception e) { - logger.error("Error creating empty input schema", e); - return null; - } - } - - /** - * Helper to create input schema for a single string parameter - */ - private McpSchema.JsonSchema createInputSchema(String paramName, String description) { - try { - // Create a plain ObjectMapper for schema parsing (no custom modules needed) - ObjectMapper schemaMapper = new ObjectMapper(); - String schemaJson = String.format( - "{\"type\":\"object\",\"properties\":{\"%s\":{\"type\":\"string\",\"description\":\"%s\"}},\"required\":[\"%s\"]}", - paramName, description, paramName - ); - return schemaMapper.readValue(schemaJson, McpSchema.JsonSchema.class); - } catch (Exception e) { - logger.error("Error creating input schema", e); - return null; - } - } - - /** - * Helper to extract string argument from CallToolRequest - */ - private String getStringArg(McpSchema.CallToolRequest request, String key) { - if (request.arguments() == null) { - return ""; - } - Object value = request.arguments().get(key); - return value != null ? value.toString() : ""; + operatorToolProvider.registerAllTools(serverBuilder); } /** diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/tools/McpSchemaGenerator.java b/mcp-service/src/main/java/org/apache/texera/mcp/tools/McpSchemaGenerator.java new file mode 100644 index 00000000000..ae2e058c6b9 --- /dev/null +++ b/mcp-service/src/main/java/org/apache/texera/mcp/tools/McpSchemaGenerator.java @@ -0,0 +1,82 @@ +/* + * 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. + */ + +package org.apache.texera.mcp.tools; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kjetland.jackson.jsonSchema.JsonSchemaConfig; +import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator; +import io.modelcontextprotocol.spec.McpSchema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility class for generating MCP JSON schemas from annotated Java classes. + * Uses Jackson's JSON Schema generation to create schemas from classes annotated with + * @JsonProperty, @JsonSchemaTitle, @JsonPropertyDescription, etc. + */ +public class McpSchemaGenerator { + + private static final Logger logger = LoggerFactory.getLogger(McpSchemaGenerator.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final JsonSchemaGenerator schemaGenerator; + + static { + // Configure JSON Schema generator with appropriate settings + JsonSchemaConfig config = JsonSchemaConfig.vanillaJsonSchemaDraft4(); + schemaGenerator = new JsonSchemaGenerator(objectMapper, config); + } + + /** + * Generate MCP JSON schema from a Java class using Jackson annotations. + * + * @param inputClass The class annotated with Jackson schema annotations + * @return McpSchema.JsonSchema object representing the input schema + */ + public static McpSchema.JsonSchema generateSchema(Class inputClass) { + try { + // Generate JSON schema from the class + JsonNode schemaNode = schemaGenerator.generateJsonSchema(inputClass); + + // Convert JsonNode to McpSchema.JsonSchema + String schemaJson = objectMapper.writeValueAsString(schemaNode); + return objectMapper.readValue(schemaJson, McpSchema.JsonSchema.class); + + } catch (Exception e) { + logger.error("Error generating schema for class {}: {}", inputClass.getName(), e.getMessage(), e); + throw new RuntimeException("Failed to generate schema for " + inputClass.getName(), e); + } + } + + /** + * Generate an empty schema for tools with no input parameters. + * + * @return McpSchema.JsonSchema with empty properties and no required fields + */ + public static McpSchema.JsonSchema generateEmptySchema() { + try { + String schemaJson = "{\"type\":\"object\",\"properties\":{},\"required\":[]}"; + return objectMapper.readValue(schemaJson, McpSchema.JsonSchema.class); + } catch (Exception e) { + logger.error("Error generating empty schema: {}", e.getMessage(), e); + throw new RuntimeException("Failed to generate empty schema", e); + } + } +} diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/tools/OperatorToolProvider.java b/mcp-service/src/main/java/org/apache/texera/mcp/tools/OperatorToolProvider.java index abda7649b28..1e884e3ed66 100644 --- a/mcp-service/src/main/java/org/apache/texera/mcp/tools/OperatorToolProvider.java +++ b/mcp-service/src/main/java/org/apache/texera/mcp/tools/OperatorToolProvider.java @@ -20,10 +20,15 @@ package org.apache.texera.mcp.tools; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.scala.DefaultScalaModule; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.spec.McpSchema; import org.apache.amber.operator.metadata.AllOperatorMetadata; import org.apache.amber.operator.metadata.GroupInfo; import org.apache.amber.operator.metadata.OperatorMetadata; import org.apache.amber.operator.metadata.OperatorMetadataGenerator; +import org.apache.texera.mcp.tools.inputs.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import scala.Option; @@ -39,6 +44,12 @@ public class OperatorToolProvider { private static final Logger logger = LoggerFactory.getLogger(OperatorToolProvider.class); + private final ObjectMapper objectMapper; + + public OperatorToolProvider() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.registerModule(new DefaultScalaModule()); + } /** * List all available Texera operators @@ -194,4 +205,197 @@ public List getOperatorsByCapability(String capability) { return List.of(); } } + + /** + * Register all operator tools with the MCP server builder. + * Uses annotation-based schema generation for type-safe input schemas. + */ + public void registerAllTools(McpServer.SyncSpecification serverBuilder) { + logger.info("Registering operator tools"); + + // Tool 1: List all operators + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("list_operators") + .description("List all available Texera operators with their metadata, groups, and schemas") + .inputSchema(McpSchemaGenerator.generateEmptySchema()) + .build(), + (exchange, request) -> { + try { + var result = listOperators(); + String jsonResult = objectMapper.writeValueAsString(result); + return new McpSchema.CallToolResult(jsonResult, false); + } catch (Exception e) { + logger.error("Error in list_operators tool: {}", e.getMessage(), e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 2: Get specific operator + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("get_operator") + .description("Get detailed metadata for a specific operator by its type identifier") + .inputSchema(McpSchemaGenerator.generateSchema(OperatorTypeInput.class)) + .build(), + (exchange, request) -> { + try { + String operatorType = getStringArg(request, "operatorType"); + var result = getOperator(operatorType); + + if (result.isDefined()) { + String jsonResult = objectMapper.writeValueAsString(result.get()); + return new McpSchema.CallToolResult(jsonResult, false); + } else { + return new McpSchema.CallToolResult("Operator not found: " + operatorType, true); + } + } catch (Exception e) { + logger.error("Error in get_operator tool: {}", e.getMessage(), e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 3: Get operator schema + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("get_operator_schema") + .description("Get the JSON schema for a specific operator's configuration") + .inputSchema(McpSchemaGenerator.generateSchema(OperatorTypeInput.class)) + .build(), + (exchange, request) -> { + try { + String operatorType = getStringArg(request, "operatorType"); + var result = getOperatorSchema(operatorType); + + if (result.isDefined()) { + String jsonResult = objectMapper.writeValueAsString(result.get()); + return new McpSchema.CallToolResult(jsonResult, false); + } else { + return new McpSchema.CallToolResult("Operator schema not found: " + operatorType, true); + } + } catch (Exception e) { + logger.error("Error in get_operator_schema tool: {}", e.getMessage(), e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 4: Search operators + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("search_operators") + .description("Search operators by name, description, or type using a query string") + .inputSchema(McpSchemaGenerator.generateSchema(SearchQueryInput.class)) + .build(), + (exchange, request) -> { + try { + String query = getStringArg(request, "query"); + var result = searchOperators(query); + String jsonResult = objectMapper.writeValueAsString(result); + return new McpSchema.CallToolResult(jsonResult, false); + } catch (Exception e) { + logger.error("Error in search_operators tool: {}", e.getMessage(), e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 5: Get operators by group + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("get_operators_by_group") + .description("Get all operators in a specific group") + .inputSchema(McpSchemaGenerator.generateSchema(GroupNameInput.class)) + .build(), + (exchange, request) -> { + try { + String groupName = getStringArg(request, "groupName"); + var result = getOperatorsByGroup(groupName); + String jsonResult = objectMapper.writeValueAsString(result); + return new McpSchema.CallToolResult(jsonResult, false); + } catch (Exception e) { + logger.error("Error in get_operators_by_group tool: {}", e.getMessage(), e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 6: Get operator groups + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("get_operator_groups") + .description("Get all operator groups in their hierarchical structure") + .inputSchema(McpSchemaGenerator.generateEmptySchema()) + .build(), + (exchange, request) -> { + try { + var result = getOperatorGroups(); + String jsonResult = objectMapper.writeValueAsString(result); + return new McpSchema.CallToolResult(jsonResult, false); + } catch (Exception e) { + logger.error("Error in get_operator_groups tool: {}", e.getMessage(), e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 7: Describe operator + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("describe_operator") + .description("Get a detailed human-readable description of an operator including ports, capabilities, and schema") + .inputSchema(McpSchemaGenerator.generateSchema(OperatorTypeInput.class)) + .build(), + (exchange, request) -> { + try { + String operatorType = getStringArg(request, "operatorType"); + var result = describeOperator(operatorType); + + if (result.isDefined()) { + return new McpSchema.CallToolResult(result.get(), false); + } else { + return new McpSchema.CallToolResult("Operator not found: " + operatorType, true); + } + } catch (Exception e) { + logger.error("Error in describe_operator tool: {}", e.getMessage(), e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + // Tool 8: Get operators by capability + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name("get_operators_by_capability") + .description("Get operators that support specific capabilities") + .inputSchema(McpSchemaGenerator.generateSchema(CapabilityInput.class)) + .build(), + (exchange, request) -> { + try { + String capability = getStringArg(request, "capability"); + var result = getOperatorsByCapability(capability); + String jsonResult = objectMapper.writeValueAsString(result); + return new McpSchema.CallToolResult(jsonResult, false); + } catch (Exception e) { + logger.error("Error in get_operators_by_capability tool: {}", e.getMessage(), e); + return new McpSchema.CallToolResult("Error: " + e.getMessage(), true); + } + } + ); + + logger.info("Registered 8 operator tools"); + } + + /** + * Helper to extract string argument from CallToolRequest + */ + private String getStringArg(McpSchema.CallToolRequest request, String key) { + if (request.arguments() == null) { + return ""; + } + Object value = request.arguments().get(key); + return value != null ? value.toString() : ""; + } } diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/CapabilityInput.java b/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/CapabilityInput.java new file mode 100644 index 00000000000..2a103c44a85 --- /dev/null +++ b/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/CapabilityInput.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package org.apache.texera.mcp.tools.inputs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle; + +/** + * Input schema for the get_operators_by_capability tool. + */ +public class CapabilityInput { + + @JsonProperty(required = true) + @JsonSchemaTitle("Capability") + @JsonPropertyDescription("The capability to filter by (reconfiguration, dynamic_input, dynamic_output, port_customization)") + public String capability; + + public CapabilityInput() { + } + + public CapabilityInput(String capability) { + this.capability = capability; + } +} diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/EmptyInput.java b/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/EmptyInput.java new file mode 100644 index 00000000000..d9f7ec81ddd --- /dev/null +++ b/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/EmptyInput.java @@ -0,0 +1,28 @@ +/* + * 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. + */ + +package org.apache.texera.mcp.tools.inputs; + +/** + * Input schema for MCP tools that accept no parameters. + * Used by: list_operators, get_operator_groups + */ +public class EmptyInput { + // No fields - this class represents tools with no input parameters +} diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/GroupNameInput.java b/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/GroupNameInput.java new file mode 100644 index 00000000000..afd6e2ec980 --- /dev/null +++ b/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/GroupNameInput.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package org.apache.texera.mcp.tools.inputs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle; + +/** + * Input schema for the get_operators_by_group tool. + */ +public class GroupNameInput { + + @JsonProperty(required = true) + @JsonSchemaTitle("Group Name") + @JsonPropertyDescription("The operator group name (e.g., 'Data Input', 'Machine Learning')") + public String groupName; + + public GroupNameInput() { + } + + public GroupNameInput(String groupName) { + this.groupName = groupName; + } +} diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/OperatorTypeInput.java b/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/OperatorTypeInput.java new file mode 100644 index 00000000000..3693fca7573 --- /dev/null +++ b/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/OperatorTypeInput.java @@ -0,0 +1,43 @@ +/* + * 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. + */ + +package org.apache.texera.mcp.tools.inputs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle; + +/** + * Input schema for MCP tools that accept an operator type parameter. + * Used by: get_operator, get_operator_schema, describe_operator + */ +public class OperatorTypeInput { + + @JsonProperty(required = true) + @JsonSchemaTitle("Operator Type") + @JsonPropertyDescription("The operator type identifier (e.g., 'CSVScanSource')") + public String operatorType; + + public OperatorTypeInput() { + } + + public OperatorTypeInput(String operatorType) { + this.operatorType = operatorType; + } +} diff --git a/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/SearchQueryInput.java b/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/SearchQueryInput.java new file mode 100644 index 00000000000..323839a37c8 --- /dev/null +++ b/mcp-service/src/main/java/org/apache/texera/mcp/tools/inputs/SearchQueryInput.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package org.apache.texera.mcp.tools.inputs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle; + +/** + * Input schema for the search_operators tool. + */ +public class SearchQueryInput { + + @JsonProperty(required = true) + @JsonSchemaTitle("Search Query") + @JsonPropertyDescription("Search query string to match against operator names, descriptions, or types") + public String query; + + public SearchQueryInput() { + } + + public SearchQueryInput(String query) { + this.query = query; + } +} From aa420fd637aaa090f869ddd9092afd49dd5ba823 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 17 Oct 2025 15:12:21 -0700 Subject: [PATCH 006/158] add frontend copilot --- frontend/package.json | 4 + frontend/proxy.config.json | 11 + .../copilot-panel.component.html | 223 ++ .../copilot-panel.component.scss | 236 ++ .../copilot-panel/copilot-panel.component.ts | 148 + .../service/copilot/texera-copilot.ts | 399 ++ .../service/copilot/workflow-tools.ts | 137 + frontend/yarn.lock | 3562 ++++++++++++++--- 8 files changed, 4197 insertions(+), 523 deletions(-) create mode 100644 frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.html create mode 100644 frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.scss create mode 100644 frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.ts create mode 100644 frontend/src/app/workspace/service/copilot/texera-copilot.ts create mode 100644 frontend/src/app/workspace/service/copilot/workflow-tools.ts diff --git a/frontend/package.json b/frontend/package.json index f1a0e0cc801..a1fed417b0b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "private": true, "dependencies": { "@abacritt/angularx-social-login": "2.3.0", + "@ai-sdk/openai": "2.0.52", "@ali-hm/angular-tree-component": "12.0.5", "@angular/animations": "16.2.12", "@angular/cdk": "16.2.12", @@ -39,6 +40,8 @@ "@codingame/monaco-vscode-r-default-extension": "8.0.4", "@loaders.gl/core": "3.4.2", "@luma.gl/core": "8.5.20", + "@mastra/core": "0.21.1", + "@mastra/mcp": "0.13.5", "@ngneat/until-destroy": "8.1.4", "@ngx-formly/core": "6.3.12", "@ngx-formly/ng-zorro-antd": "6.3.12", @@ -127,6 +130,7 @@ "@types/quill": "2.0.9", "@types/uuid": "8.3.4", "@types/validator": "13.12.0", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "7.0.2", "@typescript-eslint/parser": "7.0.2", "babel-plugin-dynamic-import-node": "2.3.3", diff --git a/frontend/proxy.config.json b/frontend/proxy.config.json index 3acc9a480c7..d355e57b4fd 100755 --- a/frontend/proxy.config.json +++ b/frontend/proxy.config.json @@ -1,4 +1,15 @@ { + "/api/copilot/mcp": { + "target": "http://localhost:9098", + "secure": false, + "changeOrigin": true, + "ws": true + }, + "/api/copilot/agent": { + "target": "http://localhost:8001", + "secure": false, + "changeOrigin": true + }, "/api/compile": { "target": "http://localhost:9090", "secure": false, diff --git a/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.html b/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.html new file mode 100644 index 00000000000..7b544facbd9 --- /dev/null +++ b/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.html @@ -0,0 +1,223 @@ + + +
+ +
+

Texera Copilot

+
+ + + + +
+
+ + +
+ + {{ copilotState.isConnected ? 'Connected' : 'Disconnected' }} + + + + Processing... + +
+ + +
+
Quick Actions
+
+ + + + +
+
+ + +
+
+ Agent Thinking Log +
+
+
+ {{ log }} +
+
+
+ + +
+
+
+ +
+ + + + {{ message.role === 'user' ? 'You' : message.role === 'assistant' ? 'Copilot' : 'System' }} + + {{ formatTimestamp(message.timestamp) }} +
+ + +
{{ message.content }}
+
+ + +
+ +

Start a conversation with Texera Copilot

+

Ask me to help you build or modify your workflow!

+
+
+
+ + +
+ + + + + + + +
+ + +
+

Copilot is disabled. Click the enable button to start.

+
+
diff --git a/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.scss b/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.scss new file mode 100644 index 00000000000..bd0b898e47f --- /dev/null +++ b/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.scss @@ -0,0 +1,236 @@ +/** + * 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. + */ + +.copilot-panel { + display: flex; + flex-direction: column; + height: 100%; + background: #fff; + border-left: 1px solid #f0f0f0; + position: relative; + + .copilot-header { + padding: 16px; + border-bottom: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; + + h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + } + + .header-actions { + display: flex; + gap: 8px; + } + } + + .status-bar { + padding: 8px 16px; + background: #fafafa; + border-bottom: 1px solid #f0f0f0; + display: flex; + gap: 8px; + } + + .quick-actions { + background: #f5f5f5; + border-bottom: 1px solid #d9d9d9; + + .quick-actions-header { + padding: 8px 16px; + background: #e8e8e8; + font-weight: 500; + font-size: 13px; + text-transform: uppercase; + color: #595959; + } + + .quick-actions-content { + padding: 8px; + display: flex; + flex-wrap: wrap; + gap: 8px; + + button { + text-align: left; + padding: 8px 12px; + + i { + margin-right: 8px; + } + } + } + } + + .thinking-log { + background: #f5f5f5; + border-bottom: 2px solid #d9d9d9; + max-height: 200px; + overflow-y: auto; + + .log-header { + padding: 8px 16px; + background: #e8e8e8; + font-weight: 500; + font-size: 12px; + text-transform: uppercase; + color: #595959; + } + + .log-content { + padding: 8px; + + .log-entry { + padding: 4px 8px; + margin-bottom: 4px; + background: white; + border-radius: 4px; + font-family: monospace; + font-size: 12px; + color: #595959; + } + } + } + + .messages-container { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + + &.with-thinking-log { + flex: 1; + } + + .messages-scroll { + flex: 1; + overflow-y: auto; + padding: 16px; + } + + .message { + margin-bottom: 16px; + + .message-header { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + + .role { + font-weight: 500; + display: flex; + align-items: center; + gap: 4px; + + i { + font-size: 14px; + } + } + + .timestamp { + font-size: 12px; + color: #8c8c8c; + } + } + + .message-content { + padding: 12px; + border-radius: 8px; + line-height: 1.5; + } + + &.message-user { + .role { + color: #1890ff; + } + .message-content { + background: #e6f7ff; + margin-left: 24px; + } + } + + &.message-assistant { + .role { + color: #52c41a; + } + .message-content { + background: #f6ffed; + margin-right: 24px; + } + } + + &.message-system { + .role { + color: #8c8c8c; + } + .message-content { + background: #fafafa; + font-style: italic; + font-size: 13px; + } + } + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: #8c8c8c; + + p { + margin: 8px 0; + } + + .hint { + font-size: 13px; + color: #bfbfbf; + } + } + } + + .input-area { + padding: 16px; + border-top: 1px solid #f0f0f0; + background: #fafafa; + } + + .disabled-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.95); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + + p { + color: #8c8c8c; + font-size: 14px; + } + } +} diff --git a/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.ts b/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.ts new file mode 100644 index 00000000000..d736752bb96 --- /dev/null +++ b/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.ts @@ -0,0 +1,148 @@ +/** + * 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 { Component, OnInit, OnDestroy } from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; +import { TexeraCopilot, CopilotState } from "../../service/copilot/texera-copilot"; + +@Component({ + selector: "texera-copilot-panel", + templateUrl: "./copilot-panel.component.html", + styleUrls: ["./copilot-panel.component.scss"], +}) +export class CopilotPanelComponent implements OnInit, OnDestroy { + public copilotState: CopilotState; + public inputControl = new FormControl(""); + public showThinkingLog = false; + public showQuickActions = false; + + private destroy$ = new Subject(); + + constructor(private copilot: TexeraCopilot) { + this.copilotState = this.copilot.getState(); + } + + ngOnInit(): void { + // Subscribe to copilot state changes + this.copilot.state$.pipe(takeUntil(this.destroy$)).subscribe(state => { + this.copilotState = state; + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + /** + * Send message to copilot + */ + async sendMessage(): Promise { + const message = this.inputControl.value?.trim(); + if (!message) return; + + // Clear input + this.inputControl.setValue(""); + + try { + await this.copilot.sendMessage(message); + } catch (error) { + console.error("Error sending message:", error); + } + } + + /** + * Toggle copilot + */ + async toggleCopilot(): Promise { + try { + await this.copilot.toggle(); + } catch (error) { + console.error("Error toggling copilot:", error); + } + } + + /** + * Clear conversation + */ + clearConversation(): void {} + + /** + * Toggle thinking log visibility + */ + toggleThinkingLog(): void { + this.showThinkingLog = !this.showThinkingLog; + } + + /** + * Toggle quick actions menu + */ + toggleQuickActions(): void { + this.showQuickActions = !this.showQuickActions; + } + + /** + * Quick action: Suggest workflow + */ + async suggestWorkflow(): Promise { + const description = prompt("Describe the workflow you want to create:"); + if (description) { + await this.copilot.suggestWorkflow(description); + } + } + + /** + * Quick action: Analyze workflow + */ + async analyzeWorkflow(): Promise { + await this.copilot.analyzeWorkflow(); + } + + /** + * Quick action: List available operators + */ + async listOperators(): Promise { + await this.copilot.sendMessage("List all available operator types grouped by category"); + } + + /** + * Quick action: Auto-layout + */ + async autoLayout(): Promise { + await this.copilot.sendMessage("Apply automatic layout to the workflow"); + } + + /** + * Format timestamp for display + */ + formatTimestamp(date: Date): string { + return new Date(date).toLocaleTimeString(); + } + + /** + * Format tool calls for display + */ + formatToolCalls(toolCalls?: any[]): string { + if (!toolCalls || toolCalls.length === 0) return ""; + + return toolCalls.map(tc => `🔧 ${tc.function?.name || tc.name}`).join(", "); + } +} diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts new file mode 100644 index 00000000000..b7f21a1335e --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -0,0 +1,399 @@ +/** + * 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 { BehaviorSubject, Observable, Subject } from "rxjs"; +import { Mastra, Agent } from "@mastra/core"; +import { MCPClient } from "@mastra/mcp"; +import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; +import { createWorkflowTools } from "./workflow-tools"; +import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; +import { createOpenAI } from "@ai-sdk/openai"; + +// API endpoints as constants +export const COPILOT_MCP_URL = "api/mcp"; +export const COPILOT_AGENT_URL = "api/copilot/agent"; + +export interface CopilotMessage { + role: "user" | "assistant" | "system"; + content: string; + timestamp: Date; + toolCalls?: any[]; +} + +export interface CopilotState { + isEnabled: boolean; + isConnected: boolean; + isProcessing: boolean; + messages: CopilotMessage[]; + thinkingLog: string[]; +} + +/** + * Texera Copilot - A Mastra-based AI assistant for workflow manipulation + * Combines MCP tools from backend with frontend workflow manipulation tools + */ +@Injectable({ + providedIn: "root", +}) +export class TexeraCopilot { + private mastra: Mastra; + private agent?: Agent; // Store agent reference + private mcpClient?: MCPClient; // Keep reference for cleanup + private mcpTools: Record = {}; + + // State management (integrated from service) + private stateSubject = new BehaviorSubject({ + isEnabled: false, + isConnected: false, + isProcessing: false, + messages: [], + thinkingLog: [], + }); + + public readonly state$ = this.stateSubject.asObservable(); + private messageStream = new Subject(); + public readonly messages$ = this.messageStream.asObservable(); + + constructor( + private workflowActionService: WorkflowActionService, + private operatorMetadataService: OperatorMetadataService + ) { + // Initialize Mastra + this.mastra = new Mastra(); + + // Initialize on construction + this.initialize(); + } + + /** + * Initialize the copilot with MCP and local tools + */ + private async initialize(): Promise { + try { + // 1. Connect to MCP server using Mastra's MCP client + await this.connectMCP(); + + // 2. Get workflow manipulation tools + const workflowTools = createWorkflowTools(this.workflowActionService, this.operatorMetadataService); + + // 3. Combine MCP tools with local workflow tools + const allTools = { + ...this.mcpTools, + ...workflowTools, + }; + + // 4. Create and store the agent + this.agent = new Agent({ + name: "texera-copilot", + instructions: `You are Texera Copilot, an AI assistant for building and modifying data workflows. + +CAPABILITIES: +1. Workflow Manipulation (Local Tools): + - Add/delete operators + - Connect operators with links + - Modify operator properties + - Auto-layout workflows + - Add comment boxes + +2. Operator Discovery (MCP Tools): + - List available operator types + - Get operator schemas + - Search operators by capability + - Get operator metadata + +CURRENT WORKFLOW STATE: +- Operators: ${this.workflowActionService.getTexeraGraph().getAllOperators().length} +- Links: ${this.workflowActionService.getTexeraGraph().getAllLinks().length} +- Workflow Name: ${this.workflowActionService.getWorkflowMetadata().name} + +GUIDELINES: +- Use meaningful operator IDs (e.g., csv_source_1, filter_age, aggregate_results) +- Validate operator types before adding them +- Ensure proper port connections when creating links +- Use MCP tools to discover available operators first +- Suggest auto-layout after adding multiple operators + +WORKFLOW PATTERNS: +- ETL Pipeline: Source → Transform → Sink +- Filtering: Source → Filter → Results +- Aggregation: Source → GroupBy → Aggregate → Results +- Join: Source1 + Source2 → Join → Results`, + tools: allTools, + model: createOpenAI({ + baseURL: COPILOT_AGENT_URL, + })("gpt-4"), + }); + + this.updateState({ + isEnabled: true, + isConnected: true, + }); + + this.addSystemMessage("Texera Copilot initialized. I can help you build and modify workflows."); + } catch (error: unknown) { + console.error("Failed to initialize copilot:", error); + this.updateState({ + isEnabled: false, + isConnected: false, + }); + this.addSystemMessage(`Initialization failed: ${error}`); + } + } + + /** + * Connect to MCP server and retrieve tools + */ + private async connectMCP(): Promise { + try { + // Create MCP client with HTTP server configuration + // Note: Auth headers can be added if needed in the future + const mcpClient = new MCPClient({ + servers: { + texeraMcp: { + url: new URL(`${window.location.origin}/${COPILOT_MCP_URL}`), + }, + }, + timeout: 30000, // 30 second timeout + }); + + // Store reference for cleanup + this.mcpClient = mcpClient; + + // Get all available MCP tools + const tools = await mcpClient.getTools(); + + // The tools from MCP are already in the correct format + this.mcpTools = tools || {}; + + console.log(`Connected to MCP server. Retrieved ${Object.keys(this.mcpTools).length} tools.`); + } catch (error) { + console.error("Failed to connect to MCP:", error); + throw error; + } + } + + /** + * Send a message to the copilot + */ + public async sendMessage(message: string): Promise { + // Check if agent is initialized + if (!this.agent) { + throw new Error("Copilot agent not initialized"); + } + + // Add user message + this.addUserMessage(message); + this.updateState({ isProcessing: true }); + + try { + // Use Mastra's built-in message handling + const response = await this.agent.generate(message); + + // Add assistant response + this.addAssistantMessage(response.text, response.toolCalls); + + // Update thinking log if available + if (response.reasoning && Array.isArray(response.reasoning)) { + // Extract text content from reasoning chunks + const reasoningTexts = response.reasoning.map((chunk: any) => + typeof chunk === "string" ? chunk : chunk.content || JSON.stringify(chunk) + ); + this.updateThinkingLog(reasoningTexts); + } + } catch (error: any) { + console.error("Error processing request:", error); + this.addSystemMessage(`Error: ${error.message}`); + } finally { + this.updateState({ isProcessing: false }); + } + } + + /** + * Get conversation history + */ + public getConversationHistory(): CopilotMessage[] { + return this.stateSubject.getValue().messages; + } + + /** + * Clear conversation + */ + public clearConversation(): void { + this.updateState({ + messages: [], + thinkingLog: [], + }); + + // Reset agent conversation + + this.addSystemMessage("Conversation cleared. Ready for new requests."); + } + + /** + * Get thinking/reasoning log + */ + public getThinkingLog(): string[] { + return this.stateSubject.getValue().thinkingLog; + } + + /** + * Toggle copilot enabled state + */ + public async toggle(): Promise { + const currentState = this.stateSubject.getValue(); + + if (currentState.isEnabled) { + await this.disable(); + } else { + await this.enable(); + } + } + + /** + * Enable copilot + */ + private async enable(): Promise { + await this.initialize(); + } + + /** + * Disable copilot + */ + private async disable(): Promise { + // Disconnect the MCP client if it exists + if (this.mcpClient) { + await this.mcpClient.disconnect(); + this.mcpClient = undefined; + } + + this.updateState({ + isEnabled: false, + isConnected: false, + }); + + this.addSystemMessage("Copilot disabled."); + } + + /** + * Get current state + */ + public getState(): CopilotState { + return this.stateSubject.getValue(); + } + + /** + * Update thinking log + */ + private updateThinkingLog(reasoning: string | string[]): void { + const logs = Array.isArray(reasoning) ? reasoning : [reasoning]; + const timestamp = new Date().toISOString(); + + const formattedLogs = logs.map(log => `[${timestamp}] ${log}`); + + const currentState = this.stateSubject.getValue(); + this.updateState({ + thinkingLog: [...currentState.thinkingLog, ...formattedLogs], + }); + } + + // Message management helpers + private addUserMessage(content: string): void { + const message: CopilotMessage = { + role: "user", + content, + timestamp: new Date(), + }; + this.addMessage(message); + } + + private addAssistantMessage(content: string, toolCalls?: any[]): void { + const message: CopilotMessage = { + role: "assistant", + content, + timestamp: new Date(), + toolCalls, + }; + this.addMessage(message); + } + + private addSystemMessage(content: string): void { + const message: CopilotMessage = { + role: "system", + content, + timestamp: new Date(), + }; + this.addMessage(message); + } + + private addMessage(message: CopilotMessage): void { + const currentState = this.stateSubject.getValue(); + const messages = [...currentState.messages, message]; + this.updateState({ messages }); + this.messageStream.next(message); + } + + private updateState(partialState: Partial): void { + const currentState = this.stateSubject.getValue(); + this.stateSubject.next({ + ...currentState, + ...partialState, + }); + } + + /** + * Execute a workflow suggestion (convenience method) + */ + public async suggestWorkflow(description: string): Promise { + const prompt = `Create a workflow for: ${description} + +Please analyze the requirement and: +1. First, list the operators needed +2. Create the workflow step by step +3. Connect the operators appropriately +4. Add helpful comments + +Start by checking what operator types are available.`; + + await this.sendMessage(prompt); + } + + /** + * Analyze current workflow (convenience method) + */ + public async analyzeWorkflow(): Promise { + const prompt = `Analyze the current workflow and provide: +1. A summary of what it does +2. Any potential issues or improvements +3. Missing connections or operators +4. Performance optimization suggestions + +Start by getting the workflow statistics and all operators.`; + + await this.sendMessage(prompt); + } + + /** + * Get auth token from session/local storage + */ + private getAuthToken(): string { + // This should get the actual JWT token from your auth service + return sessionStorage.getItem("authToken") || ""; + } +} diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts new file mode 100644 index 00000000000..b3c6ce0aff7 --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -0,0 +1,137 @@ +/** + * 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 { createTool } from "@mastra/core/tools"; +import { z } from "zod"; +import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; +import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; +import { OperatorLink, OperatorPredicate } from "../../types/workflow-common.interface"; +import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; + +/** + * Create workflow manipulation tools that work with WorkflowActionService + */ +export function createWorkflowTools( + workflowActionService: WorkflowActionService, + workflowUtilService: WorkflowUtilService, + operatorMetadataService: OperatorMetadataService +) { + // Tool: Add Operator + const addOperator = createTool({ + id: "addOperator", + description: "Add a new operator to the workflow", + inputSchema: z.object({ + operatorType: z.string().describe("Type of operator (e.g., 'CSVSource', 'Filter', 'Aggregate')"), + }), + outputSchema: z.object({ + success: z.boolean(), + operatorId: z.string().optional(), + message: z.string().optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { operatorType } = context; + + try { + // Validate operator type exists + if (!operatorMetadataService.operatorTypeExists(operatorType)) { + return { + success: false, + error: `Unknown operator type: ${operatorType}. Use listOperatorTypes tool to see available types.`, + }; + } + + // Get a new operator predicate with default settings + const operator = workflowUtilService.getNewOperatorPredicate(operatorType); + + // Calculate a default position (can be adjusted by auto-layout later) + const existingOperators = workflowActionService.getTexeraGraph().getAllOperators(); + const defaultX = 100 + (existingOperators.length % 5) * 200; + const defaultY = 100 + Math.floor(existingOperators.length / 5) * 150; + const position = { x: defaultX, y: defaultY }; + + // Add the operator to the workflow + workflowActionService.addOperator(operator, position); + + return { + success: true, + operatorId: operator.operatorID, + message: `Added ${operatorType} operator to workflow`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); + + // Tool: Add Link + const addLink = createTool({ + id: "addLink", + description: "Connect two operators with a link", + inputSchema: z.object({ + sourceOperatorId: z.string().describe("ID of the source operator"), + sourcePortId: z.string().optional().describe("Port ID on source operator (e.g., 'output-0')"), + targetOperatorId: z.string().describe("ID of the target operator"), + targetPortId: z.string().optional().describe("Port ID on target operator (e.g., 'input-0')"), + }), + outputSchema: z.object({ + success: z.boolean(), + linkId: z.string().optional(), + message: z.string().optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { sourceOperatorId, sourcePortId, targetOperatorId, targetPortId } = context; + + try { + // Default port IDs if not specified + const sourcePId = sourcePortId || "output-0"; + const targetPId = targetPortId || "input-0"; + + const link: OperatorLink = { + linkID: `link_${Date.now()}`, + source: { + operatorID: sourceOperatorId, + portID: sourcePId, + }, + target: { + operatorID: targetOperatorId, + portID: targetPId, + }, + }; + + workflowActionService.addLink(link); + + return { + success: true, + linkId: link.linkID, + message: `Connected ${sourceOperatorId}:${sourcePId} to ${targetOperatorId}:${targetPId}`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); + + // Return the tools + return { + addOperator, + addLink, + }; +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 394dcd78803..f2b2e4506f5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5,6 +5,20 @@ __metadata: version: 8 cacheKey: 10c0 +"@a2a-js/sdk@npm:~0.2.4": + version: 0.2.5 + resolution: "@a2a-js/sdk@npm:0.2.5" + dependencies: + "@types/cors": "npm:^2.8.17" + "@types/express": "npm:^4.17.23" + body-parser: "npm:^2.2.0" + cors: "npm:^2.8.5" + express: "npm:^4.21.2" + uuid: "npm:^11.1.0" + checksum: 10c0/9ea2cd5c9a2d1c94b770138ff5b57e7c880c94c60172c3104f251d645dbf14ff16f328f91d7c43c2244d05f5f528f5c6395bc212edc8b0667ca2a81d2f950743 + languageName: node + linkType: hard + "@abacritt/angularx-social-login@npm:2.3.0": version: 2.3.0 resolution: "@abacritt/angularx-social-login@npm:2.3.0" @@ -24,6 +38,180 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/anthropic-v5@npm:@ai-sdk/anthropic@2.0.23": + version: 2.0.23 + resolution: "@ai-sdk/anthropic@npm:2.0.23" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.10" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/0ed2d5ec272fb3b2eebd62a76aebbadbbda04da9fd62d08d8ff83d56fd8291a45beb2551817796beb711f7f7c8d6752f7ca8a4beef95268ec82a4c49f815c866 + languageName: node + linkType: hard + +"@ai-sdk/gateway@npm:1.0.33": + version: 1.0.33 + resolution: "@ai-sdk/gateway@npm:1.0.33" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.10" + "@vercel/oidc": "npm:^3.0.1" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/81e464b127acaf09e63830ca2c961be847a73feb2e985b721404143dbc4a516d6bc738fb9532ec6d660dc41a649e828ed10113a3b3805a31493740a0640b114c + languageName: node + linkType: hard + +"@ai-sdk/google-v5@npm:@ai-sdk/google@2.0.17": + version: 2.0.17 + resolution: "@ai-sdk/google@npm:2.0.17" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.10" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/174bcde507e5bf4bf95f20dbe4eaba73870715b13779e320f3df44995606e4d7ccd1e1f4b759d224deaf58bdfc6aa2e43a24dcbe5fa335ddfe91df1b06114218 + languageName: node + linkType: hard + +"@ai-sdk/openai-compatible-v5@npm:@ai-sdk/openai-compatible@1.0.19, @ai-sdk/openai-compatible@npm:1.0.19": + version: 1.0.19 + resolution: "@ai-sdk/openai-compatible@npm:1.0.19" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.10" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/5b7b21fb515e829c3d8a499a5760ffc035d9b8220695996110e361bd79e9928859da4ecf1ea072735bcbe4977c6dd0661f543871921692e86f8b5bfef14fe0e5 + languageName: node + linkType: hard + +"@ai-sdk/openai-v5@npm:@ai-sdk/openai@2.0.42": + version: 2.0.42 + resolution: "@ai-sdk/openai@npm:2.0.42" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.10" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/b1ab158aafc86735e53c4621ffe125d469bc1732c533193652768a9f66ecd4d169303ce7ca59069b7baf725da49e55bcf81210848f09f66deaf2a8335399e6d7 + languageName: node + linkType: hard + +"@ai-sdk/openai@npm:2.0.52": + version: 2.0.52 + resolution: "@ai-sdk/openai@npm:2.0.52" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.12" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/253125303235dc677e272eaffbcd5c788373e12f897e42da7cce827bcc952f31e4bb11b72ba06931f37d49a2588f6cba8526127d539025bbd58d78d7bcfc691d + languageName: node + linkType: hard + +"@ai-sdk/provider-utils-v5@npm:@ai-sdk/provider-utils@3.0.10, @ai-sdk/provider-utils@npm:3.0.10": + version: 3.0.10 + resolution: "@ai-sdk/provider-utils@npm:3.0.10" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@standard-schema/spec": "npm:^1.0.0" + eventsource-parser: "npm:^3.0.5" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/d2c16abdb84ba4ef48c9f56190b5ffde224b9e6ae5147c5c713d2623627732d34b96aa9aef2a2ea4b0c49e1b863cc963c7d7ff964a1dc95f0f036097aaaaaa98 + languageName: node + linkType: hard + +"@ai-sdk/provider-utils@npm:2.2.8, @ai-sdk/provider-utils@npm:^2.2.8": + version: 2.2.8 + resolution: "@ai-sdk/provider-utils@npm:2.2.8" + dependencies: + "@ai-sdk/provider": "npm:1.1.3" + nanoid: "npm:^3.3.8" + secure-json-parse: "npm:^2.7.0" + peerDependencies: + zod: ^3.23.8 + checksum: 10c0/34c72bf5f23f2d3e7aef496da7099422ba3b3ff243c35511853e16c3f1528717500262eea32b19e3e09bc4452152a5f31e650512f53f08a5f5645d907bff429e + languageName: node + linkType: hard + +"@ai-sdk/provider-utils@npm:3.0.12": + version: 3.0.12 + resolution: "@ai-sdk/provider-utils@npm:3.0.12" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@standard-schema/spec": "npm:^1.0.0" + eventsource-parser: "npm:^3.0.5" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/83886bf188cad0cc655b680b710a10413989eaba9ec59dd24a58b985c02a8a1d50ad0f96dd5259385c07592ec3c37a7769fdf4a1ef569a73c9edbdb2cd585915 + languageName: node + linkType: hard + +"@ai-sdk/provider-v5@npm:@ai-sdk/provider@2.0.0, @ai-sdk/provider@npm:2.0.0": + version: 2.0.0 + resolution: "@ai-sdk/provider@npm:2.0.0" + dependencies: + json-schema: "npm:^0.4.0" + checksum: 10c0/e50e520016c9fc0a8b5009cadd47dae2f1c81ec05c1792b9e312d7d15479f024ca8039525813a33425c884e3449019fed21043b1bfabd6a2626152ca9a388199 + languageName: node + linkType: hard + +"@ai-sdk/provider@npm:1.1.3, @ai-sdk/provider@npm:^1.1.3": + version: 1.1.3 + resolution: "@ai-sdk/provider@npm:1.1.3" + dependencies: + json-schema: "npm:^0.4.0" + checksum: 10c0/40e080e223328e7c89829865e9c48f4ce8442a6a59f7ed5dfbdb4f63e8d859a76641e2d31e91970dd389bddb910f32ec7c3dbb0ce583c119e5a1e614ea7b8bc4 + languageName: node + linkType: hard + +"@ai-sdk/react@npm:1.2.12": + version: 1.2.12 + resolution: "@ai-sdk/react@npm:1.2.12" + dependencies: + "@ai-sdk/provider-utils": "npm:2.2.8" + "@ai-sdk/ui-utils": "npm:1.2.11" + swr: "npm:^2.2.5" + throttleit: "npm:2.1.0" + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + checksum: 10c0/5422feb4ffeebd3287441cf658733e9ad7f9081fc279e85f57700d7fe9f4ed8a0504789c1be695790df44b28730e525cf12acf0f52bfa5adecc561ffd00cb2a5 + languageName: node + linkType: hard + +"@ai-sdk/ui-utils@npm:1.2.11, @ai-sdk/ui-utils@npm:^1.2.11": + version: 1.2.11 + resolution: "@ai-sdk/ui-utils@npm:1.2.11" + dependencies: + "@ai-sdk/provider": "npm:1.1.3" + "@ai-sdk/provider-utils": "npm:2.2.8" + zod-to-json-schema: "npm:^3.24.1" + peerDependencies: + zod: ^3.23.8 + checksum: 10c0/de0a10f9e16010126a21a1690aaf56d545b9c0f8d8b2cc33ffd22c2bb2e914949acb9b3f86e0e39a0e4b0d4f24db12e2b094045e34b311de0c8f84bfab48cc92 + languageName: node + linkType: hard + +"@ai-sdk/xai-v5@npm:@ai-sdk/xai@2.0.23": + version: 2.0.23 + resolution: "@ai-sdk/xai@npm:2.0.23" + dependencies: + "@ai-sdk/openai-compatible": "npm:1.0.19" + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.10" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/4cf6b3bc71024797d1b2e37b57fb746f7387f9a7c1da530fd040aad1a840603a1a86fb7df7e428c723eba9b1547f89063d68f84e6e08444d2d4f152dee321dc3 + languageName: node + linkType: hard + "@ali-hm/angular-tree-component@npm:12.0.5": version: 12.0.5 resolution: "@ali-hm/angular-tree-component@npm:12.0.5" @@ -683,6 +871,28 @@ __metadata: languageName: node linkType: hard +"@apidevtools/json-schema-ref-parser@npm:^11.1.0": + version: 11.9.3 + resolution: "@apidevtools/json-schema-ref-parser@npm:11.9.3" + dependencies: + "@jsdevtools/ono": "npm:^7.1.3" + "@types/json-schema": "npm:^7.0.15" + js-yaml: "npm:^4.1.0" + checksum: 10c0/5745813b3d964279f387677b7a903ba6634cdeaf879ff3a331a694392cbc923763f398506df190be114f2574b8b570baab3e367c2194bb35f50147ff6cf27d7a + languageName: node + linkType: hard + +"@apidevtools/json-schema-ref-parser@npm:^14.2.1": + version: 14.2.1 + resolution: "@apidevtools/json-schema-ref-parser@npm:14.2.1" + dependencies: + js-yaml: "npm:^4.1.0" + peerDependencies: + "@types/json-schema": ^7.0.15 + checksum: 10c0/ffc6d0df28c4a7da0b725cd916f92cfcef4efecb1c6054c67534886c7fb2ade7e6f77c3b5a0d6675020326280fe9bbca74e0e9da679590d9f78301cb6ffa0648 + languageName: node + linkType: hard + "@assemblyscript/loader@npm:^0.10.1": version: 0.10.1 resolution: "@assemblyscript/loader@npm:0.10.1" @@ -3167,6 +3377,30 @@ __metadata: languageName: node linkType: hard +"@grpc/grpc-js@npm:^1.7.1": + version: 1.14.0 + resolution: "@grpc/grpc-js@npm:1.14.0" + dependencies: + "@grpc/proto-loader": "npm:^0.8.0" + "@js-sdsl/ordered-map": "npm:^4.4.2" + checksum: 10c0/51e0eb32f6dac68c49502b227e565c4244f53983d2efab8ef3fd2cc923999751c059f6c77fec4941a93c44eaa58cbc321ce1e9868e1ec226fba5a6c93722c3b1 + languageName: node + linkType: hard + +"@grpc/proto-loader@npm:^0.8.0": + version: 0.8.0 + resolution: "@grpc/proto-loader@npm:0.8.0" + dependencies: + lodash.camelcase: "npm:^4.3.0" + long: "npm:^5.0.0" + protobufjs: "npm:^7.5.3" + yargs: "npm:^17.7.2" + bin: + proto-loader-gen-types: build/bin/proto-loader-gen-types.js + checksum: 10c0/a27da3b85d5d17bab956d536786c717287eae46ca264ea9ec774db90ff571955bae2705809f431b4622fbf3be9951d7c7bbb1360b2015ee88abe1587cf3d6fe0 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.14": version: 0.11.14 resolution: "@humanwhocodes/config-array@npm:0.11.14" @@ -3217,6 +3451,13 @@ __metadata: languageName: node linkType: hard +"@isaacs/ttlcache@npm:^1.4.1": + version: 1.4.1 + resolution: "@isaacs/ttlcache@npm:1.4.1" + checksum: 10c0/6921de516917b02673a58e543c2b06fd04237cbf6d089ca22d6e98defa4b1e9a48258cb071d6b581284bb497bea687320788830541511297eecbe6e93a665bbf + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -3322,6 +3563,20 @@ __metadata: languageName: node linkType: hard +"@js-sdsl/ordered-map@npm:^4.4.2": + version: 4.4.2 + resolution: "@js-sdsl/ordered-map@npm:4.4.2" + checksum: 10c0/cc7e15dc4acf6d9ef663757279600bab70533d847dcc1ab01332e9e680bd30b77cdf9ad885cc774276f51d98b05a013571c940e5b360985af5eb798dc1a2ee2b + languageName: node + linkType: hard + +"@jsdevtools/ono@npm:^7.1.3": + version: 7.1.3 + resolution: "@jsdevtools/ono@npm:7.1.3" + checksum: 10c0/a9f7e3e8e3bc315a34959934a5e2f874c423cf4eae64377d3fc9de0400ed9f36cb5fd5ebce3300d2e8f4085f557c4a8b591427a583729a87841fda46e6c216b9 + languageName: node + linkType: hard + "@jsonjoy.com/base64@npm:^1.1.1": version: 1.1.2 resolution: "@jsonjoy.com/base64@npm:1.1.2" @@ -3467,6 +3722,95 @@ __metadata: languageName: node linkType: hard +"@mastra/core@npm:0.21.1": + version: 0.21.1 + resolution: "@mastra/core@npm:0.21.1" + dependencies: + "@a2a-js/sdk": "npm:~0.2.4" + "@ai-sdk/anthropic-v5": "npm:@ai-sdk/anthropic@2.0.23" + "@ai-sdk/google-v5": "npm:@ai-sdk/google@2.0.17" + "@ai-sdk/openai-compatible-v5": "npm:@ai-sdk/openai-compatible@1.0.19" + "@ai-sdk/openai-v5": "npm:@ai-sdk/openai@2.0.42" + "@ai-sdk/provider": "npm:^1.1.3" + "@ai-sdk/provider-utils": "npm:^2.2.8" + "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.10" + "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0" + "@ai-sdk/ui-utils": "npm:^1.2.11" + "@ai-sdk/xai-v5": "npm:@ai-sdk/xai@2.0.23" + "@isaacs/ttlcache": "npm:^1.4.1" + "@mastra/schema-compat": "npm:0.11.4" + "@openrouter/ai-sdk-provider-v5": "npm:@openrouter/ai-sdk-provider@1.2.0" + "@opentelemetry/api": "npm:^1.9.0" + "@opentelemetry/auto-instrumentations-node": "npm:^0.62.1" + "@opentelemetry/core": "npm:^2.0.1" + "@opentelemetry/exporter-trace-otlp-grpc": "npm:^0.203.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:^0.203.0" + "@opentelemetry/otlp-exporter-base": "npm:^0.203.0" + "@opentelemetry/otlp-transformer": "npm:^0.203.0" + "@opentelemetry/resources": "npm:^2.0.1" + "@opentelemetry/sdk-metrics": "npm:^2.0.1" + "@opentelemetry/sdk-node": "npm:^0.203.0" + "@opentelemetry/sdk-trace-base": "npm:^2.0.1" + "@opentelemetry/sdk-trace-node": "npm:^2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.36.0" + "@sindresorhus/slugify": "npm:^2.2.1" + ai: "npm:^4.3.19" + ai-v5: "npm:ai@5.0.60" + date-fns: "npm:^3.6.0" + dotenv: "npm:^16.6.1" + hono: "npm:^4.9.7" + hono-openapi: "npm:^0.4.8" + js-tiktoken: "npm:^1.0.20" + json-schema: "npm:^0.4.0" + json-schema-to-zod: "npm:^2.6.1" + p-map: "npm:^7.0.3" + p-retry: "npm:^7.1.0" + pino: "npm:^9.7.0" + pino-pretty: "npm:^13.0.0" + radash: "npm:^12.1.1" + sift: "npm:^17.1.3" + xstate: "npm:^5.20.1" + zod-to-json-schema: "npm:^3.24.6" + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + checksum: 10c0/48e90b1d9862fb729d74f435dffa3a2d49452e15eb51cfb70f17a5229026f3e34797bd4e03f3c66f8086b4e56ca1c1c6633ca35341c673ed6251deefb02b81ac + languageName: node + linkType: hard + +"@mastra/mcp@npm:0.13.5": + version: 0.13.5 + resolution: "@mastra/mcp@npm:0.13.5" + dependencies: + "@apidevtools/json-schema-ref-parser": "npm:^14.2.1" + "@modelcontextprotocol/sdk": "npm:^1.17.5" + date-fns: "npm:^4.1.0" + exit-hook: "npm:^4.0.0" + fast-deep-equal: "npm:^3.1.3" + uuid: "npm:^11.1.0" + zod-from-json-schema: "npm:^0.5.0" + zod-from-json-schema-v3: "npm:zod-from-json-schema@^0.0.5" + peerDependencies: + "@mastra/core": ">=0.20.1-0 <0.22.0-0" + zod: ^3.25.0 || ^4.0.0 + checksum: 10c0/7045cc8cce541ff09aade261b34f3ad8353879c38b328a14b1d4b2324ce922c09bf6f038f51b0bcf62a381d63877b8ec2de824bd221c406863e0e4c652c73f59 + languageName: node + linkType: hard + +"@mastra/schema-compat@npm:0.11.4": + version: 0.11.4 + resolution: "@mastra/schema-compat@npm:0.11.4" + dependencies: + json-schema: "npm:^0.4.0" + zod-from-json-schema: "npm:^0.5.0" + zod-from-json-schema-v3: "npm:zod-from-json-schema@^0.0.5" + zod-to-json-schema: "npm:^3.24.6" + peerDependencies: + ai: ^4.0.0 || ^5.0.0 + zod: ^3.25.0 || ^4.0.0 + checksum: 10c0/95c436473a679398114b40d091151778792b5ee31bda23c1977835c41ce374e12038fb37811ebba6552c8de3ee6bad9da65b82eb12eec21624932748d350985c + languageName: node + linkType: hard + "@math.gl/core@npm:^3.5.0": version: 3.6.3 resolution: "@math.gl/core@npm:3.6.3" @@ -3531,6 +3875,26 @@ __metadata: languageName: node linkType: hard +"@modelcontextprotocol/sdk@npm:^1.17.5": + version: 1.20.1 + resolution: "@modelcontextprotocol/sdk@npm:1.20.1" + dependencies: + ajv: "npm:^6.12.6" + content-type: "npm:^1.0.5" + cors: "npm:^2.8.5" + cross-spawn: "npm:^7.0.5" + eventsource: "npm:^3.0.2" + eventsource-parser: "npm:^3.0.0" + express: "npm:^5.0.1" + express-rate-limit: "npm:^7.5.0" + pkce-challenge: "npm:^5.0.0" + raw-body: "npm:^3.0.0" + zod: "npm:^3.23.8" + zod-to-json-schema: "npm:^3.24.1" + checksum: 10c0/23e1ec9bc0f34dabb172d16175b4083d9890ed573252b9072c8bd5d22a8620afb3b4535e73f1c8ba84e3de15942b8aeecdfac76443256cf7a9aee3b51d2b6a66 + languageName: node + linkType: hard + "@module-federation/bridge-react-webpack-plugin@npm:0.6.11": version: 0.6.11 resolution: "@module-federation/bridge-react-webpack-plugin@npm:0.6.11" @@ -4598,152 +4962,1260 @@ __metadata: languageName: node linkType: hard -"@parcel/watcher-android-arm64@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-android-arm64@npm:2.4.1" - conditions: os=android & cpu=arm64 +"@openrouter/ai-sdk-provider-v5@npm:@openrouter/ai-sdk-provider@1.2.0": + version: 1.2.0 + resolution: "@openrouter/ai-sdk-provider@npm:1.2.0" + peerDependencies: + ai: ^5.0.0 + zod: ^3.24.1 || ^v4 + checksum: 10c0/4ca7c471ec46bdd48eea9c56d94778a06ca4b74b6ef2ab892ab7eadbd409e3530ac0c5791cd80e88cafc44a49a76585e59707104792e3e3124237fed767104ef languageName: node linkType: hard -"@parcel/watcher-darwin-arm64@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-darwin-arm64@npm:2.4.1" - conditions: os=darwin & cpu=arm64 +"@opentelemetry/api-logs@npm:0.203.0, @opentelemetry/api-logs@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/api-logs@npm:0.203.0" + dependencies: + "@opentelemetry/api": "npm:^1.3.0" + checksum: 10c0/e7a0a0ff46aaeb62192a99f45ef4889222e4fea09be25cab6fea811afc2df95c02ea050b2c98dfc0fc5a6ec6a623d87096af2751fdf91ddbb3afcab61b5325da languageName: node linkType: hard -"@parcel/watcher-darwin-x64@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-darwin-x64@npm:2.4.1" - conditions: os=darwin & cpu=x64 +"@opentelemetry/api@npm:1.9.0, @opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0": + version: 1.9.0 + resolution: "@opentelemetry/api@npm:1.9.0" + checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add + languageName: node + linkType: hard + +"@opentelemetry/auto-instrumentations-node@npm:^0.62.1": + version: 0.62.2 + resolution: "@opentelemetry/auto-instrumentations-node@npm:0.62.2" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/instrumentation-amqplib": "npm:^0.50.0" + "@opentelemetry/instrumentation-aws-lambda": "npm:^0.54.1" + "@opentelemetry/instrumentation-aws-sdk": "npm:^0.58.0" + "@opentelemetry/instrumentation-bunyan": "npm:^0.49.0" + "@opentelemetry/instrumentation-cassandra-driver": "npm:^0.49.0" + "@opentelemetry/instrumentation-connect": "npm:^0.47.0" + "@opentelemetry/instrumentation-cucumber": "npm:^0.19.0" + "@opentelemetry/instrumentation-dataloader": "npm:^0.21.1" + "@opentelemetry/instrumentation-dns": "npm:^0.47.0" + "@opentelemetry/instrumentation-express": "npm:^0.52.0" + "@opentelemetry/instrumentation-fastify": "npm:^0.48.0" + "@opentelemetry/instrumentation-fs": "npm:^0.23.0" + "@opentelemetry/instrumentation-generic-pool": "npm:^0.47.0" + "@opentelemetry/instrumentation-graphql": "npm:^0.51.0" + "@opentelemetry/instrumentation-grpc": "npm:^0.203.0" + "@opentelemetry/instrumentation-hapi": "npm:^0.50.0" + "@opentelemetry/instrumentation-http": "npm:^0.203.0" + "@opentelemetry/instrumentation-ioredis": "npm:^0.51.0" + "@opentelemetry/instrumentation-kafkajs": "npm:^0.13.0" + "@opentelemetry/instrumentation-knex": "npm:^0.48.0" + "@opentelemetry/instrumentation-koa": "npm:^0.51.0" + "@opentelemetry/instrumentation-lru-memoizer": "npm:^0.48.0" + "@opentelemetry/instrumentation-memcached": "npm:^0.47.0" + "@opentelemetry/instrumentation-mongodb": "npm:^0.56.0" + "@opentelemetry/instrumentation-mongoose": "npm:^0.50.0" + "@opentelemetry/instrumentation-mysql": "npm:^0.49.0" + "@opentelemetry/instrumentation-mysql2": "npm:^0.50.0" + "@opentelemetry/instrumentation-nestjs-core": "npm:^0.49.0" + "@opentelemetry/instrumentation-net": "npm:^0.47.0" + "@opentelemetry/instrumentation-oracledb": "npm:^0.29.0" + "@opentelemetry/instrumentation-pg": "npm:^0.56.1" + "@opentelemetry/instrumentation-pino": "npm:^0.50.1" + "@opentelemetry/instrumentation-redis": "npm:^0.52.0" + "@opentelemetry/instrumentation-restify": "npm:^0.49.0" + "@opentelemetry/instrumentation-router": "npm:^0.48.0" + "@opentelemetry/instrumentation-runtime-node": "npm:^0.17.1" + "@opentelemetry/instrumentation-socket.io": "npm:^0.50.0" + "@opentelemetry/instrumentation-tedious": "npm:^0.22.0" + "@opentelemetry/instrumentation-undici": "npm:^0.14.0" + "@opentelemetry/instrumentation-winston": "npm:^0.48.1" + "@opentelemetry/resource-detector-alibaba-cloud": "npm:^0.31.3" + "@opentelemetry/resource-detector-aws": "npm:^2.3.0" + "@opentelemetry/resource-detector-azure": "npm:^0.10.0" + "@opentelemetry/resource-detector-container": "npm:^0.7.3" + "@opentelemetry/resource-detector-gcp": "npm:^0.37.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/sdk-node": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.4.1 + "@opentelemetry/core": ^2.0.0 + checksum: 10c0/6f62a0ecc8fb8fa33e218810cc354005d3a5a6ab61561cf4d68a03be5b526b9fb6d9b9bfe8617928d255536e52655b170a46764818dc01bd9e5b8488825e3905 + languageName: node + linkType: hard + +"@opentelemetry/context-async-hooks@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/context-async-hooks@npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/75b06f33b9c3dccb8d9802c14badcc3b9a497b21c77bf0344fc6231041ea1bf6a2bcc195cc27fafd5914bffcc7fa160b9f4480c06a37e86e876c98bf1a533a0d languageName: node linkType: hard -"@parcel/watcher-freebsd-x64@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-freebsd-x64@npm:2.4.1" - conditions: os=freebsd & cpu=x64 +"@opentelemetry/context-async-hooks@npm:2.1.0": + version: 2.1.0 + resolution: "@opentelemetry/context-async-hooks@npm:2.1.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/f6c22e1b3075752a8c7cf34768cb3ad2906334f17efff6a8e5bf652c459697b8ce680b3f38543619639017a14b9bfd0201321536292325b4ba18c70cfa085c76 languageName: node linkType: hard -"@parcel/watcher-linux-arm-glibc@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-linux-arm-glibc@npm:2.4.1" - conditions: os=linux & cpu=arm & libc=glibc +"@opentelemetry/core@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/core@npm:2.0.1" + dependencies: + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/d587b1289559757d80da98039f9f57612f84f72ec608cd665dc467c7c6c5ce3a987dfcc2c63b521c7c86ce984a2552b3ead15a0dc458de1cf6bde5cdfe4ca9d8 languageName: node linkType: hard -"@parcel/watcher-linux-arm64-glibc@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-linux-arm64-glibc@npm:2.4.1" - conditions: os=linux & cpu=arm64 & libc=glibc +"@opentelemetry/core@npm:2.1.0, @opentelemetry/core@npm:^2.0.0, @opentelemetry/core@npm:^2.0.1": + version: 2.1.0 + resolution: "@opentelemetry/core@npm:2.1.0" + dependencies: + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/562c44c89150ef9cc7be4fcbfccfb62f71eb95d487de79b6a2716bb960da7d181f5e2ae3354ed6bd0bba0a3903fe5b7dad14b9a4a92fa90ab1b9172f11a3743d languageName: node linkType: hard -"@parcel/watcher-linux-arm64-musl@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-linux-arm64-musl@npm:2.4.1" - conditions: os=linux & cpu=arm64 & libc=musl +"@opentelemetry/exporter-logs-otlp-grpc@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-logs-otlp-grpc@npm:0.203.0" + dependencies: + "@grpc/grpc-js": "npm:^1.7.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/sdk-logs": "npm:0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/61c88ecc21063d348c885b6b21bd074f0e2a931321f17bffea4dc29c7dacccd199c3d8b317a64e3c8ebb8e00a528fa04382e288e3cd7bf459774c2b22d0b2592 languageName: node linkType: hard -"@parcel/watcher-linux-x64-glibc@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-linux-x64-glibc@npm:2.4.1" - conditions: os=linux & cpu=x64 & libc=glibc +"@opentelemetry/exporter-logs-otlp-http@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-logs-otlp-http@npm:0.203.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.203.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/sdk-logs": "npm:0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/724109336a3fd46cddddc7fce8cec6887cecce775241a3821336a68b7b643d35b8a9830658922b5ccf3df51387bd91bfcc63afc71e15dbd403d0541561477f8f languageName: node linkType: hard -"@parcel/watcher-linux-x64-musl@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-linux-x64-musl@npm:2.4.1" - conditions: os=linux & cpu=x64 & libc=musl +"@opentelemetry/exporter-logs-otlp-proto@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-logs-otlp-proto@npm:0.203.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.203.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-logs": "npm:0.203.0" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/8c85d6b07469c12c8b5e39bdd419e18224a696d2092d2a5f3df82e64b477b21c8032132eb50ad87369e9f114edb582aed4f56a457ac519411d034198916dfba0 languageName: node linkType: hard -"@parcel/watcher-win32-arm64@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-win32-arm64@npm:2.4.1" - conditions: os=win32 & cpu=arm64 +"@opentelemetry/exporter-metrics-otlp-grpc@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-metrics-otlp-grpc@npm:0.203.0" + dependencies: + "@grpc/grpc-js": "npm:^1.7.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.203.0" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/e708f099247189cbc14900c23ee32c7552b9a4356211678c3d2ed307139590cb3c5c01026304067c775fe827ccbe043e49161ad6879599a6f9305c223a85ed4e languageName: node linkType: hard -"@parcel/watcher-win32-ia32@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-win32-ia32@npm:2.4.1" - conditions: os=win32 & cpu=ia32 +"@opentelemetry/exporter-metrics-otlp-http@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-metrics-otlp-http@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/6fd10835fa998023095b57f74d550f976fe3396cfdc1004fb6f678bd9fadc86d58f428d0c0f8618d1a96f20f3fbb8c6e6e4862e4833b22bcf70dc0b7920cb049 languageName: node linkType: hard -"@parcel/watcher-win32-x64@npm:2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher-win32-x64@npm:2.4.1" - conditions: os=win32 & cpu=x64 +"@opentelemetry/exporter-metrics-otlp-proto@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-metrics-otlp-proto@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.203.0" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/0947c0b70f8644f7eabbaffb9c671752628eca3f92f372308ee51083870566c70cf8d99ca38e9b9381e6c4f78fdd8e897b1af541727faccd8ce15adb83a31aed languageName: node linkType: hard -"@parcel/watcher@npm:2.0.4": - version: 2.0.4 - resolution: "@parcel/watcher@npm:2.0.4" +"@opentelemetry/exporter-prometheus@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-prometheus@npm:0.203.0" dependencies: - node-addon-api: "npm:^3.2.1" - node-gyp: "npm:latest" - node-gyp-build: "npm:^4.3.0" - checksum: 10c0/7c7e8fa2879371135039cf6559122808fc37d436701dd804f3e0b4897d5690a2c92c73795ad4a015d8715990bfb4226dc6d14fea429522fcb5662ce370508e8d + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/e931de4a7bf3c4c6896245a242997097430f5ea4cda6f80f5fce0f987d29db2c3d0ab9dc43a2e3b30b1fa92ce7300cbef15c8ac4c70d82b3b42ea86a977aa469 languageName: node linkType: hard -"@parcel/watcher@npm:^2.4.1": - version: 2.4.1 - resolution: "@parcel/watcher@npm:2.4.1" +"@opentelemetry/exporter-trace-otlp-grpc@npm:0.203.0, @opentelemetry/exporter-trace-otlp-grpc@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-trace-otlp-grpc@npm:0.203.0" dependencies: - "@parcel/watcher-android-arm64": "npm:2.4.1" - "@parcel/watcher-darwin-arm64": "npm:2.4.1" - "@parcel/watcher-darwin-x64": "npm:2.4.1" - "@parcel/watcher-freebsd-x64": "npm:2.4.1" - "@parcel/watcher-linux-arm-glibc": "npm:2.4.1" - "@parcel/watcher-linux-arm64-glibc": "npm:2.4.1" - "@parcel/watcher-linux-arm64-musl": "npm:2.4.1" - "@parcel/watcher-linux-x64-glibc": "npm:2.4.1" - "@parcel/watcher-linux-x64-musl": "npm:2.4.1" - "@parcel/watcher-win32-arm64": "npm:2.4.1" - "@parcel/watcher-win32-ia32": "npm:2.4.1" - "@parcel/watcher-win32-x64": "npm:2.4.1" - detect-libc: "npm:^1.0.3" - is-glob: "npm:^4.0.3" - micromatch: "npm:^4.0.5" - node-addon-api: "npm:^7.0.0" - node-gyp: "npm:latest" - dependenciesMeta: - "@parcel/watcher-android-arm64": - optional: true - "@parcel/watcher-darwin-arm64": - optional: true - "@parcel/watcher-darwin-x64": - optional: true - "@parcel/watcher-freebsd-x64": - optional: true - "@parcel/watcher-linux-arm-glibc": - optional: true - "@parcel/watcher-linux-arm64-glibc": - optional: true - "@parcel/watcher-linux-arm64-musl": - optional: true - "@parcel/watcher-linux-x64-glibc": - optional: true - "@parcel/watcher-linux-x64-musl": - optional: true - "@parcel/watcher-win32-arm64": - optional: true - "@parcel/watcher-win32-ia32": - optional: true - "@parcel/watcher-win32-x64": - optional: true - checksum: 10c0/33b7112094b9eb46c234d824953967435b628d3d93a0553255e9910829b84cab3da870153c3a870c31db186dc58f3b2db81382fcaee3451438aeec4d786a6211 + "@grpc/grpc-js": "npm:^1.7.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/67f6c63c2fcb38a63db9708cd0a420c1090717f5945767fa779a7c324adbc610f3aec44259eb7b76fbf98ee684049a42a492a54df5d37db31c4d7e460596623e languageName: node linkType: hard -"@phenomnomnominal/tsquery@npm:~5.0.1": +"@opentelemetry/exporter-trace-otlp-http@npm:0.203.0, @opentelemetry/exporter-trace-otlp-http@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/21a65ebc40dcab05cf11178e5037f96847ce344c4a855aac46dcab3f74982016318ee75fafdfeeb42f10b92a0a781b7cd8b2b5b036cbe53c14714fd13940142e + languageName: node + linkType: hard + +"@opentelemetry/exporter-trace-otlp-proto@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-trace-otlp-proto@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/e7b6e159950b457970b1020f67acb1b5b8bc205c1cf099202a8d50985d674d92857633d407f81a5289235e3fccaa369900e537f312dfee47814b150d41a264b9 + languageName: node + linkType: hard + +"@opentelemetry/exporter-zipkin@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/exporter-zipkin@npm:2.0.1" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/10e0ad1dfb967c951d26bc64647e2f7d0705fdcf82449473308f277e1866552a07d7636bcf198e21662ada93df2366c4f24aec2d329d18e59f3d09ddcf65969d + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-amqplib@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-amqplib@npm:0.50.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/bf25dbbe38a56d35a66d03f6a49a949970a3dd47c2bad2ccaf68382ecffce8c1ca0e5e07db6fa2cf4c1c8567537daa74c4ae24d67b66f4628c3557456d9515ba + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-aws-lambda@npm:^0.54.1": + version: 0.54.1 + resolution: "@opentelemetry/instrumentation-aws-lambda@npm:0.54.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/aws-lambda": "npm:8.10.152" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/320004871a941dc76b5a4883c18b26d0e183cf26c50a8b3293f0f5f736826bb2166d6974c9fa28d84514ccc03a01a4b6475c37a7b648db5171f3a45c5a772d4d + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-aws-sdk@npm:^0.58.0": + version: 0.58.0 + resolution: "@opentelemetry/instrumentation-aws-sdk@npm:0.58.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.34.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/9b75243f304d01c004d3d88566a8b5d5548d0b67bcde16f59f7659ef86af253c5f84d0a6bd456a578c0ef90df0bd3d70c36eb330fde33bdcdd72985f687d7daf + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-bunyan@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-bunyan@npm:0.49.0" + dependencies: + "@opentelemetry/api-logs": "npm:^0.203.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@types/bunyan": "npm:1.8.11" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/a68805951a8becb483448536cddc10ea25e3210e7ba5ba319d5192238bce7f626bad5725f048cba245c89365ae71805c55d1fd9a3ac8a900318770c14e87e7a5 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-cassandra-driver@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-cassandra-driver@npm:0.49.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/beb08cb450eed076896bd892c1a11b93aa6155bd89e79211d5c421afd534a67ca2cec1c35f13025f4b3ea166095b15329bbb350a5ed62a78a08aaf5351a29c13 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-connect@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-connect@npm:0.47.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/connect": "npm:3.4.38" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/720016e6d5dab7ef6a484129537c2bf81ecae1fd5214632cf91fafe499f24935058bf98b3ca5b9f6798a265ed7208a4f248e95c406997706ce2001417703ce38 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-cucumber@npm:^0.19.0": + version: 0.19.0 + resolution: "@opentelemetry/instrumentation-cucumber@npm:0.19.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/b1e8fd6007a207e0714a257d30b8ceeb5291eced87f3febfb0fa7df3f27da2cf817e73c135a338b844cbe3829bc7936c9fb047b0134f4c0ebc01608e5d1796c0 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-dataloader@npm:^0.21.1": + version: 0.21.1 + resolution: "@opentelemetry/instrumentation-dataloader@npm:0.21.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/a9377dee842e48da4490fa1d35c064229e8b33f988be48aeb9d50a4dc000910e43b93f8f371ae8bddc2fc40a0f3c408f675af202c64c2ce0bd61791e216897e1 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-dns@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-dns@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/abff7a6a72aa86b1745660279ec24c74694f87887063aae6ca8fbea04082e69cd33b2aa80aa0a2127938d79d753775f371edede05d682b1a61a0459523e1026f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-express@npm:^0.52.0": + version: 0.52.0 + resolution: "@opentelemetry/instrumentation-express@npm:0.52.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/fb0e2122ae5c2003b107ff198c2efa7d261644d4c67a176d5cc53774f63745b465b8c427fda3426e417bf130b3db6591d11a7a1df9cd716999b432279acd158a + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-fastify@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-fastify@npm:0.48.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/67f730bc65e647ad6a7a1f4d2790043992ad31ef872fdb3df6b30ed65ccd05d253334428cc95efad0fcf97ceca091b785c8ba625fc1e911458bad65e579c882f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-fs@npm:^0.23.0": + version: 0.23.0 + resolution: "@opentelemetry/instrumentation-fs@npm:0.23.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/2d64470b58dd47b4f56b5304b43a3dcb72f9ca7fa509951f1b6bd045664a765f00bf761b8377171ae81fb7ab250790cb7e7ef69b23dcfead0ab51cbcc1b69e3e + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-generic-pool@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-generic-pool@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/97deb1e1cc95ad0afdfb50ccff5a526f062b40bc77d05a527d47ec561d711886eb34f7321181405d570511e8ea92c3da806bc8f56b4922f7f7b0c8cefaa7fb66 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-graphql@npm:^0.51.0": + version: 0.51.0 + resolution: "@opentelemetry/instrumentation-graphql@npm:0.51.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/bbbbd1c42104b8d349ba075ec2642d87903bd4ae74a831e725785279183267f7a73560bbe4911781c12d2768670f12fc6c3ab0b5d184a3bfc1da7c3f3328fd5a + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-grpc@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/instrumentation-grpc@npm:0.203.0" + dependencies: + "@opentelemetry/instrumentation": "npm:0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/371259fa14d7d85ae179eda4346855994e2667e3ac6aee6b141eebc9cc138d3a22b92be0da9b73a8408b80e2417377d730a7b03189569fdc5f3d9aad32959976 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-hapi@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-hapi@npm:0.50.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/aed37f4ef5934ca9f79fa596b92526c8e94e45fb0b10b8c59b6feffee1e21c558cb94cb4f635241614ff5db2c11e42830190682e7c575408270645fd3215ac1a + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-http@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/instrumentation-http@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/instrumentation": "npm:0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + forwarded-parse: "npm:2.1.2" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/424eeb5b162b480a8a6157ca147ecd074de3e6d31298fed115e4d6f47ca3f65ba0a79a43f3a998ebd9f0f6e96da1092500408590150c308c5ef91c0b760ae467 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-ioredis@npm:^0.51.0": + version: 0.51.0 + resolution: "@opentelemetry/instrumentation-ioredis@npm:0.51.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/redis-common": "npm:^0.38.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/fd07ce6c07081c8323595f7784052efdb83b3f45eda46c11eb4a14f72742280592c8d1b4dab2e8d3c645d0dfbbdcbb67d867ba8d29838a630eab4fd822d27b8b + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-kafkajs@npm:^0.13.0": + version: 0.13.0 + resolution: "@opentelemetry/instrumentation-kafkajs@npm:0.13.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.30.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/34b7d6ab7cde07657639dc9c5dc4f781f06ca199ffdab0ba93ab8008f63bb604a35c4ba49fdf5aa46c27ad06d7eafde2bf0bd071a60d1052ece63ac272e4135b + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-knex@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-knex@npm:0.48.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.33.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/540374797ac131000487dae0598aca44aa34760ec8c2f33809352dab08042d1c5993c7da0b7f6082f334601695f8456a5a745b42aa24368f5eeb38c38051a4ab + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-koa@npm:^0.51.0": + version: 0.51.0 + resolution: "@opentelemetry/instrumentation-koa@npm:0.51.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/aef15da0ae4fcc4aef59129983c143dca4e1a36d6d57dfce09bbae42d514b52a9536e7b117a8f7f6d8d006d08afdc677528653a31cea406b0ea6632a9d222c9a + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-lru-memoizer@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-lru-memoizer@npm:0.48.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/6ab7810c5602a389c0d15c2fb1f132b069e1f60ddb6ddd406a1ce5bad6f96ebe0046f1cb55cc1e4b868c7dbde45a79e8940b0128f5117a85a8fcc0a1ab78ae4f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-memcached@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-memcached@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/memcached": "npm:^2.2.6" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/a788cf9bb55cafa93292e0cc1b445938517265d78c2b84cdbbfd395cafb5ca75a726b1ad5af278edd3ab59b82f57181d82c2b259b85d149e81a0226c4b39cd76 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mongodb@npm:^0.56.0": + version: 0.56.0 + resolution: "@opentelemetry/instrumentation-mongodb@npm:0.56.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/87b4a438c4ddb9d075986ca338ba05f8f31df13b8da3254f07d9461cb7e4ff511bdeefd527833427b1a505c2d4f26a3e3ee90795c2731405d944c50323152824 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mongoose@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-mongoose@npm:0.50.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/2dd4cf1d39e3abba77eeb8540bd4fa110ddd8394f164e30fb46daaac25e65d3a021325fa096124410585cccee142c8deb99131345add6ff23617ad8d6c874b10 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mysql2@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-mysql2@npm:0.50.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/sql-common": "npm:^0.41.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/09b44e155178918567493cc00b7c9afb2eb680666205cd020cf01e670bbd4e25547b911b77c97df51ffbef277af09c66646baaff1c08020f82edf37705afcf56 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mysql@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-mysql@npm:0.49.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/mysql": "npm:2.15.27" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/f8853919f055dbdbde80cc71a214d5f5c98e7eff6749fdcd68678387124e1de6f6ba592a2461c799e364dbdeda5a2080d712d85c104d1241c59e5effcb0da3bc + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-nestjs-core@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-nestjs-core@npm:0.49.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.30.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/f44d448bb6a2d6a78af0490be6f6aad80d113fdd9a01fb32a712ad110dbbf956409da24b2df60f042a24ea5485054934f0dfeca0b2014e9070fb0eaa72daf380 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-net@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-net@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/93189616053f9ceb0a96183ef18e281d604644d42947a6faabec92046386f7a8a0b31a25220c6138171808cd51d0b150c43117fc02bd4f456da8f10ef4e138e0 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-oracledb@npm:^0.29.0": + version: 0.29.0 + resolution: "@opentelemetry/instrumentation-oracledb@npm:0.29.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/oracledb": "npm:6.5.2" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/8f6708067dc7e3ccb6b35e9552ccdf2e942f102560bfcd072a5a3bf80fdad3db9fc326abe4fd8b8b681e1558a0f59f8a3d8685de0a0a2b8aec198c471004db56 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-pg@npm:^0.56.1": + version: 0.56.1 + resolution: "@opentelemetry/instrumentation-pg@npm:0.56.1" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.34.0" + "@opentelemetry/sql-common": "npm:^0.41.0" + "@types/pg": "npm:8.15.5" + "@types/pg-pool": "npm:2.0.6" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/b88359a4e2d5d02a048eef18a0e1aeeb50a21b1a0cc524adc1784a866188e040b1215496a7f7885619f2dbbcb8558f29c8e1264d0c77beb062ff8aee73f928fa + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-pino@npm:^0.50.1": + version: 0.50.1 + resolution: "@opentelemetry/instrumentation-pino@npm:0.50.1" + dependencies: + "@opentelemetry/api-logs": "npm:^0.203.0" + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/088570ed6e1f74712888f07786e914fd6dd7d38f8dfaa55cc1b5e509868a1134244125da7cc5541c7ebb6030d73d695829071f942d45d993ba4047d0b9a51947 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-redis@npm:^0.52.0": + version: 0.52.0 + resolution: "@opentelemetry/instrumentation-redis@npm:0.52.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/redis-common": "npm:^0.38.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/dfb955f8ad18d829cc49e3e2fafb592eb10a4c1b10db883ebd05341d7841c48c42e8fa32e19b0dbf0fa359c835f908975f1c43864153c4750b32e9a7de5c47f1 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-restify@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-restify@npm:0.49.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/25b30b061e6c47f27eebc48786ee656854c3d93a64a9dc70e07846a3e08580e0bfa4c7ef089ced59089ab1b002b3cbae8b2c751a8c9536bb9973d44e1b9cbd0f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-router@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-router@npm:0.48.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/4373c80432bfc3a48b45603604c8d9f81f8e30f9f3d821a166d96a86de1e3186db1d96de4a85ba93d8fdc7ad6f8857481d3abe224228b2c5fac3bc16987c23d6 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-runtime-node@npm:^0.17.1": + version: 0.17.1 + resolution: "@opentelemetry/instrumentation-runtime-node@npm:0.17.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/96e7b1191a15d986f89f98323cf20016b9d07e298ca5173750bab2ca144734827c5a4b7e7791c08a82f2b6cb690b9d68766f0023cb92ca2a9b8834b0b1797a6d + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-socket.io@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-socket.io@npm:0.50.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/e462419086b0bed49d3b6b8b3f25b43116e8aa752d13c3bfa1ed6a42cd1dfda41cd8835fae7b9a23e26494e612ca04d5d5d3172bc942b558d66da0c4bf1be95d + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-tedious@npm:^0.22.0": + version: 0.22.0 + resolution: "@opentelemetry/instrumentation-tedious@npm:0.22.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/tedious": "npm:^4.0.14" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/bbd6f78591ba918d274e6990a6b1c46d814e60a0da1cb213190f868f4fe8e06fcabcded1e1e674afd70e9f60f670c601a842fc4a7b82c634eb19313cd7d37106 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-undici@npm:^0.14.0": + version: 0.14.0 + resolution: "@opentelemetry/instrumentation-undici@npm:0.14.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.7.0 + checksum: 10c0/0689e4836b774405e9415a4186ffa342e41bb7df7f518c1872847c64b8895e49865b6fc00a8525479f862dc97ea57684b7c0c2809cec422c8d8d3b946707f0ed + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-winston@npm:^0.48.1": + version: 0.48.1 + resolution: "@opentelemetry/instrumentation-winston@npm:0.48.1" + dependencies: + "@opentelemetry/api-logs": "npm:^0.203.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/25392239f8155232d0dabfd8d182181ddc8b6d6bfeb0105e0ed2cc389aac4080f4dba1efdabb4b48a95ac64313fae13141e0f5dc3a3552907707a4ddb75c81a4 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation@npm:0.203.0, @opentelemetry/instrumentation@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/instrumentation@npm:0.203.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.203.0" + import-in-the-middle: "npm:^1.8.1" + require-in-the-middle: "npm:^7.1.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/b9de27ea7b42c54b1d0dab15dac62d4fc71c781bb6a48e90fa4ce8ce97be1b78e1fa9f05f58c39f68ca0e4a5590b8538d04209482f6b0632958926f7e80a28c1 + languageName: node + linkType: hard + +"@opentelemetry/otlp-exporter-base@npm:0.203.0, @opentelemetry/otlp-exporter-base@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/otlp-exporter-base@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/ad5b771b06b192f06f332f60701d1ad208df88a05975b16e1cdd1dff8e1cb66e775b3e9de513c2f5d48f390f25ca35411ead08ce4849c8203b86a264d34561d3 + languageName: node + linkType: hard + +"@opentelemetry/otlp-grpc-exporter-base@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/otlp-grpc-exporter-base@npm:0.203.0" + dependencies: + "@grpc/grpc-js": "npm:^1.7.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/8e0936a7bc5359fe2f15d6302e7ba2cafe93e3dc95c0d316a95b78d8638822939612f07cb27c60544eefbabae2c8ed9b83d246695e92af30b0896a7c3888db32 + languageName: node + linkType: hard + +"@opentelemetry/otlp-transformer@npm:0.203.0, @opentelemetry/otlp-transformer@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/otlp-transformer@npm:0.203.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.203.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-logs": "npm:0.203.0" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + protobufjs: "npm:^7.3.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/3f7b4bfe4bcab4db434ff2c4e59b53de53642d379b80056610456d8e9ae0cbab0f8b69f088078637b7b5ceffd0ac2fda68469c5f295b1c0ac625f522f640338c + languageName: node + linkType: hard + +"@opentelemetry/propagator-b3@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/propagator-b3@npm:2.0.1" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/79dbfaaa867f4f71a22ab640848f797ef9789fd94fc824ca4e38f298968a3f559a895fc228a17f09b1e06ec88cbf0b1f3cadc480ea76848504c7364693fd30ca + languageName: node + linkType: hard + +"@opentelemetry/propagator-jaeger@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/propagator-jaeger@npm:2.0.1" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/e21df109b831a7efffe54459bb5da35be05eeb72581017f0ce40dee2ab98b3e8063602894a477f6c593ad1bd3a1ead36adfceee21eb2472ca88050d49f056154 + languageName: node + linkType: hard + +"@opentelemetry/redis-common@npm:^0.38.0": + version: 0.38.2 + resolution: "@opentelemetry/redis-common@npm:0.38.2" + checksum: 10c0/26fa47eb3f4663d5f38b2ca1229a01931604a9407089ca400011d50349ec03790a3c7dad1014b46110f3939108a61e499ac7f56b9c0927ceb3bc5e21a3f95d5b + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-alibaba-cloud@npm:^0.31.3": + version: 0.31.9 + resolution: "@opentelemetry/resource-detector-alibaba-cloud@npm:0.31.9" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/resources": "npm:^2.0.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/793691615b0ab0e1b7319a468252056746ce9ef682dc80334ba931b4a3a421e9fc691258b95f955766449c1c77ee0b998cce270787b37a2488ff32c93e377b20 + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-aws@npm:^2.3.0": + version: 2.6.0 + resolution: "@opentelemetry/resource-detector-aws@npm:2.6.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/e116fc1d02c909dc1eceb48ed9bd9707d5d37b8656542be5fcc0913d62f3caad9ead65c0297d0ee5ea379124ebce6b8c975ab7a2d890e7a8c2a08dc6b828b7ac + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-azure@npm:^0.10.0": + version: 0.10.0 + resolution: "@opentelemetry/resource-detector-azure@npm:0.10.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/5617dc6f5ab6476c57747a4882b05f2e086326e2ae6c86e2d06fc5e26250b1022e1c5b7968cb0bb11da98e28846220eadc9e42882e018e20c64291f278a65f9b + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-container@npm:^0.7.3": + version: 0.7.9 + resolution: "@opentelemetry/resource-detector-container@npm:0.7.9" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/resources": "npm:^2.0.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/71db11d9eeb742cc2ce4f99342a183d8ac4d89fabc3efeb8f17a4e2fa7bacda8cdbdb5f00ec35559fb8a75c6160e86e0b068a05b880deea8f2a5b148319a2cf8 + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-gcp@npm:^0.37.0": + version: 0.37.0 + resolution: "@opentelemetry/resource-detector-gcp@npm:0.37.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + gcp-metadata: "npm:^6.0.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/83f96e1ed5122154bb7d4bc7cc9707c093e775cb317cf2f3c77745987f1ab6e9618f6f0c8735dbf06d317f24f8a19bd5f33b76219d015f07783c15a47e00b2c8 + languageName: node + linkType: hard + +"@opentelemetry/resources@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/resources@npm:2.0.1" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10c0/96532b7553b26607a7a892d72f6b03ad12bd542dc23c95135a8ae40362da9c883c21a4cff3d2296d9e0e9bd899a5977e325ed52d83142621a8ffe81d08d99341 + languageName: node + linkType: hard + +"@opentelemetry/resources@npm:2.1.0, @opentelemetry/resources@npm:^2.0.0, @opentelemetry/resources@npm:^2.0.1": + version: 2.1.0 + resolution: "@opentelemetry/resources@npm:2.1.0" + dependencies: + "@opentelemetry/core": "npm:2.1.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10c0/5e33f349b088a110e3492add63a131680760f87265fa81f269dfc3e7978ab82f3e43513f8fc3b2e168127919ca50f47ffef0146c076bc1434a30a6567e28cf3d + languageName: node + linkType: hard + +"@opentelemetry/sdk-logs@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/sdk-logs@npm:0.203.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.203.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.4.0 <1.10.0" + checksum: 10c0/02dd9d9969628f05f71ae1d149f1aa6d1fee2dad607923a68a1cfc923e94b046dcc0e18e85e865324e3bda0cee7a5a0ba9fa0d57e4e95fa672be103e2ce60270 + languageName: node + linkType: hard + +"@opentelemetry/sdk-metrics@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/sdk-metrics@npm:2.0.1" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.9.0 <1.10.0" + checksum: 10c0/fcf7ae23d459e5da7cb6fe150064b6dc4e11e47925b08980c3b357bd5534ad388898bbacd0ff8befef6801f43b35142dc7123f028ffde2d0fe2bd72177d07639 + languageName: node + linkType: hard + +"@opentelemetry/sdk-metrics@npm:^2.0.1": + version: 2.1.0 + resolution: "@opentelemetry/sdk-metrics@npm:2.1.0" + dependencies: + "@opentelemetry/core": "npm:2.1.0" + "@opentelemetry/resources": "npm:2.1.0" + peerDependencies: + "@opentelemetry/api": ">=1.9.0 <1.10.0" + checksum: 10c0/2e6062d73c930d9a645678523b4b5cf7c19a2742bc5c4fdd66138cfaf02dd7d95d96494e51f122802ba9995c8fec76063414b1c893b757ecd1c6dab00439919f + languageName: node + linkType: hard + +"@opentelemetry/sdk-node@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/sdk-node@npm:0.203.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.203.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/exporter-logs-otlp-grpc": "npm:0.203.0" + "@opentelemetry/exporter-logs-otlp-http": "npm:0.203.0" + "@opentelemetry/exporter-logs-otlp-proto": "npm:0.203.0" + "@opentelemetry/exporter-metrics-otlp-grpc": "npm:0.203.0" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.203.0" + "@opentelemetry/exporter-metrics-otlp-proto": "npm:0.203.0" + "@opentelemetry/exporter-prometheus": "npm:0.203.0" + "@opentelemetry/exporter-trace-otlp-grpc": "npm:0.203.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:0.203.0" + "@opentelemetry/exporter-trace-otlp-proto": "npm:0.203.0" + "@opentelemetry/exporter-zipkin": "npm:2.0.1" + "@opentelemetry/instrumentation": "npm:0.203.0" + "@opentelemetry/propagator-b3": "npm:2.0.1" + "@opentelemetry/propagator-jaeger": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-logs": "npm:0.203.0" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + "@opentelemetry/sdk-trace-node": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10c0/2c846f40908afad73d7898516c37e35ab34a63c2147f0241d39c7442207f42de7bd6d451afbccf7a102022a5132465b98ffcdd1fbc77050b49909c501b42cbd8 + languageName: node + linkType: hard + +"@opentelemetry/sdk-trace-base@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/sdk-trace-base@npm:2.0.1" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10c0/4e3c733296012b758d007e9c0d8a5b175edbe9a680c73ec75303476e7982b73ad4209f1a2791c1a94c428e5a53eba6c2a72faa430c70336005aa58744d6cb37b + languageName: node + linkType: hard + +"@opentelemetry/sdk-trace-base@npm:2.1.0, @opentelemetry/sdk-trace-base@npm:^2.0.1": + version: 2.1.0 + resolution: "@opentelemetry/sdk-trace-base@npm:2.1.0" + dependencies: + "@opentelemetry/core": "npm:2.1.0" + "@opentelemetry/resources": "npm:2.1.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10c0/b82654a7cae0d778d08edbd5b93137991cab64ebba1c85012107d67120efa797584cd365e39836eb9644ac9119ecb777784c72fd9a7354b7820e0283a6ed4670 + languageName: node + linkType: hard + +"@opentelemetry/sdk-trace-node@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/sdk-trace-node@npm:2.0.1" + dependencies: + "@opentelemetry/context-async-hooks": "npm:2.0.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/b237efc219dc10c33746c05461c8c8741edbe7558eaf7f2dab01a3e75af4788bfd0633a049cd5dc7ecf015a2de7aa948c3989c0131d1f140109fb5e7b0313d7a + languageName: node + linkType: hard + +"@opentelemetry/sdk-trace-node@npm:^2.0.1": + version: 2.1.0 + resolution: "@opentelemetry/sdk-trace-node@npm:2.1.0" + dependencies: + "@opentelemetry/context-async-hooks": "npm:2.1.0" + "@opentelemetry/core": "npm:2.1.0" + "@opentelemetry/sdk-trace-base": "npm:2.1.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/b0aea2acb5d85280885f32efe68f320f13b284725b1bc1efb2a1eac9b823617efb634633cd0338a1d47311bcd586fda3d4aab81d92424ad716ee2c5947830fbd + languageName: node + linkType: hard + +"@opentelemetry/semantic-conventions@npm:^1.27.0, @opentelemetry/semantic-conventions@npm:^1.29.0, @opentelemetry/semantic-conventions@npm:^1.30.0, @opentelemetry/semantic-conventions@npm:^1.33.1, @opentelemetry/semantic-conventions@npm:^1.34.0, @opentelemetry/semantic-conventions@npm:^1.36.0": + version: 1.37.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.37.0" + checksum: 10c0/ddce99f36e390603d6bbc556a50c070e41303d764a830808a4c451f02f4e6a3d989dbde8bcfac15e4e5bba13686b36c6664a323321c9259f9030eb70522a7c68 + languageName: node + linkType: hard + +"@opentelemetry/sql-common@npm:^0.41.0": + version: 0.41.2 + resolution: "@opentelemetry/sql-common@npm:0.41.2" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + peerDependencies: + "@opentelemetry/api": ^1.1.0 + checksum: 10c0/1774930abdad57a716662a3feffe8a7aca8e4494303356f04c3e958fddcdb1df29d464ca91db890e0501728b4fb6c4b578f3ecb83004bcdc9d7e81b739d24459 + languageName: node + linkType: hard + +"@parcel/watcher-android-arm64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-android-arm64@npm:2.4.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-arm64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-darwin-arm64@npm:2.4.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-x64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-darwin-x64@npm:2.4.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-freebsd-x64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-freebsd-x64@npm:2.4.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-glibc@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-arm-glibc@npm:2.4.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-glibc@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-arm64-glibc@npm:2.4.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-musl@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-arm64-musl@npm:2.4.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-glibc@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-x64-glibc@npm:2.4.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-musl@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-x64-musl@npm:2.4.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-win32-arm64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-win32-arm64@npm:2.4.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-win32-ia32@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-win32-ia32@npm:2.4.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@parcel/watcher-win32-x64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-win32-x64@npm:2.4.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher@npm:2.0.4": + version: 2.0.4 + resolution: "@parcel/watcher@npm:2.0.4" + dependencies: + node-addon-api: "npm:^3.2.1" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10c0/7c7e8fa2879371135039cf6559122808fc37d436701dd804f3e0b4897d5690a2c92c73795ad4a015d8715990bfb4226dc6d14fea429522fcb5662ce370508e8d + languageName: node + linkType: hard + +"@parcel/watcher@npm:^2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher@npm:2.4.1" + dependencies: + "@parcel/watcher-android-arm64": "npm:2.4.1" + "@parcel/watcher-darwin-arm64": "npm:2.4.1" + "@parcel/watcher-darwin-x64": "npm:2.4.1" + "@parcel/watcher-freebsd-x64": "npm:2.4.1" + "@parcel/watcher-linux-arm-glibc": "npm:2.4.1" + "@parcel/watcher-linux-arm64-glibc": "npm:2.4.1" + "@parcel/watcher-linux-arm64-musl": "npm:2.4.1" + "@parcel/watcher-linux-x64-glibc": "npm:2.4.1" + "@parcel/watcher-linux-x64-musl": "npm:2.4.1" + "@parcel/watcher-win32-arm64": "npm:2.4.1" + "@parcel/watcher-win32-ia32": "npm:2.4.1" + "@parcel/watcher-win32-x64": "npm:2.4.1" + detect-libc: "npm:^1.0.3" + is-glob: "npm:^4.0.3" + micromatch: "npm:^4.0.5" + node-addon-api: "npm:^7.0.0" + node-gyp: "npm:latest" + dependenciesMeta: + "@parcel/watcher-android-arm64": + optional: true + "@parcel/watcher-darwin-arm64": + optional: true + "@parcel/watcher-darwin-x64": + optional: true + "@parcel/watcher-freebsd-x64": + optional: true + "@parcel/watcher-linux-arm-glibc": + optional: true + "@parcel/watcher-linux-arm64-glibc": + optional: true + "@parcel/watcher-linux-arm64-musl": + optional: true + "@parcel/watcher-linux-x64-glibc": + optional: true + "@parcel/watcher-linux-x64-musl": + optional: true + "@parcel/watcher-win32-arm64": + optional: true + "@parcel/watcher-win32-ia32": + optional: true + "@parcel/watcher-win32-x64": + optional: true + checksum: 10c0/33b7112094b9eb46c234d824953967435b628d3d93a0553255e9910829b84cab3da870153c3a870c31db186dc58f3b2db81382fcaee3451438aeec4d786a6211 + languageName: node + linkType: hard + +"@phenomnomnominal/tsquery@npm:~5.0.1": version: 5.0.1 resolution: "@phenomnomnominal/tsquery@npm:5.0.1" dependencies: @@ -4854,6 +6326,79 @@ __metadata: languageName: node linkType: hard +"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/aspromise@npm:1.1.2" + checksum: 10c0/a83343a468ff5b5ec6bff36fd788a64c839e48a07ff9f4f813564f58caf44d011cd6504ed2147bf34835bd7a7dd2107052af755961c6b098fd8902b4f6500d0f + languageName: node + linkType: hard + +"@protobufjs/base64@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/base64@npm:1.1.2" + checksum: 10c0/eec925e681081af190b8ee231f9bad3101e189abbc182ff279da6b531e7dbd2a56f1f306f37a80b1be9e00aa2d271690d08dcc5f326f71c9eed8546675c8caf6 + languageName: node + linkType: hard + +"@protobufjs/codegen@npm:^2.0.4": + version: 2.0.4 + resolution: "@protobufjs/codegen@npm:2.0.4" + checksum: 10c0/26ae337c5659e41f091606d16465bbcc1df1f37cc1ed462438b1f67be0c1e28dfb2ca9f294f39100c52161aef82edf758c95d6d75650a1ddf31f7ddee1440b43 + languageName: node + linkType: hard + +"@protobufjs/eventemitter@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/eventemitter@npm:1.1.0" + checksum: 10c0/1eb0a75180e5206d1033e4138212a8c7089a3d418c6dfa5a6ce42e593a4ae2e5892c4ef7421f38092badba4040ea6a45f0928869989411001d8c1018ea9a6e70 + languageName: node + linkType: hard + +"@protobufjs/fetch@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/fetch@npm:1.1.0" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.1" + "@protobufjs/inquire": "npm:^1.1.0" + checksum: 10c0/cda6a3dc2d50a182c5865b160f72077aac197046600091dbb005dd0a66db9cce3c5eaed6d470ac8ed49d7bcbeef6ee5f0bc288db5ff9a70cbd003e5909065233 + languageName: node + linkType: hard + +"@protobufjs/float@npm:^1.0.2": + version: 1.0.2 + resolution: "@protobufjs/float@npm:1.0.2" + checksum: 10c0/18f2bdede76ffcf0170708af15c9c9db6259b771e6b84c51b06df34a9c339dbbeec267d14ce0bddd20acc142b1d980d983d31434398df7f98eb0c94a0eb79069 + languageName: node + linkType: hard + +"@protobufjs/inquire@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/inquire@npm:1.1.0" + checksum: 10c0/64372482efcba1fb4d166a2664a6395fa978b557803857c9c03500e0ac1013eb4b1aacc9ed851dd5fc22f81583670b4f4431bae186f3373fedcfde863ef5921a + languageName: node + linkType: hard + +"@protobufjs/path@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/path@npm:1.1.2" + checksum: 10c0/cece0a938e7f5dfd2fa03f8c14f2f1cf8b0d6e13ac7326ff4c96ea311effd5fb7ae0bba754fbf505312af2e38500250c90e68506b97c02360a43793d88a0d8b4 + languageName: node + linkType: hard + +"@protobufjs/pool@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/pool@npm:1.1.0" + checksum: 10c0/eda2718b7f222ac6e6ad36f758a92ef90d26526026a19f4f17f668f45e0306a5bd734def3f48f51f8134ae0978b6262a5c517c08b115a551756d1a3aadfcf038 + languageName: node + linkType: hard + +"@protobufjs/utf8@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/utf8@npm:1.1.0" + checksum: 10c0/a3fe31fe3fa29aa3349e2e04ee13dc170cc6af7c23d92ad49e3eeaf79b9766264544d3da824dba93b7855bd6a2982fb40032ef40693da98a136d835752beb487 + languageName: node + linkType: hard + "@schematics/angular@npm:16.2.12": version: 16.2.12 resolution: "@schematics/angular@npm:16.2.12" @@ -4909,6 +6454,25 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/slugify@npm:^2.2.1": + version: 2.2.1 + resolution: "@sindresorhus/slugify@npm:2.2.1" + dependencies: + "@sindresorhus/transliterate": "npm:^1.0.0" + escape-string-regexp: "npm:^5.0.0" + checksum: 10c0/c3fe41d917347f0e2a1e25a48225afffde8ef379a26217e749d5267e965f564c6a555fa17475b637d6fd84645f42e1e4b530477b57110fa80428024a0fadba25 + languageName: node + linkType: hard + +"@sindresorhus/transliterate@npm:^1.0.0": + version: 1.6.0 + resolution: "@sindresorhus/transliterate@npm:1.6.0" + dependencies: + escape-string-regexp: "npm:^5.0.0" + checksum: 10c0/c5552abd98eb4ab3a8653ccb7addf24e0b6f2aa2a4c420689033f8c9d292abb2222fc08e330adf4055580ac78fe810b7467ed012cdf38f4d64175c42571b8b15 + languageName: node + linkType: hard + "@socket.io/component-emitter@npm:~3.1.0": version: 3.1.2 resolution: "@socket.io/component-emitter@npm:3.1.2" @@ -4916,6 +6480,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/spec@npm:^1.0.0": + version: 1.0.0 + resolution: "@standard-schema/spec@npm:1.0.0" + checksum: 10c0/a1ab9a8bdc09b5b47aa8365d0e0ec40cc2df6437be02853696a0e377321653b0d3ac6f079a8c67d5ddbe9821025584b1fb71d9cc041a6666a96f1fadf2ece15f + languageName: node + linkType: hard + "@stoplight/json-ref-resolver@npm:3.1.5": version: 3.1.5 resolution: "@stoplight/json-ref-resolver@npm:3.1.5" @@ -5047,6 +6618,13 @@ __metadata: languageName: node linkType: hard +"@types/aws-lambda@npm:8.10.152": + version: 8.10.152 + resolution: "@types/aws-lambda@npm:8.10.152" + checksum: 10c0/ddb3858d961b88a3c5eb947ddd8890bf6d0ae8d24ce5bf7d4fd8b54f7d45646864da10cdde038755d191e834201624a40eb3240dbe48f633f4dd18137ac2e3d7 + languageName: node + linkType: hard + "@types/backbone@npm:1.4.15": version: 1.4.15 resolution: "@types/backbone@npm:1.4.15" @@ -5076,10 +6654,12 @@ __metadata: languageName: node linkType: hard -"@types/concaveman@npm:1.1.6": - version: 1.1.6 - resolution: "@types/concaveman@npm:1.1.6" - checksum: 10c0/5cc36f8d85eeb15e976d4592c0cffca42607d1de14180fc15d5518fd309a714f05fcf9ecf03f43432fd014905aa78801a4004773ce00293424263738467e65da +"@types/bunyan@npm:1.8.11": + version: 1.8.11 + resolution: "@types/bunyan@npm:1.8.11" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/07d762499307a1c3f04f56f2c62417b909f86f6090cee29b73a00dde323a4463cfd2e78888598cb1cd3b1eb88e6c47ef2a58e17f119dae27ff04cd361c0a1d4c languageName: node linkType: hard @@ -5093,7 +6673,7 @@ __metadata: languageName: node linkType: hard -"@types/connect@npm:*": +"@types/connect@npm:*, @types/connect@npm:3.4.38": version: 3.4.38 resolution: "@types/connect@npm:3.4.38" dependencies: @@ -5125,19 +6705,12 @@ __metadata: languageName: node linkType: hard -"@types/d3-path@npm:^2": - version: 2.0.4 - resolution: "@types/d3-path@npm:2.0.4" - checksum: 10c0/82214a9644cfffe0c1f9a7aab00e3912aaba89115c60d94ecf716d282eac71671761962a9e911a8ebc457777e3db42f80c355b61010e5e27218f6aed32128d39 - languageName: node - linkType: hard - -"@types/d3-shape@npm:2.1.2": - version: 2.1.2 - resolution: "@types/d3-shape@npm:2.1.2" +"@types/cors@npm:^2.8.17": + version: 2.8.19 + resolution: "@types/cors@npm:2.8.19" dependencies: - "@types/d3-path": "npm:^2" - checksum: 10c0/f32e49cc41089ba9658716e14626831b20f564ac95be9a363fbd119913e223aa771dd703a2a602a2db8f25ba2eada1a73c12c2cff057526e0c3f9519756e5a4c + "@types/node": "npm:*" + checksum: 10c0/b5dd407040db7d8aa1bd36e79e5f3f32292f6b075abc287529e9f48df1a25fda3e3799ba30b4656667ffb931d3b75690c1d6ca71e39f7337ea6dfda8581916d0 languageName: node linkType: hard @@ -5148,6 +6721,13 @@ __metadata: languageName: node linkType: hard +"@types/diff-match-patch@npm:^1.0.36": + version: 1.0.36 + resolution: "@types/diff-match-patch@npm:1.0.36" + checksum: 10c0/0bad011ab138baa8bde94e7815064bb881f010452463272644ddbbb0590659cb93f7aa2776ff442c6721d70f202839e1053f8aa62d801cc4166f7a3ea9130055 + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.3": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" @@ -5223,6 +6803,18 @@ __metadata: languageName: node linkType: hard +"@types/express@npm:^4.17.23": + version: 4.17.23 + resolution: "@types/express@npm:4.17.23" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^4.17.33" + "@types/qs": "npm:*" + "@types/serve-static": "npm:*" + checksum: 10c0/60490cd4f73085007247e7d4fafad0a7abdafa34fa3caba2757512564ca5e094ece7459f0f324030a63d513f967bb86579a8682af76ae2fd718e889b0a2a4fe8 + languageName: node + linkType: hard + "@types/file-saver@npm:2.0.5": version: 2.0.5 resolution: "@types/file-saver@npm:2.0.5" @@ -5294,7 +6886,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db @@ -5338,6 +6930,15 @@ __metadata: languageName: node linkType: hard +"@types/memcached@npm:^2.2.6": + version: 2.2.10 + resolution: "@types/memcached@npm:2.2.10" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/0c5214a73c9abb3d1bbf91d2890d38476961ae8aa387f71235519be65a537c654ca0380a468cf3ab49d3b9409c441580d081f16f14ed6aea3339144aee0f16fb + languageName: node + linkType: hard + "@types/mime@npm:^1": version: 1.3.5 resolution: "@types/mime@npm:1.3.5" @@ -5345,6 +6946,15 @@ __metadata: languageName: node linkType: hard +"@types/mysql@npm:2.15.27": + version: 2.15.27 + resolution: "@types/mysql@npm:2.15.27" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/d34064de1697e9e29dbad313df759a8c7aff9d9d1918c9b666b1ebc894b9a0c1c6f4ae779453fdcd20b892fa60a8e55640138c292c6c2a28d2f758eaeb539ce3 + languageName: node + linkType: hard + "@types/node-forge@npm:^1.3.0": version: 1.3.11 resolution: "@types/node-forge@npm:1.3.11" @@ -5370,6 +6980,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>=13.7.0": + version: 24.8.1 + resolution: "@types/node@npm:24.8.1" + dependencies: + undici-types: "npm:~7.14.0" + checksum: 10c0/d185f2f14aa26cc2b482aa730bfc452943f9636df37aad6ceed80aa397f1278f894043336bd72f74c47b3dbef23e772ac9b1a256168984aa8aee26836132d290 + languageName: node + linkType: hard + "@types/offscreencanvas@npm:^2019.7.0": version: 2019.7.3 resolution: "@types/offscreencanvas@npm:2019.7.3" @@ -5377,6 +6996,15 @@ __metadata: languageName: node linkType: hard +"@types/oracledb@npm:6.5.2": + version: 6.5.2 + resolution: "@types/oracledb@npm:6.5.2" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/16e6d2e4247222dddf7be01273946b7f6a686327ce440be861671a2a0b98fe1a0d42df849d039a3f58aa1014f1c9d803f3c9793531a476077d762423ac911e65 + languageName: node + linkType: hard + "@types/papaparse@npm:5.3.5": version: 5.3.5 resolution: "@types/papaparse@npm:5.3.5" @@ -5393,6 +7021,26 @@ __metadata: languageName: node linkType: hard +"@types/pg-pool@npm:2.0.6": + version: 2.0.6 + resolution: "@types/pg-pool@npm:2.0.6" + dependencies: + "@types/pg": "npm:*" + checksum: 10c0/41965d4d0b677c54ce45d36add760e496d356b78019cb062d124af40287cf6b0fd4d86e3b0085f443856c185983a60c8b0795ff76d15683e2a93c62f5ac0125f + languageName: node + linkType: hard + +"@types/pg@npm:*, @types/pg@npm:8.15.5": + version: 8.15.5 + resolution: "@types/pg@npm:8.15.5" + dependencies: + "@types/node": "npm:*" + pg-protocol: "npm:*" + pg-types: "npm:^2.2.0" + checksum: 10c0/19a3cc1811918753f8c827733648c3a85c7b0355bf207c44eb1a3b79b2e6a0d85cb5457ec550d860fc9be7e88c7587a3600958ec8c61fa1ad573061c63af93f0 + languageName: node + linkType: hard + "@types/plotly.js-basic-dist-min@npm:2.12.4": version: 2.12.4 resolution: "@types/plotly.js-basic-dist-min@npm:2.12.4" @@ -5514,6 +7162,15 @@ __metadata: languageName: node linkType: hard +"@types/tedious@npm:^4.0.14": + version: 4.0.14 + resolution: "@types/tedious@npm:4.0.14" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/d2914f8e9b5b998e4275ec5f0130cba1c2fb47e75616b5c125a65ef6c1db2f1dc3f978c7900693856a15d72bbb4f4e94f805537a4ecb6dc126c64415d31c0590 + languageName: node + linkType: hard + "@types/underscore@npm:*": version: 1.13.0 resolution: "@types/underscore@npm:1.13.0" @@ -5542,6 +7199,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^8.18.1": + version: 8.18.1 + resolution: "@types/ws@npm:8.18.1" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/61aff1129143fcc4312f083bc9e9e168aa3026b7dd6e70796276dcfb2c8211c4292603f9c4864fae702f2ed86e4abd4d38aa421831c2fd7f856c931a481afbab + languageName: node + linkType: hard + "@types/ws@npm:^8.5.10, @types/ws@npm:^8.5.5": version: 8.5.12 resolution: "@types/ws@npm:8.5.12" @@ -5927,6 +7593,13 @@ __metadata: languageName: node linkType: hard +"@vercel/oidc@npm:^3.0.1": + version: 3.0.3 + resolution: "@vercel/oidc@npm:3.0.3" + checksum: 10c0/c8eecb1324559435f4ab8a955f5ef44f74f546d11c2ddcf28151cb636d989bd4b34e0673fd8716cb21bb21afb34b3de663bacc30c9506036eeecbcbf2fd86241 + languageName: node + linkType: hard + "@vitejs/plugin-basic-ssl@npm:1.0.1": version: 1.0.1 resolution: "@vitejs/plugin-basic-ssl@npm:1.0.1" @@ -6225,6 +7898,16 @@ __metadata: languageName: node linkType: hard +"accepts@npm:^2.0.0": + version: 2.0.0 + resolution: "accepts@npm:2.0.0" + dependencies: + mime-types: "npm:^3.0.0" + negotiator: "npm:^1.0.0" + checksum: 10c0/98374742097e140891546076215f90c32644feacf652db48412329de4c2a529178a81aa500fbb13dd3e6cbf6e68d829037b123ac037fc9a08bcec4b87b358eef + languageName: node + linkType: hard + "acorn-globals@npm:^6.0.0": version: 6.0.0 resolution: "acorn-globals@npm:6.0.0" @@ -6244,6 +7927,15 @@ __metadata: languageName: node linkType: hard +"acorn-import-attributes@npm:^1.9.5": + version: 1.9.5 + resolution: "acorn-import-attributes@npm:1.9.5" + peerDependencies: + acorn: ^8 + checksum: 10c0/5926eaaead2326d5a86f322ff1b617b0f698aa61dc719a5baa0e9d955c9885cc71febac3fb5bacff71bbf2c4f9c12db2056883c68c53eb962c048b952e1e013d + languageName: node + linkType: hard + "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -6287,6 +7979,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.14.0": + version: 8.15.0 + resolution: "acorn@npm:8.15.0" + bin: + acorn: bin/acorn + checksum: 10c0/dec73ff59b7d6628a01eebaece7f2bdb8bb62b9b5926dcad0f8931f2b8b79c2be21f6c68ac095592adb5adb15831a3635d9343e6a91d028bbe85d564875ec3ec + languageName: node + linkType: hard + "address@npm:^1.0.1": version: 1.2.2 resolution: "address@npm:1.2.2" @@ -6348,6 +8049,40 @@ __metadata: languageName: node linkType: hard +"ai-v5@npm:ai@5.0.60": + version: 5.0.60 + resolution: "ai@npm:5.0.60" + dependencies: + "@ai-sdk/gateway": "npm:1.0.33" + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.10" + "@opentelemetry/api": "npm:1.9.0" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/290a9da9891e1e61a294e327a78f1f6a5ed58c92b1c14e89fd0102ae9369d803e58c299195fa8abc437380b26241b5c066e73f9a7d43cca13fe5f956629cae82 + languageName: node + linkType: hard + +"ai@npm:^4.3.19": + version: 4.3.19 + resolution: "ai@npm:4.3.19" + dependencies: + "@ai-sdk/provider": "npm:1.1.3" + "@ai-sdk/provider-utils": "npm:2.2.8" + "@ai-sdk/react": "npm:1.2.12" + "@ai-sdk/ui-utils": "npm:1.2.11" + "@opentelemetry/api": "npm:1.9.0" + jsondiffpatch: "npm:0.6.0" + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + react: + optional: true + checksum: 10c0/738ac453b3e61b2f2282941fe8af946c42696fbdcffa5ac213823377bcddf475f26923cf2ca5656d5655e5c351e355e1af62dcb04a6df6139b67bac650b01af2 + languageName: node + linkType: hard + "ajv-formats@npm:2.1.1, ajv-formats@npm:^2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" @@ -6406,7 +8141,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:^6.12.6": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -6745,6 +8480,13 @@ __metadata: languageName: node linkType: hard +"atomic-sleep@npm:^1.0.0": + version: 1.0.0 + resolution: "atomic-sleep@npm:1.0.0" + checksum: 10c0/e329a6665512736a9bbb073e1761b4ec102f7926cce35037753146a9db9c8104f5044c1662e4a863576ce544fb8be27cd2be6bc8c1a40147d03f31eb1cfb6e8a + languageName: node + linkType: hard + "autoprefixer@npm:10.4.14": version: 10.4.14 resolution: "autoprefixer@npm:10.4.14" @@ -6973,7 +8715,7 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.2.0, base64-js@npm:^1.3.1": +"base64-js@npm:^1.2.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf @@ -7028,6 +8770,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.0.0": + version: 9.3.1 + resolution: "bignumber.js@npm:9.3.1" + checksum: 10c0/61342ba5fe1c10887f0ecf5be02ff6709271481aff48631f86b4d37d55a99b87ce441cfd54df3d16d10ee07ceab7e272fc0be430c657ffafbbbf7b7d631efb75 + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0": version: 2.3.0 resolution: "binary-extensions@npm:2.3.0" @@ -7083,6 +8832,23 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:^2.2.0": + version: 2.2.0 + resolution: "body-parser@npm:2.2.0" + dependencies: + bytes: "npm:^3.1.2" + content-type: "npm:^1.0.5" + debug: "npm:^4.4.0" + http-errors: "npm:^2.0.0" + iconv-lite: "npm:^0.6.3" + on-finished: "npm:^2.4.1" + qs: "npm:^6.14.0" + raw-body: "npm:^3.0.0" + type-is: "npm:^2.0.0" + checksum: 10c0/a9ded39e71ac9668e2211afa72e82ff86cc5ef94de1250b7d1ba9cc299e4150408aaa5f1e8b03dd4578472a3ce6d1caa2a23b27a6c18e526e48b4595174c116c + languageName: node + linkType: hard + "bonjour-service@npm:^1.0.11, bonjour-service@npm:^1.2.1": version: 1.2.1 resolution: "bonjour-service@npm:1.2.1" @@ -7219,7 +8985,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2": +"bytes@npm:3.1.2, bytes@npm:^3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e @@ -7332,6 +9098,16 @@ __metadata: languageName: node linkType: hard +"call-bound@npm:^1.0.2": + version: 1.0.4 + resolution: "call-bound@npm:1.0.4" + dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + get-intrinsic: "npm:^1.3.0" + checksum: 10c0/f4796a6a0941e71c766aea672f63b72bc61234c4f4964dc6d7606e3664c307e7d77845328a8f3359ce39ddb377fed67318f9ee203dea1d47e46165dcf2917644 + languageName: node + linkType: hard + "callsites@npm:^3.0.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -7451,6 +9227,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.3.0": + version: 5.6.2 + resolution: "chalk@npm:5.6.2" + checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976 + languageName: node + linkType: hard + "chardet@npm:^0.7.0": version: 0.7.0 resolution: "chardet@npm:0.7.0" @@ -7526,6 +9309,13 @@ __metadata: languageName: node linkType: hard +"cjs-module-lexer@npm:^1.2.2": + version: 1.4.3 + resolution: "cjs-module-lexer@npm:1.4.3" + checksum: 10c0/076b3af85adc4d65dbdab1b5b240fe5b45d44fcf0ef9d429044dd94d19be5589376805c44fb2d4b3e684e5fe6a9b7cf3e426476a6507c45283c5fc6ff95240be + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -7614,7 +9404,7 @@ __metadata: languageName: node linkType: hard -"clone@npm:^2.1.1": +"clone@npm:^2.1.1, clone@npm:^2.1.2": version: 2.1.2 resolution: "clone@npm:2.1.2" checksum: 10c0/ed0601cd0b1606bc7d82ee7175b97e68d1dd9b91fd1250a3617b38d34a095f8ee0431d40a1a611122dcccb4f93295b4fdb94942aa763392b5fe44effa50c2d5e @@ -7676,7 +9466,7 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.10": +"colorette@npm:^2.0.10, colorette@npm:^2.0.7": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: 10c0/e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 @@ -7709,13 +9499,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:2, commander@npm:^2.20.0": - version: 2.20.3 - resolution: "commander@npm:2.20.3" - checksum: 10c0/74c781a5248c2402a0a3e966a0a2bba3c054aad144f5c023364be83265e796b20565aa9feff624132ff629aa64e16999fa40a743c10c12f7c61e96a794b99288 - languageName: node - linkType: hard - "commander@npm:7, commander@npm:^7.2.0": version: 7.2.0 resolution: "commander@npm:7.2.0" @@ -7723,6 +9506,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^2.20.0": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: 10c0/74c781a5248c2402a0a3e966a0a2bba3c054aad144f5c023364be83265e796b20565aa9feff624132ff629aa64e16999fa40a743c10c12f7c61e96a794b99288 + languageName: node + linkType: hard + "commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" @@ -7789,18 +9579,6 @@ __metadata: languageName: node linkType: hard -"concaveman@npm:2.0.0": - version: 2.0.0 - resolution: "concaveman@npm:2.0.0" - dependencies: - point-in-polygon: "npm:^1.1.0" - rbush: "npm:^4.0.1" - robust-predicates: "npm:^3.0.2" - tinyqueue: "npm:^3.0.0" - checksum: 10c0/d277ee4dd5e5af21f0a22d056e1edad66db8f5515a9feb92d95f40407d819c17a0b4cd7f5a07a028d72cac05e85305dee0118036bc295424740f01af08ecda98 - languageName: node - linkType: hard - "concurrently@npm:7.4.0": version: 7.4.0 resolution: "concurrently@npm:7.4.0" @@ -7856,7 +9634,16 @@ __metadata: languageName: node linkType: hard -"content-type@npm:^1.0.4, content-type@npm:~1.0.4, content-type@npm:~1.0.5": +"content-disposition@npm:^1.0.0": + version: 1.0.0 + resolution: "content-disposition@npm:1.0.0" + dependencies: + safe-buffer: "npm:5.2.1" + checksum: 10c0/c7b1ba0cea2829da0352ebc1b7f14787c73884bc707c8bc2271d9e3bf447b372270d09f5d3980dc5037c749ceef56b9a13fccd0b0001c87c3f12579967e4dd27 + languageName: node + linkType: hard + +"content-type@npm:^1.0.4, content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af @@ -7884,6 +9671,13 @@ __metadata: languageName: node linkType: hard +"cookie-signature@npm:^1.2.1": + version: 1.2.2 + resolution: "cookie-signature@npm:1.2.2" + checksum: 10c0/54e05df1a293b3ce81589b27dddc445f462f6fa6812147c033350cd3561a42bc14481674e05ed14c7bd0ce1e8bb3dc0e40851bad75415733711294ddce0b7bc6 + languageName: node + linkType: hard + "cookie@npm:0.7.1": version: 0.7.1 resolution: "cookie@npm:0.7.1" @@ -7891,7 +9685,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:~0.7.2": +"cookie@npm:^0.7.1, cookie@npm:~0.7.2": version: 0.7.2 resolution: "cookie@npm:0.7.2" checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 @@ -7972,7 +9766,7 @@ __metadata: languageName: node linkType: hard -"cors@npm:~2.8.5": +"cors@npm:^2.8.5, cors@npm:~2.8.5": version: 2.8.5 resolution: "cors@npm:2.8.5" dependencies: @@ -8092,6 +9886,17 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^7.0.5": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 + languageName: node + linkType: hard + "css-declaration-sorter@npm:^7.2.0": version: 7.2.0 resolution: "css-declaration-sorter@npm:7.2.0" @@ -8368,22 +10173,6 @@ __metadata: languageName: node linkType: hard -"d3-array@npm:2, d3-array@npm:^2.3.0, d3-array@npm:^2.5.0": - version: 2.12.1 - resolution: "d3-array@npm:2.12.1" - dependencies: - internmap: "npm:^1.0.0" - checksum: 10c0/7eca10427a9f113a4ca6a0f7301127cab26043fd5e362631ef5a0edd1c4b2dd70c56ed317566700c31e4a6d88b55f3951aaba192291817f243b730cb2352882e - languageName: node - linkType: hard - -"d3-axis@npm:2": - version: 2.1.0 - resolution: "d3-axis@npm:2.1.0" - checksum: 10c0/812558c0a016f8541e4704837b4428060695e2397e00a374caab5ae060a8ea6254bb097a4cfb7e01deff61da8a91c582eda614659ada5fd590da902a397428cc - languageName: node - linkType: hard - "d3-axis@npm:3": version: 3.0.0 resolution: "d3-axis@npm:3.0.0" @@ -8391,19 +10180,6 @@ __metadata: languageName: node linkType: hard -"d3-brush@npm:2": - version: 2.1.0 - resolution: "d3-brush@npm:2.1.0" - dependencies: - d3-dispatch: "npm:1 - 2" - d3-drag: "npm:2" - d3-interpolate: "npm:1 - 2" - d3-selection: "npm:2" - d3-transition: "npm:2" - checksum: 10c0/f4221f815f8b0fb5af42a2c5baf7cb884067fdddfe5e3d9ae8b603912a47c205e138b8cdcb19a693feac46aa790c0dc9204c893e4b09cfdb944d31bde0b81126 - languageName: node - linkType: hard - "d3-brush@npm:3": version: 3.0.0 resolution: "d3-brush@npm:3.0.0" @@ -8417,15 +10193,6 @@ __metadata: languageName: node linkType: hard -"d3-chord@npm:2": - version: 2.0.0 - resolution: "d3-chord@npm:2.0.0" - dependencies: - d3-path: "npm:1 - 2" - checksum: 10c0/675c4941cd9ea8dcf462622409a42b5de4120198b5016bf9a13e67074487b30a29ab5b7adbae3cab6ee56820255c320a56657fdf55538c4c7439cb9a4248dbb4 - languageName: node - linkType: hard - "d3-chord@npm:3": version: 3.0.1 resolution: "d3-chord@npm:3.0.1" @@ -8435,13 +10202,6 @@ __metadata: languageName: node linkType: hard -"d3-color@npm:1 - 2, d3-color@npm:2": - version: 2.0.0 - resolution: "d3-color@npm:2.0.0" - checksum: 10c0/5aa58dfb78e3db764373a904eabb643dc024ff6071128a41e86faafa100e0e17a796e06ac3f2662e9937242bb75b8286788629773d76936f11c17bd5fe5e15cd - languageName: node - linkType: hard - "d3-color@npm:1 - 3, d3-color@npm:3": version: 3.1.0 resolution: "d3-color@npm:3.1.0" @@ -8449,15 +10209,6 @@ __metadata: languageName: node linkType: hard -"d3-contour@npm:2": - version: 2.0.0 - resolution: "d3-contour@npm:2.0.0" - dependencies: - d3-array: "npm:2" - checksum: 10c0/00b58abff5648438928571efc75a56e7459edcbb63ad807bcb38de13b337ac4a83286dabe33f53562ee5ebd2d83da31835cf46a1775fe35e03101772769d10ac - languageName: node - linkType: hard - "d3-contour@npm:4": version: 4.0.2 resolution: "d3-contour@npm:4.0.2" @@ -8467,15 +10218,6 @@ __metadata: languageName: node linkType: hard -"d3-delaunay@npm:5": - version: 5.3.0 - resolution: "d3-delaunay@npm:5.3.0" - dependencies: - delaunator: "npm:4" - checksum: 10c0/214aaddd01c58cadcfeea917ff7b7c3202083ac66fe5948d14558385d1ce495b33498ce10a3188ff144377d3da2f2ad72c2c3885407c325a24266c8f4c09347e - languageName: node - linkType: hard - "d3-delaunay@npm:6": version: 6.0.4 resolution: "d3-delaunay@npm:6.0.4" @@ -8485,13 +10227,6 @@ __metadata: languageName: node linkType: hard -"d3-dispatch@npm:1 - 2, d3-dispatch@npm:2": - version: 2.0.0 - resolution: "d3-dispatch@npm:2.0.0" - checksum: 10c0/379f7ce1510f529da00a34016630e92e41c0f6bbffef7b849f4e46733c188c67418df266a9a541cda17572b5286e32fbaf66308fe04dcfe52aa551830825bc93 - languageName: node - linkType: hard - "d3-dispatch@npm:1 - 3, d3-dispatch@npm:3": version: 3.0.1 resolution: "d3-dispatch@npm:3.0.1" @@ -8499,16 +10234,6 @@ __metadata: languageName: node linkType: hard -"d3-drag@npm:2": - version: 2.0.0 - resolution: "d3-drag@npm:2.0.0" - dependencies: - d3-dispatch: "npm:1 - 2" - d3-selection: "npm:2" - checksum: 10c0/a6f2cfea47aa888b56476454554b37befb0d332f326704913142c6a89b295edfc0947a9fb5afdd0c6d0028878b832a975c80df55d009ab7ccf7b59faa99e926c - languageName: node - linkType: hard - "d3-drag@npm:2 - 3, d3-drag@npm:3": version: 3.0.0 resolution: "d3-drag@npm:3.0.0" @@ -8519,27 +10244,6 @@ __metadata: languageName: node linkType: hard -"d3-dsv@npm:1 - 2, d3-dsv@npm:2": - version: 2.0.0 - resolution: "d3-dsv@npm:2.0.0" - dependencies: - commander: "npm:2" - iconv-lite: "npm:0.4" - rw: "npm:1" - bin: - csv2json: bin/dsv2json - csv2tsv: bin/dsv2dsv - dsv2dsv: bin/dsv2dsv - dsv2json: bin/dsv2json - json2csv: bin/json2dsv - json2dsv: bin/json2dsv - json2tsv: bin/json2dsv - tsv2csv: bin/dsv2dsv - tsv2json: bin/dsv2json - checksum: 10c0/b5e4a0945664a941ad37d3c5ee8c8cbb3c99712ff7f36b2420446ebfc3b3909eb3d9ef2682b4a77afa6a6c5f5af1d659e17d6678863d95b244aed479b6c7c5c3 - languageName: node - linkType: hard - "d3-dsv@npm:1 - 3, d3-dsv@npm:3": version: 3.0.1 resolution: "d3-dsv@npm:3.0.1" @@ -8561,13 +10265,6 @@ __metadata: languageName: node linkType: hard -"d3-ease@npm:1 - 2, d3-ease@npm:2": - version: 2.0.0 - resolution: "d3-ease@npm:2.0.0" - checksum: 10c0/4c2e74417739b73f5d185675be0a72b19df1f729b87c457d4b13976f7eef5c1f54739b4ccd71d9730521c959cfac28a56f5f4040dd2baf5729ac7e4adce344f1 - languageName: node - linkType: hard - "d3-ease@npm:1 - 3, d3-ease@npm:3": version: 3.0.1 resolution: "d3-ease@npm:3.0.1" @@ -8575,15 +10272,6 @@ __metadata: languageName: node linkType: hard -"d3-fetch@npm:2": - version: 2.0.0 - resolution: "d3-fetch@npm:2.0.0" - dependencies: - d3-dsv: "npm:1 - 2" - checksum: 10c0/c508e1fb9d0aa04af04d8021a658ce2726239c265ffe0762d9fda3f02a1d1958d87984b486a41141660f61258f5977deff84d87767422876aed12dca3407bf73 - languageName: node - linkType: hard - "d3-fetch@npm:3": version: 3.0.1 resolution: "d3-fetch@npm:3.0.1" @@ -8593,17 +10281,6 @@ __metadata: languageName: node linkType: hard -"d3-force@npm:2": - version: 2.1.1 - resolution: "d3-force@npm:2.1.1" - dependencies: - d3-dispatch: "npm:1 - 2" - d3-quadtree: "npm:1 - 2" - d3-timer: "npm:1 - 2" - checksum: 10c0/0044ad969c524fa32d39d414fb698d183a10b7a9ec9369ac04aa00cc026bc1262308971d70bf20873adfb3d73f557aea9203797d40bd7d7247ed9714be946fb4 - languageName: node - linkType: hard - "d3-force@npm:3": version: 3.0.0 resolution: "d3-force@npm:3.0.0" @@ -8615,13 +10292,6 @@ __metadata: languageName: node linkType: hard -"d3-format@npm:1 - 2, d3-format@npm:2": - version: 2.0.0 - resolution: "d3-format@npm:2.0.0" - checksum: 10c0/c869af459e20767dc3d9cbb2946ba79cc266ae4fb35d11c50c63fc89ea4ed168c702c7e3db94d503b3618de9609bf3bf2d855ef53e21109ddd7eb9c8f3fcf8a1 - languageName: node - linkType: hard - "d3-format@npm:1 - 3, d3-format@npm:3": version: 3.1.0 resolution: "d3-format@npm:3.1.0" @@ -8629,15 +10299,6 @@ __metadata: languageName: node linkType: hard -"d3-geo@npm:2": - version: 2.0.2 - resolution: "d3-geo@npm:2.0.2" - dependencies: - d3-array: "npm:^2.5.0" - checksum: 10c0/6836feb33036f03ec1dc728d64171f3eb40ee2c1c0f2e28232055aa317e5e2d481075294babb9553596e2080496eda119b8a43c51f71c893e6258ccf002bd7f8 - languageName: node - linkType: hard - "d3-geo@npm:3": version: 3.1.1 resolution: "d3-geo@npm:3.1.1" @@ -8647,13 +10308,6 @@ __metadata: languageName: node linkType: hard -"d3-hierarchy@npm:2": - version: 2.0.0 - resolution: "d3-hierarchy@npm:2.0.0" - checksum: 10c0/3216f9b8537ae2bf331006630f2075a81df4943edff077a60e19ca6f9a619c8494bf45bf7d89b409160b49edd1a49c0050002cdc8a9ae6e3672d20018a821fac - languageName: node - linkType: hard - "d3-hierarchy@npm:3": version: 3.1.2 resolution: "d3-hierarchy@npm:3.1.2" @@ -8661,15 +10315,6 @@ __metadata: languageName: node linkType: hard -"d3-interpolate@npm:1 - 2, d3-interpolate@npm:1.2.0 - 2, d3-interpolate@npm:2": - version: 2.0.1 - resolution: "d3-interpolate@npm:2.0.1" - dependencies: - d3-color: "npm:1 - 2" - checksum: 10c0/2a5725b0c9c7fef3e8878cf75ad67be851b1472de3dda1f694c441786a1a32e198ddfaa6880d6b280401c1af5b844b61ccdd63d85d1607c1e6bb3a3f0bf532ea - languageName: node - linkType: hard - "d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:3": version: 3.0.1 resolution: "d3-interpolate@npm:3.0.1" @@ -8679,13 +10324,6 @@ __metadata: languageName: node linkType: hard -"d3-path@npm:1 - 2, d3-path@npm:2": - version: 2.0.0 - resolution: "d3-path@npm:2.0.0" - checksum: 10c0/ef206f83c1123d4ad364c23b6fe877a7cd8afa76c30e13bd0588cd9b7c4ed4b577ebfd9499cdcac63268d7ae29c0a53ec38d6623a7c98585f3c118323e8f473f - languageName: node - linkType: hard - "d3-path@npm:1 - 3, d3-path@npm:3, d3-path@npm:^3.1.0": version: 3.1.0 resolution: "d3-path@npm:3.1.0" @@ -8693,13 +10331,6 @@ __metadata: languageName: node linkType: hard -"d3-polygon@npm:2": - version: 2.0.0 - resolution: "d3-polygon@npm:2.0.0" - checksum: 10c0/cb2f24cbdd0743ecaf7d0d7e060a00fd4ed8b1fbeb43b6db16878f7a8145ed5bedd60055d5f96b4ffc9aff3fd731926534af66007855e9eddb106c99a153ff5f - languageName: node - linkType: hard - "d3-polygon@npm:3": version: 3.0.1 resolution: "d3-polygon@npm:3.0.1" @@ -8707,13 +10338,6 @@ __metadata: languageName: node linkType: hard -"d3-quadtree@npm:1 - 2, d3-quadtree@npm:2": - version: 2.0.0 - resolution: "d3-quadtree@npm:2.0.0" - checksum: 10c0/b8dcbda82a04915018ea5309bcdc045971afbea293d87d32a34c31c69351c153a9e1a4d0ea77163a8ebe8f4fd9a6b114a7360e436f34552f284c1457275eda97 - languageName: node - linkType: hard - "d3-quadtree@npm:1 - 3, d3-quadtree@npm:3": version: 3.0.1 resolution: "d3-quadtree@npm:3.0.1" @@ -8721,13 +10345,6 @@ __metadata: languageName: node linkType: hard -"d3-random@npm:2": - version: 2.2.2 - resolution: "d3-random@npm:2.2.2" - checksum: 10c0/2c7413c4c626272cf12945ff14dacffdccc531989b551f8cfdd9c0266ae902cd9ffffc9ff98b693594a5ff342fb0f22e70afcce908cc6c43fb082bd52ace5acf - languageName: node - linkType: hard - "d3-random@npm:3": version: 3.0.1 resolution: "d3-random@npm:3.0.1" @@ -8735,16 +10352,6 @@ __metadata: languageName: node linkType: hard -"d3-scale-chromatic@npm:2": - version: 2.0.0 - resolution: "d3-scale-chromatic@npm:2.0.0" - dependencies: - d3-color: "npm:1 - 2" - d3-interpolate: "npm:1 - 2" - checksum: 10c0/93cafe497b00046b1d4e237a8bb8981fbb35ba03070f420bd913872f6e9d2c9628ed8bb8c84c6a6ffe16029359fa74b646c5c5129732ef4186ab059a77da3021 - languageName: node - linkType: hard - "d3-scale-chromatic@npm:3": version: 3.1.0 resolution: "d3-scale-chromatic@npm:3.1.0" @@ -8755,19 +10362,6 @@ __metadata: languageName: node linkType: hard -"d3-scale@npm:3": - version: 3.3.0 - resolution: "d3-scale@npm:3.3.0" - dependencies: - d3-array: "npm:^2.3.0" - d3-format: "npm:1 - 2" - d3-interpolate: "npm:1.2.0 - 2" - d3-time: "npm:^2.1.1" - d3-time-format: "npm:2 - 3" - checksum: 10c0/cb63c271ec9c5b632c245c63e0d0716b32adcc468247972c552f5be62fb34a17f71e4ac29fd8976704369f4b958bc6789c61a49427efe2160ae979d7843569dc - languageName: node - linkType: hard - "d3-scale@npm:4": version: 4.0.2 resolution: "d3-scale@npm:4.0.2" @@ -8781,13 +10375,6 @@ __metadata: languageName: node linkType: hard -"d3-selection@npm:2": - version: 2.0.0 - resolution: "d3-selection@npm:2.0.0" - checksum: 10c0/cd38f5e0baf2011f421e909ff5748235b18510175b7433ddddb04df325a4a03d7db07da09993afb464d5f050174c13cb5d843da902e71e3e8eb5f06c28fc8689 - languageName: node - linkType: hard - "d3-selection@npm:2 - 3, d3-selection@npm:3": version: 3.0.0 resolution: "d3-selection@npm:3.0.0" @@ -8795,15 +10382,6 @@ __metadata: languageName: node linkType: hard -"d3-shape@npm:2": - version: 2.1.0 - resolution: "d3-shape@npm:2.1.0" - dependencies: - d3-path: "npm:1 - 2" - checksum: 10c0/d86c84322b0ccd550df93ec4986359548cb2c25c9fae326984a59d1caeeb40c78f62678bb93951eed65536bc2d8efa446b13386acb79875234dc79dbcf16dbd7 - languageName: node - linkType: hard - "d3-shape@npm:3": version: 3.2.0 resolution: "d3-shape@npm:3.2.0" @@ -8813,15 +10391,6 @@ __metadata: languageName: node linkType: hard -"d3-time-format@npm:2 - 3, d3-time-format@npm:3": - version: 3.0.0 - resolution: "d3-time-format@npm:3.0.0" - dependencies: - d3-time: "npm:1 - 2" - checksum: 10c0/0abe3379f07d1c12ce8930cdddad1223c99cd3e4eac05cf409b5a7953e9ebed56a95a64b0977f63958cfb6101fa4a2a85533a5eae40df84f22c0117dbf5e8982 - languageName: node - linkType: hard - "d3-time-format@npm:2 - 4, d3-time-format@npm:4": version: 4.1.0 resolution: "d3-time-format@npm:4.1.0" @@ -8831,15 +10400,6 @@ __metadata: languageName: node linkType: hard -"d3-time@npm:1 - 2, d3-time@npm:2, d3-time@npm:^2.1.1": - version: 2.1.1 - resolution: "d3-time@npm:2.1.1" - dependencies: - d3-array: "npm:2" - checksum: 10c0/4a01770a857bc37d2bafb8f00250e0e6a1fcc8051aea93e5eed168d8ee93e92da508a75ab5e42fc5472aa37e2a83aac68afaf3f12d9167c184ce781faadf5682 - languageName: node - linkType: hard - "d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:3": version: 3.1.0 resolution: "d3-time@npm:3.1.0" @@ -8849,13 +10409,6 @@ __metadata: languageName: node linkType: hard -"d3-timer@npm:1 - 2, d3-timer@npm:2": - version: 2.0.0 - resolution: "d3-timer@npm:2.0.0" - checksum: 10c0/95f92ed8edbd0844c023de543ebca4d6aba7f9f8b2ecdbc3d61e01e4df5e74ffbce81238a3c4fd63d118bb1d05ca6331522df565fab146a2790e5c6a847f6275 - languageName: node - linkType: hard - "d3-timer@npm:1 - 3, d3-timer@npm:3": version: 3.0.1 resolution: "d3-timer@npm:3.0.1" @@ -8863,21 +10416,6 @@ __metadata: languageName: node linkType: hard -"d3-transition@npm:2": - version: 2.0.0 - resolution: "d3-transition@npm:2.0.0" - dependencies: - d3-color: "npm:1 - 2" - d3-dispatch: "npm:1 - 2" - d3-ease: "npm:1 - 2" - d3-interpolate: "npm:1 - 2" - d3-timer: "npm:1 - 2" - peerDependencies: - d3-selection: 2 - checksum: 10c0/a775365c90acfcf5745fb18146ce0a5cb71bfdf69433f4d12cc286da0f88b3b1fbdd6782ccfe8e60de95133107cd8a47a4736fa19e94ffb08f1000e6cd9ebdb6 - languageName: node - linkType: hard - "d3-transition@npm:2 - 3, d3-transition@npm:3": version: 3.0.1 resolution: "d3-transition@npm:3.0.1" @@ -8893,19 +10431,6 @@ __metadata: languageName: node linkType: hard -"d3-zoom@npm:2": - version: 2.0.0 - resolution: "d3-zoom@npm:2.0.0" - dependencies: - d3-dispatch: "npm:1 - 2" - d3-drag: "npm:2" - d3-interpolate: "npm:1 - 2" - d3-selection: "npm:2" - d3-transition: "npm:2" - checksum: 10c0/d21bc3f5f7b92809d4bf75889ef6c91e708becc6a148148bc80847b2efdf96233c4437d96b9e1623dde33922e63d505debec1616906448780e0017a04aa7c09f - languageName: node - linkType: hard - "d3-zoom@npm:3": version: 3.0.0 resolution: "d3-zoom@npm:3.0.0" @@ -8919,44 +10444,6 @@ __metadata: languageName: node linkType: hard -"d3@npm:6.4.0": - version: 6.4.0 - resolution: "d3@npm:6.4.0" - dependencies: - d3-array: "npm:2" - d3-axis: "npm:2" - d3-brush: "npm:2" - d3-chord: "npm:2" - d3-color: "npm:2" - d3-contour: "npm:2" - d3-delaunay: "npm:5" - d3-dispatch: "npm:2" - d3-drag: "npm:2" - d3-dsv: "npm:2" - d3-ease: "npm:2" - d3-fetch: "npm:2" - d3-force: "npm:2" - d3-format: "npm:2" - d3-geo: "npm:2" - d3-hierarchy: "npm:2" - d3-interpolate: "npm:2" - d3-path: "npm:2" - d3-polygon: "npm:2" - d3-quadtree: "npm:2" - d3-random: "npm:2" - d3-scale: "npm:3" - d3-scale-chromatic: "npm:2" - d3-selection: "npm:2" - d3-shape: "npm:2" - d3-time: "npm:2" - d3-time-format: "npm:3" - d3-timer: "npm:2" - d3-transition: "npm:2" - d3-zoom: "npm:2" - checksum: 10c0/1f5e523c6004ee8610638fd51c9e68543ddcb875184f359ef0be8b6dfba5ff4b0139228e9ec1d9c0f035ef90055001897d69c451bfbe60142d4e99429769d951 - languageName: node - linkType: hard - "d3@npm:^7.4.0, d3@npm:^7.8.2": version: 7.9.0 resolution: "d3@npm:7.9.0" @@ -9078,6 +10565,20 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^3.6.0": + version: 3.6.0 + resolution: "date-fns@npm:3.6.0" + checksum: 10c0/0b5fb981590ef2f8e5a3ba6cd6d77faece0ea7f7158948f2eaae7bbb7c80a8f63ae30b01236c2923cf89bb3719c33aeb150c715ea4fe4e86e37dcf06bed42fb6 + languageName: node + linkType: hard + +"date-fns@npm:^4.1.0": + version: 4.1.0 + resolution: "date-fns@npm:4.1.0" + checksum: 10c0/b79ff32830e6b7faa009590af6ae0fb8c3fd9ffad46d930548fbb5acf473773b4712ae887e156ba91a7b3dc30591ce0f517d69fd83bd9c38650fdc03b4e0bac8 + languageName: node + linkType: hard + "date-format@npm:^4.0.14": version: 4.0.14 resolution: "date-format@npm:4.0.14" @@ -9085,6 +10586,13 @@ __metadata: languageName: node linkType: hard +"dateformat@npm:^4.6.3": + version: 4.6.3 + resolution: "dateformat@npm:4.6.3" + checksum: 10c0/e2023b905e8cfe2eb8444fb558562b524807a51cdfe712570f360f873271600b5c94aebffaf11efb285e2c072264a7cf243eadb68f3eba0f8cc85fb86cd25df6 + languageName: node + linkType: hard + "dayjs@npm:^1.11.7": version: 1.11.13 resolution: "dayjs@npm:1.11.13" @@ -9122,6 +10630,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.5, debug@npm:^4.4.0": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + "decamelize@npm:^5.0.0": version: 5.0.1 resolution: "decamelize@npm:5.0.1" @@ -9262,13 +10782,6 @@ __metadata: languageName: node linkType: hard -"delaunator@npm:4": - version: 4.0.1 - resolution: "delaunator@npm:4.0.1" - checksum: 10c0/8d5be959a4bf79e5297ca58a3dc223434302200ac0efc2cee5434755b557957a824ee32328ed97f69df93d3819e063f3b4637dd6db4d14d50aa8591aeb6f98a7 - languageName: node - linkType: hard - "delaunator@npm:5": version: 5.0.1 resolution: "delaunator@npm:5.0.1" @@ -9370,6 +10883,13 @@ __metadata: languageName: node linkType: hard +"diff-match-patch@npm:^1.0.5": + version: 1.0.5 + resolution: "diff-match-patch@npm:1.0.5" + checksum: 10c0/142b6fad627b9ef309d11bd935e82b84c814165a02500f046e2773f4ea894d10ed3017ac20454900d79d4a0322079f5b713cf0986aaf15fce0ec4a2479980c86 + languageName: node + linkType: hard + "diff-sequences@npm:^29.6.3": version: 29.6.3 resolution: "diff-sequences@npm:29.6.3" @@ -9523,6 +11043,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.6.1": + version: 16.6.1 + resolution: "dotenv@npm:16.6.1" + checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc + languageName: node + linkType: hard + "dotenv@npm:~10.0.0": version: 10.0.0 resolution: "dotenv@npm:10.0.0" @@ -9647,7 +11174,7 @@ __metadata: languageName: node linkType: hard -"encodeurl@npm:~2.0.0": +"encodeurl@npm:^2.0.0, encodeurl@npm:~2.0.0": version: 2.0.0 resolution: "encodeurl@npm:2.0.0" checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb @@ -9675,6 +11202,15 @@ __metadata: languageName: node linkType: hard +"end-of-stream@npm:^1.1.0": + version: 1.4.5 + resolution: "end-of-stream@npm:1.4.5" + dependencies: + once: "npm:^1.4.0" + checksum: 10c0/b0701c92a10b89afb1cb45bf54a5292c6f008d744eb4382fa559d54775ff31617d1d7bc3ef617575f552e24fad2c7c1a1835948c66b3f3a4be0a6c1f35c883d8 + languageName: node + linkType: hard + "end-of-stream@npm:^1.4.1": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" @@ -10159,6 +11695,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 10c0/6366f474c6f37a802800a435232395e04e9885919873e382b157ab7e8f0feb8fed71497f84a6f6a81a49aab41815522f5839112bd38026d203aea0c91622df95 + languageName: node + linkType: hard + "escodegen@npm:^2.0.0": version: 2.1.0 resolution: "escodegen@npm:2.1.0" @@ -10520,7 +12063,7 @@ __metadata: languageName: node linkType: hard -"etag@npm:~1.8.1": +"etag@npm:^1.8.1, etag@npm:~1.8.1": version: 1.8.1 resolution: "etag@npm:1.8.1" checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84 @@ -10558,10 +12101,26 @@ __metadata: languageName: node linkType: hard -"events@npm:^3.2.0": - version: 3.3.0 - resolution: "events@npm:3.3.0" - checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 +"events@npm:^3.2.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 + languageName: node + linkType: hard + +"eventsource-parser@npm:^3.0.0, eventsource-parser@npm:^3.0.1, eventsource-parser@npm:^3.0.5": + version: 3.0.6 + resolution: "eventsource-parser@npm:3.0.6" + checksum: 10c0/70b8ccec7dac767ef2eca43f355e0979e70415701691382a042a2df8d6a68da6c2fca35363669821f3da876d29c02abe9b232964637c1b6635c940df05ada78a + languageName: node + linkType: hard + +"eventsource@npm:^3.0.2": + version: 3.0.7 + resolution: "eventsource@npm:3.0.7" + dependencies: + eventsource-parser: "npm:^3.0.1" + checksum: 10c0/c48a73c38f300e33e9f11375d4ee969f25cbb0519608a12378a38068055ae8b55b6e0e8a49c3f91c784068434efe1d9f01eb49b6315b04b0da9157879ce2f67d languageName: node linkType: hard @@ -10582,6 +12141,13 @@ __metadata: languageName: node linkType: hard +"exit-hook@npm:^4.0.0": + version: 4.0.0 + resolution: "exit-hook@npm:4.0.0" + checksum: 10c0/7fb33eaeb9050aee9479da9c93d42b796fb409c40e1d2b6ea2f40786ae7d7db6dc6a0f6ecc7bc24e479f957b7844bcb880044ded73320334743c64e3ecef48d7 + languageName: node + linkType: hard + "expand-tilde@npm:^2.0.0, expand-tilde@npm:^2.0.2": version: 2.0.2 resolution: "expand-tilde@npm:2.0.2" @@ -10598,6 +12164,15 @@ __metadata: languageName: node linkType: hard +"express-rate-limit@npm:^7.5.0": + version: 7.5.1 + resolution: "express-rate-limit@npm:7.5.1" + peerDependencies: + express: ">= 4.11" + checksum: 10c0/b07de84d700a2c07c4bf2f040e7558ed5a1f660f03ed5f30bf8ff7b51e98ba7a85215640e70fc48cbbb9151066ea51239d9a1b41febc9b84d98c7915b0186161 + languageName: node + linkType: hard + "express@npm:^4.17.3, express@npm:^4.19.2": version: 4.21.1 resolution: "express@npm:4.21.1" @@ -10637,6 +12212,80 @@ __metadata: languageName: node linkType: hard +"express@npm:^4.21.2": + version: 4.21.2 + resolution: "express@npm:4.21.2" + dependencies: + accepts: "npm:~1.3.8" + array-flatten: "npm:1.1.1" + body-parser: "npm:1.20.3" + content-disposition: "npm:0.5.4" + content-type: "npm:~1.0.4" + cookie: "npm:0.7.1" + cookie-signature: "npm:1.0.6" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + finalhandler: "npm:1.3.1" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + merge-descriptors: "npm:1.0.3" + methods: "npm:~1.1.2" + on-finished: "npm:2.4.1" + parseurl: "npm:~1.3.3" + path-to-regexp: "npm:0.1.12" + proxy-addr: "npm:~2.0.7" + qs: "npm:6.13.0" + range-parser: "npm:~1.2.1" + safe-buffer: "npm:5.2.1" + send: "npm:0.19.0" + serve-static: "npm:1.16.2" + setprototypeof: "npm:1.2.0" + statuses: "npm:2.0.1" + type-is: "npm:~1.6.18" + utils-merge: "npm:1.0.1" + vary: "npm:~1.1.2" + checksum: 10c0/38168fd0a32756600b56e6214afecf4fc79ec28eca7f7a91c2ab8d50df4f47562ca3f9dee412da7f5cea6b1a1544b33b40f9f8586dbacfbdada0fe90dbb10a1f + languageName: node + linkType: hard + +"express@npm:^5.0.1": + version: 5.1.0 + resolution: "express@npm:5.1.0" + dependencies: + accepts: "npm:^2.0.0" + body-parser: "npm:^2.2.0" + content-disposition: "npm:^1.0.0" + content-type: "npm:^1.0.5" + cookie: "npm:^0.7.1" + cookie-signature: "npm:^1.2.1" + debug: "npm:^4.4.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + etag: "npm:^1.8.1" + finalhandler: "npm:^2.1.0" + fresh: "npm:^2.0.0" + http-errors: "npm:^2.0.0" + merge-descriptors: "npm:^2.0.0" + mime-types: "npm:^3.0.0" + on-finished: "npm:^2.4.1" + once: "npm:^1.4.0" + parseurl: "npm:^1.3.3" + proxy-addr: "npm:^2.0.7" + qs: "npm:^6.14.0" + range-parser: "npm:^1.2.1" + router: "npm:^2.2.0" + send: "npm:^1.1.0" + serve-static: "npm:^2.2.0" + statuses: "npm:^2.0.1" + type-is: "npm:^2.0.1" + vary: "npm:^1.1.2" + checksum: 10c0/80ce7c53c5f56887d759b94c3f2283e2e51066c98d4b72a4cc1338e832b77f1e54f30d0239cc10815a0f849bdb753e6a284d2fa48d4ab56faf9c501f55d751d6 + languageName: node + linkType: hard + "ext@npm:^1.7.0": version: 1.7.0 resolution: "ext@npm:1.7.0" @@ -10664,6 +12313,13 @@ __metadata: languageName: node linkType: hard +"fast-copy@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-copy@npm:3.0.2" + checksum: 10c0/02e8b9fd03c8c024d2987760ce126456a0e17470850b51e11a1c3254eed6832e4733ded2d93316c82bc0b36aeb991ad1ff48d1ba95effe7add7c3ab8d8eb554a + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -10772,6 +12428,13 @@ __metadata: languageName: node linkType: hard +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10c0/d90ec1c963394919828872f21edaa3ad6f1dddd288d2bd4e977027afff09f5db40f94e39536d4646f7e01761d704d72d51dce5af1b93717f3489ef808f5f4e4d + languageName: node + linkType: hard + "fast-uri@npm:^3.0.1": version: 3.0.3 resolution: "fast-uri@npm:3.0.3" @@ -10877,6 +12540,20 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:^2.1.0": + version: 2.1.0 + resolution: "finalhandler@npm:2.1.0" + dependencies: + debug: "npm:^4.4.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + on-finished: "npm:^2.4.1" + parseurl: "npm:^1.3.3" + statuses: "npm:^2.0.1" + checksum: 10c0/da0bbca6d03873472ee890564eb2183f4ed377f25f3628a0fc9d16dac40bed7b150a0d82ebb77356e4c6d97d2796ad2dba22948b951dddee2c8768b0d1b9fb1f + languageName: node + linkType: hard + "find-cache-dir@npm:^3.3.2": version: 3.3.2 resolution: "find-cache-dir@npm:3.3.2" @@ -11055,6 +12732,13 @@ __metadata: languageName: node linkType: hard +"forwarded-parse@npm:2.1.2": + version: 2.1.2 + resolution: "forwarded-parse@npm:2.1.2" + checksum: 10c0/0c6b4c631775f272b4475e935108635495e8a5b261d1b4a5caef31c47c5a0b04134adc564e655aadfef366a02647fa3ae90a1d3ac19929f3ade47f9bed53036a + languageName: node + linkType: hard + "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -11076,6 +12760,13 @@ __metadata: languageName: node linkType: hard +"fresh@npm:^2.0.0": + version: 2.0.0 + resolution: "fresh@npm:2.0.0" + checksum: 10c0/0557548194cb9a809a435bf92bcfbc20c89e8b5eb38861b73ced36750437251e39a111fc3a18b98531be9dd91fe1411e4969f229dc579ec0251ce6c5d4900bbc + languageName: node + linkType: hard + "front-matter@npm:^4.0.2": version: 4.0.2 resolution: "front-matter@npm:4.0.2" @@ -11260,6 +12951,30 @@ __metadata: languageName: node linkType: hard +"gaxios@npm:^6.1.1": + version: 6.7.1 + resolution: "gaxios@npm:6.7.1" + dependencies: + extend: "npm:^3.0.2" + https-proxy-agent: "npm:^7.0.1" + is-stream: "npm:^2.0.0" + node-fetch: "npm:^2.6.9" + uuid: "npm:^9.0.1" + checksum: 10c0/53e92088470661c5bc493a1de29d05aff58b1f0009ec5e7903f730f892c3642a93e264e61904383741ccbab1ce6e519f12a985bba91e13527678b32ee6d7d3fd + languageName: node + linkType: hard + +"gcp-metadata@npm:^6.0.0": + version: 6.1.1 + resolution: "gcp-metadata@npm:6.1.1" + dependencies: + gaxios: "npm:^6.1.1" + google-logging-utils: "npm:^0.0.2" + json-bigint: "npm:^1.0.0" + checksum: 10c0/71f6ad4800aa622c246ceec3955014c0c78cdcfe025971f9558b9379f4019f5e65772763428ee8c3244fa81b8631977316eaa71a823493f82e5c44d7259ffac8 + languageName: node + linkType: hard + "generator-function@npm:^2.0.0": version: 2.0.1 resolution: "generator-function@npm:2.0.1" @@ -11294,7 +13009,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.6": +"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": version: 1.3.1 resolution: "get-intrinsic@npm:1.3.1" dependencies: @@ -11560,6 +13275,13 @@ __metadata: languageName: node linkType: hard +"google-logging-utils@npm:^0.0.2": + version: 0.0.2 + resolution: "google-logging-utils@npm:0.0.2" + checksum: 10c0/9a4bbd470dd101c77405e450fffca8592d1d7114f245a121288d04a957aca08c9dea2dd1a871effe71e41540d1bb0494731a0b0f6fea4358e77f06645e4268c1 + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -11615,6 +13337,7 @@ __metadata: resolution: "gui@workspace:." dependencies: "@abacritt/angularx-social-login": "npm:2.3.0" + "@ai-sdk/openai": "npm:2.0.52" "@ali-hm/angular-tree-component": "npm:12.0.5" "@angular-builders/custom-webpack": "npm:16.0.1" "@angular-devkit/build-angular": "npm:16.2.12" @@ -11639,6 +13362,8 @@ __metadata: "@codingame/monaco-vscode-r-default-extension": "npm:8.0.4" "@loaders.gl/core": "npm:3.4.2" "@luma.gl/core": "npm:8.5.20" + "@mastra/core": "npm:0.21.1" + "@mastra/mcp": "npm:0.13.5" "@ngneat/until-destroy": "npm:8.1.4" "@ngx-formly/core": "npm:6.3.12" "@ngx-formly/ng-zorro-antd": "npm:6.3.12" @@ -11647,9 +13372,7 @@ __metadata: "@nx/angular": "npm:20.0.3" "@stoplight/json-ref-resolver": "npm:3.1.5" "@types/backbone": "npm:1.4.15" - "@types/concaveman": "npm:1.1.6" "@types/content-disposition": "npm:0" - "@types/d3-shape": "npm:2.1.2" "@types/dagre": "npm:0.7.47" "@types/file-saver": "npm:2.0.5" "@types/graphlib": "npm:2.1.8" @@ -11663,15 +13386,14 @@ __metadata: "@types/quill": "npm:2.0.9" "@types/uuid": "npm:8.3.4" "@types/validator": "npm:13.12.0" + "@types/ws": "npm:^8.18.1" "@typescript-eslint/eslint-plugin": "npm:7.0.2" "@typescript-eslint/parser": "npm:7.0.2" ajv: "npm:8.10.0" babel-plugin-dynamic-import-node: "npm:2.3.3" backbone: "npm:1.4.1" - concaveman: "npm:2.0.0" concurrently: "npm:7.4.0" content-disposition: "npm:0.5.4" - d3: "npm:6.4.0" dagre: "npm:0.8.5" deep-map: "npm:2.0.0" edit-distance: "npm:1.0.4" @@ -11873,6 +13595,13 @@ __metadata: languageName: node linkType: hard +"help-me@npm:^5.0.0": + version: 5.0.0 + resolution: "help-me@npm:5.0.0" + checksum: 10c0/054c0e2e9ae2231c85ab5e04f75109b9d068ffcc54e58fb22079822a5ace8ff3d02c66fd45379c902ad5ab825e5d2e1451fcc2f7eab1eb49e7d488133ba4cacb + languageName: node + linkType: hard + "homedir-polyfill@npm:^1.0.1": version: 1.0.3 resolution: "homedir-polyfill@npm:1.0.3" @@ -11882,6 +13611,64 @@ __metadata: languageName: node linkType: hard +"hono-openapi@npm:^0.4.8": + version: 0.4.8 + resolution: "hono-openapi@npm:0.4.8" + dependencies: + json-schema-walker: "npm:^2.0.0" + peerDependencies: + "@hono/arktype-validator": ^2.0.0 + "@hono/effect-validator": ^1.2.0 + "@hono/typebox-validator": ^0.2.0 || ^0.3.0 + "@hono/valibot-validator": ^0.5.1 + "@hono/zod-validator": ^0.4.1 + "@sinclair/typebox": ^0.34.9 + "@valibot/to-json-schema": ^1.0.0-beta.3 + arktype: ^2.0.0 + effect: ^3.11.3 + hono: ^4.6.13 + openapi-types: ^12.1.3 + valibot: ^1.0.0-beta.9 + zod: ^3.23.8 + zod-openapi: ^4.0.0 + peerDependenciesMeta: + "@hono/arktype-validator": + optional: true + "@hono/effect-validator": + optional: true + "@hono/typebox-validator": + optional: true + "@hono/valibot-validator": + optional: true + "@hono/zod-validator": + optional: true + "@sinclair/typebox": + optional: true + "@valibot/to-json-schema": + optional: true + arktype: + optional: true + effect: + optional: true + hono: + optional: true + valibot: + optional: true + zod: + optional: true + zod-openapi: + optional: true + checksum: 10c0/e1304dc2b6e016a59ae163d7c8f372c2fa0179f4a5614d5b6499f284deb4325ab0f7d562163b28fb8174de03ef0b1c02afc8b1e26c6a86b2c41bc1842ae92baf + languageName: node + linkType: hard + +"hono@npm:^4.9.7": + version: 4.10.1 + resolution: "hono@npm:4.10.1" + checksum: 10c0/a66e2791f62db07f1b24897a0f6848f5457101be12fd1a0e317b397b487ef2afa7ac6c01373347f3795ccbe09c68cd3fc7d00a43f75cc3a1e0fb6e85bec2e821 + languageName: node + linkType: hard + "hosted-git-info@npm:^6.0.0": version: 6.1.1 resolution: "hosted-git-info@npm:6.1.1" @@ -11983,7 +13770,7 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:2.0.0": +"http-errors@npm:2.0.0, http-errors@npm:^2.0.0": version: 2.0.0 resolution: "http-errors@npm:2.0.0" dependencies: @@ -12169,7 +13956,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.4, iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": +"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" dependencies: @@ -12187,6 +13974,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:0.7.0": + version: 0.7.0 + resolution: "iconv-lite@npm:0.7.0" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f + languageName: node + linkType: hard + "icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0": version: 5.1.0 resolution: "icss-utils@npm:5.1.0" @@ -12266,6 +14062,18 @@ __metadata: languageName: node linkType: hard +"import-in-the-middle@npm:^1.8.1": + version: 1.15.0 + resolution: "import-in-the-middle@npm:1.15.0" + dependencies: + acorn: "npm:^8.14.0" + acorn-import-attributes: "npm:^1.9.5" + cjs-module-lexer: "npm:^1.2.2" + module-details-from-path: "npm:^1.0.3" + checksum: 10c0/43d4efbe75a89c04343fd052ca5d2193adc0e2df93325e50d8b32c31403b2f089a5e2b6e47f4e5413bc4058b9781aaaf61bfe3f0e5e6d7f9487eb112fd095e0d + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -12373,13 +14181,6 @@ __metadata: languageName: node linkType: hard -"internmap@npm:^1.0.0": - version: 1.0.1 - resolution: "internmap@npm:1.0.1" - checksum: 10c0/60942be815ca19da643b6d4f23bd0bf4e8c97abbd080fb963fe67583b60bdfb3530448ad4486bae40810e92317bded9995cc31411218acc750d72cd4e8646eee - languageName: node - linkType: hard - "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -12484,6 +14285,15 @@ __metadata: languageName: node linkType: hard +"is-core-module@npm:^2.16.0": + version: 2.16.1 + resolution: "is-core-module@npm:2.16.1" + dependencies: + hasown: "npm:^2.0.2" + checksum: 10c0/898443c14780a577e807618aaae2b6f745c8538eca5c7bc11388a3f2dc6de82b9902bcc7eb74f07be672b11bbe82dd6a6edded44a00cb3d8f933d0459905eedd + languageName: node + linkType: hard + "is-data-view@npm:^1.0.1": version: 1.0.1 resolution: "is-data-view@npm:1.0.1" @@ -12591,6 +14401,13 @@ __metadata: languageName: node linkType: hard +"is-network-error@npm:^1.1.0": + version: 1.3.0 + resolution: "is-network-error@npm:1.3.0" + checksum: 10c0/3e85a69e957988db66d5af5412efdd531a5a63e150d1bdd5647cfd4dc54fd89b1dbdd472621f8915233c3176ba1e6922afa8a51a9e363ba4693edf96a294f898 + languageName: node + linkType: hard + "is-number-object@npm:^1.0.4": version: 1.0.7 resolution: "is-number-object@npm:1.0.7" @@ -12644,6 +14461,13 @@ __metadata: languageName: node linkType: hard +"is-promise@npm:^4.0.0": + version: 4.0.0 + resolution: "is-promise@npm:4.0.0" + checksum: 10c0/ebd5c672d73db781ab33ccb155fb9969d6028e37414d609b115cc534654c91ccd061821d5b987eefaa97cf4c62f0b909bb2f04db88306de26e91bfe8ddc01503 + languageName: node + linkType: hard + "is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -12951,6 +14775,13 @@ __metadata: languageName: node linkType: hard +"joycon@npm:^3.1.1": + version: 3.1.1 + resolution: "joycon@npm:3.1.1" + checksum: 10c0/131fb1e98c9065d067fd49b6e685487ac4ad4d254191d7aa2c9e3b90f4e9ca70430c43cad001602bdbdabcf58717d3b5c5b7461c1bd8e39478c8de706b3fe6ae + languageName: node + linkType: hard + "jquery@npm:~3.6.0": version: 3.6.4 resolution: "jquery@npm:3.6.4" @@ -12965,6 +14796,15 @@ __metadata: languageName: node linkType: hard +"js-tiktoken@npm:^1.0.20": + version: 1.0.21 + resolution: "js-tiktoken@npm:1.0.21" + dependencies: + base64-js: "npm:^1.5.1" + checksum: 10c0/49a0b1e8915d271b84b5c2217fcab37de3124cfbba7e9901d3d9afb1acdd931f0ff7025ed94587dd1b59ae5c04cc19949c2869823854ee7f2f3f81678db276f3 + languageName: node + linkType: hard + "js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -13074,6 +14914,15 @@ __metadata: languageName: node linkType: hard +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: "npm:^9.0.0" + checksum: 10c0/e3f34e43be3284b573ea150a3890c92f06d54d8ded72894556357946aeed9877fd795f62f37fe16509af189fd314ab1104d0fd0f163746ad231b9f378f5b33f4 + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -13095,6 +14944,15 @@ __metadata: languageName: node linkType: hard +"json-schema-to-zod@npm:^2.6.1": + version: 2.6.1 + resolution: "json-schema-to-zod@npm:2.6.1" + bin: + json-schema-to-zod: dist/cjs/cli.js + checksum: 10c0/4edbb6de157b463407d935c53a5b0b24e2c547dc44495cb514e946e0022385a1a18db97b2714a140431e34f56c8090d373612812ccb7c45312368ca9a36ed341 + languageName: node + linkType: hard + "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -13109,6 +14967,23 @@ __metadata: languageName: node linkType: hard +"json-schema-walker@npm:^2.0.0": + version: 2.0.0 + resolution: "json-schema-walker@npm:2.0.0" + dependencies: + "@apidevtools/json-schema-ref-parser": "npm:^11.1.0" + clone: "npm:^2.1.2" + checksum: 10c0/be257826bbaa615349dbf6594826403938c50a9906e52407fcd8191b4f31cfcf22310efbd762382e8c9cbdcc6a75d4f3f9348c3bd6763a7b0eb4f70009b92f6d + languageName: node + linkType: hard + +"json-schema@npm:^0.4.0": + version: 0.4.0 + resolution: "json-schema@npm:0.4.0" + checksum: 10c0/d4a637ec1d83544857c1c163232f3da46912e971d5bf054ba44fdb88f07d8d359a462b4aec46f2745efbc57053365608d88bc1d7b1729f7b4fc3369765639ed3 + languageName: node + linkType: hard + "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -13150,6 +15025,19 @@ __metadata: languageName: node linkType: hard +"jsondiffpatch@npm:0.6.0": + version: 0.6.0 + resolution: "jsondiffpatch@npm:0.6.0" + dependencies: + "@types/diff-match-patch": "npm:^1.0.36" + chalk: "npm:^5.3.0" + diff-match-patch: "npm:^1.0.5" + bin: + jsondiffpatch: bin/jsondiffpatch.js + checksum: 10c0/f7822e48a8ef8b9f7c6024cc59b7d3707a9fe6d84fd776d169de5a1803ad551ffe7cfdc7587f3900f224bc70897355884ed43eb1c8ccd02e7f7b43a7ebcfed4f + languageName: node + linkType: hard + "jsonfile@npm:^4.0.0": version: 4.0.0 resolution: "jsonfile@npm:4.0.0" @@ -13676,6 +15564,13 @@ __metadata: languageName: node linkType: hard +"lodash.camelcase@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.camelcase@npm:4.3.0" + checksum: 10c0/fcba15d21a458076dd309fce6b1b4bf611d84a0ec252cb92447c948c533ac250b95d2e00955801ebc367e5af5ed288b996d75d37d2035260a937008e14eaf432 + languageName: node + linkType: hard + "lodash.clonedeep@npm:^4.5.0": version: 4.5.0 resolution: "lodash.clonedeep@npm:4.5.0" @@ -13779,6 +15674,13 @@ __metadata: languageName: node linkType: hard +"long@npm:^5.0.0": + version: 5.3.2 + resolution: "long@npm:5.3.2" + checksum: 10c0/7130fe1cbce2dca06734b35b70d380ca3f70271c7f8852c922a7c62c86c4e35f0c39290565eca7133c625908d40e126ac57c02b1b1a4636b9457d77e1e60b981 + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -13987,6 +15889,13 @@ __metadata: languageName: node linkType: hard +"media-typer@npm:^1.1.0": + version: 1.1.0 + resolution: "media-typer@npm:1.1.0" + checksum: 10c0/7b4baa40b25964bb90e2121ee489ec38642127e48d0cc2b6baa442688d3fde6262bfdca86d6bbf6ba708784afcac168c06840c71facac70e390f5f759ac121b9 + languageName: node + linkType: hard + "memfs@npm:^3.4.1, memfs@npm:^3.4.12, memfs@npm:^3.4.3": version: 3.5.3 resolution: "memfs@npm:3.5.3" @@ -14015,6 +15924,13 @@ __metadata: languageName: node linkType: hard +"merge-descriptors@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-descriptors@npm:2.0.0" + checksum: 10c0/95389b7ced3f9b36fbdcf32eb946dc3dd1774c2fdf164609e55b18d03aa499b12bd3aae3a76c1c7185b96279e9803525550d3eb292b5224866060a288f335cb3 + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -14084,6 +16000,13 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:^1.54.0": + version: 1.54.0 + resolution: "mime-db@npm:1.54.0" + checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284 + languageName: node + linkType: hard + "mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -14093,6 +16016,15 @@ __metadata: languageName: node linkType: hard +"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1": + version: 3.0.1 + resolution: "mime-types@npm:3.0.1" + dependencies: + mime-db: "npm:^1.54.0" + checksum: 10c0/bd8c20d3694548089cf229016124f8f40e6a60bbb600161ae13e45f793a2d5bb40f96bbc61f275836696179c77c1d6bf4967b2a75e0a8ad40fe31f4ed5be4da5 + languageName: node + linkType: hard + "mime@npm:1.6.0, mime@npm:^1.4.1, mime@npm:^1.6.0": version: 1.6.0 resolution: "mime@npm:1.6.0" @@ -14344,6 +16276,13 @@ __metadata: languageName: node linkType: hard +"module-details-from-path@npm:^1.0.3": + version: 1.0.4 + resolution: "module-details-from-path@npm:1.0.4" + checksum: 10c0/10863413e96dab07dee917eae07afe46f7bf853065cc75a7d2a718adf67574857fb64f8a2c0c9af12ac733a9a8cf652db7ed39b95f7a355d08106cb9cc50c83b + languageName: node + linkType: hard + "monaco-breakpoints@npm:0.2.0": version: 0.2.0 resolution: "monaco-breakpoints@npm:0.2.0" @@ -14485,6 +16424,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.8": + version: 3.3.11 + resolution: "nanoid@npm:3.3.11" + bin: + nanoid: bin/nanoid.cjs + checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b + languageName: node + linkType: hard + "napi-macros@npm:~2.0.0": version: 2.0.0 resolution: "napi-macros@npm:2.0.0" @@ -14525,6 +16473,13 @@ __metadata: languageName: node linkType: hard +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b + languageName: node + linkType: hard + "neo-async@npm:^2.6.2": version: 2.6.2 resolution: "neo-async@npm:2.6.2" @@ -14685,6 +16640,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.6.9": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 + languageName: node + linkType: hard + "node-forge@npm:^1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" @@ -15182,6 +17151,13 @@ __metadata: languageName: node linkType: hard +"object-inspect@npm:^1.13.3": + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692 + languageName: node + linkType: hard + "object-is@npm:^1.1.5": version: 1.1.6 resolution: "object-is@npm:1.1.6" @@ -15259,6 +17235,13 @@ __metadata: languageName: node linkType: hard +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 10c0/faea2e1c9d696ecee919026c32be8d6a633a7ac1240b3b87e944a380e8a11dc9c95c4a1f8fb0568de7ab8db3823e790f12bda45296b1d111e341aad3922a0570 + languageName: node + linkType: hard + "on-finished@npm:2.4.1, on-finished@npm:^2.3.0, on-finished@npm:^2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -15284,7 +17267,7 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0, once@npm:^1.4.0": +"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -15458,6 +17441,13 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^7.0.3": + version: 7.0.3 + resolution: "p-map@npm:7.0.3" + checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c + languageName: node + linkType: hard + "p-retry@npm:^4.5.0": version: 4.6.2 resolution: "p-retry@npm:4.6.2" @@ -15479,6 +17469,15 @@ __metadata: languageName: node linkType: hard +"p-retry@npm:^7.1.0": + version: 7.1.0 + resolution: "p-retry@npm:7.1.0" + dependencies: + is-network-error: "npm:^1.1.0" + checksum: 10c0/fe756dac411bd104901b383f6b95214f453a5f5aa4e289d3642b937c62cf4fcfc77f6276b570388a686b2923af6d43e71b29c3b2abe062cb7070176d00627081 + languageName: node + linkType: hard + "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -15620,7 +17619,7 @@ __metadata: languageName: node linkType: hard -"parseurl@npm:^1.3.2, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": +"parseurl@npm:^1.3.2, parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 @@ -15686,6 +17685,20 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:0.1.12": + version: 0.1.12 + resolution: "path-to-regexp@npm:0.1.12" + checksum: 10c0/1c6ff10ca169b773f3bba943bbc6a07182e332464704572962d277b900aeee81ac6aa5d060ff9e01149636c30b1f63af6e69dd7786ba6e0ddb39d4dee1f0645b + languageName: node + linkType: hard + +"path-to-regexp@npm:^8.0.0": + version: 8.3.0 + resolution: "path-to-regexp@npm:8.3.0" + checksum: 10c0/ee1544a73a3f294a97a4c663b0ce71bbf1621d732d80c9c9ed201b3e911a86cb628ebad691b9d40f40a3742fe22011e5a059d8eed2cf63ec2cb94f6fb4efe67c + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -15708,6 +17721,33 @@ __metadata: languageName: node linkType: hard +"pg-int8@npm:1.0.1": + version: 1.0.1 + resolution: "pg-int8@npm:1.0.1" + checksum: 10c0/be6a02d851fc2a4ae3e9de81710d861de3ba35ac927268973eb3cb618873a05b9424656df464dd43bd7dc3fc5295c3f5b3c8349494f87c7af50ec59ef14e0b98 + languageName: node + linkType: hard + +"pg-protocol@npm:*": + version: 1.10.3 + resolution: "pg-protocol@npm:1.10.3" + checksum: 10c0/f7ef54708c93ee6d271e37678296fc5097e4337fca91a88a3d99359b78633dbdbf6e983f0adb34b7cdd261b7ec7266deb20c3233bf3dfdb498b3e1098e8750b9 + languageName: node + linkType: hard + +"pg-types@npm:^2.2.0": + version: 2.2.0 + resolution: "pg-types@npm:2.2.0" + dependencies: + pg-int8: "npm:1.0.1" + postgres-array: "npm:~2.0.0" + postgres-bytea: "npm:~1.0.0" + postgres-date: "npm:~1.0.4" + postgres-interval: "npm:^1.1.0" + checksum: 10c0/ab3f8069a323f601cd2d2279ca8c425447dab3f9b61d933b0601d7ffc00d6200df25e26a4290b2b0783b59278198f7dd2ed03e94c4875797919605116a577c65 + languageName: node + linkType: hard + "picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -15736,6 +17776,66 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:^2.0.0": + version: 2.0.0 + resolution: "pino-abstract-transport@npm:2.0.0" + dependencies: + split2: "npm:^4.0.0" + checksum: 10c0/02c05b8f2ffce0d7c774c8e588f61e8b77de8ccb5f8125afd4a7325c9ea0e6af7fb78168999657712ae843e4462bb70ac550dfd6284f930ee57f17f486f25a9f + languageName: node + linkType: hard + +"pino-pretty@npm:^13.0.0": + version: 13.1.2 + resolution: "pino-pretty@npm:13.1.2" + dependencies: + colorette: "npm:^2.0.7" + dateformat: "npm:^4.6.3" + fast-copy: "npm:^3.0.2" + fast-safe-stringify: "npm:^2.1.1" + help-me: "npm:^5.0.0" + joycon: "npm:^3.1.1" + minimist: "npm:^1.2.6" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^2.0.0" + pump: "npm:^3.0.0" + secure-json-parse: "npm:^4.0.0" + sonic-boom: "npm:^4.0.1" + strip-json-comments: "npm:^5.0.2" + bin: + pino-pretty: bin.js + checksum: 10c0/4d8e7472e37bdb6e0d6d7d34f25f65ced46c0f64a9579bb805602321caf1c0b10359f89a1ee9742bea875f411a02ce7c19730f7a1e5387dfcfd10ff5c9804709 + languageName: node + linkType: hard + +"pino-std-serializers@npm:^7.0.0": + version: 7.0.0 + resolution: "pino-std-serializers@npm:7.0.0" + checksum: 10c0/73e694d542e8de94445a03a98396cf383306de41fd75ecc07085d57ed7a57896198508a0dec6eefad8d701044af21eb27253ccc352586a03cf0d4a0bd25b4133 + languageName: node + linkType: hard + +"pino@npm:^9.7.0": + version: 9.13.1 + resolution: "pino@npm:9.13.1" + dependencies: + atomic-sleep: "npm:^1.0.0" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^2.0.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^5.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + slow-redact: "npm:^0.3.0" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" + bin: + pino: bin.js + checksum: 10c0/c99e879f9538f7255488ad276a46a857cf9114217b754b850b7f1441e31b724a6d6f0697228ead954d3d9601522704e03cad5d441c228108073eed2f37ea0e41 + languageName: node + linkType: hard + "piscina@npm:4.0.0": version: 4.0.0 resolution: "piscina@npm:4.0.0" @@ -15763,6 +17863,13 @@ __metadata: languageName: node linkType: hard +"pkce-challenge@npm:^5.0.0": + version: 5.0.0 + resolution: "pkce-challenge@npm:5.0.0" + checksum: 10c0/c6706d627fdbb6f22bf8cc5d60d96d6b6a7bb481399b336a3d3f4e9bfba3e167a2c32f8ec0b5e74be686a0ba3bcc9894865d4c2dd1b91cea4c05dba1f28602c3 + languageName: node + linkType: hard + "pkg-dir@npm:^4.1.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" @@ -15788,13 +17895,6 @@ __metadata: languageName: node linkType: hard -"point-in-polygon@npm:^1.1.0": - version: 1.1.0 - resolution: "point-in-polygon@npm:1.1.0" - checksum: 10c0/de00419585ee25555d97585b7a23eeb2464a87ef29404264bee55654ca2ecab5a5a99d33e689c07d045faf80091e838f44a1fd130bdd6134493df53114947343 - languageName: node - linkType: hard - "popper.js@npm:1.16.1": version: 1.16.1 resolution: "popper.js@npm:1.16.1" @@ -16248,6 +18348,36 @@ __metadata: languageName: node linkType: hard +"postgres-array@npm:~2.0.0": + version: 2.0.0 + resolution: "postgres-array@npm:2.0.0" + checksum: 10c0/cbd56207e4141d7fbf08c86f2aebf21fa7064943d3f808ec85f442ff94b48d891e7a144cc02665fb2de5dbcb9b8e3183a2ac749959e794b4a4cfd379d7a21d08 + languageName: node + linkType: hard + +"postgres-bytea@npm:~1.0.0": + version: 1.0.0 + resolution: "postgres-bytea@npm:1.0.0" + checksum: 10c0/febf2364b8a8953695cac159eeb94542ead5886792a9627b97e33f6b5bb6e263bc0706ab47ec221516e79fbd6b2452d668841830fb3b49ec6c0fc29be61892ce + languageName: node + linkType: hard + +"postgres-date@npm:~1.0.4": + version: 1.0.7 + resolution: "postgres-date@npm:1.0.7" + checksum: 10c0/0ff91fccc64003e10b767fcfeefb5eaffbc522c93aa65d5051c49b3c4ce6cb93ab091a7d22877a90ad60b8874202c6f1d0f935f38a7235ed3b258efd54b97ca9 + languageName: node + linkType: hard + +"postgres-interval@npm:^1.1.0": + version: 1.2.0 + resolution: "postgres-interval@npm:1.2.0" + dependencies: + xtend: "npm:^4.0.0" + checksum: 10c0/c1734c3cb79e7f22579af0b268a463b1fa1d084e742a02a7a290c4f041e349456f3bee3b4ee0bb3f226828597f7b76deb615c1b857db9a742c45520100456272 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -16361,6 +18491,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^5.0.0": + version: 5.0.0 + resolution: "process-warning@npm:5.0.0" + checksum: 10c0/941f48863d368ec161e0b5890ba0c6af94170078f3d6b5e915c19b36fb59edb0dc2f8e834d25e0d375a8bf368a49d490f080508842168832b93489d17843ec29 + languageName: node + linkType: hard + "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -16388,7 +18525,27 @@ __metadata: languageName: node linkType: hard -"proxy-addr@npm:~2.0.7": +"protobufjs@npm:^7.3.0, protobufjs@npm:^7.5.3": + version: 7.5.4 + resolution: "protobufjs@npm:7.5.4" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.4" + "@protobufjs/eventemitter": "npm:^1.1.0" + "@protobufjs/fetch": "npm:^1.1.0" + "@protobufjs/float": "npm:^1.0.2" + "@protobufjs/inquire": "npm:^1.1.0" + "@protobufjs/path": "npm:^1.1.2" + "@protobufjs/pool": "npm:^1.1.0" + "@protobufjs/utf8": "npm:^1.1.0" + "@types/node": "npm:>=13.7.0" + long: "npm:^5.0.0" + checksum: 10c0/913b676109ffb3c05d3d31e03a684e569be91f3bba8613da4a683d69d9dba948daa2afd7d2e7944d1aa6c417890c35d9d9a8883c1160affafb0f9670d59ef722 + languageName: node + linkType: hard + +"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" dependencies: @@ -16419,6 +18576,16 @@ __metadata: languageName: node linkType: hard +"pump@npm:^3.0.0": + version: 3.0.3 + resolution: "pump@npm:3.0.3" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10c0/ada5cdf1d813065bbc99aa2c393b8f6beee73b5de2890a8754c9f488d7323ffd2ca5f5a0943b48934e3fcbd97637d0337369c3c631aeb9614915db629f1c75c9 + languageName: node + linkType: hard + "punycode@npm:^1.4.1": version: 1.4.1 resolution: "punycode@npm:1.4.1" @@ -16449,6 +18616,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.14.0": + version: 6.14.0 + resolution: "qs@npm:6.14.0" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10c0/8ea5d91bf34f440598ee389d4a7d95820e3b837d3fd9f433871f7924801becaa0cd3b3b4628d49a7784d06a8aea9bc4554d2b6d8d584e2d221dc06238a42909c + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -16463,6 +18639,13 @@ __metadata: languageName: node linkType: hard +"quick-format-unescaped@npm:^4.0.3": + version: 4.0.4 + resolution: "quick-format-unescaped@npm:4.0.4" + checksum: 10c0/fe5acc6f775b172ca5b4373df26f7e4fd347975578199e7d74b2ae4077f0af05baa27d231de1e80e8f72d88275ccc6028568a7a8c9ee5e7368ace0e18eff93a4 + languageName: node + linkType: hard + "quick-lru@npm:^6.1.1": version: 6.1.2 resolution: "quick-lru@npm:6.1.2" @@ -16470,13 +18653,6 @@ __metadata: languageName: node linkType: hard -"quickselect@npm:^3.0.0": - version: 3.0.0 - resolution: "quickselect@npm:3.0.0" - checksum: 10c0/3a0d33b0ec06841d953accdfd735aa3d8b7922cddd12970544a2c4b0278871280d8f5ba496803600693c1e7b7b2fb57c31d2b14d99132f478888006a1be6e6b7 - languageName: node - linkType: hard - "quill-cursors@npm:3.1.2": version: 3.1.2 resolution: "quill-cursors@npm:3.1.2" @@ -16520,6 +18696,13 @@ __metadata: languageName: node linkType: hard +"radash@npm:^12.1.1": + version: 12.1.1 + resolution: "radash@npm:12.1.1" + checksum: 10c0/f8cbe85c0a8a444f1d2892c82875e00333e58a7b8afdbf3d8d04574d0532d895484bae20650c197c87dd91abfbd1e0af1c819cd12e6dc306633acec30222af7e + languageName: node + linkType: hard + "rambda@npm:^9.1.0": version: 9.3.0 resolution: "rambda@npm:9.3.0" @@ -16555,12 +18738,15 @@ __metadata: languageName: node linkType: hard -"rbush@npm:^4.0.1": - version: 4.0.1 - resolution: "rbush@npm:4.0.1" +"raw-body@npm:^3.0.0": + version: 3.0.1 + resolution: "raw-body@npm:3.0.1" dependencies: - quickselect: "npm:^3.0.0" - checksum: 10c0/1d3c2e2c8b8111a6bcac0b36348ef55c977bc533a7a7bef44c423e24de531e43749bcf5b939008de69d97d3c6e7cf0e9040cecb4492358e6d562ea85165a1620 + bytes: "npm:3.1.2" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.7.0" + unpipe: "npm:1.0.0" + checksum: 10c0/892f4fbd21ecab7e2fed0f045f7af9e16df7e8050879639d4e482784a2f4640aaaa33d916a0e98013f23acb82e09c2e3c57f84ab97104449f728d22f65a7d79a languageName: node linkType: hard @@ -16655,6 +18841,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: 10c0/23eea5623642f0477412ef8b91acd3969015a1501ed34992ada0e3af521d3c865bb2fe4cdbfec5fe4b505f6d1ef6a03e5c3652520837a8c3b53decff7e74b6a0 + languageName: node + linkType: hard + "reflect-metadata@npm:^0.1.2": version: 0.1.14 resolution: "reflect-metadata@npm:0.1.14" @@ -16766,6 +18959,17 @@ __metadata: languageName: node linkType: hard +"require-in-the-middle@npm:^7.1.1": + version: 7.5.2 + resolution: "require-in-the-middle@npm:7.5.2" + dependencies: + debug: "npm:^4.3.5" + module-details-from-path: "npm:^1.0.3" + resolve: "npm:^1.22.8" + checksum: 10c0/43a2dac5520e39d13c413650895715e102d6802e6cc6ff322017bd948f12a9657fe28435f7cbbcba437b167f02e192ac7af29fa35cabd5d0c375d071c0605e01 + languageName: node + linkType: hard + "require-relative@npm:^0.8.7": version: 0.8.7 resolution: "require-relative@npm:0.8.7" @@ -16850,6 +19054,19 @@ __metadata: languageName: node linkType: hard +"resolve@npm:^1.22.8": + version: 1.22.10 + resolution: "resolve@npm:1.22.10" + dependencies: + is-core-module: "npm:^2.16.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/8967e1f4e2cc40f79b7e080b4582b9a8c5ee36ffb46041dccb20e6461161adf69f843b43067b4a375de926a2cd669157e29a29578191def399dd5ef89a1b5203 + languageName: node + linkType: hard + "resolve@patch:resolve@npm%3A1.22.2#optional!builtin": version: 1.22.2 resolution: "resolve@patch:resolve@npm%3A1.22.2#optional!builtin::version=1.22.2&hash=c3c19d" @@ -16876,6 +19093,19 @@ __metadata: languageName: node linkType: hard +"resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": + version: 1.22.10 + resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" + dependencies: + is-core-module: "npm:^2.16.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/52a4e505bbfc7925ac8f4cd91fd8c4e096b6a89728b9f46861d3b405ac9a1ccf4dcbf8befb4e89a2e11370dacd0160918163885cbc669369590f2f31f4c58939 + languageName: node + linkType: hard + "restore-cursor@npm:^3.1.0": version: 3.1.0 resolution: "restore-cursor@npm:3.1.0" @@ -16964,6 +19194,19 @@ __metadata: languageName: node linkType: hard +"router@npm:^2.2.0": + version: 2.2.0 + resolution: "router@npm:2.2.0" + dependencies: + debug: "npm:^4.4.0" + depd: "npm:^2.0.0" + is-promise: "npm:^4.0.0" + parseurl: "npm:^1.3.3" + path-to-regexp: "npm:^8.0.0" + checksum: 10c0/3279de7450c8eae2f6e095e9edacbdeec0abb5cb7249c6e719faa0db2dba43574b4fff5892d9220631c9abaff52dd3cad648cfea2aaace845e1a071915ac8867 + languageName: node + linkType: hard + "run-applescript@npm:^7.0.0": version: 7.0.0 resolution: "run-applescript@npm:7.0.0" @@ -17083,6 +19326,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.3.1": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: 10c0/baea14971858cadd65df23894a40588ed791769db21bafb7fd7608397dbdce9c5aac60748abae9995e0fc37e15f2061980501e012cd48859740796bea2987f49 + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -17239,6 +19489,20 @@ __metadata: languageName: node linkType: hard +"secure-json-parse@npm:^2.7.0": + version: 2.7.0 + resolution: "secure-json-parse@npm:2.7.0" + checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4 + languageName: node + linkType: hard + +"secure-json-parse@npm:^4.0.0": + version: 4.1.0 + resolution: "secure-json-parse@npm:4.1.0" + checksum: 10c0/52b3f8125ea974db1333a5b63e6a1df550c36c0d5f9a263911d6732812bd02e938b30be324dcbbb9da3ef9bf5a84849e0dd911f56544003d3c09e8eee12504de + languageName: node + linkType: hard + "select-hose@npm:^2.0.0": version: 2.0.0 resolution: "select-hose@npm:2.0.0" @@ -17322,6 +19586,25 @@ __metadata: languageName: node linkType: hard +"send@npm:^1.1.0, send@npm:^1.2.0": + version: 1.2.0 + resolution: "send@npm:1.2.0" + dependencies: + debug: "npm:^4.3.5" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + etag: "npm:^1.8.1" + fresh: "npm:^2.0.0" + http-errors: "npm:^2.0.0" + mime-types: "npm:^3.0.1" + ms: "npm:^2.1.3" + on-finished: "npm:^2.4.1" + range-parser: "npm:^1.2.1" + statuses: "npm:^2.0.1" + checksum: 10c0/531bcfb5616948d3468d95a1fd0adaeb0c20818ba4a500f439b800ca2117971489e02074ce32796fd64a6772ea3e7235fe0583d8241dbd37a053dc3378eff9a5 + languageName: node + linkType: hard + "serialize-javascript@npm:^6.0.0, serialize-javascript@npm:^6.0.1": version: 6.0.2 resolution: "serialize-javascript@npm:6.0.2" @@ -17358,6 +19641,18 @@ __metadata: languageName: node linkType: hard +"serve-static@npm:^2.2.0": + version: 2.2.0 + resolution: "serve-static@npm:2.2.0" + dependencies: + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + parseurl: "npm:^1.3.3" + send: "npm:^1.2.0" + checksum: 10c0/30e2ed1dbff1984836cfd0c65abf5d3f3f83bcd696c99d2d3c97edbd4e2a3ff4d3f87108a7d713640d290a7b6fe6c15ddcbc61165ab2eaad48ea8d3b52c7f913 + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -17444,6 +19739,41 @@ __metadata: languageName: node linkType: hard +"side-channel-list@npm:^1.0.0": + version: 1.0.0 + resolution: "side-channel-list@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + checksum: 10c0/644f4ac893456c9490ff388bf78aea9d333d5e5bfc64cfb84be8f04bf31ddc111a8d4b83b85d7e7e8a7b845bc185a9ad02c052d20e086983cf59f0be517d9b3d + languageName: node + linkType: hard + +"side-channel-map@npm:^1.0.1": + version: 1.0.1 + resolution: "side-channel-map@npm:1.0.1" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672 + languageName: node + linkType: hard + +"side-channel-weakmap@npm:^1.0.2": + version: 1.0.2 + resolution: "side-channel-weakmap@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + side-channel-map: "npm:^1.0.1" + checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185 + languageName: node + linkType: hard + "side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": version: 1.0.6 resolution: "side-channel@npm:1.0.6" @@ -17456,6 +19786,26 @@ __metadata: languageName: node linkType: hard +"side-channel@npm:^1.1.0": + version: 1.1.0 + resolution: "side-channel@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + side-channel-list: "npm:^1.0.0" + side-channel-map: "npm:^1.0.1" + side-channel-weakmap: "npm:^1.0.2" + checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6 + languageName: node + linkType: hard + +"sift@npm:^17.1.3": + version: 17.1.3 + resolution: "sift@npm:17.1.3" + checksum: 10c0/bb05d1d65cc9b549b402c1366ba1fcf685311808b6d5c2f4fa2f477d7b524218bbf6c99587562d5613d407820a6b5a7cad809f89c3f75c513ff5d8c0e0a0cead + languageName: node + linkType: hard + "signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -17517,6 +19867,13 @@ __metadata: languageName: node linkType: hard +"slow-redact@npm:^0.3.0": + version: 0.3.2 + resolution: "slow-redact@npm:0.3.2" + checksum: 10c0/d6611e518461d918eda9a77903100e097870035c8ef8ce95eec7d7a2eafc6c0cdfc37476a1fecf9d70e0b6b36eb9d862f4ac58e931c305b3fc010939226fa803 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -17602,6 +19959,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^4.0.1": + version: 4.2.0 + resolution: "sonic-boom@npm:4.2.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10c0/ae897e6c2cd6d3cb7cdcf608bc182393b19c61c9413a85ce33ffd25891485589f39bece0db1de24381d0a38fc03d08c9862ded0c60f184f1b852f51f97af9684 + languageName: node + linkType: hard + "sorted-array-functions@npm:^1.3.0": version: 1.3.0 resolution: "sorted-array-functions@npm:1.3.0" @@ -17753,6 +20119,13 @@ __metadata: languageName: node linkType: hard +"split2@npm:^4.0.0": + version: 4.2.0 + resolution: "split2@npm:4.2.0" + checksum: 10c0/b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534 + languageName: node + linkType: hard + "sprintf-js@npm:^1.1.3": version: 1.1.3 resolution: "sprintf-js@npm:1.1.3" @@ -17799,6 +20172,13 @@ __metadata: languageName: node linkType: hard +"statuses@npm:^2.0.1": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f + languageName: node + linkType: hard + "streamroller@npm:^3.1.5": version: 3.1.5 resolution: "streamroller@npm:3.1.5" @@ -17939,6 +20319,13 @@ __metadata: languageName: node linkType: hard +"strip-json-comments@npm:^5.0.2": + version: 5.0.3 + resolution: "strip-json-comments@npm:5.0.3" + checksum: 10c0/daaf20b29f69fb51112698f4a9a662490dbb78d5baf6127c75a0a83c2ac6c078a8c0f74b389ad5e0519d6fc359c4a57cb9971b1ae201aef62ce45a13247791e0 + languageName: node + linkType: hard + "strong-log-transformer@npm:^2.1.0": version: 2.1.0 resolution: "strong-log-transformer@npm:2.1.0" @@ -18066,6 +20453,18 @@ __metadata: languageName: node linkType: hard +"swr@npm:^2.2.5": + version: 2.3.6 + resolution: "swr@npm:2.3.6" + dependencies: + dequal: "npm:^2.0.3" + use-sync-external-store: "npm:^1.4.0" + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/9534f350982e36a3ae0a13da8c0f7da7011fc979e77f306e60c4e5db0f9b84f17172c44f973441ba56bb684b69b0d9838ab40011a6b6b3e32d0cd7f3d5405f99 + languageName: node + linkType: hard + "symbol-observable@npm:4.0.0": version: 4.0.0 resolution: "symbol-observable@npm:4.0.0" @@ -18210,6 +20609,22 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^3.0.0": + version: 3.1.0 + resolution: "thread-stream@npm:3.1.0" + dependencies: + real-require: "npm:^0.2.0" + checksum: 10c0/c36118379940b77a6ef3e6f4d5dd31e97b8210c3f7b9a54eb8fe6358ab173f6d0acfaf69b9c3db024b948c0c5fd2a7df93e2e49151af02076b35ada3205ec9a6 + languageName: node + linkType: hard + +"throttleit@npm:2.1.0": + version: 2.1.0 + resolution: "throttleit@npm:2.1.0" + checksum: 10c0/1696ae849522cea6ba4f4f3beac1f6655d335e51b42d99215e196a718adced0069e48deaaf77f7e89f526ab31de5b5c91016027da182438e6f9280be2f3d5265 + languageName: node + linkType: hard + "through@npm:^2.3.4, through@npm:^2.3.6": version: 2.3.8 resolution: "through@npm:2.3.8" @@ -18238,13 +20653,6 @@ __metadata: languageName: node linkType: hard -"tinyqueue@npm:^3.0.0": - version: 3.0.0 - resolution: "tinyqueue@npm:3.0.0" - checksum: 10c0/edd6f1a6146aa3aa7bc85b44b3aacf44cddfa805b0901019272d7e9235894b4f28b2f9d09c1bce500ae48883b03708b6b871aa33920e895d2943720f7a9ad3ba - languageName: node - linkType: hard - "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" @@ -18312,6 +20720,13 @@ __metadata: languageName: node linkType: hard +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 + languageName: node + linkType: hard + "traverse@npm:>=0.3.0 <0.4": version: 0.3.9 resolution: "traverse@npm:0.3.9" @@ -18664,6 +21079,17 @@ __metadata: languageName: node linkType: hard +"type-is@npm:^2.0.0, type-is@npm:^2.0.1": + version: 2.0.1 + resolution: "type-is@npm:2.0.1" + dependencies: + content-type: "npm:^1.0.5" + media-typer: "npm:^1.1.0" + mime-types: "npm:^3.0.0" + checksum: 10c0/7f7ec0a060b16880bdad36824ab37c26019454b67d73e8a465ed5a3587440fbe158bc765f0da68344498235c877e7dbbb1600beccc94628ed05599d667951b99 + languageName: node + linkType: hard + "type@npm:^2.7.2": version: 2.7.3 resolution: "type@npm:2.7.3" @@ -18825,6 +21251,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.14.0": + version: 7.14.0 + resolution: "undici-types@npm:7.14.0" + checksum: 10c0/e7f3214b45d788f03c51ceb33817be99c65dae203863aa9386b3ccc47201a245a7955fc721fb581da9c888b6ebad59fa3f53405214afec04c455a479908f0f14 + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.1 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1" @@ -19001,6 +21434,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.4.0": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -19040,7 +21482,16 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0": +"uuid@npm:^11.1.0": + version: 11.1.0 + resolution: "uuid@npm:11.1.0" + bin: + uuid: dist/esm/bin/uuid + checksum: 10c0/34aa51b9874ae398c2b799c88a127701408cd581ee89ec3baa53509dd8728cbb25826f2a038f9465f8b7be446f0fbf11558862965b18d21c993684297628d4d3 + languageName: node + linkType: hard + +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: @@ -19349,6 +21800,13 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db + languageName: node + linkType: hard + "webidl-conversions@npm:^5.0.0": version: 5.0.0 resolution: "webidl-conversions@npm:5.0.0" @@ -19674,6 +22132,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 + languageName: node + linkType: hard + "whatwg-url@npm:^8.0.0, whatwg-url@npm:^8.5.0": version: 8.7.0 resolution: "whatwg-url@npm:8.7.0" @@ -19875,7 +22343,14 @@ __metadata: languageName: node linkType: hard -"xtend@npm:^4.0.2, xtend@npm:~4.0.0": +"xstate@npm:^5.20.1": + version: 5.23.0 + resolution: "xstate@npm:5.23.0" + checksum: 10c0/f23610aab8700b84fee4477fda28ef4f92adedd3e2816ac85c9f57a1257485a9ea44942e73db6fa11a383e9752635e6c88a62b89d47fd5443e68b2505f0ff438 + languageName: node + linkType: hard + +"xtend@npm:^4.0.0, xtend@npm:^4.0.2, xtend@npm:~4.0.0": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: 10c0/366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e @@ -20078,6 +22553,47 @@ __metadata: languageName: node linkType: hard +"zod-from-json-schema-v3@npm:zod-from-json-schema@^0.0.5": + version: 0.0.5 + resolution: "zod-from-json-schema@npm:0.0.5" + dependencies: + zod: "npm:^3.24.2" + checksum: 10c0/ca7f3240361d48e3e5eac35ca2adc194b73ffe6ef35215e43fb8da674371efc2d40ee986279ebb7ed4a0d4e044b1219b6f51594f9073e7a528323f79123f56fe + languageName: node + linkType: hard + +"zod-from-json-schema@npm:^0.5.0": + version: 0.5.0 + resolution: "zod-from-json-schema@npm:0.5.0" + dependencies: + zod: "npm:^4.0.17" + checksum: 10c0/9e547639fa6cd3b7f7f516979b85b14a871a33794b8fc31a374b63e07357e79dae1fd474f0309e8ebdeefac1c945115179a0545f856bdf9a27fcea91c5a65c9d + languageName: node + linkType: hard + +"zod-to-json-schema@npm:^3.24.1, zod-to-json-schema@npm:^3.24.6": + version: 3.24.6 + resolution: "zod-to-json-schema@npm:3.24.6" + peerDependencies: + zod: ^3.24.1 + checksum: 10c0/b907ab6d057100bd25a37e5545bf5f0efa5902cd84d3c3ec05c2e51541431a47bd9bf1e5e151a244273409b45f5986d55b26e5d207f98abc5200702f733eb368 + languageName: node + linkType: hard + +"zod@npm:^3.23.8, zod@npm:^3.24.2": + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c + languageName: node + linkType: hard + +"zod@npm:^4.0.17": + version: 4.1.12 + resolution: "zod@npm:4.1.12" + checksum: 10c0/b64c1feb19e99d77075261eaf613e0b2be4dfcd3551eff65ad8b4f2a079b61e379854d066f7d447491fcf193f45babd8095551a9d47973d30b46b6d8e2c46774 + languageName: node + linkType: hard + "zone.js@npm:0.13.0": version: 0.13.0 resolution: "zone.js@npm:0.13.0" From 7d2cbdab10c64816e20b3ed0cac71af26f5a14ec Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 17 Oct 2025 23:42:40 -0700 Subject: [PATCH 007/158] finish end2end v1 --- bin/litellm-config.yaml | 5 + frontend/custom-webpack.config.js | 6 + frontend/package.json | 5 +- frontend/proxy.config.json | 6 +- frontend/src/app/app.module.ts | 2 + .../copilot-avatar.component.html | 172 ++ .../copilot-avatar.component.scss | 226 ++ .../copilot-avatar.component.spec.ts | 42 + .../copilot-avatar.component.ts | 118 + .../copilot-panel.component.html | 223 -- .../copilot-panel.component.scss | 236 -- .../copilot-panel/copilot-panel.component.ts | 148 - .../component/menu/menu.component.html | 10 + .../component/menu/menu.component.ts | 19 +- .../service/copilot/texera-copilot.ts | 211 +- .../service/copilot/workflow-tools.ts | 124 +- .../util/workflow-util.service.ts | 7 + frontend/yarn.lock | 2644 +---------------- 18 files changed, 920 insertions(+), 3284 deletions(-) create mode 100644 bin/litellm-config.yaml create mode 100644 frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.html create mode 100644 frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.scss create mode 100644 frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.spec.ts create mode 100644 frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.ts delete mode 100644 frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.html delete mode 100644 frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.scss delete mode 100644 frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.ts diff --git a/bin/litellm-config.yaml b/bin/litellm-config.yaml new file mode 100644 index 00000000000..359c1bc246f --- /dev/null +++ b/bin/litellm-config.yaml @@ -0,0 +1,5 @@ +model_list: + - model_name: claude-3.7 + litellm_params: + model: claude-3-7-sonnet-20250219 + api_key: "os.environ/ANTHROPIC_API_KEY" \ No newline at end of file diff --git a/frontend/custom-webpack.config.js b/frontend/custom-webpack.config.js index df1d742b920..2e099a79fff 100644 --- a/frontend/custom-webpack.config.js +++ b/frontend/custom-webpack.config.js @@ -18,6 +18,12 @@ */ module.exports = { + resolve: { + fallback: { + // Minimal polyfill for path (needed by some dependencies) + "path": require.resolve("path-browserify"), + } + }, module: { rules: [ { diff --git a/frontend/package.json b/frontend/package.json index a1fed417b0b..6c93fc9f2fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,14 +40,14 @@ "@codingame/monaco-vscode-r-default-extension": "8.0.4", "@loaders.gl/core": "3.4.2", "@luma.gl/core": "8.5.20", - "@mastra/core": "0.21.1", - "@mastra/mcp": "0.13.5", + "@modelcontextprotocol/sdk": "^1.20.1", "@ngneat/until-destroy": "8.1.4", "@ngx-formly/core": "6.3.12", "@ngx-formly/ng-zorro-antd": "6.3.12", "@stoplight/json-ref-resolver": "3.1.5", "@types/lodash-es": "4.17.4", "@types/plotly.js-basic-dist-min": "2.12.4", + "ai": "^5.0.76", "ajv": "8.10.0", "backbone": "1.4.1", "concaveman": "2.0.0", @@ -97,6 +97,7 @@ "y-quill": "0.1.5", "y-websocket": "1.4.0", "yjs": "13.5.41", + "zod": "^3.25.76", "zone.js": "0.13.0" }, "resolutions": { diff --git a/frontend/proxy.config.json b/frontend/proxy.config.json index d355e57b4fd..53037aaeb62 100755 --- a/frontend/proxy.config.json +++ b/frontend/proxy.config.json @@ -1,12 +1,12 @@ { - "/api/copilot/mcp": { + "/api/mcp": { "target": "http://localhost:9098", "secure": false, "changeOrigin": true, "ws": true }, - "/api/copilot/agent": { - "target": "http://localhost:8001", + "/api/chat/completion": { + "target": "http://0.0.0.0:4000", "secure": false, "changeOrigin": true }, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 257c2961fa4..8de1bc8ca67 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -100,6 +100,7 @@ import { NzPopconfirmModule } from "ng-zorro-antd/popconfirm"; import { AdminGuardService } from "./dashboard/service/admin/guard/admin-guard.service"; import { ContextMenuComponent } from "./workspace/component/workflow-editor/context-menu/context-menu/context-menu.component"; import { CoeditorUserIconComponent } from "./workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component"; +import { CopilotAvatarComponent } from "./workspace/component/copilot-avatar/copilot-avatar.component"; import { InputAutoCompleteComponent } from "./workspace/component/input-autocomplete/input-autocomplete.component"; import { CollabWrapperComponent } from "./common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; import { NzSwitchModule } from "ng-zorro-antd/switch"; @@ -242,6 +243,7 @@ registerLocaleData(en); LocalLoginComponent, ContextMenuComponent, CoeditorUserIconComponent, + CopilotAvatarComponent, InputAutoCompleteComponent, FileSelectionComponent, CollabWrapperComponent, diff --git a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.html b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.html new file mode 100644 index 00000000000..a59dcbb302b --- /dev/null +++ b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.html @@ -0,0 +1,172 @@ + + +
+ + + + + + + + + +
+ +
+
+ + Texera Copilot + + +
+ +
+ + +
+
+
+
+ + + + {{ message.role === 'user' ? 'You' : message.role === 'assistant' ? 'Copilot' : 'System' }} +
+
{{ message.content }}
+
+
+ + {{ tool.name || 'Tool' }} +
+
+
+
+ + +
+
+
+ + Copilot +
+
+ + Thinking... +
+
+
+ + +
+ +

Start a conversation with the copilot

+

Ask me to help you build workflows!

+
+
+ + +
+ + +
+ + +
+ + Copilot is disconnected. Please check your connection. +
+
+
diff --git a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.scss b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.scss new file mode 100644 index 00000000000..f5fda922900 --- /dev/null +++ b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.scss @@ -0,0 +1,226 @@ +/** + * 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. + */ + +.copilot-avatar-container { + position: relative; + display: inline-block; + margin-right: 8px; +} + +.copilot-avatar { + transition: all 0.3s ease; + border: 2px solid transparent; + + &:hover { + transform: scale(1.1); + border-color: #1890ff; + } +} + +.status-indicator { + position: absolute; + bottom: 0; + right: 0; + pointer-events: none; +} + +.chat-box { + position: fixed; + top: 60px; + right: 20px; + width: 400px; + height: 600px; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + z-index: 1000; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.chat-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 8px 8px 0 0; +} + +.chat-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 16px; + + i { + font-size: 18px; + } +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 16px; + background: #fafafa; + display: flex; + flex-direction: column; + gap: 12px; +} + +.message { + display: flex; + flex-direction: column; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.message-content { + max-width: 80%; + padding: 10px 14px; + border-radius: 12px; + word-wrap: break-word; +} + +.message-user .message-content { + align-self: flex-end; + background: #1890ff; + color: white; +} + +.message-assistant .message-content { + align-self: flex-start; + background: white; + border: 1px solid #d9d9d9; +} + +.message-system .message-content { + align-self: center; + background: #f0f0f0; + color: #595959; + font-size: 12px; + max-width: 90%; +} + +.message-role { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; + font-size: 12px; + opacity: 0.8; +} + +.message-text { + line-height: 1.5; + white-space: pre-wrap; +} + +.tool-calls { + margin-top: 8px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.tool-call { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: rgba(0, 0, 0, 0.05); + border-radius: 4px; + font-size: 12px; + opacity: 0.7; +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: #8c8c8c; + text-align: center; + + p { + margin: 8px 0; + } + + .empty-hint { + font-size: 12px; + color: #bfbfbf; + } +} + +.chat-input { + display: flex; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid #f0f0f0; + background: white; + border-radius: 0 0 8px 8px; + + textarea { + flex: 1; + resize: none; + } + + button { + height: 100%; + } +} + +.connection-warning { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: #fff7e6; + border-top: 1px solid #ffd666; + color: #d46b08; + font-size: 12px; + + i { + color: #faad14; + } +} diff --git a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.spec.ts b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.spec.ts new file mode 100644 index 00000000000..decf80a424e --- /dev/null +++ b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.spec.ts @@ -0,0 +1,42 @@ +/** + * 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 { ComponentFixture, TestBed } from "@angular/core/testing"; +import { CopilotAvatarComponent } from "./copilot-avatar.component"; + +describe("CopilotAvatarComponent", () => { + let component: CopilotAvatarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CopilotAvatarComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CopilotAvatarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.ts b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.ts new file mode 100644 index 00000000000..3fb857fd537 --- /dev/null +++ b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.ts @@ -0,0 +1,118 @@ +/** + * 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 { Component, OnDestroy, OnInit } from "@angular/core"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { TexeraCopilot, CopilotMessage } from "../../service/copilot/texera-copilot"; + +/** + * CopilotAvatarComponent displays an AI assistant avatar in the menu bar + * When clicked, it shows a floating chat interface for interacting with the copilot + */ +@UntilDestroy() +@Component({ + selector: "texera-copilot-avatar", + templateUrl: "copilot-avatar.component.html", + styleUrls: ["copilot-avatar.component.scss"], +}) +export class CopilotAvatarComponent implements OnInit, OnDestroy { + public isVisible: boolean = false; + public isChatVisible: boolean = false; + public isConnected: boolean = false; + public isProcessing: boolean = false; + public messages: CopilotMessage[] = []; + public userInput: string = ""; + + constructor(public copilotService: TexeraCopilot) {} + + ngOnInit(): void { + // Subscribe to copilot state changes + this.copilotService.state$.pipe(untilDestroyed(this)).subscribe(state => { + this.isVisible = state.isEnabled; + this.isConnected = state.isConnected; + this.isProcessing = state.isProcessing; + this.messages = state.messages; + }); + } + + ngOnDestroy(): void { + // Cleanup handled by @UntilDestroy() + } + + /** + * Toggle the chat box visibility + */ + public toggleChat(): void { + this.isChatVisible = !this.isChatVisible; + } + + /** + * Send a message to the copilot + */ + public async sendMessage(): Promise { + if (!this.userInput.trim() || this.isProcessing) { + return; + } + + const message = this.userInput.trim(); + this.userInput = ""; + + try { + await this.copilotService.sendMessage(message); + } catch (error) { + console.error("Error sending message:", error); + } + } + + /** + * Handle Enter key press in input field + */ + public onKeyPress(event: KeyboardEvent): void { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + this.sendMessage(); + } + } + + /** + * Get the status indicator color + */ + public getStatusColor(): string { + if (!this.isConnected) { + return "red"; + } + if (this.isProcessing) { + return "orange"; + } + return "green"; + } + + /** + * Get the status tooltip text + */ + public getStatusTooltip(): string { + if (!this.isConnected) { + return "Disconnected"; + } + if (this.isProcessing) { + return "Processing..."; + } + return "Connected"; + } +} diff --git a/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.html b/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.html deleted file mode 100644 index 7b544facbd9..00000000000 --- a/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.html +++ /dev/null @@ -1,223 +0,0 @@ - - -
- -
-

Texera Copilot

-
- - - - -
-
- - -
- - {{ copilotState.isConnected ? 'Connected' : 'Disconnected' }} - - - - Processing... - -
- - -
-
Quick Actions
-
- - - - -
-
- - -
-
- Agent Thinking Log -
-
-
- {{ log }} -
-
-
- - -
-
-
- -
- - - - {{ message.role === 'user' ? 'You' : message.role === 'assistant' ? 'Copilot' : 'System' }} - - {{ formatTimestamp(message.timestamp) }} -
- - -
{{ message.content }}
-
- - -
- -

Start a conversation with Texera Copilot

-

Ask me to help you build or modify your workflow!

-
-
-
- - -
- - - - - - - -
- - -
-

Copilot is disabled. Click the enable button to start.

-
-
diff --git a/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.scss b/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.scss deleted file mode 100644 index bd0b898e47f..00000000000 --- a/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.scss +++ /dev/null @@ -1,236 +0,0 @@ -/** - * 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. - */ - -.copilot-panel { - display: flex; - flex-direction: column; - height: 100%; - background: #fff; - border-left: 1px solid #f0f0f0; - position: relative; - - .copilot-header { - padding: 16px; - border-bottom: 1px solid #f0f0f0; - display: flex; - justify-content: space-between; - align-items: center; - - h3 { - margin: 0; - font-size: 16px; - font-weight: 600; - } - - .header-actions { - display: flex; - gap: 8px; - } - } - - .status-bar { - padding: 8px 16px; - background: #fafafa; - border-bottom: 1px solid #f0f0f0; - display: flex; - gap: 8px; - } - - .quick-actions { - background: #f5f5f5; - border-bottom: 1px solid #d9d9d9; - - .quick-actions-header { - padding: 8px 16px; - background: #e8e8e8; - font-weight: 500; - font-size: 13px; - text-transform: uppercase; - color: #595959; - } - - .quick-actions-content { - padding: 8px; - display: flex; - flex-wrap: wrap; - gap: 8px; - - button { - text-align: left; - padding: 8px 12px; - - i { - margin-right: 8px; - } - } - } - } - - .thinking-log { - background: #f5f5f5; - border-bottom: 2px solid #d9d9d9; - max-height: 200px; - overflow-y: auto; - - .log-header { - padding: 8px 16px; - background: #e8e8e8; - font-weight: 500; - font-size: 12px; - text-transform: uppercase; - color: #595959; - } - - .log-content { - padding: 8px; - - .log-entry { - padding: 4px 8px; - margin-bottom: 4px; - background: white; - border-radius: 4px; - font-family: monospace; - font-size: 12px; - color: #595959; - } - } - } - - .messages-container { - flex: 1; - overflow: hidden; - display: flex; - flex-direction: column; - - &.with-thinking-log { - flex: 1; - } - - .messages-scroll { - flex: 1; - overflow-y: auto; - padding: 16px; - } - - .message { - margin-bottom: 16px; - - .message-header { - display: flex; - justify-content: space-between; - margin-bottom: 8px; - - .role { - font-weight: 500; - display: flex; - align-items: center; - gap: 4px; - - i { - font-size: 14px; - } - } - - .timestamp { - font-size: 12px; - color: #8c8c8c; - } - } - - .message-content { - padding: 12px; - border-radius: 8px; - line-height: 1.5; - } - - &.message-user { - .role { - color: #1890ff; - } - .message-content { - background: #e6f7ff; - margin-left: 24px; - } - } - - &.message-assistant { - .role { - color: #52c41a; - } - .message-content { - background: #f6ffed; - margin-right: 24px; - } - } - - &.message-system { - .role { - color: #8c8c8c; - } - .message-content { - background: #fafafa; - font-style: italic; - font-size: 13px; - } - } - } - - .empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - color: #8c8c8c; - - p { - margin: 8px 0; - } - - .hint { - font-size: 13px; - color: #bfbfbf; - } - } - } - - .input-area { - padding: 16px; - border-top: 1px solid #f0f0f0; - background: #fafafa; - } - - .disabled-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(255, 255, 255, 0.95); - display: flex; - align-items: center; - justify-content: center; - z-index: 10; - - p { - color: #8c8c8c; - font-size: 14px; - } - } -} diff --git a/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.ts b/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.ts deleted file mode 100644 index d736752bb96..00000000000 --- a/frontend/src/app/workspace/component/copilot-panel/copilot-panel.component.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * 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 { Component, OnInit, OnDestroy } from "@angular/core"; -import { FormControl } from "@angular/forms"; -import { Subject } from "rxjs"; -import { takeUntil } from "rxjs/operators"; -import { TexeraCopilot, CopilotState } from "../../service/copilot/texera-copilot"; - -@Component({ - selector: "texera-copilot-panel", - templateUrl: "./copilot-panel.component.html", - styleUrls: ["./copilot-panel.component.scss"], -}) -export class CopilotPanelComponent implements OnInit, OnDestroy { - public copilotState: CopilotState; - public inputControl = new FormControl(""); - public showThinkingLog = false; - public showQuickActions = false; - - private destroy$ = new Subject(); - - constructor(private copilot: TexeraCopilot) { - this.copilotState = this.copilot.getState(); - } - - ngOnInit(): void { - // Subscribe to copilot state changes - this.copilot.state$.pipe(takeUntil(this.destroy$)).subscribe(state => { - this.copilotState = state; - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - /** - * Send message to copilot - */ - async sendMessage(): Promise { - const message = this.inputControl.value?.trim(); - if (!message) return; - - // Clear input - this.inputControl.setValue(""); - - try { - await this.copilot.sendMessage(message); - } catch (error) { - console.error("Error sending message:", error); - } - } - - /** - * Toggle copilot - */ - async toggleCopilot(): Promise { - try { - await this.copilot.toggle(); - } catch (error) { - console.error("Error toggling copilot:", error); - } - } - - /** - * Clear conversation - */ - clearConversation(): void {} - - /** - * Toggle thinking log visibility - */ - toggleThinkingLog(): void { - this.showThinkingLog = !this.showThinkingLog; - } - - /** - * Toggle quick actions menu - */ - toggleQuickActions(): void { - this.showQuickActions = !this.showQuickActions; - } - - /** - * Quick action: Suggest workflow - */ - async suggestWorkflow(): Promise { - const description = prompt("Describe the workflow you want to create:"); - if (description) { - await this.copilot.suggestWorkflow(description); - } - } - - /** - * Quick action: Analyze workflow - */ - async analyzeWorkflow(): Promise { - await this.copilot.analyzeWorkflow(); - } - - /** - * Quick action: List available operators - */ - async listOperators(): Promise { - await this.copilot.sendMessage("List all available operator types grouped by category"); - } - - /** - * Quick action: Auto-layout - */ - async autoLayout(): Promise { - await this.copilot.sendMessage("Apply automatic layout to the workflow"); - } - - /** - * Format timestamp for display - */ - formatTimestamp(date: Date): string { - return new Date(date).toLocaleTimeString(); - } - - /** - * Format tool calls for display - */ - formatToolCalls(toolCalls?: any[]): string { - if (!toolCalls || toolCalls.length === 0) return ""; - - return toolCalls.map(tc => `🔧 ${tc.function?.name || tc.name}`).join(", "); - } -} diff --git a/frontend/src/app/workspace/component/menu/menu.component.html b/frontend/src/app/workspace/component/menu/menu.component.html index c9828e9d77e..63944fcf746 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.html +++ b/frontend/src/app/workspace/component/menu/menu.component.html @@ -79,6 +79,7 @@ +
@@ -232,6 +233,15 @@ nz-icon nzType="comment"> +
- -
-
-
-
- - - - {{ message.role === 'user' ? 'You' : message.role === 'assistant' ? 'Copilot' : 'System' }} -
-
{{ message.content }}
-
-
- - {{ tool.name || 'Tool' }} -
-
-
-
- - -
-
-
- - Copilot -
-
- - Thinking... -
-
-
- - -
- -

Start a conversation with the copilot

-

Ask me to help you build workflows!

-
-
- - -
- - -
+ + +
{ + try { + const last = body?.messages?.[body.messages.length - 1]; + const userText: string = typeof last?.text === "string" ? last.text : ""; + const reply = await this.copilotService.sendMessage(userText); + signals.onResponse({ text: reply }); + } catch (e: any) { + signals.onResponse({ error: e?.message ?? "Unknown error" }); + } + }, + }, + demo: false, + introMessage: { text: "Hi! I'm Texera Copilot. I can help you build and modify workflows." }, + textInput: { placeholder: { text: "Ask me anything about workflows..." } }, + }; constructor(public copilotService: TexeraCopilot) {} ngOnInit(): void { - // Subscribe to copilot state changes this.copilotService.state$.pipe(untilDestroyed(this)).subscribe(state => { this.isVisible = state.isEnabled; this.isConnected = state.isConnected; this.isProcessing = state.isProcessing; - this.messages = state.messages; }); } - ngOnDestroy(): void { - // Cleanup handled by @UntilDestroy() + ngAfterViewInit(): void { + // Load existing messages if any + if (this.deepChatElement?.nativeElement) { + const existing = this.copilotService.getMessages(); + if (existing.length > 0) { + this.deepChatElement.nativeElement.messages = existing.map((m: ModelMessage) => ({ + role: m.role === "assistant" ? "ai" : "user", + text: typeof m.content === "string" ? m.content : JSON.stringify(m.content), + })); + } + } } - /** - * Toggle the chat box visibility - */ public toggleChat(): void { this.isChatVisible = !this.isChatVisible; } - /** - * Send a message to the copilot - */ - public async sendMessage(): Promise { - if (!this.userInput.trim() || this.isProcessing) { - return; - } - - const message = this.userInput.trim(); - this.userInput = ""; - - try { - await this.copilotService.sendMessage(message); - } catch (error) { - console.error("Error sending message:", error); - } - } - - /** - * Handle Enter key press in input field - */ - public onKeyPress(event: KeyboardEvent): void { - if (event.key === "Enter" && !event.shiftKey) { - event.preventDefault(); - this.sendMessage(); - } - } - - /** - * Get the status indicator color - */ public getStatusColor(): string { - if (!this.isConnected) { - return "red"; - } - if (this.isProcessing) { - return "orange"; - } + if (!this.isConnected) return "red"; + if (this.isProcessing) return "orange"; return "green"; } - - /** - * Get the status tooltip text - */ public getStatusTooltip(): string { - if (!this.isConnected) { - return "Disconnected"; - } - if (this.isProcessing) { - return "Processing..."; - } + if (!this.isConnected) return "Disconnected"; + if (this.isProcessing) return "Processing..."; return "Connected"; } } diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 381c6e9ed31..d30830a6e7f 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -31,27 +31,21 @@ import { } from "./workflow-tools"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { createOpenAI } from "@ai-sdk/openai"; -import { generateText, type ModelMessage} from "ai"; +import { generateText, type ModelMessage } from "ai"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { AppSettings } from "../../../common/app-setting"; // API endpoints as constants export const COPILOT_MCP_URL = "mcp"; -export const AGENT_MODEL_ID = "claude-3.7" - -export interface CopilotMessage { - role: "user" | "assistant" | "system"; - content: string; - timestamp: Date; - toolCalls?: any[]; -} +export const AGENT_MODEL_ID = "claude-3.7"; +/** + * Simplified copilot state for UI + */ export interface CopilotState { isEnabled: boolean; isConnected: boolean; isProcessing: boolean; - messages: CopilotMessage[]; - thinkingLog: string[]; } /** @@ -66,17 +60,20 @@ export class TexeraCopilot { private mcpTools: any[] = []; private model: any; + // Message history using AI SDK's ModelMessage type + private messages: ModelMessage[] = []; + // State management private stateSubject = new BehaviorSubject({ isEnabled: false, isConnected: false, isProcessing: false, - messages: [], - thinkingLog: [], }); public readonly state$ = this.stateSubject.asObservable(); - private messageStream = new Subject(); + + // Message stream for real-time updates + private messageStream = new Subject(); public readonly messages$ = this.messageStream.asObservable(); constructor( @@ -98,7 +95,7 @@ export class TexeraCopilot { // 2. Initialize OpenAI model this.model = createOpenAI({ baseURL: new URL(`${AppSettings.getApiEndpoint()}`, document.baseURI).toString(), - apiKey: "dummy" + apiKey: "dummy", }).chat(AGENT_MODEL_ID); this.updateState({ @@ -106,14 +103,14 @@ export class TexeraCopilot { isConnected: true, }); - this.addSystemMessage("Texera Copilot initialized. I can help you build and modify workflows."); + console.log("Texera Copilot initialized successfully"); } catch (error: unknown) { console.error("Failed to initialize copilot:", error); this.updateState({ isEnabled: false, isConnected: false, }); - this.addSystemMessage(`Initialization failed: ${error}`); + throw error; } } @@ -177,15 +174,21 @@ export class TexeraCopilot { } /** - * Send a message to the copilot + * Send a message to the copilot and get a response */ - public async sendMessage(message: string): Promise { + public async sendMessage(message: string): Promise { if (!this.model) { throw new Error("Copilot not initialized"); } - // Add user message - this.addUserMessage(message); + // Add user message to history + const userMessage: ModelMessage = { + role: "user", + content: message, + }; + this.messages.push(userMessage); + this.messageStream.next(userMessage); + this.updateState({ isProcessing: true }); try { @@ -216,10 +219,7 @@ export class TexeraCopilot { // Generate response using Vercel AI SDK const response = await generateText({ model: this.model, - messages: this.getConversationHistory().map(msg => ({ - role: msg.role as "user" | "assistant" | "system", - content: msg.content, - })), + messages: [...this.messages], tools: allTools, system: `You are Texera Copilot, an AI assistant for building and modifying data workflows. @@ -238,40 +238,53 @@ CAPABILITIES: - Get operator metadata`, }); - // Add assistant response - this.addAssistantMessage(response.text, response.toolCalls); + // Add assistant response to history + const assistantMessage: ModelMessage = { + role: "assistant", + content: response.text, + }; + this.messages.push(assistantMessage); + this.messageStream.next(assistantMessage); + + return response.text; } catch (error: any) { console.error("Error processing request:", error); - this.addSystemMessage(`Error: ${error.message}`); + const errorMessage = `Error: ${error.message}`; + + // Add error as assistant message + const assistantMessage: ModelMessage = { + role: "assistant", + content: errorMessage, + }; + this.messages.push(assistantMessage); + this.messageStream.next(assistantMessage); + + return errorMessage; } finally { this.updateState({ isProcessing: false }); } } /** - * Get conversation history + * Get conversation history as ModelMessage array */ - public getConversationHistory(): CopilotMessage[] { - return this.stateSubject.getValue().messages; + public getMessages(): ModelMessage[] { + return [...this.messages]; } /** - * Clear conversation + * Add a message to the conversation history */ - public clearConversation(): void { - this.updateState({ - messages: [], - thinkingLog: [], - }); - - this.addSystemMessage("Conversation cleared. Ready for new requests."); + public addMessage(message: ModelMessage): void { + this.messages.push(message); + this.messageStream.next(message); } /** - * Get thinking/reasoning log + * Clear conversation history */ - public getThinkingLog(): string[] { - return this.stateSubject.getValue().thinkingLog; + public clearConversation(): void { + this.messages = []; } /** @@ -309,7 +322,7 @@ CAPABILITIES: isConnected: false, }); - this.addSystemMessage("Copilot disabled."); + console.log("Copilot disabled"); } /** @@ -320,56 +333,8 @@ CAPABILITIES: } /** - * Update thinking log + * Update state */ - private updateThinkingLog(reasoning: string | string[]): void { - const logs = Array.isArray(reasoning) ? reasoning : [reasoning]; - const timestamp = new Date().toISOString(); - - const formattedLogs = logs.map(log => `[${timestamp}] ${log}`); - - const currentState = this.stateSubject.getValue(); - this.updateState({ - thinkingLog: [...currentState.thinkingLog, ...formattedLogs], - }); - } - - // Message management helpers - private addUserMessage(content: string): void { - const message: CopilotMessage = { - role: "user", - content, - timestamp: new Date(), - }; - this.addMessage(message); - } - - private addAssistantMessage(content: string, toolCalls?: any[]): void { - const message: CopilotMessage = { - role: "assistant", - content, - timestamp: new Date(), - toolCalls, - }; - this.addMessage(message); - } - - private addSystemMessage(content: string): void { - const message: CopilotMessage = { - role: "system", - content, - timestamp: new Date(), - }; - this.addMessage(message); - } - - private addMessage(message: CopilotMessage): void { - const currentState = this.stateSubject.getValue(); - const messages = [...currentState.messages, message]; - this.updateState({ messages }); - this.messageStream.next(message); - } - private updateState(partialState: Partial): void { const currentState = this.stateSubject.getValue(); this.stateSubject.next({ diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index b3bc3955d64..4b55fc40a8e 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -85,7 +85,12 @@ export function createAddLinkTool(workflowActionService: WorkflowActionService) targetOperatorId: z.string().describe("ID of the target operator"), targetPortId: z.string().optional().describe("Port ID on target operator (e.g., 'input-0')"), }), - execute: async (args: { sourceOperatorId: string; sourcePortId?: string; targetOperatorId: string; targetPortId?: string }) => { + execute: async (args: { + sourceOperatorId: string; + sourcePortId?: string; + targetOperatorId: string; + targetPortId?: string; + }) => { try { // Default port IDs if not specified const sourcePId = args.sourcePortId || "output-0"; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 24ed90f7bba..64e293cc409 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3578,6 +3578,13 @@ __metadata: languageName: node linkType: hard +"@microsoft/fetch-event-source@npm:^2.0.1": + version: 2.0.1 + resolution: "@microsoft/fetch-event-source@npm:2.0.1" + checksum: 10c0/38c69e9b9990e6cee715c7bbfa2752f943b42575acadb36facf19bb831f1520c469f854277439154258e0e1dc8650cc85038230d1f451e3f6b62e8faeaa1126c + languageName: node + linkType: hard + "@modelcontextprotocol/sdk@npm:^1.20.1": version: 1.20.1 resolution: "@modelcontextprotocol/sdk@npm:1.20.1" @@ -6654,7 +6661,7 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^1.0.7": +"argparse@npm:^1.0.10, argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" dependencies: @@ -6843,6 +6850,15 @@ __metadata: languageName: node linkType: hard +"autolinker@npm:^3.11.0": + version: 3.16.2 + resolution: "autolinker@npm:3.16.2" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10c0/91e083bfa4393fdcd29f595e1db657d852fd74cbd1fec719f30f3d57c910e72d5e0a0b10f2b17e1e6297b52b2f5c12eb6d0cbe024c0d92671e81d8ab906fe981 + languageName: node + linkType: hard + "autoprefixer@npm:10.4.14": version: 10.4.14 resolution: "autoprefixer@npm:10.4.14" @@ -8970,6 +8986,17 @@ __metadata: languageName: node linkType: hard +"deep-chat@npm:^2.2.2": + version: 2.2.2 + resolution: "deep-chat@npm:2.2.2" + dependencies: + "@microsoft/fetch-event-source": "npm:^2.0.1" + remarkable: "npm:^2.0.1" + speech-to-element: "npm:^1.0.4" + checksum: 10c0/67a4fa2062d6f170394c46e4ea57f63685b4f024f5390d8878a430e16ba99753e3a30322e0de0ac00629cc3249a73e37eb3297bfe5c73461b13ed6861cf3e578 + languageName: node + linkType: hard + "deep-equal@npm:^1.0.1": version: 1.1.2 resolution: "deep-equal@npm:1.1.2" @@ -11581,6 +11608,7 @@ __metadata: concurrently: "npm:7.4.0" content-disposition: "npm:0.5.4" dagre: "npm:0.8.5" + deep-chat: "npm:^2.2.2" deep-map: "npm:2.0.0" edit-distance: "npm:1.0.4" es6-weak-map: "npm:2.0.3" @@ -16733,6 +16761,18 @@ __metadata: languageName: node linkType: hard +"remarkable@npm:^2.0.1": + version: 2.0.1 + resolution: "remarkable@npm:2.0.1" + dependencies: + argparse: "npm:^1.0.10" + autolinker: "npm:^3.11.0" + bin: + remarkable: bin/remarkable.js + checksum: 10c0/e2c23bfd2e45234110bc3220e44fcac5e4a8199691ff6959d9cd0bac34ffca2f123d3913946cbef517018bc8e5ab00beafc527a04782b7afbe5e9706d1c0c77a + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -17826,6 +17866,13 @@ __metadata: languageName: node linkType: hard +"speech-to-element@npm:^1.0.4": + version: 1.0.4 + resolution: "speech-to-element@npm:1.0.4" + checksum: 10c0/ae9e5bbefb13fa6920df6817ed02f463fa29a0cfd52a1fcc4b7864da816085d185087571e72bd1557c43255592ad39e17d403a2e4950c1233eaa822efd923268 + languageName: node + linkType: hard + "sprintf-js@npm:^1.1.3": version: 1.1.3 resolution: "sprintf-js@npm:1.1.3" From 00a5a0ffb899e703731847fcadd30ae4bdc7b97e Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sun, 19 Oct 2025 17:48:57 -0700 Subject: [PATCH 010/158] make tool call automated --- .../service/copilot/texera-copilot.ts | 149 ++++++++---------- 1 file changed, 65 insertions(+), 84 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index d30830a6e7f..6021a83a400 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -31,7 +31,7 @@ import { } from "./workflow-tools"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { createOpenAI } from "@ai-sdk/openai"; -import { generateText, type ModelMessage } from "ai"; +import { AssistantModelMessage, generateText, type ModelMessage, stepCountIs, UserModelMessage } from "ai"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { AppSettings } from "../../../common/app-setting"; @@ -173,98 +173,86 @@ export class TexeraCopilot { return tools; } - /** - * Send a message to the copilot and get a response - */ public async sendMessage(message: string): Promise { - if (!this.model) { - throw new Error("Copilot not initialized"); - } + if (!this.model) throw new Error("Copilot not initialized"); - // Add user message to history - const userMessage: ModelMessage = { - role: "user", - content: message, - }; + // 1) push the user message + const userMessage: UserModelMessage = { role: "user", content: message }; this.messages.push(userMessage); this.messageStream.next(userMessage); - this.updateState({ isProcessing: true }); try { - // Get workflow manipulation tools - const addOperatorTool = createAddOperatorTool( - this.workflowActionService, - this.workflowUtilService, - this.operatorMetadataService - ); - const addLinkTool = createAddLinkTool(this.workflowActionService); - const listOperatorsTool = createListOperatorsTool(this.workflowActionService); - const listLinksTool = createListLinksTool(this.workflowActionService); - const listOperatorTypesTool = createListOperatorTypesTool(this.workflowUtilService); - - // Get MCP tools in AI SDK format - // const mcpToolsForAI = this.getMCPToolsForAI(); - - // Combine all tools - const allTools = { - // ...mcpToolsForAI, - addOperator: addOperatorTool, - addLink: addLinkTool, - listOperators: listOperatorsTool, - listLinks: listLinksTool, - listOperatorTypes: listOperatorTypesTool, - }; + // 2) define tools (your existing helpers) + const tools = this.createWorkflowTools(); - // Generate response using Vercel AI SDK - const response = await generateText({ + // 3) run multi-step with stopWhen + const { text, steps, response } = await generateText({ model: this.model, - messages: [...this.messages], - tools: allTools, - system: `You are Texera Copilot, an AI assistant for building and modifying data workflows. - -CAPABILITIES: -1. Workflow Manipulation (Local Tools): - - Add/delete operators - - Connect operators with links - - Modify operator properties - - Auto-layout workflows - - Add comment boxes - -2. Operator Discovery (MCP Tools): - - List available operator types - - Get operator schemas - - Search operators by capability - - Get operator metadata`, + messages: this.messages, // full history + tools, + system: "You are Texera Copilot, an AI assistant for building and modifying data workflows.", + // <-- THIS enables looping: the SDK will call tools, inject results, + // and re-generate until the condition is met or no more tool calls. + stopWhen: stepCountIs(10), + + // optional: observe every completed step (tool calls + results available) + onStepFinish({ text, toolCalls, toolResults, finishReason, usage }) { + // e.g., log or trace each iteration + // console.debug('step finished', { text, toolCalls, toolResults, finishReason, usage }); + }, }); - // Add assistant response to history - const assistantMessage: ModelMessage = { - role: "assistant", - content: response.text, - }; - this.messages.push(assistantMessage); - this.messageStream.next(assistantMessage); - - return response.text; - } catch (error: any) { - console.error("Error processing request:", error); - const errorMessage = `Error: ${error.message}`; - - // Add error as assistant message - const assistantMessage: ModelMessage = { - role: "assistant", - content: errorMessage, - }; - this.messages.push(assistantMessage); - this.messageStream.next(assistantMessage); - - return errorMessage; + // 4) append ALL messages the SDK produced this turn (assistant + tool messages) + // This keeps your history perfectly aligned with the SDK's internal state. + this.messages.push(...response.messages); + for (const m of response.messages) this.messageStream.next(m); + + // 5) optional diagnostics + if (steps?.length) { + const totalToolCalls = steps.flatMap(s => s.toolCalls || []).length; + console.log(`Agent loop finished in ${steps.length} step(s), ${totalToolCalls} tool call(s).`); + } + + return text; + } catch (err: any) { + const errorText = `Error: ${err?.message ?? String(err)}`; + const assistantError: AssistantModelMessage = { role: "assistant", content: errorText }; + this.messages.push(assistantError); + this.messageStream.next(assistantError); + return errorText; } finally { this.updateState({ isProcessing: false }); } } + /** + * Create workflow manipulation tools + */ + private createWorkflowTools(): Record { + const addOperatorTool = createAddOperatorTool( + this.workflowActionService, + this.workflowUtilService, + this.operatorMetadataService + ); + const addLinkTool = createAddLinkTool(this.workflowActionService); + const listOperatorsTool = createListOperatorsTool(this.workflowActionService); + const listLinksTool = createListLinksTool(this.workflowActionService); + const listOperatorTypesTool = createListOperatorTypesTool(this.workflowUtilService); + + // Get MCP tools in AI SDK format + // const mcpToolsForAI = this.getMCPToolsForAI(); + + return { + // ...mcpToolsForAI, + addOperator: addOperatorTool, + addLink: addLinkTool, + listOperators: listOperatorsTool, + listLinks: listLinksTool, + listOperatorTypes: listOperatorTypesTool, + }; + } + /** * Get conversation history as ModelMessage array */ @@ -280,13 +268,6 @@ CAPABILITIES: this.messageStream.next(message); } - /** - * Clear conversation history - */ - public clearConversation(): void { - this.messages = []; - } - /** * Toggle copilot enabled state */ From f5849eb67b478276d6af3f3cff982830fc34dd5b Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sun, 19 Oct 2025 21:34:26 -0700 Subject: [PATCH 011/158] add more tools --- .../service/copilot/texera-copilot.ts | 37 ++++++- .../service/copilot/workflow-tools.ts | 102 ++++++++++++++++++ 2 files changed, 135 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 6021a83a400..502bdb36e98 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -28,6 +28,10 @@ import { createListOperatorsTool, createListLinksTool, createListOperatorTypesTool, + createGetOperatorTool, + createDeleteOperatorTool, + createDeleteLinkTool, + createSetOperatorPropertyTool, } from "./workflow-tools"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { createOpenAI } from "@ai-sdk/openai"; @@ -189,7 +193,7 @@ export class TexeraCopilot { // 3) run multi-step with stopWhen const { text, steps, response } = await generateText({ model: this.model, - messages: this.messages, // full history + messages: this.messages, // full history tools, system: "You are Texera Copilot, an AI assistant for building and modifying data workflows.", // <-- THIS enables looping: the SDK will call tools, inject results, @@ -197,9 +201,26 @@ export class TexeraCopilot { stopWhen: stepCountIs(10), // optional: observe every completed step (tool calls + results available) - onStepFinish({ text, toolCalls, toolResults, finishReason, usage }) { - // e.g., log or trace each iteration - // console.debug('step finished', { text, toolCalls, toolResults, finishReason, usage }); + onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage }) => { + // Log each step for debugging + console.debug("step finished", { text, toolCalls, toolResults, finishReason, usage }); + + // If there are tool calls, send a trace message to the chat + if (toolCalls && toolCalls.length > 0) { + const traceContent = toolCalls + .map(tc => { + const result = toolResults?.find(tr => tr.toolCallId === tc.toolCallId); + return `🔧 ${tc.toolName}(${JSON.stringify(tc.args, null, 2)})\n→ ${JSON.stringify(result?.result, null, 2)}`; + }) + .join("\n\n"); + + // Send as a system-style message to the stream + const traceMessage: ModelMessage = { + role: "assistant", + content: `[Tool Trace]\n${traceContent}`, + }; + this.messageStream.next(traceMessage); + } }, }); @@ -239,6 +260,10 @@ export class TexeraCopilot { const listOperatorsTool = createListOperatorsTool(this.workflowActionService); const listLinksTool = createListLinksTool(this.workflowActionService); const listOperatorTypesTool = createListOperatorTypesTool(this.workflowUtilService); + const getOperatorTool = createGetOperatorTool(this.workflowActionService); + const deleteOperatorTool = createDeleteOperatorTool(this.workflowActionService); + const deleteLinkTool = createDeleteLinkTool(this.workflowActionService); + const setOperatorPropertyTool = createSetOperatorPropertyTool(this.workflowActionService); // Get MCP tools in AI SDK format // const mcpToolsForAI = this.getMCPToolsForAI(); @@ -250,6 +275,10 @@ export class TexeraCopilot { listOperators: listOperatorsTool, listLinks: listLinksTool, listOperatorTypes: listOperatorTypesTool, + getOperator: getOperatorTool, + deleteOperator: deleteOperatorTool, + deleteLink: deleteLinkTool, + setOperatorProperty: setOperatorPropertyTool, }; } diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index 4b55fc40a8e..61c424e8f18 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -190,3 +190,105 @@ export function createListOperatorTypesTool(workflowUtilService: WorkflowUtilSer }, }); } + +/** + * Create getOperator tool for getting detailed information about a specific operator + */ +export function createGetOperatorTool(workflowActionService: WorkflowActionService) { + return tool({ + name: "getOperator", + description: "Get detailed information about a specific operator in the workflow", + inputSchema: z.object({ + operatorId: z.string().describe("ID of the operator to retrieve"), + }), + execute: async (args: { operatorId: string }) => { + try { + const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); + return { + success: true, + operator: operator, + message: `Retrieved operator ${args.operatorId}`, + }; + } catch (error: any) { + return { + success: false, + error: error.message || `Operator ${args.operatorId} not found`, + }; + } + }, + }); +} + +/** + * Create deleteOperator tool for removing an operator from the workflow + */ +export function createDeleteOperatorTool(workflowActionService: WorkflowActionService) { + return tool({ + name: "deleteOperator", + description: "Delete an operator from the workflow", + inputSchema: z.object({ + operatorId: z.string().describe("ID of the operator to delete"), + }), + execute: async (args: { operatorId: string }) => { + try { + workflowActionService.deleteOperator(args.operatorId); + return { + success: true, + message: `Deleted operator ${args.operatorId}`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} + +/** + * Create deleteLink tool for removing a link from the workflow + */ +export function createDeleteLinkTool(workflowActionService: WorkflowActionService) { + return tool({ + name: "deleteLink", + description: "Delete a link between two operators in the workflow by link ID", + inputSchema: z.object({ + linkId: z.string().describe("ID of the link to delete"), + }), + execute: async (args: { linkId: string }) => { + try { + workflowActionService.deleteLinkWithID(args.linkId); + return { + success: true, + message: `Deleted link ${args.linkId}`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} + +/** + * Create setOperatorProperty tool for modifying operator properties + */ +export function createSetOperatorPropertyTool(workflowActionService: WorkflowActionService) { + return tool({ + name: "setOperatorProperty", + description: "Set or update properties of an operator in the workflow", + inputSchema: z.object({ + operatorId: z.string().describe("ID of the operator to modify"), + properties: z.record(z.any()).describe("Properties object to set on the operator"), + }), + execute: async (args: { operatorId: string; properties: Record }) => { + try { + workflowActionService.setOperatorProperty(args.operatorId, args.properties); + return { + success: true, + message: `Updated properties for operator ${args.operatorId}`, + properties: args.properties, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} From 5873b6d405e8a31ca7954e36e40dfb5af7eddfa6 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sun, 19 Oct 2025 22:16:27 -0700 Subject: [PATCH 012/158] fix styles --- .../copilot-avatar/copilot-avatar.component.html | 16 ++-------------- .../copilot-avatar/copilot-avatar.component.scss | 15 ++++----------- .../workspace/service/copilot/texera-copilot.ts | 13 ++++++++----- 3 files changed, 14 insertions(+), 30 deletions(-) diff --git a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.html b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.html index e9629899af0..24afe0dd4d1 100644 --- a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.html +++ b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.html @@ -23,7 +23,7 @@ - - - -
Texera Copilot - -
- -
- - - - - - -
- - Copilot is disconnected. Please check your connection. -
- - diff --git a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.ts b/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.ts deleted file mode 100644 index 4b4313e5cee..00000000000 --- a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.ts +++ /dev/null @@ -1,128 +0,0 @@ -// copilot-avatar.component.ts -import { Component, OnInit, ViewChild, ElementRef } from "@angular/core"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { TexeraCopilot, AgentResponse } from "../../service/copilot/texera-copilot"; - -@UntilDestroy() -@Component({ - selector: "texera-copilot-avatar", - templateUrl: "copilot-avatar.component.html", - styleUrls: ["copilot-avatar.component.scss"], -}) -export class CopilotAvatarComponent implements OnInit { - @ViewChild("deepChat", { static: false }) deepChatElement?: ElementRef; - - public isVisible = true; - public isChatVisible = false; - public isConnected = false; - public isProcessing = false; - private isInitialized = false; - - // Deep-chat configuration - public deepChatConfig = { - connect: { - handler: (body: any, signals: any) => { - const last = body?.messages?.[body.messages.length - 1]; - const userText: string = typeof last?.text === "string" ? last.text : ""; - - // Send message to copilot and process AgentResponse - this.copilotService - .sendMessage(userText) - .pipe(untilDestroyed(this)) - .subscribe({ - next: (response: AgentResponse) => { - // Format the response based on type - let displayText = ""; - - if (response.type === "trace") { - // Format tool traces - displayText = this.formatToolTrace(response); - // Add trace message via addMessage API - if (displayText && this.deepChatElement?.nativeElement?.addMessage) { - this.deepChatElement.nativeElement.addMessage({ role: "ai", text: displayText }); - } - } else if (response.type === "response") { - // For final response, signal completion with the content - // This will let deep-chat handle adding the message - if (response.isDone) { - signals.onResponse({ text: response.content }); - } - } - }, - error: (e: unknown) => { - signals.onResponse({ error: e ?? "Unknown error" }); - }, - }); - }, - }, - demo: false, - introMessage: { text: "Hi! I'm Texera Copilot. I can help you build and modify workflows." }, - textInput: { placeholder: { text: "Ask me anything about workflows..." } }, - }; - - /** - * Format tool trace for display - */ - private formatToolTrace(response: AgentResponse): string { - if (!response.toolCalls || response.toolCalls.length === 0) { - return ""; - } - - const traces = response.toolCalls.map(tc => { - const result = response.toolResults?.find(tr => tr.toolCallId === tc.toolCallId); - return `🔧 ${tc.toolName}(${JSON.stringify(tc.args, null, 2)})\n→ ${JSON.stringify(result?.result, null, 2)}`; - }); - - return `[Tool Trace]\n${traces.join("\n\n")}`; - } - - constructor(public copilotService: TexeraCopilot) {} - - ngOnInit(): void { - // Update connection status - this.updateConnectionStatus(); - } - - public async toggleChat(): Promise { - // Initialize copilot on first toggle if not already initialized - if (!this.isInitialized) { - try { - await this.copilotService.initialize(); - this.isInitialized = true; - this.updateConnectionStatus(); - } catch (error) { - console.error("Failed to initialize copilot:", error); - return; - } - } - - this.isChatVisible = !this.isChatVisible; - } - - public toggleVisibility(): void { - this.isVisible = !this.isVisible; - - // If hiding and copilot is initialized, disconnect - if (!this.isVisible && this.isInitialized) { - this.copilotService.disconnect().then(() => { - this.isInitialized = false; - this.updateConnectionStatus(); - }); - } - } - - private updateConnectionStatus(): void { - this.isConnected = this.copilotService.isConnected(); - } - - public getStatusColor(): string { - if (!this.isConnected) return "red"; - if (this.isProcessing) return "orange"; - return "green"; - } - public getStatusTooltip(): string { - if (!this.isConnected) return "Disconnected"; - if (this.isProcessing) return "Processing..."; - return "Connected"; - } -} diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html new file mode 100644 index 00000000000..b38b7022ca3 --- /dev/null +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html @@ -0,0 +1,70 @@ + + + +
+ +
+
+ + Texera Copilot +
+ +
+ + + + + + +
+ + Copilot is disconnected. Please check your connection. +
+
diff --git a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.scss b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.scss similarity index 100% rename from frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.scss rename to frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.scss diff --git a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.spec.ts b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.spec.ts similarity index 78% rename from frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.spec.ts rename to frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.spec.ts index decf80a424e..63146f25fb0 100644 --- a/frontend/src/app/workspace/component/copilot-avatar/copilot-avatar.component.spec.ts +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.spec.ts @@ -18,20 +18,20 @@ */ import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { CopilotAvatarComponent } from "./copilot-avatar.component"; +import { CopilotChatComponent } from "./copilot-chat.component"; -describe("CopilotAvatarComponent", () => { - let component: CopilotAvatarComponent; - let fixture: ComponentFixture; +describe("CopilotChatComponent", () => { + let component: CopilotChatComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [CopilotAvatarComponent], + declarations: [CopilotChatComponent], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(CopilotAvatarComponent); + fixture = TestBed.createComponent(CopilotChatComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts new file mode 100644 index 00000000000..a1bacaf43c3 --- /dev/null +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts @@ -0,0 +1,192 @@ +// copilot-chat.component.ts +import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from "@angular/core"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { TexeraCopilot, AgentResponse } from "../../service/copilot/texera-copilot"; +import { CopilotCoeditorService } from "../../service/copilot/copilot-coeditor.service"; + +@UntilDestroy() +@Component({ + selector: "texera-copilot-chat", + templateUrl: "copilot-chat.component.html", + styleUrls: ["copilot-chat.component.scss"], +}) +export class CopilotChatComponent implements OnInit, OnDestroy { + @ViewChild("deepChat", { static: false }) deepChatElement?: ElementRef; + + public isChatVisible = false; + public isConnected = false; + public isProcessing = false; + private isInitialized = false; + + // Deep-chat configuration + public deepChatConfig = { + connect: { + handler: (body: any, signals: any) => { + const last = body?.messages?.[body.messages.length - 1]; + const userText: string = typeof last?.text === "string" ? last.text : ""; + + // Send message to copilot and process AgentResponse + this.copilotService + .sendMessage(userText) + .pipe(untilDestroyed(this)) + .subscribe({ + next: (response: AgentResponse) => { + // Format the response based on type + let displayText = ""; + + if (response.type === "trace") { + // Format tool traces + displayText = this.formatToolTrace(response); + // Add trace message via addMessage API + if (displayText && this.deepChatElement?.nativeElement?.addMessage) { + this.deepChatElement.nativeElement.addMessage({ role: "ai", text: displayText }); + } + } else if (response.type === "response") { + // For final response, signal completion with the content + // This will let deep-chat handle adding the message + if (response.isDone) { + signals.onResponse({ text: response.content }); + } + } + }, + error: (e: unknown) => { + signals.onResponse({ error: e ?? "Unknown error" }); + }, + }); + }, + }, + demo: false, + introMessage: { text: "Hi! I'm Texera Copilot. I can help you build and modify workflows." }, + textInput: { placeholder: { text: "Ask me anything about workflows..." } }, + }; + + /** + * Format tool trace for display with markdown + */ + private formatToolTrace(response: AgentResponse): string { + if (!response.toolCalls || response.toolCalls.length === 0) { + return ""; + } + + // Include agent's thinking/text if available + let output = ""; + if (response.content && response.content.trim()) { + output += `💭 **Agent:** ${response.content}\n\n`; + } + + // Format each tool call - show only tool name and parameters + const traces = response.toolCalls.map((tc: any) => { + // Log the actual structure to debug + console.log("Tool call structure:", tc); + + // Try multiple possible property names for arguments + const args = tc.args || tc.arguments || tc.parameters || tc.input || {}; + + // Format args nicely + let argsDisplay = ""; + if (Object.keys(args).length > 0) { + argsDisplay = Object.entries(args) + .map(([key, value]) => ` **${key}:** \`${JSON.stringify(value)}\``) + .join("\n"); + } else { + argsDisplay = " *(no parameters)*"; + } + + return `🔧 **${tc.toolName}**\n${argsDisplay}`; + }); + + output += traces.join("\n\n"); + return output; + } + + constructor( + public copilotService: TexeraCopilot, + private copilotCoeditorService: CopilotCoeditorService + ) {} + + ngOnInit(): void { + // Don't auto-initialize, wait for user to toggle chat + } + + ngOnDestroy(): void { + // Cleanup when component is destroyed + this.disconnect(); + } + + /** + * Connect to copilot - called from menu button + * Registers copilot as coeditor and shows chat + */ + public async connect(): Promise { + if (this.isInitialized) return; + + try { + // Register copilot as virtual coeditor + this.copilotCoeditorService.register(); + + // Initialize copilot service + await this.copilotService.initialize(); + + this.isInitialized = true; + this.isChatVisible = true; // Show chat on connect + this.updateConnectionStatus(); + console.log("Copilot connected and registered as coeditor"); + } catch (error) { + console.error("Failed to connect copilot:", error); + this.copilotCoeditorService.unregister(); + } + } + + /** + * Disconnect from copilot - called from menu button + * Unregisters copilot and clears all messages + */ + public disconnect(): void { + if (!this.isInitialized) return; + + // Unregister copilot coeditor + this.copilotCoeditorService.unregister(); + + // Disconnect copilot service (this clears the connection) + this.copilotService.disconnect(); + + // Clear messages by resetting the message history + // The copilot service will need to be re-initialized next time + this.isInitialized = false; + this.isChatVisible = false; + this.updateConnectionStatus(); + console.log("Copilot disconnected and messages cleared"); + } + + /** + * Check if copilot is currently connected + */ + public isActive(): boolean { + return this.isInitialized; + } + + /** + * Show the chat box (expand) + */ + public showChat(): void { + this.isChatVisible = true; + } + + /** + * Collapse the chat box (hide without disconnecting) + */ + public collapseChat(): void { + this.isChatVisible = false; + } + + /** + * Get the copilot coeditor object (for displaying in UI) + */ + public getCopilot() { + return this.copilotCoeditorService.getCopilot(); + } + + private updateConnectionStatus(): void { + this.isConnected = this.copilotService.isConnected(); + } +} diff --git a/frontend/src/app/workspace/component/menu/menu.component.html b/frontend/src/app/workspace/component/menu/menu.component.html index 123554afec3..2f5045953bc 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.html +++ b/frontend/src/app/workspace/component/menu/menu.component.html @@ -79,7 +79,7 @@ - +
@@ -142,6 +142,16 @@ nz-icon nzType="download"> + diff --git a/frontend/src/app/workspace/component/menu/menu.component.ts b/frontend/src/app/workspace/component/menu/menu.component.ts index 1096b3af0d4..954b26f71b6 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.ts @@ -56,6 +56,7 @@ import { ComputingUnitSelectionComponent } from "../power-button/computing-unit- import { GuiConfigService } from "../../../common/service/gui-config.service"; import { DashboardWorkflowComputingUnit } from "../../types/workflow-computing-unit"; import { Privilege } from "../../../dashboard/type/share-access.interface"; +import { CopilotChatComponent } from "../copilot-chat/copilot-chat.component"; /** * MenuComponent is the top level menu bar that shows @@ -119,6 +120,7 @@ export class MenuComponent implements OnInit, OnDestroy { public computingUnitStatus: ComputingUnitState = ComputingUnitState.NoComputingUnit; @ViewChild(ComputingUnitSelectionComponent) computingUnitSelectionComponent!: ComputingUnitSelectionComponent; + @ViewChild(CopilotChatComponent) copilotChat?: CopilotChatComponent; constructor( public executeWorkflowService: ExecuteWorkflowService, @@ -519,6 +521,23 @@ export class MenuComponent implements OnInit, OnDestroy { this.workflowActionService.deleteOperatorsAndLinks(allOperatorIDs); } + /** + * Toggle AI Copilot connection + * - If not connected: registers copilot as coeditor and shows chat + * - If connected: unregisters copilot, disconnects, and clears messages + */ + public async toggleCopilotChat(): Promise { + if (!this.copilotChat) return; + + if (this.copilotChat.isActive()) { + // Disconnect: remove from coeditors, clear messages + this.copilotChat.disconnect(); + } else { + // Connect: register as coeditor, show chat + await this.copilotChat.connect(); + } + } + public onClickImportWorkflow = (file: NzUploadFile): boolean => { const reader = new FileReader(); reader.readAsText(file as any); diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 8fb7088086b..b7f36e8a187 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -47,6 +47,7 @@ import { AppSettings } from "../../../common/app-setting"; import { DynamicSchemaService } from "../dynamic-schema/dynamic-schema.service"; import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; import { WorkflowResultService } from "../workflow-result/workflow-result.service"; +import { CopilotCoeditorService } from "./copilot-coeditor.service"; // API endpoints as constants export const COPILOT_MCP_URL = "mcp"; @@ -85,7 +86,8 @@ export class TexeraCopilot { private operatorMetadataService: OperatorMetadataService, private dynamicSchemaService: DynamicSchemaService, private executeWorkflowService: ExecuteWorkflowService, - private workflowResultService: WorkflowResultService + private workflowResultService: WorkflowResultService, + private copilotCoeditorService: CopilotCoeditorService ) { // Don't auto-initialize, wait for user to enable } @@ -190,7 +192,7 @@ export class TexeraCopilot { messages: this.messages, // full history tools, system: "You are Texera Copilot, an AI assistant for building and modifying data workflows.", - stopWhen: stepCountIs(10), + stopWhen: stepCountIs(50), // optional: observe every completed step (tool calls + results available) onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage }) => { @@ -248,22 +250,42 @@ export class TexeraCopilot { const addOperatorTool = createAddOperatorTool( this.workflowActionService, this.workflowUtilService, - this.operatorMetadataService + this.operatorMetadataService, + this.copilotCoeditorService ); const addLinkTool = createAddLinkTool(this.workflowActionService); - const listOperatorsTool = createListOperatorsTool(this.workflowActionService); + const listOperatorsTool = createListOperatorsTool(this.workflowActionService, this.copilotCoeditorService); const listLinksTool = createListLinksTool(this.workflowActionService); const listOperatorTypesTool = createListOperatorTypesTool(this.workflowUtilService); - const getOperatorTool = createGetOperatorTool(this.workflowActionService); - const deleteOperatorTool = createDeleteOperatorTool(this.workflowActionService); + const getOperatorTool = createGetOperatorTool(this.workflowActionService, this.copilotCoeditorService); + const deleteOperatorTool = createDeleteOperatorTool(this.workflowActionService, this.copilotCoeditorService); const deleteLinkTool = createDeleteLinkTool(this.workflowActionService); - const setOperatorPropertyTool = createSetOperatorPropertyTool(this.workflowActionService); - const getDynamicSchemaTool = createGetDynamicSchemaTool(this.dynamicSchemaService); + const setOperatorPropertyTool = createSetOperatorPropertyTool( + this.workflowActionService, + this.copilotCoeditorService + ); + const getDynamicSchemaTool = createGetDynamicSchemaTool( + this.dynamicSchemaService, + this.workflowActionService, + this.copilotCoeditorService + ); const executeWorkflowTool = createExecuteWorkflowTool(this.executeWorkflowService); const getExecutionStateTool = createGetExecutionStateTool(this.executeWorkflowService); - const hasOperatorResultTool = createHasOperatorResultTool(this.workflowResultService); - const getOperatorResultSnapshotTool = createGetOperatorResultSnapshotTool(this.workflowResultService); - const getOperatorResultInfoTool = createGetOperatorResultInfoTool(this.workflowResultService); + const hasOperatorResultTool = createHasOperatorResultTool( + this.workflowResultService, + this.workflowActionService, + this.copilotCoeditorService + ); + const getOperatorResultSnapshotTool = createGetOperatorResultSnapshotTool( + this.workflowResultService, + this.workflowActionService, + this.copilotCoeditorService + ); + const getOperatorResultInfoTool = createGetOperatorResultInfoTool( + this.workflowResultService, + this.workflowActionService, + this.copilotCoeditorService + ); // Get MCP tools in AI SDK format // const mcpToolsForAI = this.getMCPToolsForAI(); diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index f4f4814ee30..f5b3ef3a296 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -27,6 +27,7 @@ import { DynamicSchemaService } from "../dynamic-schema/dynamic-schema.service"; import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { ExecutionState } from "../../types/execute-workflow.interface"; +import { CopilotCoeditorService } from "./copilot-coeditor.service"; /** * Create addOperator tool for adding a new operator to the workflow @@ -34,7 +35,8 @@ import { ExecutionState } from "../../types/execute-workflow.interface"; export function createAddOperatorTool( workflowActionService: WorkflowActionService, workflowUtilService: WorkflowUtilService, - operatorMetadataService: OperatorMetadataService + operatorMetadataService: OperatorMetadataService, + copilotCoeditor: CopilotCoeditorService ) { return tool({ name: "addOperator", @@ -61,9 +63,17 @@ export function createAddOperatorTool( const defaultY = 100 + Math.floor(existingOperators.length / 5) * 150; const position = { x: defaultX, y: defaultY }; + // Show copilot is adding this operator + copilotCoeditor.showEditingOperator(operator.operatorID); + // Add the operator to the workflow workflowActionService.addOperator(operator, position); + // Clear presence indicator after a brief delay + setTimeout(() => { + copilotCoeditor.clearEditingOperator(); + }, 1000); + return { success: true, operatorId: operator.operatorID, @@ -129,7 +139,10 @@ export function createAddLinkTool(workflowActionService: WorkflowActionService) /** * Create listOperators tool for getting all operators in the workflow */ -export function createListOperatorsTool(workflowActionService: WorkflowActionService) { +export function createListOperatorsTool( + workflowActionService: WorkflowActionService, + copilotCoeditor: CopilotCoeditorService +) { return tool({ name: "listOperators", description: "Get all operators in the current workflow", @@ -137,12 +150,23 @@ export function createListOperatorsTool(workflowActionService: WorkflowActionSer execute: async () => { try { const operators = workflowActionService.getTexeraGraph().getAllOperators(); + + // Highlight all operators to show copilot is inspecting them + const operatorIds = operators.map(op => op.operatorID); + copilotCoeditor.highlightOperators(operatorIds); + + // Clear highlights after a brief delay + setTimeout(() => { + copilotCoeditor.clearHighlights(); + }, 1500); + return { success: true, operators: operators, count: operators.length, }; } catch (error: any) { + // Can't clear highlights without operator IDs return { success: false, error: error.message }; } }, @@ -198,7 +222,10 @@ export function createListOperatorTypesTool(workflowUtilService: WorkflowUtilSer /** * Create getOperator tool for getting detailed information about a specific operator */ -export function createGetOperatorTool(workflowActionService: WorkflowActionService) { +export function createGetOperatorTool( + workflowActionService: WorkflowActionService, + copilotCoeditor: CopilotCoeditorService +) { return tool({ name: "getOperator", description: "Get detailed information about a specific operator in the workflow", @@ -207,13 +234,26 @@ export function createGetOperatorTool(workflowActionService: WorkflowActionServi }), execute: async (args: { operatorId: string }) => { try { + // Show copilot is viewing this operator + copilotCoeditor.showEditingOperator(args.operatorId); + copilotCoeditor.highlightOperators([args.operatorId]); + const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); + + // Clear viewing state after a brief delay + setTimeout(() => { + copilotCoeditor.clearEditingOperator(); + copilotCoeditor.clearHighlights(); + }, 1200); + return { success: true, operator: operator, message: `Retrieved operator ${args.operatorId}`, }; } catch (error: any) { + copilotCoeditor.clearEditingOperator(); + copilotCoeditor.clearHighlights(); return { success: false, error: error.message || `Operator ${args.operatorId} not found`, @@ -226,7 +266,10 @@ export function createGetOperatorTool(workflowActionService: WorkflowActionServi /** * Create deleteOperator tool for removing an operator from the workflow */ -export function createDeleteOperatorTool(workflowActionService: WorkflowActionService) { +export function createDeleteOperatorTool( + workflowActionService: WorkflowActionService, + copilotCoeditor: CopilotCoeditorService +) { return tool({ name: "deleteOperator", description: "Delete an operator from the workflow", @@ -235,12 +278,20 @@ export function createDeleteOperatorTool(workflowActionService: WorkflowActionSe }), execute: async (args: { operatorId: string }) => { try { + // Show copilot is editing this operator before deletion + copilotCoeditor.showEditingOperator(args.operatorId); + workflowActionService.deleteOperator(args.operatorId); + + // Clear editing state after deletion + copilotCoeditor.clearEditingOperator(); + return { success: true, message: `Deleted operator ${args.operatorId}`, }; } catch (error: any) { + copilotCoeditor.clearEditingOperator(); return { success: false, error: error.message }; } }, @@ -274,7 +325,10 @@ export function createDeleteLinkTool(workflowActionService: WorkflowActionServic /** * Create setOperatorProperty tool for modifying operator properties */ -export function createSetOperatorPropertyTool(workflowActionService: WorkflowActionService) { +export function createSetOperatorPropertyTool( + workflowActionService: WorkflowActionService, + copilotCoeditor: CopilotCoeditorService +) { return tool({ name: "setOperatorProperty", description: "Set or update properties of an operator in the workflow", @@ -284,13 +338,26 @@ export function createSetOperatorPropertyTool(workflowActionService: WorkflowAct }), execute: async (args: { operatorId: string; properties: Record }) => { try { + // Show copilot is editing this operator + copilotCoeditor.showEditingOperator(args.operatorId); + workflowActionService.setOperatorProperty(args.operatorId, args.properties); + + // Show property was changed + copilotCoeditor.showPropertyChanged(args.operatorId); + + // Clear currently editing state + setTimeout(() => { + copilotCoeditor.clearEditingOperator(); + }, 1000); + return { success: true, message: `Updated properties for operator ${args.operatorId}`, properties: args.properties, }; } catch (error: any) { + copilotCoeditor.clearEditingOperator(); return { success: false, error: error.message }; } }, @@ -300,7 +367,11 @@ export function createSetOperatorPropertyTool(workflowActionService: WorkflowAct /** * Create getDynamicSchema tool for getting operator schema information */ -export function createGetDynamicSchemaTool(dynamicSchemaService: DynamicSchemaService) { +export function createGetDynamicSchemaTool( + dynamicSchemaService: DynamicSchemaService, + workflowActionService: WorkflowActionService, + copilotCoeditor: CopilotCoeditorService +) { return tool({ name: "getDynamicSchema", description: @@ -310,13 +381,23 @@ export function createGetDynamicSchemaTool(dynamicSchemaService: DynamicSchemaSe }), execute: async (args: { operatorId: string }) => { try { + // Highlight the operator being inspected + copilotCoeditor.highlightOperators([args.operatorId]); + const schema = dynamicSchemaService.getDynamicSchema(args.operatorId); + + // Clear highlight after a brief delay + setTimeout(() => { + copilotCoeditor.clearHighlights(); + }, 1200); + return { success: true, schema: schema, message: `Retrieved schema for operator ${args.operatorId}`, }; } catch (error: any) { + copilotCoeditor.clearHighlights(); return { success: false, error: error.message }; } }, @@ -382,7 +463,11 @@ export function createGetExecutionStateTool(executeWorkflowService: ExecuteWorkf /** * Create hasOperatorResult tool for checking if an operator has results */ -export function createHasOperatorResultTool(workflowResultService: WorkflowResultService) { +export function createHasOperatorResultTool( + workflowResultService: WorkflowResultService, + workflowActionService: WorkflowActionService, + copilotCoeditor: CopilotCoeditorService +) { return tool({ name: "hasOperatorResult", description: "Check if an operator has any execution results available", @@ -391,7 +476,16 @@ export function createHasOperatorResultTool(workflowResultService: WorkflowResul }), execute: async (args: { operatorId: string }) => { try { + // Highlight operator being checked + copilotCoeditor.highlightOperators([args.operatorId]); + const hasResult = workflowResultService.hasAnyResult(args.operatorId); + + // Clear highlight + setTimeout(() => { + copilotCoeditor.clearHighlights(); + }, 1000); + return { success: true, hasResult: hasResult, @@ -400,6 +494,7 @@ export function createHasOperatorResultTool(workflowResultService: WorkflowResul : `Operator ${args.operatorId} has no results`, }; } catch (error: any) { + copilotCoeditor.clearHighlights(); return { success: false, error: error.message }; } }, @@ -409,7 +504,11 @@ export function createHasOperatorResultTool(workflowResultService: WorkflowResul /** * Create getOperatorResultSnapshot tool for getting operator result data */ -export function createGetOperatorResultSnapshotTool(workflowResultService: WorkflowResultService) { +export function createGetOperatorResultSnapshotTool( + workflowResultService: WorkflowResultService, + workflowActionService: WorkflowActionService, + copilotCoeditor: CopilotCoeditorService +) { return tool({ name: "getOperatorResultSnapshot", description: "Get the result snapshot data for an operator (for visualization outputs)", @@ -418,14 +517,24 @@ export function createGetOperatorResultSnapshotTool(workflowResultService: Workf }), execute: async (args: { operatorId: string }) => { try { + // Highlight operator being inspected + copilotCoeditor.highlightOperators([args.operatorId]); + const resultService = workflowResultService.getResultService(args.operatorId); if (!resultService) { + copilotCoeditor.clearHighlights(); return { success: false, error: `No result snapshot available for operator ${args.operatorId}. It may use paginated results instead.`, }; } const snapshot = resultService.getCurrentResultSnapshot(); + + // Clear highlight + setTimeout(() => { + copilotCoeditor.clearHighlights(); + }, 1000); + return { success: true, operatorId: args.operatorId, @@ -433,6 +542,7 @@ export function createGetOperatorResultSnapshotTool(workflowResultService: Workf message: `Retrieved result snapshot for operator ${args.operatorId}`, }; } catch (error: any) { + copilotCoeditor.clearHighlights(); return { success: false, error: error.message }; } }, @@ -442,7 +552,11 @@ export function createGetOperatorResultSnapshotTool(workflowResultService: Workf /** * Create getOperatorResultInfo tool for getting operator result information */ -export function createGetOperatorResultInfoTool(workflowResultService: WorkflowResultService) { +export function createGetOperatorResultInfoTool( + workflowResultService: WorkflowResultService, + workflowActionService: WorkflowActionService, + copilotCoeditor: CopilotCoeditorService +) { return tool({ name: "getOperatorResultInfo", description: "Get information about an operator's results, including total count and pagination details", @@ -451,8 +565,12 @@ export function createGetOperatorResultInfoTool(workflowResultService: WorkflowR }), execute: async (args: { operatorId: string }) => { try { + // Highlight operator being inspected + copilotCoeditor.highlightOperators([args.operatorId]); + const paginatedResultService = workflowResultService.getPaginatedResultService(args.operatorId); if (!paginatedResultService) { + copilotCoeditor.clearHighlights(); return { success: false, error: `No paginated results available for operator ${args.operatorId}`, @@ -461,6 +579,12 @@ export function createGetOperatorResultInfoTool(workflowResultService: WorkflowR const totalTuples = paginatedResultService.getCurrentTotalNumTuples(); const currentPage = paginatedResultService.getCurrentPageIndex(); const schema = paginatedResultService.getSchema(); + + // Clear highlight + setTimeout(() => { + copilotCoeditor.clearHighlights(); + }, 1000); + return { success: true, operatorId: args.operatorId, @@ -470,6 +594,7 @@ export function createGetOperatorResultInfoTool(workflowResultService: WorkflowR message: `Operator ${args.operatorId} has ${totalTuples} result tuples`, }; } catch (error: any) { + copilotCoeditor.clearHighlights(); return { success: false, error: error.message }; } }, From 219a23183d8e30532bf833f8eb3cd3c2fd049dbb Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Mon, 20 Oct 2025 19:47:23 -0700 Subject: [PATCH 019/158] improve styles --- .../copilot/copilot-coeditor.service.ts | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts diff --git a/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts b/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts new file mode 100644 index 00000000000..06c0ad7b5ac --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts @@ -0,0 +1,203 @@ +/** + * 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 { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; +import { CoeditorPresenceService } from "../workflow-graph/model/coeditor-presence.service"; +import { Coeditor, CoeditorState } from "../../../common/type/user"; + +/** + * CopilotCoeditorService manages the AI copilot as a virtual coeditor, + * allowing it to appear in the collaborative editing UI with its own avatar and presence indicators. + */ +@Injectable({ + providedIn: "root", +}) +export class CopilotCoeditorService { + // Virtual copilot coeditor + private readonly COPILOT_COEDITOR: Coeditor = { + uid: -1, + name: "AI", + email: "copilot@texera.ai", + role: "ADMIN" as any, + color: "#9333ea", // Purple color for copilot + clientId: "copilot-virtual", + comment: "", + }; + + private isRegistered = false; + private currentState: Partial = {}; + private currentEditingIntervalId?: NodeJS.Timer; + + constructor( + private workflowActionService: WorkflowActionService, + private coeditorPresenceService: CoeditorPresenceService + ) {} + + /** + * Register the copilot as a virtual coeditor + */ + public register(): void { + if (this.isRegistered) return; + + // Manually add copilot to coeditors list + if (!this.coeditorPresenceService.coeditors.find(c => c.clientId === this.COPILOT_COEDITOR.clientId)) { + this.coeditorPresenceService.coeditors.push(this.COPILOT_COEDITOR); + } + + this.isRegistered = true; + console.log("Copilot registered as virtual coeditor"); + } + + /** + * Unregister the copilot and clean up all visual indicators + */ + public unregister(): void { + if (!this.isRegistered) return; + + // Clear all current indicators + this.clearAll(); + + // Remove from coeditors list + const index = this.coeditorPresenceService.coeditors.findIndex(c => c.clientId === this.COPILOT_COEDITOR.clientId); + if (index >= 0) { + this.coeditorPresenceService.coeditors.splice(index, 1); + } + + this.isRegistered = false; + console.log("Copilot unregistered"); + } + + /** + * Check if copilot is registered + */ + public isActive(): boolean { + return this.isRegistered; + } + + /** + * Get the copilot coeditor object + */ + public getCopilot(): Coeditor { + return this.COPILOT_COEDITOR; + } + + /** + * Show that the copilot is currently viewing/editing an operator + */ + public showEditingOperator(operatorId: string): void { + if (!this.isRegistered) return; + + const jointWrapper = this.workflowActionService.getJointGraphWrapper(); + + // Clear previous editing indicator + if (this.currentEditingIntervalId && this.currentState.currentlyEditing) { + jointWrapper.removeCurrentEditing( + this.COPILOT_COEDITOR, + this.currentState.currentlyEditing, + this.currentEditingIntervalId + ); + } + + // Set new editing indicator + this.currentEditingIntervalId = jointWrapper.setCurrentEditing(this.COPILOT_COEDITOR, operatorId); + this.currentState.currentlyEditing = operatorId; + } + + /** + * Clear the copilot's editing indicator + */ + public clearEditingOperator(): void { + if (!this.isRegistered || !this.currentState.currentlyEditing) return; + + const jointWrapper = this.workflowActionService.getJointGraphWrapper(); + if (this.currentEditingIntervalId) { + jointWrapper.removeCurrentEditing( + this.COPILOT_COEDITOR, + this.currentState.currentlyEditing, + this.currentEditingIntervalId + ); + this.currentEditingIntervalId = undefined; + } + this.currentState.currentlyEditing = undefined; + } + + /** + * Show that the copilot changed a property on an operator + */ + public showPropertyChanged(operatorId: string): void { + if (!this.isRegistered) return; + + const jointWrapper = this.workflowActionService.getJointGraphWrapper(); + jointWrapper.setPropertyChanged(this.COPILOT_COEDITOR, operatorId); + + // Auto-clear after 2 seconds + setTimeout(() => { + jointWrapper.removePropertyChanged(this.COPILOT_COEDITOR, operatorId); + }, 2000); + + this.currentState.changed = operatorId; + } + + /** + * Highlight operators to show copilot is inspecting them + */ + public highlightOperators(operatorIds: string[]): void { + if (!this.isRegistered) return; + + const jointWrapper = this.workflowActionService.getJointGraphWrapper(); + + // Clear previous highlights + if (this.currentState.highlighted) { + this.currentState.highlighted.forEach(opId => { + jointWrapper.deleteCoeditorOperatorHighlight(this.COPILOT_COEDITOR, opId); + }); + } + + // Add new highlights + operatorIds.forEach(opId => { + jointWrapper.addCoeditorOperatorHighlight(this.COPILOT_COEDITOR, opId); + }); + + this.currentState.highlighted = operatorIds; + } + + /** + * Clear operator highlights + */ + public clearHighlights(): void { + if (!this.isRegistered || !this.currentState.highlighted) return; + + const jointWrapper = this.workflowActionService.getJointGraphWrapper(); + this.currentState.highlighted.forEach(opId => { + jointWrapper.deleteCoeditorOperatorHighlight(this.COPILOT_COEDITOR, opId); + }); + + this.currentState.highlighted = undefined; + } + + /** + * Clear all copilot presence indicators + */ + public clearAll(): void { + this.clearEditingOperator(); + this.clearHighlights(); + this.currentState = {}; + } +} From fcd8ad532fdc75934f21322fafc4d8720047bc6e Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Mon, 20 Oct 2025 20:45:00 -0700 Subject: [PATCH 020/158] fix add operator issue --- .../copilot-chat/copilot-chat.component.html | 52 +++++++++++----- .../copilot-chat/copilot-chat.component.scss | 36 +++++++++++ .../copilot-chat/copilot-chat.component.ts | 62 ++++++++++++++----- .../copilot/copilot-coeditor.service.ts | 8 ++- .../service/copilot/texera-copilot.ts | 7 +++ .../service/copilot/workflow-tools.ts | 14 +++-- 6 files changed, 142 insertions(+), 37 deletions(-) diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html index b38b7022ca3..1810d830c9a 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html @@ -20,8 +20,9 @@
- + class="chat-box" + [class.minimized]="!isExpanded"> +
Texera Copilot
- +
+ + + Show Results +
+ + +
+
- + { + // Format each tool call - show tool name, parameters, and optionally results + const traces = response.toolCalls.map((tc: any, index: number) => { // Log the actual structure to debug console.log("Tool call structure:", tc); @@ -92,10 +94,44 @@ export class CopilotChatComponent implements OnInit, OnDestroy { argsDisplay = " *(no parameters)*"; } - return `🔧 **${tc.toolName}**\n${argsDisplay}`; + let toolTrace = `🔧 **${tc.toolName}**\n${argsDisplay}`; + + // Add tool result if showToolResults is enabled + if (this.showToolResults && response.toolResults && response.toolResults[index]) { + const result = response.toolResults[index]; + const resultOutput = result.output || result.result || {}; + + // Format result based on success/error + if (resultOutput.success === false) { + toolTrace += `\n ❌ **Error:** ${resultOutput.error || "Unknown error"}`; + } else if (resultOutput.success === true) { + toolTrace += `\n ✅ **Success:** ${resultOutput.message || "Operation completed"}`; + // Include additional result details if present + const details = Object.entries(resultOutput) + .filter(([key]) => key !== "success" && key !== "message") + .map(([key, value]) => ` **${key}:** \`${JSON.stringify(value)}\``) + .join("\n"); + if (details) { + toolTrace += `\n${details}`; + } + } else { + // Show raw result if format is unexpected + toolTrace += `\n **Result:** \`${JSON.stringify(resultOutput)}\``; + } + } + + return toolTrace; }); output += traces.join("\n\n"); + + // Add token usage information if available + if (response.usage) { + const inputTokens = response.usage.inputTokens || 0; + const outputTokens = response.usage.outputTokens || 0; + output += `\n\n📊 **Tokens:** ${inputTokens} input, ${outputTokens} output`; + } + return output; } @@ -105,7 +141,7 @@ export class CopilotChatComponent implements OnInit, OnDestroy { ) {} ngOnInit(): void { - // Don't auto-initialize, wait for user to toggle chat + // Component initialization - copilot connection is triggered by menu button } ngOnDestroy(): void { @@ -128,7 +164,8 @@ export class CopilotChatComponent implements OnInit, OnDestroy { await this.copilotService.initialize(); this.isInitialized = true; - this.isChatVisible = true; // Show chat on connect + this.isChatVisible = true; // Show chat panel on connect + this.isExpanded = true; // Expand chat content by default this.updateConnectionStatus(); console.log("Copilot connected and registered as coeditor"); } catch (error) { @@ -166,17 +203,10 @@ export class CopilotChatComponent implements OnInit, OnDestroy { } /** - * Show the chat box (expand) + * Toggle expand/collapse of chat content (keeps header visible) */ - public showChat(): void { - this.isChatVisible = true; - } - - /** - * Collapse the chat box (hide without disconnecting) - */ - public collapseChat(): void { - this.isChatVisible = false; + public toggleExpand(): void { + this.isExpanded = !this.isExpanded; } /** diff --git a/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts b/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts index 06c0ad7b5ac..5054b2169b6 100644 --- a/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts +++ b/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts @@ -33,7 +33,7 @@ export class CopilotCoeditorService { // Virtual copilot coeditor private readonly COPILOT_COEDITOR: Coeditor = { uid: -1, - name: "AI", + name: "AI Agent", email: "copilot@texera.ai", role: "ADMIN" as any, color: "#9333ea", // Purple color for copilot @@ -102,7 +102,10 @@ export class CopilotCoeditorService { * Show that the copilot is currently viewing/editing an operator */ public showEditingOperator(operatorId: string): void { - if (!this.isRegistered) return; + if (!this.isRegistered) { + console.warn("Copilot not registered, cannot show editing indicator"); + return; + } const jointWrapper = this.workflowActionService.getJointGraphWrapper(); @@ -116,6 +119,7 @@ export class CopilotCoeditorService { } // Set new editing indicator + console.log(`Copilot showing editing for operator: ${operatorId}`); this.currentEditingIntervalId = jointWrapper.setCurrentEditing(this.COPILOT_COEDITOR, operatorId); this.currentState.currentlyEditing = operatorId; } diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index b7f36e8a187..0b88a2dcb66 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -63,6 +63,12 @@ export interface AgentResponse { // Raw data for subscribers to process toolCalls?: any[]; toolResults?: any[]; + usage?: { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + cachedInputTokens?: number; + }; } /** @@ -207,6 +213,7 @@ export class TexeraCopilot { isDone: false, toolCalls, toolResults, + usage, }; // Emit raw trace data diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index f5b3ef3a296..ee6fdc813a3 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -63,16 +63,20 @@ export function createAddOperatorTool( const defaultY = 100 + Math.floor(existingOperators.length / 5) * 150; const position = { x: defaultX, y: defaultY }; - // Show copilot is adding this operator - copilotCoeditor.showEditingOperator(operator.operatorID); - - // Add the operator to the workflow + // Add the operator to the workflow first workflowActionService.addOperator(operator, position); + // Show copilot is adding this operator (after it's added to graph) + setTimeout(() => { + copilotCoeditor.showEditingOperator(operator.operatorID); + copilotCoeditor.highlightOperators([operator.operatorID]); + }, 100); + // Clear presence indicator after a brief delay setTimeout(() => { copilotCoeditor.clearEditingOperator(); - }, 1000); + copilotCoeditor.clearHighlights(); + }, 1500); return { success: true, From 1ed62c5cc90643b8e24d5d498598a13ac3dac819 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Mon, 20 Oct 2025 21:54:59 -0700 Subject: [PATCH 021/158] refine the tool calls --- .../copilot-chat/copilot-chat.component.html | 39 ++++--- .../copilot-chat/copilot-chat.component.scss | 42 +++++++ .../copilot-chat/copilot-chat.component.ts | 26 +++-- .../copilot/copilot-coeditor.service.ts | 2 +- .../service/copilot/texera-copilot.ts | 28 ++++- .../service/copilot/workflow-tools.ts | 103 ++++++++++++++++-- 6 files changed, 201 insertions(+), 39 deletions(-) diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html index 1810d830c9a..a7b382a82c3 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html @@ -67,19 +67,32 @@
- - +
+ + + + +
+ + Processing... +
+
{ - // Format the response based on type - let displayText = ""; - if (response.type === "trace") { // Format tool traces - displayText = this.formatToolTrace(response); + const displayText = this.formatToolTrace(response); + // Add trace message via addMessage API if (displayText && this.deepChatElement?.nativeElement?.addMessage) { this.deepChatElement.nativeElement.addMessage({ role: "ai", text: displayText }); } + + // Keep processing state true - loading indicator stays visible } else if (response.type === "response") { // For final response, signal completion with the content - // This will let deep-chat handle adding the message + // This will let deep-chat handle adding the message and clearing loading if (response.isDone) { + this.isProcessing = false; // Clear processing state signals.onResponse({ text: response.content }); } } }, error: (e: unknown) => { + this.isProcessing = false; // Clear processing state on error signals.onResponse({ error: e ?? "Unknown error" }); }, }); @@ -60,6 +65,7 @@ export class CopilotChatComponent implements OnInit, OnDestroy { demo: false, introMessage: { text: "Hi! I'm Texera Copilot. I can help you build and modify workflows." }, textInput: { placeholder: { text: "Ask me anything about workflows..." } }, + requestBodyLimits: { maxMessages: -1 }, // Allow unlimited message history }; /** @@ -140,10 +146,6 @@ export class CopilotChatComponent implements OnInit, OnDestroy { private copilotCoeditorService: CopilotCoeditorService ) {} - ngOnInit(): void { - // Component initialization - copilot connection is triggered by menu button - } - ngOnDestroy(): void { // Cleanup when component is destroyed this.disconnect(); diff --git a/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts b/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts index 5054b2169b6..46efdf15aac 100644 --- a/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts +++ b/frontend/src/app/workspace/service/copilot/copilot-coeditor.service.ts @@ -33,7 +33,7 @@ export class CopilotCoeditorService { // Virtual copilot coeditor private readonly COPILOT_COEDITOR: Coeditor = { uid: -1, - name: "AI Agent", + name: "AI", email: "copilot@texera.ai", role: "ADMIN" as any, color: "#9333ea", // Purple color for copilot diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 0b88a2dcb66..695946abc59 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -32,7 +32,9 @@ import { createDeleteOperatorTool, createDeleteLinkTool, createSetOperatorPropertyTool, - createGetDynamicSchemaTool, + createGetOperatorSchemaTool, + createGetOperatorInputSchemaTool, + createGetWorkflowCompilationStateTool, createExecuteWorkflowTool, createGetExecutionStateTool, createHasOperatorResultTool, @@ -48,6 +50,7 @@ import { DynamicSchemaService } from "../dynamic-schema/dynamic-schema.service"; import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { CopilotCoeditorService } from "./copilot-coeditor.service"; +import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; // API endpoints as constants export const COPILOT_MCP_URL = "mcp"; @@ -93,7 +96,8 @@ export class TexeraCopilot { private dynamicSchemaService: DynamicSchemaService, private executeWorkflowService: ExecuteWorkflowService, private workflowResultService: WorkflowResultService, - private copilotCoeditorService: CopilotCoeditorService + private copilotCoeditorService: CopilotCoeditorService, + private workflowCompilingService: WorkflowCompilingService ) { // Don't auto-initialize, wait for user to enable } @@ -197,7 +201,12 @@ export class TexeraCopilot { model: this.model, messages: this.messages, // full history tools, - system: "You are Texera Copilot, an AI assistant for building and modifying data workflows.", + system: + "You are Texera Copilot, an AI assistant for building and modifying data workflows. " + + "Your task is helping user explore the data using operators. " + + "Common operators would be Limit to limit the size of data; " + + "Aggregate to do some aggregation; and some visualization operator. " + + "A good generation style is adding an operator, configuring its property and then executing it to make sure each editing is valid. Generate 3-5 operators is enough for every round of generation", stopWhen: stepCountIs(50), // optional: observe every completed step (tool calls + results available) @@ -271,11 +280,16 @@ export class TexeraCopilot { this.workflowActionService, this.copilotCoeditorService ); - const getDynamicSchemaTool = createGetDynamicSchemaTool( - this.dynamicSchemaService, + const getOperatorSchemaTool = createGetOperatorSchemaTool( this.workflowActionService, + this.operatorMetadataService, + this.copilotCoeditorService + ); + const getOperatorInputSchemaTool = createGetOperatorInputSchemaTool( + this.workflowCompilingService, this.copilotCoeditorService ); + const getWorkflowCompilationStateTool = createGetWorkflowCompilationStateTool(this.workflowCompilingService); const executeWorkflowTool = createExecuteWorkflowTool(this.executeWorkflowService); const getExecutionStateTool = createGetExecutionStateTool(this.executeWorkflowService); const hasOperatorResultTool = createHasOperatorResultTool( @@ -308,7 +322,9 @@ export class TexeraCopilot { deleteOperator: deleteOperatorTool, deleteLink: deleteLinkTool, setOperatorProperty: setOperatorPropertyTool, - getDynamicSchema: getDynamicSchemaTool, + getOperatorSchema: getOperatorSchemaTool, + getOperatorInputSchema: getOperatorInputSchemaTool, + getWorkflowCompilationState: getWorkflowCompilationStateTool, executeWorkflow: executeWorkflowTool, getExecutionState: getExecutionStateTool, hasOperatorResult: hasOperatorResultTool, diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index ee6fdc813a3..8db27a64e14 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -28,6 +28,7 @@ import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.ser import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { ExecutionState } from "../../types/execute-workflow.interface"; import { CopilotCoeditorService } from "./copilot-coeditor.service"; +import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; /** * Create addOperator tool for adding a new operator to the workflow @@ -369,17 +370,18 @@ export function createSetOperatorPropertyTool( } /** - * Create getDynamicSchema tool for getting operator schema information + * Create getOperatorSchema tool for getting operator schema information + * Returns the original operator schema (not the dynamic one) to save tokens */ -export function createGetDynamicSchemaTool( - dynamicSchemaService: DynamicSchemaService, +export function createGetOperatorSchemaTool( workflowActionService: WorkflowActionService, + operatorMetadataService: OperatorMetadataService, copilotCoeditor: CopilotCoeditorService ) { return tool({ - name: "getDynamicSchema", + name: "getOperatorSchema", description: - "Get the dynamic schema of an operator, which includes all available properties and their types. Use this to understand what properties can be edited on an operator before modifying it.", + "Get the original schema of an operator, which includes all available properties and their types. Use this to understand what properties can be edited on an operator before modifying it.", inputSchema: z.object({ operatorId: z.string().describe("ID of the operator to get schema for"), }), @@ -388,7 +390,15 @@ export function createGetDynamicSchemaTool( // Highlight the operator being inspected copilotCoeditor.highlightOperators([args.operatorId]); - const schema = dynamicSchemaService.getDynamicSchema(args.operatorId); + // Get the operator to find its type + const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); + if (!operator) { + copilotCoeditor.clearHighlights(); + return { success: false, error: `Operator ${args.operatorId} not found` }; + } + + // Get the original operator schema from metadata + const schema = operatorMetadataService.getOperatorSchema(operator.operatorType); // Clear highlight after a brief delay setTimeout(() => { @@ -398,7 +408,54 @@ export function createGetDynamicSchemaTool( return { success: true, schema: schema, - message: `Retrieved schema for operator ${args.operatorId}`, + message: `Retrieved original schema for operator ${args.operatorId} (type: ${operator.operatorType})`, + }; + } catch (error: any) { + copilotCoeditor.clearHighlights(); + return { success: false, error: error.message }; + } + }, + }); +} + +/** + * Create getOperatorInputSchema tool for getting operator's input schema from compilation + */ +export function createGetOperatorInputSchemaTool( + workflowCompilingService: WorkflowCompilingService, + copilotCoeditor: CopilotCoeditorService +) { + return tool({ + name: "getOperatorInputSchema", + description: + "Get the input schema for an operator, which shows what columns/attributes are available from upstream operators. This is determined by workflow compilation and schema propagation.", + inputSchema: z.object({ + operatorId: z.string().describe("ID of the operator to get input schema for"), + }), + execute: async (args: { operatorId: string }) => { + try { + // Highlight the operator being inspected + copilotCoeditor.highlightOperators([args.operatorId]); + + const inputSchemaMap = workflowCompilingService.getOperatorInputSchemaMap(args.operatorId); + + // Clear highlight after a brief delay + setTimeout(() => { + copilotCoeditor.clearHighlights(); + }, 1200); + + if (!inputSchemaMap) { + return { + success: true, + inputSchema: null, + message: `Operator ${args.operatorId} has no input schema (may be a source operator or not connected)`, + }; + } + + return { + success: true, + inputSchema: inputSchemaMap, + message: `Retrieved input schema for operator ${args.operatorId}`, }; } catch (error: any) { copilotCoeditor.clearHighlights(); @@ -408,6 +465,38 @@ export function createGetDynamicSchemaTool( }); } +/** + * Create getWorkflowCompilationState tool for checking compilation status and errors + */ +export function createGetWorkflowCompilationStateTool(workflowCompilingService: WorkflowCompilingService) { + return tool({ + name: "getWorkflowCompilationState", + description: + "Get the current workflow compilation state and any compilation errors. Use this to check if the workflow is valid and identify any operator configuration issues.", + inputSchema: z.object({}), + execute: async () => { + try { + const compilationState = workflowCompilingService.getWorkflowCompilationState(); + const compilationErrors = workflowCompilingService.getWorkflowCompilationErrors(); + + const hasErrors = Object.keys(compilationErrors).length > 0; + + return { + success: true, + state: compilationState, + hasErrors: hasErrors, + errors: hasErrors ? compilationErrors : undefined, + message: hasErrors + ? `Workflow compilation failed with ${Object.keys(compilationErrors).length} error(s)` + : `Workflow compilation state: ${compilationState}`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} + /** * Create executeWorkflow tool for running the workflow */ From c02190d0604f6ec3116b7331d1cea8171b6cbdc4 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 21 Oct 2025 09:17:43 -0700 Subject: [PATCH 022/158] further improve the style --- .../copilot-chat/copilot-chat.component.html | 13 +++ .../copilot-chat/copilot-chat.component.ts | 8 ++ .../service/copilot/texera-copilot.ts | 51 ++++++++++- .../service/copilot/workflow-tools.ts | 91 ++++++------------- 4 files changed, 97 insertions(+), 66 deletions(-) diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html index a7b382a82c3..0e5d8796359 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html @@ -40,6 +40,19 @@ Show Results
+ + +
diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts index 626200ebb22..48c38a365dc 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts @@ -1,7 +1,7 @@ // copilot-chat.component.ts import { Component, OnDestroy, ViewChild, ElementRef } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { TexeraCopilot, AgentResponse } from "../../service/copilot/texera-copilot"; +import { TexeraCopilot, AgentResponse, CopilotState } from "../../service/copilot/texera-copilot"; import { CopilotCoeditorService } from "../../service/copilot/copilot-coeditor.service"; @UntilDestroy() @@ -17,7 +17,6 @@ export class CopilotChatComponent implements OnDestroy { public isExpanded = true; // Whether chat content is expanded or minimized public showToolResults = false; // Whether to show tool call results public isConnected = false; - public isProcessing = false; // Whether agent is currently processing a request private isInitialized = false; // Deep-chat configuration @@ -27,9 +26,6 @@ export class CopilotChatComponent implements OnDestroy { const last = body?.messages?.[body.messages.length - 1]; const userText: string = typeof last?.text === "string" ? last.text : ""; - // Set processing state to show loading indicator - this.isProcessing = true; - // Send message to copilot and process AgentResponse this.copilotService .sendMessage(userText) @@ -50,20 +46,22 @@ export class CopilotChatComponent implements OnDestroy { // For final response, signal completion with the content // This will let deep-chat handle adding the message and clearing loading if (response.isDone) { - this.isProcessing = false; // Clear processing state signals.onResponse({ text: response.content }); } } }, error: (e: unknown) => { - this.isProcessing = false; // Clear processing state on error signals.onResponse({ error: e ?? "Unknown error" }); }, complete: () => { // Handle completion without final response (happens when generation is stopped) - if (this.isProcessing) { - this.isProcessing = false; - signals.onResponse({ text: "_Generation stopped by user._" }); + const currentState = this.copilotService.getState(); + if (currentState === CopilotState.STOPPING) { + // Generation was stopped by user - show completion message + signals.onResponse({ text: "_Generation stopped._" }); + } else if (currentState === CopilotState.GENERATING) { + // Generation completed unexpectedly + signals.onResponse({ text: "_Generation completed._" }); } }, }); @@ -223,7 +221,39 @@ export class CopilotChatComponent implements OnDestroy { */ public stopGeneration(): void { this.copilotService.stopGeneration(); - this.isProcessing = false; + } + + /** + * Clear message history + */ + public clearMessages(): void { + this.copilotService.clearMessages(); + + // Clear deep-chat UI messages + if (this.deepChatElement?.nativeElement?.clearMessages) { + this.deepChatElement.nativeElement.clearMessages(true); + } + } + + /** + * Check if copilot is currently generating + */ + public isGenerating(): boolean { + return this.copilotService.getState() === CopilotState.GENERATING; + } + + /** + * Check if copilot is currently stopping + */ + public isStopping(): boolean { + return this.copilotService.getState() === CopilotState.STOPPING; + } + + /** + * Check if copilot is available (can send messages) + */ + public isAvailable(): boolean { + return this.copilotService.getState() === CopilotState.AVAILABLE; } /** diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index cef896beae2..b87a8cf99d1 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -67,6 +67,16 @@ import { COPILOT_SYSTEM_PROMPT } from "./copilot-prompts"; export const COPILOT_MCP_URL = "mcp"; export const AGENT_MODEL_ID = "claude-3.7"; +/** + * Copilot state enum + */ +export enum CopilotState { + UNAVAILABLE = "Unavailable", + AVAILABLE = "Available", + GENERATING = "Generating", + STOPPING = "Stopping", +} + /** * Agent response structure for streaming intermediate and final results */ @@ -100,8 +110,8 @@ export class TexeraCopilot { // Message history using AI SDK's ModelMessage type private messages: ModelMessage[] = []; - // AbortController for stopping generation - private currentAbortController?: AbortController; + // Copilot state management + private state: CopilotState = CopilotState.UNAVAILABLE; constructor( private workflowActionService: WorkflowActionService, @@ -131,9 +141,13 @@ export class TexeraCopilot { apiKey: "dummy", }).chat(AGENT_MODEL_ID); + // 3. Set state to Available + this.state = CopilotState.AVAILABLE; + console.log("Texera Copilot initialized successfully"); } catch (error: unknown) { console.error("Failed to initialize copilot:", error); + this.state = CopilotState.UNAVAILABLE; throw error; } } @@ -204,8 +218,8 @@ export class TexeraCopilot { return; } - // Create new AbortController for this generation - this.currentAbortController = new AbortController(); + // Set state to Generating + this.state = CopilotState.GENERATING; // 1) push the user message (don't emit to stream - already handled by UI) const userMessage: UserModelMessage = { role: "user", content: message }; @@ -214,15 +228,22 @@ export class TexeraCopilot { // 2) define tools (your existing helpers) const tools = this.createWorkflowTools(); - // 3) run multi-step with stopWhen + // 3) run multi-step with stopWhen to check for user stop request generateText({ model: this.model, messages: this.messages, // full history tools, - abortSignal: this.currentAbortController.signal, system: COPILOT_SYSTEM_PROMPT, - stopWhen: stepCountIs(50), - + // Stop when: user requested stop OR reached 50 steps + stopWhen: ({ steps }) => { + // Check if user requested stop + if (this.state === CopilotState.STOPPING) { + console.log("Stopping generation due to user request"); + return true; + } + // Otherwise use the default step count limit + return stepCountIs(50)({ steps }); + }, // optional: observe every completed step (tool calls + results available) onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage }) => { // Log each step for debugging @@ -258,6 +279,9 @@ export class TexeraCopilot { // Clear all copilot presence indicators when generation completes this.copilotCoeditorService.clearAll(); + // Set state back to Available + this.state = CopilotState.AVAILABLE; + // Emit final response with raw data const finalResponse: AgentResponse = { type: "response", @@ -271,18 +295,14 @@ export class TexeraCopilot { // Clear all copilot presence indicators on error this.copilotCoeditorService.clearAll(); - // Check if error is due to user abort - if (err?.name === "AbortError" || err?.message?.includes("aborted")) { - console.log("Generation stopped by user"); - // Just complete the observable without adding error message to history - observer.complete(); - } else { - // For real errors, add to message history and propagate error - const errorText = `Error: ${err?.message ?? String(err)}`; - const assistantError: AssistantModelMessage = { role: "assistant", content: errorText }; - this.messages.push(assistantError); - observer.error(err); - } + // Set state back to Available + this.state = CopilotState.AVAILABLE; + + // For errors, add to message history and propagate error + const errorText = `Error: ${err?.message ?? String(err)}`; + const assistantError: AssistantModelMessage = { role: "assistant", content: errorText }; + this.messages.push(assistantError); + observer.error(err); }); }); } @@ -425,14 +445,34 @@ export class TexeraCopilot { } /** - * Stop the current generation without clearing messages + * Stop the current generation (async - waits for generation to actually stop) */ public stopGeneration(): void { - if (this.currentAbortController) { - this.currentAbortController.abort(); - this.currentAbortController = undefined; - console.log("Generation stopped"); + if (this.state !== CopilotState.GENERATING) { + console.log("Not generating, nothing to stop"); + return; } + + // Set state to Stopping - stopWhen callback will detect this and stop generation + this.state = CopilotState.STOPPING; + console.log("Stopping generation..."); + + // State will be set back to Available when the generation completes via stopWhen + } + + /** + * Clear message history + */ + public clearMessages(): void { + this.messages = []; + console.log("Message history cleared"); + } + + /** + * Get current copilot state + */ + public getState(): CopilotState { + return this.state; } /** @@ -440,7 +480,12 @@ export class TexeraCopilot { */ public async disconnect(): Promise { // Stop any ongoing generation - this.stopGeneration(); + if (this.state === CopilotState.GENERATING) { + this.stopGeneration(); + } + + // Clear message history + this.clearMessages(); // Disconnect the MCP client if it exists if (this.mcpClient) { @@ -448,6 +493,9 @@ export class TexeraCopilot { this.mcpClient = undefined; } + // Set state to Unavailable + this.state = CopilotState.UNAVAILABLE; + console.log("Copilot disconnected"); } @@ -455,6 +503,6 @@ export class TexeraCopilot { * Check if copilot is connected */ public isConnected(): boolean { - return this.mcpClient !== undefined && this.model !== undefined; + return this.state !== CopilotState.UNAVAILABLE; } } From 6e276452c82e20b3e49a71fe1011c9cd1fe24874 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sun, 26 Oct 2025 23:30:32 -0700 Subject: [PATCH 029/158] correct style --- .../copilot-chat/copilot-chat.component.html | 29 +++++++-------- .../copilot-chat/copilot-chat.component.scss | 35 ++++++++++++++++++- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html index cd46a744b70..29f675f165c 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html @@ -40,20 +40,6 @@ Show Results
- -
diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.scss b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.scss index 16900bb983e..94c0c219bd8 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.scss +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.scss @@ -39,6 +39,10 @@ right: 20px; width: 500px; height: 500px; + min-width: 350px; + min-height: 300px; + max-width: 90vw; + max-height: 90vh; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); @@ -46,10 +50,19 @@ flex-direction: column; z-index: 1000; animation: slideIn 0.3s ease; - transition: height 0.3s ease; + overflow: auto; + resize: both; + // Flip horizontally to move resize handle to bottom-left + transform: scaleX(-1); + + // Flip content back to normal + > * { + transform: scaleX(-1); + } &.minimized { height: 60px; + resize: none; } } @@ -152,6 +165,26 @@ background-color: white; } } + + .stop-button { + color: white; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + margin-left: 4px; + + &:hover { + background: rgba(255, 255, 255, 0.2); + } + + i { + font-size: 14px; + } + } } @keyframes fadeIn { From dc94ef74cec5a8354221f90a65a7d01f34f54c5b Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Mon, 27 Oct 2025 09:48:39 -0700 Subject: [PATCH 030/158] enable AI customized name --- .../app/workspace/service/copilot/workflow-tools.ts | 10 +++++++--- .../workflow-graph/util/workflow-util.service.ts | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index 71b3eff7a9b..ad813e33c81 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -105,8 +105,12 @@ export function createAddOperatorTool( description: "Add a new operator to the workflow", inputSchema: z.object({ operatorType: z.string().describe("Type of operator (e.g., 'CSVSource', 'Filter', 'Aggregate')"), + customDisplayName: z + .string() + .optional() + .describe("Brief custom name summarizing what this operator does in one sentence"), }), - execute: async (args: { operatorType: string }) => { + execute: async (args: { operatorType: string; customDisplayName?: string }) => { try { // Clear previous highlights at start of tool execution copilotCoeditor.clearAll(); @@ -119,8 +123,8 @@ export function createAddOperatorTool( }; } - // Get a new operator predicate with default settings - const operator = workflowUtilService.getNewOperatorPredicate(args.operatorType); + // Get a new operator predicate with default settings and optional custom display name + const operator = workflowUtilService.getNewOperatorPredicate(args.operatorType, args.customDisplayName); // Calculate a default position (can be adjusted by auto-layout later) const existingOperators = workflowActionService.getTexeraGraph().getAllOperators(); diff --git a/frontend/src/app/workspace/service/workflow-graph/util/workflow-util.service.ts b/frontend/src/app/workspace/service/workflow-graph/util/workflow-util.service.ts index a85048249cd..8bca014062a 100644 --- a/frontend/src/app/workspace/service/workflow-graph/util/workflow-util.service.ts +++ b/frontend/src/app/workspace/service/workflow-graph/util/workflow-util.service.ts @@ -113,7 +113,7 @@ export class WorkflowUtilService { * @param operatorType type of an Operator * @returns a new OperatorPredicate of the operatorType */ - public getNewOperatorPredicate(operatorType: string): OperatorPredicate { + public getNewOperatorPredicate(operatorType: string, customDisplayName?: string): OperatorPredicate { const operatorSchema = this.operatorSchemaList.find(schema => schema.operatorType === operatorType); if (operatorSchema === undefined) { throw new Error(`operatorType ${operatorType} doesn't exist in operator metadata`); @@ -138,8 +138,8 @@ export class WorkflowUtilService { // by default, the operator is not disabled const isDisabled = false; - // by default, the operator name is the user friendly name - const customDisplayName = operatorSchema.additionalMetadata.userFriendlyName; + // Use provided customDisplayName or default to the user friendly name from schema + const displayName = customDisplayName ?? operatorSchema.additionalMetadata.userFriendlyName; const dynamicInputPorts = operatorSchema.additionalMetadata.dynamicInputPorts ?? false; const dynamicOutputPorts = operatorSchema.additionalMetadata.dynamicOutputPorts ?? false; @@ -167,7 +167,7 @@ export class WorkflowUtilService { outputPorts, showAdvanced, isDisabled, - customDisplayName, + customDisplayName: displayName, dynamicInputPorts, dynamicOutputPorts, }; From e3a01e76eec15c893b11b780b3654f873a5dbb6c Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 28 Oct 2025 09:04:02 -0700 Subject: [PATCH 031/158] add experimental message click button pop up --- .../copilot-chat/copilot-chat.component.html | 36 ++++ .../copilot-chat/copilot-chat.component.scss | 61 +++++++ .../copilot-chat/copilot-chat.component.ts | 169 +++++++++++++++++- 3 files changed, 264 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html index 29f675f165c..9d793018442 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html @@ -97,6 +97,42 @@ [requestBodyLimits]="deepChatConfig.requestBodyLimits"> + +
+ + + +
+
{ + this.handleDeepChatClick(event); + }); + console.log("Click listener attached to shadow root"); + } else { + // Fallback: try attaching to the element itself + deepChatElement.addEventListener("click", (event: MouseEvent) => { + this.handleDeepChatClick(event); + }); + console.log("Click listener attached to deep-chat element (no shadow root)"); + } + + // Also set up a MutationObserver to handle dynamically added messages + const targetNode = shadowRoot || deepChatElement; + const observer = new MutationObserver(() => { + console.log("DOM mutation detected in deep-chat"); + }); + + observer.observe(targetNode, { + childList: true, + subtree: true, + }); + } + + /** + * Handle clicks within the deep-chat component + */ + private handleDeepChatClick(event: MouseEvent): void { + const target = event.target as HTMLElement; + + // Log the clicked element for debugging + console.log("Clicked element:", target); + console.log("Element classes:", target.className); + console.log("Element tag:", target.tagName); + + // Try to find the message bubble by traversing up the DOM tree + let messageElement = this.findMessageBubble(target); + + if (messageElement) { + // Check if it's an AI message (not a user message) + const isAiMessage = this.isAiMessage(messageElement); + + console.log("Found message element, is AI:", isAiMessage); + + if (isAiMessage) { + this.onMessageClick(messageElement); + } + } + } + + /** + * Find the message bubble element by traversing up the DOM + */ + private findMessageBubble(element: HTMLElement): HTMLElement | null { + let current: HTMLElement | null = element; + let depth = 0; + const maxDepth = 10; // Prevent infinite loops + + while (current && depth < maxDepth) { + // Log each level for debugging + console.log(`Level ${depth}:`, current.className, current.tagName); + + // Check if this element looks like a message bubble + // Common patterns: message-bubble, message, bubble, etc. + const className = current.className || ""; + if ( + className.includes("message") || + className.includes("bubble") || + current.hasAttribute("data-message") || + current.classList?.contains("message") + ) { + console.log("Found message bubble:", current); + return current; + } + + current = current.parentElement; + depth++; + } + + return null; + } + + /** + * Check if a message element is from the AI + */ + private isAiMessage(element: HTMLElement): boolean { + const className = element.className || ""; + const parent = element.parentElement; + const parentClass = parent?.className || ""; + + // Check various patterns that might indicate an AI message + return ( + className.includes("ai") || + className.includes("assistant") || + className.includes("bot") || + parentClass.includes("ai") || + parentClass.includes("assistant") || + parentClass.includes("bot") || + (element.hasAttribute("role") && element.getAttribute("role") === "ai") + ); + } + + /** + * Handle message click event + */ + private onMessageClick(messageElement: HTMLElement): void { + // Remove 'selected' class from all previously selected messages + const deepChat = this.deepChatElement?.nativeElement; + if (deepChat) { + const allElements = deepChat.querySelectorAll(".selected"); + allElements.forEach((el: Element) => el.classList.remove("selected")); + } + + // Add 'selected' class to clicked message + messageElement.classList.add("selected"); + + // Update selected message index (use a timestamp or unique ID) + this.selectedMessageIndex = Date.now(); + + console.log("Agent message selected!"); + } + + /** + * Close message action buttons + */ + public closeMessageActions(): void { + this.selectedMessageIndex = null; + + // Remove 'selected' class from all messages + const allMessages = this.deepChatElement?.nativeElement.querySelectorAll(".ai-message, [class*=\"ai\"], [role=\"ai\"]"); + allMessages?.forEach((msg: Element) => msg.classList.remove("selected")); + } + ngOnDestroy(): void { // Cleanup when component is destroyed this.disconnect(); @@ -175,6 +335,11 @@ export class CopilotChatComponent implements OnDestroy { this.isExpanded = true; // Expand chat content by default this.updateConnectionStatus(); console.log("Copilot connected and registered as coeditor"); + + // Set up click listeners after the chat panel is rendered + setTimeout(() => { + this.setupMessageClickListeners(); + }, 1000); } catch (error) { console.error("Failed to connect copilot:", error); this.copilotCoeditorService.unregister(); From 7ee327a1fb0b7898b1dfa2d7d8a0f6c1b826ecb6 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 28 Oct 2025 09:10:15 -0700 Subject: [PATCH 032/158] Revert "add experimental message click button pop up" This reverts commit 79f04901715ac2edc846b731535f288dd4617ca3. --- .../copilot-chat/copilot-chat.component.html | 36 ---- .../copilot-chat/copilot-chat.component.scss | 61 ------- .../copilot-chat/copilot-chat.component.ts | 169 +----------------- 3 files changed, 2 insertions(+), 264 deletions(-) diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html index 9d793018442..29f675f165c 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html @@ -97,42 +97,6 @@ [requestBodyLimits]="deepChatConfig.requestBodyLimits"> - -
- - - -
-
{ - this.handleDeepChatClick(event); - }); - console.log("Click listener attached to shadow root"); - } else { - // Fallback: try attaching to the element itself - deepChatElement.addEventListener("click", (event: MouseEvent) => { - this.handleDeepChatClick(event); - }); - console.log("Click listener attached to deep-chat element (no shadow root)"); - } - - // Also set up a MutationObserver to handle dynamically added messages - const targetNode = shadowRoot || deepChatElement; - const observer = new MutationObserver(() => { - console.log("DOM mutation detected in deep-chat"); - }); - - observer.observe(targetNode, { - childList: true, - subtree: true, - }); - } - - /** - * Handle clicks within the deep-chat component - */ - private handleDeepChatClick(event: MouseEvent): void { - const target = event.target as HTMLElement; - - // Log the clicked element for debugging - console.log("Clicked element:", target); - console.log("Element classes:", target.className); - console.log("Element tag:", target.tagName); - - // Try to find the message bubble by traversing up the DOM tree - let messageElement = this.findMessageBubble(target); - - if (messageElement) { - // Check if it's an AI message (not a user message) - const isAiMessage = this.isAiMessage(messageElement); - - console.log("Found message element, is AI:", isAiMessage); - - if (isAiMessage) { - this.onMessageClick(messageElement); - } - } - } - - /** - * Find the message bubble element by traversing up the DOM - */ - private findMessageBubble(element: HTMLElement): HTMLElement | null { - let current: HTMLElement | null = element; - let depth = 0; - const maxDepth = 10; // Prevent infinite loops - - while (current && depth < maxDepth) { - // Log each level for debugging - console.log(`Level ${depth}:`, current.className, current.tagName); - - // Check if this element looks like a message bubble - // Common patterns: message-bubble, message, bubble, etc. - const className = current.className || ""; - if ( - className.includes("message") || - className.includes("bubble") || - current.hasAttribute("data-message") || - current.classList?.contains("message") - ) { - console.log("Found message bubble:", current); - return current; - } - - current = current.parentElement; - depth++; - } - - return null; - } - - /** - * Check if a message element is from the AI - */ - private isAiMessage(element: HTMLElement): boolean { - const className = element.className || ""; - const parent = element.parentElement; - const parentClass = parent?.className || ""; - - // Check various patterns that might indicate an AI message - return ( - className.includes("ai") || - className.includes("assistant") || - className.includes("bot") || - parentClass.includes("ai") || - parentClass.includes("assistant") || - parentClass.includes("bot") || - (element.hasAttribute("role") && element.getAttribute("role") === "ai") - ); - } - - /** - * Handle message click event - */ - private onMessageClick(messageElement: HTMLElement): void { - // Remove 'selected' class from all previously selected messages - const deepChat = this.deepChatElement?.nativeElement; - if (deepChat) { - const allElements = deepChat.querySelectorAll(".selected"); - allElements.forEach((el: Element) => el.classList.remove("selected")); - } - - // Add 'selected' class to clicked message - messageElement.classList.add("selected"); - - // Update selected message index (use a timestamp or unique ID) - this.selectedMessageIndex = Date.now(); - - console.log("Agent message selected!"); - } - - /** - * Close message action buttons - */ - public closeMessageActions(): void { - this.selectedMessageIndex = null; - - // Remove 'selected' class from all messages - const allMessages = this.deepChatElement?.nativeElement.querySelectorAll(".ai-message, [class*=\"ai\"], [role=\"ai\"]"); - allMessages?.forEach((msg: Element) => msg.classList.remove("selected")); - } - ngOnDestroy(): void { // Cleanup when component is destroyed this.disconnect(); @@ -335,11 +175,6 @@ export class CopilotChatComponent implements OnDestroy, AfterViewInit { this.isExpanded = true; // Expand chat content by default this.updateConnectionStatus(); console.log("Copilot connected and registered as coeditor"); - - // Set up click listeners after the chat panel is rendered - setTimeout(() => { - this.setupMessageClickListeners(); - }, 1000); } catch (error) { console.error("Failed to connect copilot:", error); this.copilotCoeditorService.unregister(); From 00df02aee667a866e3c95c7e558e386b1abccad1 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 28 Oct 2025 11:15:49 -0700 Subject: [PATCH 033/158] add experimental streamText --- .../copilot-chat/copilot-chat.component.ts | 73 +++----- .../service/copilot/texera-copilot.ts | 156 +++++++++++------- 2 files changed, 124 insertions(+), 105 deletions(-) diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts index 48c38a365dc..4766099dccf 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts @@ -33,20 +33,22 @@ export class CopilotChatComponent implements OnDestroy { .subscribe({ next: (response: AgentResponse) => { if (response.type === "trace") { - // Format tool traces - const displayText = this.formatToolTrace(response); - - // Add trace message via addMessage API - if (displayText && this.deepChatElement?.nativeElement?.addMessage) { - this.deepChatElement.nativeElement.addMessage({ role: "ai", text: displayText }); + // Append tool trace message + if (response.toolCalls && response.toolCalls.length > 0) { + const displayText = this.formatToolTrace(response); + if (displayText && this.deepChatElement?.nativeElement?.addMessage) { + this.deepChatElement.nativeElement.addMessage({ role: "ai", text: displayText }); + } } - - // Keep processing state true - loading indicator stays visible } else if (response.type === "response") { - // For final response, signal completion with the content - // This will let deep-chat handle adding the message and clearing loading if (response.isDone) { - signals.onResponse({ text: response.content }); + // Final signal - clear loading indicator + signals.onResponse({ text: "" }); + } else { + // Append accumulated text as a new message + if (response.content && this.deepChatElement?.nativeElement?.addMessage) { + this.deepChatElement.nativeElement.addMessage({ role: "ai", text: response.content }); + } } } }, @@ -54,15 +56,7 @@ export class CopilotChatComponent implements OnDestroy { signals.onResponse({ error: e ?? "Unknown error" }); }, complete: () => { - // Handle completion without final response (happens when generation is stopped) - const currentState = this.copilotService.getState(); - if (currentState === CopilotState.STOPPING) { - // Generation was stopped by user - show completion message - signals.onResponse({ text: "_Generation stopped._" }); - } else if (currentState === CopilotState.GENERATING) { - // Generation completed unexpectedly - signals.onResponse({ text: "_Generation completed._" }); - } + // Observable complete }, }); }, @@ -81,53 +75,36 @@ export class CopilotChatComponent implements OnDestroy { return ""; } - // Include agent's thinking/text if available let output = ""; - if (response.content && response.content.trim()) { - output += `💭 **Agent:** ${response.content}\n\n`; - } - // Format each tool call - show tool name, parameters, and optionally results + // Format each tool call const traces = response.toolCalls.map((tc: any, index: number) => { - // Log the actual structure to debug - console.log("Tool call structure:", tc); - - // Try multiple possible property names for arguments - const args = tc.args || tc.arguments || tc.parameters || tc.input || {}; + // Handle tool call chunk (from onChunk) or full tool call (from onStepFinish) + const toolName = tc.toolName || tc.name || "unknown"; + const args = tc.args || tc.arguments || {}; // Format args nicely let argsDisplay = ""; if (Object.keys(args).length > 0) { - argsDisplay = Object.entries(args) + argsDisplay = "\n" + Object.entries(args) .map(([key, value]) => ` **${key}:** \`${JSON.stringify(value)}\``) .join("\n"); - } else { - argsDisplay = " *(no parameters)*"; } - let toolTrace = `🔧 **${tc.toolName}**\n${argsDisplay}`; + let toolTrace = `🔧 **${toolName}**${argsDisplay}`; - // Add tool result if showToolResults is enabled + // Add tool result if available and enabled if (this.showToolResults && response.toolResults && response.toolResults[index]) { const result = response.toolResults[index]; const resultOutput = result.output || result.result || {}; // Format result based on success/error if (resultOutput.success === false) { - toolTrace += `\n ❌ **Error:** ${resultOutput.error || "Unknown error"}`; + toolTrace += `\n❌ **Error:** ${resultOutput.error || "Unknown error"}`; } else if (resultOutput.success === true) { - toolTrace += `\n ✅ **Success:** ${resultOutput.message || "Operation completed"}`; - // Include additional result details if present - const details = Object.entries(resultOutput) - .filter(([key]) => key !== "success" && key !== "message") - .map(([key, value]) => ` **${key}:** \`${JSON.stringify(value)}\``) - .join("\n"); - if (details) { - toolTrace += `\n${details}`; - } + toolTrace += `\n✅ **Success:** ${resultOutput.message || "Operation completed"}`; } else { - // Show raw result if format is unexpected - toolTrace += `\n **Result:** \`${JSON.stringify(resultOutput)}\``; + toolTrace += `\n**Result:** \`${JSON.stringify(resultOutput)}\``; } } @@ -136,7 +113,7 @@ export class CopilotChatComponent implements OnDestroy { output += traces.join("\n\n"); - // Add token usage information if available + // Add token usage if available if (response.usage) { const inputTokens = response.usage.inputTokens || 0; const outputTokens = response.usage.outputTokens || 0; diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index b87a8cf99d1..626b48df4c1 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -52,7 +52,7 @@ import { } from "./workflow-tools"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { createOpenAI } from "@ai-sdk/openai"; -import { AssistantModelMessage, generateText, type ModelMessage, stepCountIs, UserModelMessage } from "ai"; +import { AssistantModelMessage, streamText, type ModelMessage, UserModelMessage } from "ai"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { AppSettings } from "../../../common/app-setting"; import { DynamicSchemaService } from "../dynamic-schema/dynamic-schema.service"; @@ -228,70 +228,111 @@ export class TexeraCopilot { // 2) define tools (your existing helpers) const tools = this.createWorkflowTools(); - // 3) run multi-step with stopWhen to check for user stop request - generateText({ - model: this.model, - messages: this.messages, // full history - tools, - system: COPILOT_SYSTEM_PROMPT, - // Stop when: user requested stop OR reached 50 steps - stopWhen: ({ steps }) => { - // Check if user requested stop - if (this.state === CopilotState.STOPPING) { - console.log("Stopping generation due to user request"); - return true; - } - // Otherwise use the default step count limit - return stepCountIs(50)({ steps }); - }, - // optional: observe every completed step (tool calls + results available) - onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage }) => { - // Log each step for debugging - console.debug("step finished", { text, toolCalls, toolResults, finishReason, usage }); - - // If there are tool calls, emit raw trace data - if (toolCalls && toolCalls.length > 0) { - const traceResponse: AgentResponse = { - type: "trace", - content: text || "", - isDone: false, - toolCalls, - toolResults, - usage, - }; - - // Emit raw trace data - observer.next(traceResponse); - } - }, - }) - .then(({ text, steps, response }) => { - // 4) append ALL messages the SDK produced this turn (assistant + tool messages) - // This keeps your history perfectly aligned with the SDK's internal state. - this.messages.push(...response.messages); - - // 5) optional diagnostics - if (steps?.length) { - const totalToolCalls = steps.flatMap(s => s.toolCalls || []).length; - console.log(`Agent loop finished in ${steps.length} step(s), ${totalToolCalls} tool call(s).`); - } + // 3) use streamText with onChunk callback + (async () => { + try { + // Accumulate text deltas + let accumulatedText = ""; + + const flushText = () => { + if (accumulatedText.trim()) { + // Emit accumulated text as a message to append + observer.next({ + type: "response", + content: accumulatedText, + isDone: false, + }); + accumulatedText = ""; // Clear after flushing + } + }; + + const result = streamText({ + model: this.model, + messages: this.messages, // full history + tools, + system: COPILOT_SYSTEM_PROMPT, + // Stop when: user requested stop OR reached 50 steps + stopWhen: ({ steps }: any) => { + if (this.state === CopilotState.STOPPING) { + console.log("Stopping generation due to user request"); + return true; + } + if (steps && steps.length >= 50) { + console.log(`Reached maximum steps (50)`); + return true; + } + return false; + }, + // Callback for each chunk as it arrives + onChunk: ({ chunk }: any) => { + if (this.state === CopilotState.STOPPING) { + console.log("User stop detected in onChunk"); + return; + } + + if (chunk.type === "text-delta") { + // Accumulate text delta + accumulatedText += chunk.text; + } else if (chunk.type === "tool-call") { + // Flush accumulated text before tool call + flushText(); + + // Emit tool call as separate message + observer.next({ + type: "trace", + content: `🔧 **${chunk.toolName}**`, + isDone: false, + toolCalls: [chunk], + toolResults: undefined, + }); + } + }, + // Observe every completed step + onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage }: any) => { + console.debug("step finished", { text, toolCalls, toolResults, finishReason, usage }); + + // Flush any remaining text after step completes + flushText(); + + // If there are tool results, emit them + if (toolResults && toolResults.length > 0) { + const traceResponse: AgentResponse = { + type: "trace", + content: "", // Will be formatted in frontend + isDone: false, + toolCalls, + toolResults, + usage, + }; + observer.next(traceResponse); + } + }, + }); + + // Wait for the final result + const finalText = await result.text; + const responseMetadata = await result.response; + + // Flush any remaining text + flushText(); + + // Append ALL messages the SDK produced this turn + this.messages.push(...responseMetadata.messages); - // Clear all copilot presence indicators when generation completes + // Clear all copilot presence indicators this.copilotCoeditorService.clearAll(); // Set state back to Available this.state = CopilotState.AVAILABLE; - // Emit final response with raw data - const finalResponse: AgentResponse = { + // Signal completion + observer.next({ type: "response", - content: text, + content: "", isDone: true, - }; - observer.next(finalResponse); + }); observer.complete(); - }) - .catch((err: any) => { + } catch (err: any) { // Clear all copilot presence indicators on error this.copilotCoeditorService.clearAll(); @@ -303,7 +344,8 @@ export class TexeraCopilot { const assistantError: AssistantModelMessage = { role: "assistant", content: errorText }; this.messages.push(assistantError); observer.error(err); - }); + } + })(); }); } From 002553b0ae153f2ad4d152418816a14066136927 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 28 Oct 2025 11:16:03 -0700 Subject: [PATCH 034/158] Revert "add experimental streamText" This reverts commit 19bbba1b3c83e4cd67fcb52cd583ba13364d3f51. --- .../copilot-chat/copilot-chat.component.ts | 73 +++++--- .../service/copilot/texera-copilot.ts | 156 +++++++----------- 2 files changed, 105 insertions(+), 124 deletions(-) diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts index 4766099dccf..48c38a365dc 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts @@ -33,22 +33,20 @@ export class CopilotChatComponent implements OnDestroy { .subscribe({ next: (response: AgentResponse) => { if (response.type === "trace") { - // Append tool trace message - if (response.toolCalls && response.toolCalls.length > 0) { - const displayText = this.formatToolTrace(response); - if (displayText && this.deepChatElement?.nativeElement?.addMessage) { - this.deepChatElement.nativeElement.addMessage({ role: "ai", text: displayText }); - } + // Format tool traces + const displayText = this.formatToolTrace(response); + + // Add trace message via addMessage API + if (displayText && this.deepChatElement?.nativeElement?.addMessage) { + this.deepChatElement.nativeElement.addMessage({ role: "ai", text: displayText }); } + + // Keep processing state true - loading indicator stays visible } else if (response.type === "response") { + // For final response, signal completion with the content + // This will let deep-chat handle adding the message and clearing loading if (response.isDone) { - // Final signal - clear loading indicator - signals.onResponse({ text: "" }); - } else { - // Append accumulated text as a new message - if (response.content && this.deepChatElement?.nativeElement?.addMessage) { - this.deepChatElement.nativeElement.addMessage({ role: "ai", text: response.content }); - } + signals.onResponse({ text: response.content }); } } }, @@ -56,7 +54,15 @@ export class CopilotChatComponent implements OnDestroy { signals.onResponse({ error: e ?? "Unknown error" }); }, complete: () => { - // Observable complete + // Handle completion without final response (happens when generation is stopped) + const currentState = this.copilotService.getState(); + if (currentState === CopilotState.STOPPING) { + // Generation was stopped by user - show completion message + signals.onResponse({ text: "_Generation stopped._" }); + } else if (currentState === CopilotState.GENERATING) { + // Generation completed unexpectedly + signals.onResponse({ text: "_Generation completed._" }); + } }, }); }, @@ -75,36 +81,53 @@ export class CopilotChatComponent implements OnDestroy { return ""; } + // Include agent's thinking/text if available let output = ""; + if (response.content && response.content.trim()) { + output += `💭 **Agent:** ${response.content}\n\n`; + } - // Format each tool call + // Format each tool call - show tool name, parameters, and optionally results const traces = response.toolCalls.map((tc: any, index: number) => { - // Handle tool call chunk (from onChunk) or full tool call (from onStepFinish) - const toolName = tc.toolName || tc.name || "unknown"; - const args = tc.args || tc.arguments || {}; + // Log the actual structure to debug + console.log("Tool call structure:", tc); + + // Try multiple possible property names for arguments + const args = tc.args || tc.arguments || tc.parameters || tc.input || {}; // Format args nicely let argsDisplay = ""; if (Object.keys(args).length > 0) { - argsDisplay = "\n" + Object.entries(args) + argsDisplay = Object.entries(args) .map(([key, value]) => ` **${key}:** \`${JSON.stringify(value)}\``) .join("\n"); + } else { + argsDisplay = " *(no parameters)*"; } - let toolTrace = `🔧 **${toolName}**${argsDisplay}`; + let toolTrace = `🔧 **${tc.toolName}**\n${argsDisplay}`; - // Add tool result if available and enabled + // Add tool result if showToolResults is enabled if (this.showToolResults && response.toolResults && response.toolResults[index]) { const result = response.toolResults[index]; const resultOutput = result.output || result.result || {}; // Format result based on success/error if (resultOutput.success === false) { - toolTrace += `\n❌ **Error:** ${resultOutput.error || "Unknown error"}`; + toolTrace += `\n ❌ **Error:** ${resultOutput.error || "Unknown error"}`; } else if (resultOutput.success === true) { - toolTrace += `\n✅ **Success:** ${resultOutput.message || "Operation completed"}`; + toolTrace += `\n ✅ **Success:** ${resultOutput.message || "Operation completed"}`; + // Include additional result details if present + const details = Object.entries(resultOutput) + .filter(([key]) => key !== "success" && key !== "message") + .map(([key, value]) => ` **${key}:** \`${JSON.stringify(value)}\``) + .join("\n"); + if (details) { + toolTrace += `\n${details}`; + } } else { - toolTrace += `\n**Result:** \`${JSON.stringify(resultOutput)}\``; + // Show raw result if format is unexpected + toolTrace += `\n **Result:** \`${JSON.stringify(resultOutput)}\``; } } @@ -113,7 +136,7 @@ export class CopilotChatComponent implements OnDestroy { output += traces.join("\n\n"); - // Add token usage if available + // Add token usage information if available if (response.usage) { const inputTokens = response.usage.inputTokens || 0; const outputTokens = response.usage.outputTokens || 0; diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 626b48df4c1..b87a8cf99d1 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -52,7 +52,7 @@ import { } from "./workflow-tools"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { createOpenAI } from "@ai-sdk/openai"; -import { AssistantModelMessage, streamText, type ModelMessage, UserModelMessage } from "ai"; +import { AssistantModelMessage, generateText, type ModelMessage, stepCountIs, UserModelMessage } from "ai"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { AppSettings } from "../../../common/app-setting"; import { DynamicSchemaService } from "../dynamic-schema/dynamic-schema.service"; @@ -228,111 +228,70 @@ export class TexeraCopilot { // 2) define tools (your existing helpers) const tools = this.createWorkflowTools(); - // 3) use streamText with onChunk callback - (async () => { - try { - // Accumulate text deltas - let accumulatedText = ""; - - const flushText = () => { - if (accumulatedText.trim()) { - // Emit accumulated text as a message to append - observer.next({ - type: "response", - content: accumulatedText, - isDone: false, - }); - accumulatedText = ""; // Clear after flushing - } - }; - - const result = streamText({ - model: this.model, - messages: this.messages, // full history - tools, - system: COPILOT_SYSTEM_PROMPT, - // Stop when: user requested stop OR reached 50 steps - stopWhen: ({ steps }: any) => { - if (this.state === CopilotState.STOPPING) { - console.log("Stopping generation due to user request"); - return true; - } - if (steps && steps.length >= 50) { - console.log(`Reached maximum steps (50)`); - return true; - } - return false; - }, - // Callback for each chunk as it arrives - onChunk: ({ chunk }: any) => { - if (this.state === CopilotState.STOPPING) { - console.log("User stop detected in onChunk"); - return; - } - - if (chunk.type === "text-delta") { - // Accumulate text delta - accumulatedText += chunk.text; - } else if (chunk.type === "tool-call") { - // Flush accumulated text before tool call - flushText(); - - // Emit tool call as separate message - observer.next({ - type: "trace", - content: `🔧 **${chunk.toolName}**`, - isDone: false, - toolCalls: [chunk], - toolResults: undefined, - }); - } - }, - // Observe every completed step - onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage }: any) => { - console.debug("step finished", { text, toolCalls, toolResults, finishReason, usage }); - - // Flush any remaining text after step completes - flushText(); - - // If there are tool results, emit them - if (toolResults && toolResults.length > 0) { - const traceResponse: AgentResponse = { - type: "trace", - content: "", // Will be formatted in frontend - isDone: false, - toolCalls, - toolResults, - usage, - }; - observer.next(traceResponse); - } - }, - }); - - // Wait for the final result - const finalText = await result.text; - const responseMetadata = await result.response; - - // Flush any remaining text - flushText(); - - // Append ALL messages the SDK produced this turn - this.messages.push(...responseMetadata.messages); + // 3) run multi-step with stopWhen to check for user stop request + generateText({ + model: this.model, + messages: this.messages, // full history + tools, + system: COPILOT_SYSTEM_PROMPT, + // Stop when: user requested stop OR reached 50 steps + stopWhen: ({ steps }) => { + // Check if user requested stop + if (this.state === CopilotState.STOPPING) { + console.log("Stopping generation due to user request"); + return true; + } + // Otherwise use the default step count limit + return stepCountIs(50)({ steps }); + }, + // optional: observe every completed step (tool calls + results available) + onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage }) => { + // Log each step for debugging + console.debug("step finished", { text, toolCalls, toolResults, finishReason, usage }); + + // If there are tool calls, emit raw trace data + if (toolCalls && toolCalls.length > 0) { + const traceResponse: AgentResponse = { + type: "trace", + content: text || "", + isDone: false, + toolCalls, + toolResults, + usage, + }; + + // Emit raw trace data + observer.next(traceResponse); + } + }, + }) + .then(({ text, steps, response }) => { + // 4) append ALL messages the SDK produced this turn (assistant + tool messages) + // This keeps your history perfectly aligned with the SDK's internal state. + this.messages.push(...response.messages); + + // 5) optional diagnostics + if (steps?.length) { + const totalToolCalls = steps.flatMap(s => s.toolCalls || []).length; + console.log(`Agent loop finished in ${steps.length} step(s), ${totalToolCalls} tool call(s).`); + } - // Clear all copilot presence indicators + // Clear all copilot presence indicators when generation completes this.copilotCoeditorService.clearAll(); // Set state back to Available this.state = CopilotState.AVAILABLE; - // Signal completion - observer.next({ + // Emit final response with raw data + const finalResponse: AgentResponse = { type: "response", - content: "", + content: text, isDone: true, - }); + }; + observer.next(finalResponse); observer.complete(); - } catch (err: any) { + }) + .catch((err: any) => { // Clear all copilot presence indicators on error this.copilotCoeditorService.clearAll(); @@ -344,8 +303,7 @@ export class TexeraCopilot { const assistantError: AssistantModelMessage = { role: "assistant", content: errorText }; this.messages.push(assistantError); observer.error(err); - } - })(); + }); }); } From 808f629020876a5ff959c9fb30bbbcb62b0bf128 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 28 Oct 2025 14:42:23 -0700 Subject: [PATCH 035/158] add inconsistency panel --- frontend/src/app/app.module.ts | 4 + .../add-inconsistency-modal.component.html | 78 +++++++++ .../add-inconsistency-modal.component.scss | 35 ++++ .../add-inconsistency-modal.component.ts | 57 ++++++ .../inconsistency-list.component.html | 80 +++++++++ .../inconsistency-list.component.scss | 84 +++++++++ .../inconsistency-list.component.ts | 65 +++++++ .../left-panel/left-panel.component.ts | 7 + .../context-menu/context-menu.component.html | 13 ++ .../context-menu/context-menu.component.ts | 16 ++ .../service/copilot/copilot-prompts.ts | 14 +- .../service/copilot/texera-copilot.ts | 22 ++- .../service/copilot/workflow-tools.ts | 162 +++++++++++++++++- .../data-inconsistency.service.ts | 157 +++++++++++++++++ 14 files changed, 784 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.html create mode 100644 frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.scss create mode 100644 frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.ts create mode 100644 frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html create mode 100644 frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.scss create mode 100644 frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.ts create mode 100644 frontend/src/app/workspace/service/data-inconsistency/data-inconsistency.service.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 100b90ad779..6213199d20f 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -129,6 +129,8 @@ import { ErrorFrameComponent } from "./workspace/component/result-panel/error-fr import { NzResizableModule } from "ng-zorro-antd/resizable"; import { WorkflowRuntimeStatisticsComponent } from "./dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-runtime-statistics/workflow-runtime-statistics.component"; import { TimeTravelComponent } from "./workspace/component/left-panel/time-travel/time-travel.component"; +import { InconsistencyListComponent } from "./workspace/component/left-panel/inconsistency-list/inconsistency-list.component"; +import { AddInconsistencyModalComponent } from "./workspace/component/add-inconsistency-modal/add-inconsistency-modal.component"; import { NzMessageModule } from "ng-zorro-antd/message"; import { NzModalModule } from "ng-zorro-antd/modal"; import { OverlayModule } from "@angular/cdk/overlay"; @@ -194,6 +196,8 @@ registerLocaleData(en); PropertyEditorComponent, VersionsListComponent, TimeTravelComponent, + InconsistencyListComponent, + AddInconsistencyModalComponent, WorkflowEditorComponent, ResultPanelComponent, ResultExportationComponent, diff --git a/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.html b/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.html new file mode 100644 index 00000000000..ca2b1d0cae0 --- /dev/null +++ b/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.html @@ -0,0 +1,78 @@ + + +
+ + Name + + + + + + + Description + + + + + + + Operator ID + + + + + + +
diff --git a/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.scss b/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.scss new file mode 100644 index 00000000000..9ccec70d955 --- /dev/null +++ b/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.scss @@ -0,0 +1,35 @@ +/** + * 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. + */ + +.add-inconsistency-form { + padding: 16px 0; + + nz-form-item { + margin-bottom: 16px; + } + + .modal-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + } +} diff --git a/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.ts b/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.ts new file mode 100644 index 00000000000..4ebe08b25ab --- /dev/null +++ b/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.ts @@ -0,0 +1,57 @@ +/** + * 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 { Component, OnInit } from "@angular/core"; +import { NzModalRef } from "ng-zorro-antd/modal"; +import { DataInconsistencyService } from "../../service/data-inconsistency/data-inconsistency.service"; + +@Component({ + selector: "texera-add-inconsistency-modal", + templateUrl: "./add-inconsistency-modal.component.html", + styleUrls: ["./add-inconsistency-modal.component.scss"], +}) +export class AddInconsistencyModalComponent implements OnInit { + name: string = ""; + description: string = ""; + operatorId: string = ""; + + constructor( + private modalRef: NzModalRef, + private dataInconsistencyService: DataInconsistencyService + ) {} + + ngOnInit(): void { + // Get the operatorId passed from the modal + const data = this.modalRef.getConfig().nzData; + if (data && data.operatorId) { + this.operatorId = data.operatorId; + } + } + + onSubmit(): void { + if (this.name.trim() && this.description.trim()) { + this.dataInconsistencyService.addInconsistency(this.name.trim(), this.description.trim(), this.operatorId); + this.modalRef.close(); + } + } + + onCancel(): void { + this.modalRef.close(); + } +} diff --git a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html new file mode 100644 index 00000000000..38b81b88d95 --- /dev/null +++ b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html @@ -0,0 +1,80 @@ + + +
+
+

Data Inconsistencies

+ +
+ +
+ +
+ + + + + +

{{ inconsistency.description }}

+

Operator: {{ inconsistency.operatorId }}

+ + + + +
+
+
+
+
diff --git a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.scss b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.scss new file mode 100644 index 00000000000..26d9d401e9b --- /dev/null +++ b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.scss @@ -0,0 +1,84 @@ +/** + * 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. + */ + +.inconsistency-container { + padding: 16px; + height: 100%; + overflow-y: auto; +} + +.inconsistency-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + + h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + } +} + +.empty-state { + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; +} + +.inconsistency-card { + margin-bottom: 12px; + width: 100%; + background-color: #fafafa; + + p { + margin-bottom: 8px; + } + + .operator-id { + color: #8c8c8c; + font-size: 12px; + margin-bottom: 0; + } +} + +::ng-deep { + .inconsistency-card { + background-color: #fafafa; + + .ant-card-head { + min-height: auto; + padding: 4px 12px; + background-color: #f0f0f0; + } + + .ant-card-head-title { + font-size: 13px; + font-weight: 600; + padding: 0; + line-height: 1.3; + } + + .ant-card-body { + padding: 8px 12px; + background-color: #fafafa; + } + } +} diff --git a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.ts b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.ts new file mode 100644 index 00000000000..72bdec03d2b --- /dev/null +++ b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.ts @@ -0,0 +1,65 @@ +/** + * 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 { Component, OnInit, OnDestroy } from "@angular/core"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { + DataInconsistencyService, + DataInconsistency, +} from "../../../service/data-inconsistency/data-inconsistency.service"; + +@UntilDestroy() +@Component({ + selector: "texera-inconsistency-list", + templateUrl: "./inconsistency-list.component.html", + styleUrls: ["./inconsistency-list.component.scss"], +}) +export class InconsistencyListComponent implements OnInit, OnDestroy { + inconsistencies: DataInconsistency[] = []; + + constructor(private inconsistencyService: DataInconsistencyService) {} + + ngOnInit(): void { + // Subscribe to inconsistency updates + this.inconsistencyService + .getInconsistencies() + .pipe(untilDestroyed(this)) + .subscribe(inconsistencies => { + this.inconsistencies = inconsistencies; + }); + } + + ngOnDestroy(): void { + // Cleanup handled by @UntilDestroy + } + + /** + * Delete an inconsistency + */ + deleteInconsistency(id: string): void { + this.inconsistencyService.deleteInconsistency(id); + } + + /** + * Clear all inconsistencies + */ + clearAll(): void { + this.inconsistencyService.clearAll(); + } +} diff --git a/frontend/src/app/workspace/component/left-panel/left-panel.component.ts b/frontend/src/app/workspace/component/left-panel/left-panel.component.ts index 8dd9a5748b8..a5feef2542a 100644 --- a/frontend/src/app/workspace/component/left-panel/left-panel.component.ts +++ b/frontend/src/app/workspace/component/left-panel/left-panel.component.ts @@ -25,6 +25,7 @@ import { OperatorMenuComponent } from "./operator-menu/operator-menu.component"; import { VersionsListComponent } from "./versions-list/versions-list.component"; import { WorkflowExecutionHistoryComponent } from "../../../dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component"; import { TimeTravelComponent } from "./time-travel/time-travel.component"; +import { InconsistencyListComponent } from "./inconsistency-list/inconsistency-list.component"; import { SettingsComponent } from "./settings/settings.component"; import { calculateTotalTranslate3d } from "../../../common/util/panel-dock"; import { PanelService } from "../../service/panel/panel.service"; @@ -69,6 +70,12 @@ export class LeftPanelComponent implements OnDestroy, OnInit, AfterViewInit { icon: "clock-circle", enabled: false, }, + { + component: InconsistencyListComponent, + title: "Data Inconsistencies", + icon: "warning", + enabled: true, + }, ]; order = Array.from({ length: this.items.length - 1 }, (_, index) => index + 1); diff --git a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html index 7a6fcba6f25..d68ef3c024c 100644 --- a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html @@ -165,6 +165,19 @@ execute to this operator +
  • + + Add to Inconsistency +
  • +
  • { + try { + const inconsistency = service.addInconsistency(args.name, args.description, args.operatorId); + return { + success: true, + message: `Added inconsistency: ${args.name}`, + inconsistency, + }; + } catch (error: any) { + return { + success: false, + error: error.message || String(error), + }; + } + }, + }); +} + +/** + * Tool to list all data inconsistencies + */ +export function createListInconsistenciesTool(service: DataInconsistencyService) { + return tool({ + name: "listInconsistencies", + description: "Get all data inconsistencies found so far", + inputSchema: z.object({}), + execute: async (args: {}) => { + try { + const inconsistencies = service.getAllInconsistencies(); + return { + success: true, + count: inconsistencies.length, + inconsistencies, + }; + } catch (error: any) { + return { + success: false, + error: error.message || String(error), + }; + } + }, + }); +} + +/** + * Tool to update an existing data inconsistency + */ +export function createUpdateInconsistencyTool(service: DataInconsistencyService) { + return tool({ + name: "updateInconsistency", + description: "Update an existing data inconsistency", + inputSchema: z.object({ + id: z.string().describe("ID of the inconsistency to update"), + name: z.string().optional().describe("New name for the inconsistency"), + description: z.string().optional().describe("New description"), + operatorId: z.string().optional().describe("New operator ID"), + }), + execute: async (args: { id: string; name?: string; description?: string; operatorId?: string }) => { + try { + const updates: any = {}; + if (args.name !== undefined) updates.name = args.name; + if (args.description !== undefined) updates.description = args.description; + if (args.operatorId !== undefined) updates.operatorId = args.operatorId; + + const updated = service.updateInconsistency(args.id, updates); + if (!updated) { + return { + success: false, + error: `Inconsistency not found: ${args.id}`, + }; + } + + return { + success: true, + message: `Updated inconsistency: ${args.id}`, + inconsistency: updated, + }; + } catch (error: any) { + return { + success: false, + error: error.message || String(error), + }; + } + }, + }); +} + +/** + * Tool to delete a data inconsistency + */ +export function createDeleteInconsistencyTool(service: DataInconsistencyService) { + return tool({ + name: "deleteInconsistency", + description: "Delete a data inconsistency from the list", + inputSchema: z.object({ + id: z.string().describe("ID of the inconsistency to delete"), + }), + execute: async (args: { id: string }) => { + try { + const deleted = service.deleteInconsistency(args.id); + if (!deleted) { + return { + success: false, + error: `Inconsistency not found: ${args.id}`, + }; + } + + return { + success: true, + message: `Deleted inconsistency: ${args.id}`, + }; + } catch (error: any) { + return { + success: false, + error: error.message || String(error), + }; + } + }, + }); +} + +/** + * Tool to clear all data inconsistencies + */ +export function createClearInconsistenciesTool(service: DataInconsistencyService) { + return tool({ + name: "clearInconsistencies", + description: "Clear all data inconsistencies from the list", + inputSchema: z.object({}), + execute: async (args: {}) => { + try { + service.clearAll(); + return { + success: true, + message: "Cleared all inconsistencies", + }; + } catch (error: any) { + return { + success: false, + error: error.message || String(error), + }; + } + }, + }); +} diff --git a/frontend/src/app/workspace/service/data-inconsistency/data-inconsistency.service.ts b/frontend/src/app/workspace/service/data-inconsistency/data-inconsistency.service.ts new file mode 100644 index 00000000000..d5cb1b6defc --- /dev/null +++ b/frontend/src/app/workspace/service/data-inconsistency/data-inconsistency.service.ts @@ -0,0 +1,157 @@ +/** + * 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 { BehaviorSubject, Observable } from "rxjs"; + +/** + * Interface for a data inconsistency item + */ +export interface DataInconsistency { + id: string; + name: string; + description: string; + operatorId: string; +} + +/** + * Service to manage data inconsistencies found in workflows + * Singleton service that maintains an in-memory list of inconsistencies + */ +@Injectable({ + providedIn: "root", +}) +export class DataInconsistencyService { + private inconsistencies: Map = new Map(); + private inconsistenciesSubject = new BehaviorSubject([]); + + constructor() {} + + /** + * Get all inconsistencies as an observable + */ + public getInconsistencies(): Observable { + return this.inconsistenciesSubject.asObservable(); + } + + /** + * Get all inconsistencies as an array + */ + public getAllInconsistencies(): DataInconsistency[] { + return Array.from(this.inconsistencies.values()); + } + + /** + * Get a specific inconsistency by ID + */ + public getInconsistency(id: string): DataInconsistency | undefined { + return this.inconsistencies.get(id); + } + + /** + * Add a new inconsistency + * Returns the created inconsistency + */ + public addInconsistency(name: string, description: string, operatorId: string): DataInconsistency { + const id = this.generateId(); + const inconsistency: DataInconsistency = { + id, + name, + description, + operatorId, + }; + + this.inconsistencies.set(id, inconsistency); + this.emitUpdate(); + + console.log(`Added inconsistency: ${name} (ID: ${id})`); + return inconsistency; + } + + /** + * Update an existing inconsistency + * Returns the updated inconsistency or undefined if not found + */ + public updateInconsistency( + id: string, + updates: Partial> + ): DataInconsistency | undefined { + const existing = this.inconsistencies.get(id); + if (!existing) { + console.warn(`Inconsistency not found: ${id}`); + return undefined; + } + + const updated: DataInconsistency = { + ...existing, + ...updates, + }; + + this.inconsistencies.set(id, updated); + this.emitUpdate(); + + console.log(`Updated inconsistency: ${id}`); + return updated; + } + + /** + * Delete an inconsistency by ID + * Returns true if deleted, false if not found + */ + public deleteInconsistency(id: string): boolean { + const existed = this.inconsistencies.delete(id); + if (existed) { + this.emitUpdate(); + console.log(`Deleted inconsistency: ${id}`); + } else { + console.warn(`Inconsistency not found: ${id}`); + } + return existed; + } + + /** + * Clear all inconsistencies + */ + public clearAll(): void { + this.inconsistencies.clear(); + this.emitUpdate(); + console.log("Cleared all inconsistencies"); + } + + /** + * Get inconsistencies for a specific operator + */ + public getInconsistenciesForOperator(operatorId: string): DataInconsistency[] { + return Array.from(this.inconsistencies.values()).filter(inc => inc.operatorId === operatorId); + } + + /** + * Generate a unique ID for an inconsistency + */ + private generateId(): string { + return `inc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + + /** + * Emit updated list to subscribers + */ + private emitUpdate(): void { + this.inconsistenciesSubject.next(this.getAllInconsistencies()); + } +} From aef712d141c873441a281ba163591f8c3b6359f7 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 28 Oct 2025 16:29:29 -0700 Subject: [PATCH 036/158] finish highlight --- .../inconsistency-list.component.html | 6 ++- .../inconsistency-list.component.ts | 28 +++++++++- .../model/workflow-action.service.ts | 52 +++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html index 38b81b88d95..802afbf6059 100644 --- a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html +++ b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html @@ -54,7 +54,9 @@

    Data Inconsistencies

    + class="inconsistency-card" + (click)="onInconsistencyClick(inconsistency)" + style="cursor: pointer;">

    {{ inconsistency.description }}

    Operator: {{ inconsistency.operatorId }}

    @@ -64,7 +66,7 @@

    Data Inconsistencies

    nzType="text" nzSize="small" nzDanger - (click)="deleteInconsistency(inconsistency.id)" + (click)="deleteInconsistency(inconsistency.id); $event.stopPropagation()" nz-tooltip nzTooltipTitle="Delete"> 0 || pathResult.links.length > 0) { + // Highlight operators and links on the upstream path + this.workflowActionService.highlightOperators(false, ...pathResult.operators); + this.workflowActionService.highlightLinks(false, ...pathResult.links); + } + } } diff --git a/frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts b/frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts index 40092419fd2..348c5cc5663 100644 --- a/frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts +++ b/frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts @@ -919,4 +919,56 @@ export class WorkflowActionService { public getHighlightingEnabled() { return this.highlightingEnabled; } + + /** + * Find all operators and links on any upstream path leading to the given destination operator. + * Uses BFS to traverse backwards from the destination to find all contributing operators. + * @param destinationOperatorId The operator ID to find upstream paths to + * @returns Object containing arrays of operator IDs and link IDs on upstream paths + */ + public findUpstreamPath(destinationOperatorId: string): { operators: string[]; links: string[] } { + const allLinks = this.getTexeraGraph().getAllLinks(); + + // Build reverse adjacency list (from target to source) + const reverseAdjacencyMap = new Map>(); + allLinks.forEach(link => { + const source = link.source.operatorID; + const target = link.target.operatorID; + if (!reverseAdjacencyMap.has(target)) { + reverseAdjacencyMap.set(target, []); + } + reverseAdjacencyMap.get(target)!.push({ neighbor: source, linkId: link.linkID }); + }); + + // BFS to find all upstream operators and links + const queue: string[] = [destinationOperatorId]; + const visitedOperators = new Set(); + const allOperatorsOnPaths = new Set(); + const allLinksOnPaths = new Set(); + + allOperatorsOnPaths.add(destinationOperatorId); // Include the destination operator + + while (queue.length > 0) { + const current = queue.shift()!; + + if (visitedOperators.has(current)) { + continue; + } + visitedOperators.add(current); + + const upstreamNeighbors = reverseAdjacencyMap.get(current) || []; + for (const { neighbor, linkId } of upstreamNeighbors) { + allOperatorsOnPaths.add(neighbor); + allLinksOnPaths.add(linkId); + if (!visitedOperators.has(neighbor)) { + queue.push(neighbor); + } + } + } + + return { + operators: Array.from(allOperatorsOnPaths), + links: Array.from(allLinksOnPaths), + }; + } } From 4053012c0bd291f2915c9b112151afbc55ff8806 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 28 Oct 2025 21:05:31 -0700 Subject: [PATCH 037/158] add initial action lineage --- frontend/src/app/app.module.ts | 2 + .../copilot-chat/copilot-chat.component.ts | 39 +++- .../inconsistency-list.component.html | 2 +- .../operator-response-panel.component.html | 82 +++++++ .../operator-response-panel.component.scss | 116 ++++++++++ .../operator-response-panel.component.ts | 129 +++++++++++ .../workflow-editor.component.html | 1 + .../workflow-editor.component.scss | 1 + .../service/copilot/action-lineage.service.ts | 210 ++++++++++++++++++ .../service/copilot/texera-copilot.ts | 98 +++++++- 10 files changed, 669 insertions(+), 11 deletions(-) create mode 100644 frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.html create mode 100644 frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.scss create mode 100644 frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.ts create mode 100644 frontend/src/app/workspace/service/copilot/action-lineage.service.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6213199d20f..f40b92e42c8 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -179,6 +179,7 @@ import { AdminSettingsComponent } from "./dashboard/component/admin/settings/adm import { FormlyRepeatDndComponent } from "./common/formly/repeat-dnd/repeat-dnd.component"; import { NzInputNumberModule } from "ng-zorro-antd/input-number"; import { NzCheckboxModule } from "ng-zorro-antd/checkbox"; +import { OperatorResponsePanelComponent } from "./workspace/component/operator-response-panel/operator-response-panel.component"; import "deep-chat"; registerLocaleData(en); @@ -275,6 +276,7 @@ registerLocaleData(en); HubSearchResultComponent, ComputingUnitSelectionComponent, AdminSettingsComponent, + OperatorResponsePanelComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts index 48c38a365dc..abccdbe3c2d 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts @@ -26,6 +26,9 @@ export class CopilotChatComponent implements OnDestroy { const last = body?.messages?.[body.messages.length - 1]; const userText: string = typeof last?.text === "string" ? last.text : ""; + // Track if we've sent a final response + let finalResponseSent = false; + // Send message to copilot and process AgentResponse this.copilotService .sendMessage(userText) @@ -47,21 +50,39 @@ export class CopilotChatComponent implements OnDestroy { // This will let deep-chat handle adding the message and clearing loading if (response.isDone) { signals.onResponse({ text: response.content }); + finalResponseSent = true; } } }, error: (e: unknown) => { - signals.onResponse({ error: e ?? "Unknown error" }); + // Format error message properly + let errorMessage = "Unknown error"; + if (e instanceof Error) { + errorMessage = e.message; + } else if (typeof e === "string") { + errorMessage = e; + } else if (e && typeof e === "object") { + errorMessage = JSON.stringify(e); + } + signals.onResponse({ error: errorMessage }); + console.error("Copilot error:", e); }, complete: () => { - // Handle completion without final response (happens when generation is stopped) - const currentState = this.copilotService.getState(); - if (currentState === CopilotState.STOPPING) { - // Generation was stopped by user - show completion message - signals.onResponse({ text: "_Generation stopped._" }); - } else if (currentState === CopilotState.GENERATING) { - // Generation completed unexpectedly - signals.onResponse({ text: "_Generation completed._" }); + // Only send a response if we haven't already sent the final response + if (!finalResponseSent) { + const currentState = this.copilotService.getState(); + if (currentState === CopilotState.STOPPING) { + // Generation was stopped by user - show completion message + signals.onResponse({ text: "_Generation stopped._" }); + } else if (currentState === CopilotState.GENERATING) { + // Generation completed unexpectedly + signals.onResponse({ text: "_Generation completed._" }); + } else { + // Observable completed without a final response and state is not STOPPING or GENERATING + // This might happen if there was an issue - send a generic completion message + console.warn("Observable completed without final response. State:", currentState); + signals.onResponse({ text: "_Response completed._" }); + } } }, }); diff --git a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html index 802afbf6059..5da008b07bd 100644 --- a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html +++ b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html @@ -56,7 +56,7 @@

    Data Inconsistencies

    [nzExtra]="extraTemplate" class="inconsistency-card" (click)="onInconsistencyClick(inconsistency)" - style="cursor: pointer;"> + style="cursor: pointer">

    {{ inconsistency.description }}

    Operator: {{ inconsistency.operatorId }}

    diff --git a/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.html b/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.html new file mode 100644 index 00000000000..9abd3062963 --- /dev/null +++ b/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.html @@ -0,0 +1,82 @@ + + +
    +
    +

    Copilot Actions

    + +
    + +
    +
    +

    No copilot actions found for this operator.

    +
    + +
    + + +
    +
    Type: {{ response.type }}
    +
    + Content: +
    {{ response.content || '(No content)' }}
    +
    +
    + Tool Calls: +
      +
    • {{ toolCall.toolName }}
    • +
    +
    +
    + Token Usage: +
    + Input: {{ response.usage.inputTokens }} + Output: {{ response.usage.outputTokens }} + Total: {{ response.usage.totalTokens }} +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.scss b/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.scss new file mode 100644 index 00000000000..0f9af8d8923 --- /dev/null +++ b/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.scss @@ -0,0 +1,116 @@ +/** + * 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. + */ + +.operator-response-panel { + position: absolute; + width: 350px; + max-height: 500px; + background: white; + border: 1px solid #d9d9d9; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + z-index: 1000; + pointer-events: all; + + .panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + + h4 { + margin: 0; + font-size: 14px; + font-weight: 600; + } + } + + .panel-content { + flex: 1; + overflow-y: auto; + padding: 16px; + + .no-responses { + text-align: center; + color: #8c8c8c; + padding: 20px; + + p { + margin: 0; + } + } + + .responses-list { + .response-details { + font-size: 12px; + + > div { + margin-bottom: 12px; + + &:last-child { + margin-bottom: 0; + } + } + + strong { + display: block; + margin-bottom: 4px; + color: #262626; + } + + .content-text { + padding: 8px; + background: #f5f5f5; + border-radius: 2px; + white-space: pre-wrap; + word-break: break-word; + max-height: 200px; + overflow-y: auto; + } + + .tool-calls { + ul { + margin: 0; + padding-left: 20px; + + li { + margin-bottom: 4px; + } + } + } + + .usage-details { + display: flex; + gap: 12px; + font-size: 11px; + color: #595959; + + span { + padding: 2px 6px; + background: #fafafa; + border-radius: 2px; + } + } + } + } + } +} diff --git a/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.ts b/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.ts new file mode 100644 index 00000000000..88f190c4fc1 --- /dev/null +++ b/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.ts @@ -0,0 +1,129 @@ +/** + * 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 { Component, OnInit } from "@angular/core"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { ActionLineageService } from "../../service/copilot/action-lineage.service"; +import { AgentResponse } from "../../service/copilot/texera-copilot"; +import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; +import { JointUIService } from "../../service/joint-ui/joint-ui.service"; + +@UntilDestroy() +@Component({ + selector: "texera-operator-response-panel", + templateUrl: "./operator-response-panel.component.html", + styleUrls: ["./operator-response-panel.component.scss"], +}) +export class OperatorResponsePanelComponent implements OnInit { + public isVisible = false; + public selectedOperatorId: string | null = null; + public relatedResponses: AgentResponse[] = []; + public panelTop = 0; + public panelLeft = 0; + + constructor( + private actionLineageService: ActionLineageService, + private workflowActionService: WorkflowActionService + ) {} + + ngOnInit(): void { + // Listen to operator selection events + this.workflowActionService + .getJointGraphWrapper() + .getJointOperatorHighlightStream() + .pipe(untilDestroyed(this)) + .subscribe((highlightedOperatorIDs: readonly string[]) => { + if (highlightedOperatorIDs.length === 1) { + // Single operator selected + this.selectedOperatorId = highlightedOperatorIDs[0]; + this.loadRelatedResponses(highlightedOperatorIDs[0]); + this.updatePanelPosition(highlightedOperatorIDs[0]); + this.isVisible = true; + } else { + // Multiple or no operators selected + this.isVisible = false; + this.selectedOperatorId = null; + this.relatedResponses = []; + } + }); + + // Listen to operator position changes to update panel position + this.workflowActionService + .getJointGraphWrapper() + .getElementPositionChangeEvent() + .pipe(untilDestroyed(this)) + .subscribe(event => { + if (this.isVisible && this.selectedOperatorId === event.elementID) { + this.updatePanelPosition(event.elementID); + } + }); + } + + /** + * Load related responses for a given operator + */ + private loadRelatedResponses(operatorId: string): void { + this.relatedResponses = this.actionLineageService.getResponsesByOperator(operatorId); + } + + /** + * Update panel position to display below the operator + */ + private updatePanelPosition(operatorId: string): void { + try { + const position = this.workflowActionService.getJointGraphWrapper().getElementPosition(operatorId); + const operatorHeight = JointUIService.DEFAULT_OPERATOR_HEIGHT; + + // Position the panel below the operator with some spacing + const spacing = 10; + this.panelLeft = position.x; + this.panelTop = position.y + operatorHeight + spacing; + } catch (error) { + console.error("Failed to get operator position:", error); + // If we can't get the position, hide the panel + this.isVisible = false; + } + } + + /** + * Format timestamp for display + */ + public formatTimestamp(timestamp: number): string { + return new Date(timestamp).toLocaleString(); + } + + /** + * Get a preview of the response content (first 100 characters) + */ + public getContentPreview(content: string): string { + if (content.length <= 100) { + return content; + } + return content.substring(0, 100) + "..."; + } + + /** + * Close the panel + */ + public closePanel(): void { + this.isVisible = false; + this.selectedOperatorId = null; + this.relatedResponses = []; + } +} diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html index 8f87c66c7a7..e9e5c43e432 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html @@ -21,6 +21,7 @@
    + diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss index 72caad0296d..3024bee5d72 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss @@ -19,6 +19,7 @@ #workflow-editor-wrapper { height: 100%; + position: relative; } #workflow-editor { diff --git a/frontend/src/app/workspace/service/copilot/action-lineage.service.ts b/frontend/src/app/workspace/service/copilot/action-lineage.service.ts new file mode 100644 index 00000000000..72b716fe36d --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/action-lineage.service.ts @@ -0,0 +1,210 @@ +/** + * 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 { AgentResponse } from "./texera-copilot"; + +/** + * Represents an affected entity (operator or link) in the workflow + */ +export interface AffectedEntity { + type: "operator" | "link"; + id: string; +} + +/** + * Service for managing the lineage between agent responses and workflow entities + * Tracks which operators/links were affected by each agent response + */ +@Injectable({ + providedIn: "root", +}) +export class ActionLineageService { + // Map from AgentResponse id to list of affected operator/link ids + private responseToEntities: Map = new Map(); + + // Map from operator/link id to list of AgentResponse ids + private entityToResponses: Map = new Map(); + + // Map from AgentResponse id to the full AgentResponse object for retrieval + private responseMap: Map = new Map(); + + constructor() {} + + /** + * Record that a response affected certain operators/links + * @param response The agent response + * @param affectedEntities List of operators/links that were affected + */ + recordResponseAction(response: AgentResponse, affectedEntities: AffectedEntity[]): void { + // Store the response object + this.responseMap.set(response.id, response); + + // Store response -> entities mapping + this.responseToEntities.set(response.id, affectedEntities); + + // Update entity -> responses mapping + for (const entity of affectedEntities) { + const entityKey = `${entity.type}:${entity.id}`; + const existingResponses = this.entityToResponses.get(entityKey) || []; + if (!existingResponses.includes(response.id)) { + existingResponses.push(response.id); + this.entityToResponses.set(entityKey, existingResponses); + } + } + } + + /** + * Get all entities affected by a specific response + * @param responseId The agent response id + * @returns List of affected entities or empty array if not found + */ + getAffectedEntitiesByResponse(responseId: string): AffectedEntity[] { + return this.responseToEntities.get(responseId) || []; + } + + /** + * Get all responses that affected a specific operator + * @param operatorId The operator id + * @returns List of agent response ids sorted by timestamp (oldest first) + */ + getResponsesByOperator(operatorId: string): AgentResponse[] { + const entityKey = `operator:${operatorId}`; + const responseIds = this.entityToResponses.get(entityKey) || []; + return this.getResponsesById(responseIds); + } + + /** + * Get all responses that affected a specific link + * @param linkId The link id + * @returns List of agent response ids sorted by timestamp (oldest first) + */ + getResponsesByLink(linkId: string): AgentResponse[] { + const entityKey = `link:${linkId}`; + const responseIds = this.entityToResponses.get(entityKey) || []; + return this.getResponsesById(responseIds); + } + + /** + * Get all responses that affected a specific entity (operator or link) + * @param entityType Type of entity ("operator" or "link") + * @param entityId The entity id + * @returns List of agent responses sorted by timestamp (oldest first) + */ + getResponsesByEntity(entityType: "operator" | "link", entityId: string): AgentResponse[] { + const entityKey = `${entityType}:${entityId}`; + const responseIds = this.entityToResponses.get(entityKey) || []; + return this.getResponsesById(responseIds); + } + + /** + * Get response objects by their ids, sorted by timestamp + * @param responseIds List of response ids + * @returns List of agent responses sorted by timestamp (oldest first) + */ + private getResponsesById(responseIds: string[]): AgentResponse[] { + const responses: AgentResponse[] = []; + for (const id of responseIds) { + const response = this.responseMap.get(id); + if (response) { + responses.push(response); + } + } + // Sort by timestamp (oldest first) + return responses.sort((a, b) => a.timestamp - b.timestamp); + } + + /** + * Get a specific response by id + * @param responseId The response id + * @returns The agent response or undefined if not found + */ + getResponseById(responseId: string): AgentResponse | undefined { + return this.responseMap.get(responseId); + } + + /** + * Remove a response from the lineage tracking + * @param responseId The response id to remove + */ + removeResponse(responseId: string): void { + // Get affected entities before removing + const affectedEntities = this.responseToEntities.get(responseId) || []; + + // Remove from response -> entities mapping + this.responseToEntities.delete(responseId); + + // Remove from response map + this.responseMap.delete(responseId); + + // Remove from entity -> responses mapping + for (const entity of affectedEntities) { + const entityKey = `${entity.type}:${entity.id}`; + const responses = this.entityToResponses.get(entityKey); + if (responses) { + const filtered = responses.filter(id => id !== responseId); + if (filtered.length > 0) { + this.entityToResponses.set(entityKey, filtered); + } else { + this.entityToResponses.delete(entityKey); + } + } + } + } + + /** + * Remove all responses associated with a specific entity + * @param entityType Type of entity ("operator" or "link") + * @param entityId The entity id + */ + removeEntity(entityType: "operator" | "link", entityId: string): void { + const entityKey = `${entityType}:${entityId}`; + const responseIds = this.entityToResponses.get(entityKey) || []; + + // Remove the entity from all its associated responses + for (const responseId of responseIds) { + const affectedEntities = this.responseToEntities.get(responseId); + if (affectedEntities) { + const filtered = affectedEntities.filter(e => !(e.type === entityType && e.id === entityId)); + this.responseToEntities.set(responseId, filtered); + } + } + + // Remove the entity key + this.entityToResponses.delete(entityKey); + } + + /** + * Clear all lineage data + */ + clearAll(): void { + this.responseToEntities.clear(); + this.entityToResponses.clear(); + this.responseMap.clear(); + } + + /** + * Get all responses (sorted by timestamp) + * @returns List of all agent responses sorted by timestamp (oldest first) + */ + getAllResponses(): AgentResponse[] { + const responses = Array.from(this.responseMap.values()); + return responses.sort((a, b) => a.timestamp - b.timestamp); + } +} diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index a9140439bcc..de87bcca80a 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -21,6 +21,7 @@ import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp"; +import { v4 as uuid } from "uuid"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { createAddOperatorTool, @@ -68,6 +69,7 @@ import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling import { ValidationWorkflowService } from "../validation/validation-workflow.service"; import { COPILOT_SYSTEM_PROMPT } from "./copilot-prompts"; import { DataInconsistencyService } from "../data-inconsistency/data-inconsistency.service"; +import { ActionLineageService, AffectedEntity } from "./action-lineage.service"; // API endpoints as constants export const COPILOT_MCP_URL = "mcp"; @@ -87,6 +89,8 @@ export enum CopilotState { * Agent response structure for streaming intermediate and final results */ export interface AgentResponse { + id: string; // Unique identifier for this response + timestamp: number; // Unix timestamp in milliseconds type: "trace" | "response"; content: string; isDone: boolean; @@ -116,6 +120,9 @@ export class TexeraCopilot { // Message history using AI SDK's ModelMessage type private messages: ModelMessage[] = []; + // Map from message UUID to ModelMessage for tracking + private messageMap: Map = new Map(); + // Copilot state management private state: CopilotState = CopilotState.UNAVAILABLE; @@ -129,7 +136,8 @@ export class TexeraCopilot { private copilotCoeditorService: CopilotCoeditorService, private workflowCompilingService: WorkflowCompilingService, private validationWorkflowService: ValidationWorkflowService, - private dataInconsistencyService: DataInconsistencyService + private dataInconsistencyService: DataInconsistencyService, + private actionLineageService: ActionLineageService ) { // Don't auto-initialize, wait for user to enable } @@ -230,7 +238,9 @@ export class TexeraCopilot { // 1) push the user message (don't emit to stream - already handled by UI) const userMessage: UserModelMessage = { role: "user", content: message }; + const userMessageId = uuid(); this.messages.push(userMessage); + this.messageMap.set(userMessageId, userMessage); // 2) define tools (your existing helpers) const tools = this.createWorkflowTools(); @@ -259,6 +269,8 @@ export class TexeraCopilot { // If there are tool calls, emit raw trace data if (toolCalls && toolCalls.length > 0) { const traceResponse: AgentResponse = { + id: uuid(), + timestamp: Date.now(), type: "trace", content: text || "", isDone: false, @@ -267,6 +279,15 @@ export class TexeraCopilot { usage, }; + // Extract and record affected entities + try { + const affectedEntities = this.extractAffectedEntities(toolCalls, toolResults); + this.actionLineageService.recordResponseAction(traceResponse, affectedEntities); + } catch (error) { + console.error("Failed to record action lineage:", error); + // Continue execution even if lineage recording fails + } + // Emit raw trace data observer.next(traceResponse); } @@ -291,10 +312,21 @@ export class TexeraCopilot { // Emit final response with raw data const finalResponse: AgentResponse = { + id: uuid(), + timestamp: Date.now(), type: "response", content: text, isDone: true, }; + + // Record final response (may not have specific affected entities) + try { + this.actionLineageService.recordResponseAction(finalResponse, []); + } catch (error) { + console.error("Failed to record final response lineage:", error); + // Continue execution even if lineage recording fails + } + observer.next(finalResponse); observer.complete(); }) @@ -450,6 +482,70 @@ export class TexeraCopilot { }; } + /** + * Extract affected entities (operators and links) from tool calls and results + */ + private extractAffectedEntities(toolCalls: any[], toolResults?: any[]): AffectedEntity[] { + const affected: AffectedEntity[] = []; + + for (const toolCall of toolCalls) { + // Safely access toolName and args + if (!toolCall || typeof toolCall !== "object") { + continue; + } + + const toolName = toolCall.toolName; + const args = toolCall.args; + + // Skip if toolName or args are missing + if (!toolName || !args || typeof args !== "object") { + continue; + } + + // Extract based on tool type + switch (toolName) { + case "addOperator": + case "deleteOperator": + case "getOperator": + case "setOperatorProperty": + case "setPortProperty": + case "getOperatorPropertiesSchema": + case "getOperatorPortsInfo": + case "getOperatorMetadata": + case "getOperatorInputSchema": + case "hasOperatorResult": + case "getOperatorResult": + case "getOperatorResultInfo": + case "validateOperator": + if (args.operatorId) { + affected.push({ type: "operator", id: args.operatorId }); + } + break; + + case "addLink": + case "deleteLink": + if (args.linkId) { + affected.push({ type: "link", id: args.linkId }); + } + // Links also involve source and target operators + if (args.source?.operatorId) { + affected.push({ type: "operator", id: args.source.operatorId }); + } + if (args.target?.operatorId) { + affected.push({ type: "operator", id: args.target.operatorId }); + } + break; + + default: + // For other tools, we don't track specific entities + break; + } + } + + // Remove duplicates + return Array.from(new Map(affected.map(item => [`${item.type}:${item.id}`, item])).values()); + } + /** * Get conversation history as ModelMessage array */ From 8dd3c63b2e6992817a5a6d823d0781771370430e Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 28 Oct 2025 22:19:13 -0700 Subject: [PATCH 038/158] Revert "add initial action lineage" This reverts commit e93c682d56b540a1959395995dd73a0501a01591. --- frontend/src/app/app.module.ts | 2 - .../copilot-chat/copilot-chat.component.ts | 39 +--- .../inconsistency-list.component.html | 2 +- .../operator-response-panel.component.html | 82 ------- .../operator-response-panel.component.scss | 116 ---------- .../operator-response-panel.component.ts | 129 ----------- .../workflow-editor.component.html | 1 - .../workflow-editor.component.scss | 1 - .../service/copilot/action-lineage.service.ts | 210 ------------------ .../service/copilot/texera-copilot.ts | 98 +------- 10 files changed, 11 insertions(+), 669 deletions(-) delete mode 100644 frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.html delete mode 100644 frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.scss delete mode 100644 frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.ts delete mode 100644 frontend/src/app/workspace/service/copilot/action-lineage.service.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index f40b92e42c8..6213199d20f 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -179,7 +179,6 @@ import { AdminSettingsComponent } from "./dashboard/component/admin/settings/adm import { FormlyRepeatDndComponent } from "./common/formly/repeat-dnd/repeat-dnd.component"; import { NzInputNumberModule } from "ng-zorro-antd/input-number"; import { NzCheckboxModule } from "ng-zorro-antd/checkbox"; -import { OperatorResponsePanelComponent } from "./workspace/component/operator-response-panel/operator-response-panel.component"; import "deep-chat"; registerLocaleData(en); @@ -276,7 +275,6 @@ registerLocaleData(en); HubSearchResultComponent, ComputingUnitSelectionComponent, AdminSettingsComponent, - OperatorResponsePanelComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts index abccdbe3c2d..48c38a365dc 100644 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts +++ b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts @@ -26,9 +26,6 @@ export class CopilotChatComponent implements OnDestroy { const last = body?.messages?.[body.messages.length - 1]; const userText: string = typeof last?.text === "string" ? last.text : ""; - // Track if we've sent a final response - let finalResponseSent = false; - // Send message to copilot and process AgentResponse this.copilotService .sendMessage(userText) @@ -50,39 +47,21 @@ export class CopilotChatComponent implements OnDestroy { // This will let deep-chat handle adding the message and clearing loading if (response.isDone) { signals.onResponse({ text: response.content }); - finalResponseSent = true; } } }, error: (e: unknown) => { - // Format error message properly - let errorMessage = "Unknown error"; - if (e instanceof Error) { - errorMessage = e.message; - } else if (typeof e === "string") { - errorMessage = e; - } else if (e && typeof e === "object") { - errorMessage = JSON.stringify(e); - } - signals.onResponse({ error: errorMessage }); - console.error("Copilot error:", e); + signals.onResponse({ error: e ?? "Unknown error" }); }, complete: () => { - // Only send a response if we haven't already sent the final response - if (!finalResponseSent) { - const currentState = this.copilotService.getState(); - if (currentState === CopilotState.STOPPING) { - // Generation was stopped by user - show completion message - signals.onResponse({ text: "_Generation stopped._" }); - } else if (currentState === CopilotState.GENERATING) { - // Generation completed unexpectedly - signals.onResponse({ text: "_Generation completed._" }); - } else { - // Observable completed without a final response and state is not STOPPING or GENERATING - // This might happen if there was an issue - send a generic completion message - console.warn("Observable completed without final response. State:", currentState); - signals.onResponse({ text: "_Response completed._" }); - } + // Handle completion without final response (happens when generation is stopped) + const currentState = this.copilotService.getState(); + if (currentState === CopilotState.STOPPING) { + // Generation was stopped by user - show completion message + signals.onResponse({ text: "_Generation stopped._" }); + } else if (currentState === CopilotState.GENERATING) { + // Generation completed unexpectedly + signals.onResponse({ text: "_Generation completed._" }); } }, }); diff --git a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html index 5da008b07bd..802afbf6059 100644 --- a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html +++ b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html @@ -56,7 +56,7 @@

    Data Inconsistencies

    [nzExtra]="extraTemplate" class="inconsistency-card" (click)="onInconsistencyClick(inconsistency)" - style="cursor: pointer"> + style="cursor: pointer;">

    {{ inconsistency.description }}

    Operator: {{ inconsistency.operatorId }}

    diff --git a/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.html b/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.html deleted file mode 100644 index 9abd3062963..00000000000 --- a/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.html +++ /dev/null @@ -1,82 +0,0 @@ - - -
    -
    -

    Copilot Actions

    - -
    - -
    -
    -

    No copilot actions found for this operator.

    -
    - -
    - - -
    -
    Type: {{ response.type }}
    -
    - Content: -
    {{ response.content || '(No content)' }}
    -
    -
    - Tool Calls: -
      -
    • {{ toolCall.toolName }}
    • -
    -
    -
    - Token Usage: -
    - Input: {{ response.usage.inputTokens }} - Output: {{ response.usage.outputTokens }} - Total: {{ response.usage.totalTokens }} -
    -
    -
    -
    -
    -
    -
    -
    diff --git a/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.scss b/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.scss deleted file mode 100644 index 0f9af8d8923..00000000000 --- a/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.scss +++ /dev/null @@ -1,116 +0,0 @@ -/** - * 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. - */ - -.operator-response-panel { - position: absolute; - width: 350px; - max-height: 500px; - background: white; - border: 1px solid #d9d9d9; - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - display: flex; - flex-direction: column; - z-index: 1000; - pointer-events: all; - - .panel-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - border-bottom: 1px solid #f0f0f0; - - h4 { - margin: 0; - font-size: 14px; - font-weight: 600; - } - } - - .panel-content { - flex: 1; - overflow-y: auto; - padding: 16px; - - .no-responses { - text-align: center; - color: #8c8c8c; - padding: 20px; - - p { - margin: 0; - } - } - - .responses-list { - .response-details { - font-size: 12px; - - > div { - margin-bottom: 12px; - - &:last-child { - margin-bottom: 0; - } - } - - strong { - display: block; - margin-bottom: 4px; - color: #262626; - } - - .content-text { - padding: 8px; - background: #f5f5f5; - border-radius: 2px; - white-space: pre-wrap; - word-break: break-word; - max-height: 200px; - overflow-y: auto; - } - - .tool-calls { - ul { - margin: 0; - padding-left: 20px; - - li { - margin-bottom: 4px; - } - } - } - - .usage-details { - display: flex; - gap: 12px; - font-size: 11px; - color: #595959; - - span { - padding: 2px 6px; - background: #fafafa; - border-radius: 2px; - } - } - } - } - } -} diff --git a/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.ts b/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.ts deleted file mode 100644 index 88f190c4fc1..00000000000 --- a/frontend/src/app/workspace/component/operator-response-panel/operator-response-panel.component.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * 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 { Component, OnInit } from "@angular/core"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { ActionLineageService } from "../../service/copilot/action-lineage.service"; -import { AgentResponse } from "../../service/copilot/texera-copilot"; -import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; -import { JointUIService } from "../../service/joint-ui/joint-ui.service"; - -@UntilDestroy() -@Component({ - selector: "texera-operator-response-panel", - templateUrl: "./operator-response-panel.component.html", - styleUrls: ["./operator-response-panel.component.scss"], -}) -export class OperatorResponsePanelComponent implements OnInit { - public isVisible = false; - public selectedOperatorId: string | null = null; - public relatedResponses: AgentResponse[] = []; - public panelTop = 0; - public panelLeft = 0; - - constructor( - private actionLineageService: ActionLineageService, - private workflowActionService: WorkflowActionService - ) {} - - ngOnInit(): void { - // Listen to operator selection events - this.workflowActionService - .getJointGraphWrapper() - .getJointOperatorHighlightStream() - .pipe(untilDestroyed(this)) - .subscribe((highlightedOperatorIDs: readonly string[]) => { - if (highlightedOperatorIDs.length === 1) { - // Single operator selected - this.selectedOperatorId = highlightedOperatorIDs[0]; - this.loadRelatedResponses(highlightedOperatorIDs[0]); - this.updatePanelPosition(highlightedOperatorIDs[0]); - this.isVisible = true; - } else { - // Multiple or no operators selected - this.isVisible = false; - this.selectedOperatorId = null; - this.relatedResponses = []; - } - }); - - // Listen to operator position changes to update panel position - this.workflowActionService - .getJointGraphWrapper() - .getElementPositionChangeEvent() - .pipe(untilDestroyed(this)) - .subscribe(event => { - if (this.isVisible && this.selectedOperatorId === event.elementID) { - this.updatePanelPosition(event.elementID); - } - }); - } - - /** - * Load related responses for a given operator - */ - private loadRelatedResponses(operatorId: string): void { - this.relatedResponses = this.actionLineageService.getResponsesByOperator(operatorId); - } - - /** - * Update panel position to display below the operator - */ - private updatePanelPosition(operatorId: string): void { - try { - const position = this.workflowActionService.getJointGraphWrapper().getElementPosition(operatorId); - const operatorHeight = JointUIService.DEFAULT_OPERATOR_HEIGHT; - - // Position the panel below the operator with some spacing - const spacing = 10; - this.panelLeft = position.x; - this.panelTop = position.y + operatorHeight + spacing; - } catch (error) { - console.error("Failed to get operator position:", error); - // If we can't get the position, hide the panel - this.isVisible = false; - } - } - - /** - * Format timestamp for display - */ - public formatTimestamp(timestamp: number): string { - return new Date(timestamp).toLocaleString(); - } - - /** - * Get a preview of the response content (first 100 characters) - */ - public getContentPreview(content: string): string { - if (content.length <= 100) { - return content; - } - return content.substring(0, 100) + "..."; - } - - /** - * Close the panel - */ - public closePanel(): void { - this.isVisible = false; - this.selectedOperatorId = null; - this.relatedResponses = []; - } -} diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html index e9e5c43e432..8f87c66c7a7 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html @@ -21,7 +21,6 @@
    - diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss index 3024bee5d72..72caad0296d 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss @@ -19,7 +19,6 @@ #workflow-editor-wrapper { height: 100%; - position: relative; } #workflow-editor { diff --git a/frontend/src/app/workspace/service/copilot/action-lineage.service.ts b/frontend/src/app/workspace/service/copilot/action-lineage.service.ts deleted file mode 100644 index 72b716fe36d..00000000000 --- a/frontend/src/app/workspace/service/copilot/action-lineage.service.ts +++ /dev/null @@ -1,210 +0,0 @@ -/** - * 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 { AgentResponse } from "./texera-copilot"; - -/** - * Represents an affected entity (operator or link) in the workflow - */ -export interface AffectedEntity { - type: "operator" | "link"; - id: string; -} - -/** - * Service for managing the lineage between agent responses and workflow entities - * Tracks which operators/links were affected by each agent response - */ -@Injectable({ - providedIn: "root", -}) -export class ActionLineageService { - // Map from AgentResponse id to list of affected operator/link ids - private responseToEntities: Map = new Map(); - - // Map from operator/link id to list of AgentResponse ids - private entityToResponses: Map = new Map(); - - // Map from AgentResponse id to the full AgentResponse object for retrieval - private responseMap: Map = new Map(); - - constructor() {} - - /** - * Record that a response affected certain operators/links - * @param response The agent response - * @param affectedEntities List of operators/links that were affected - */ - recordResponseAction(response: AgentResponse, affectedEntities: AffectedEntity[]): void { - // Store the response object - this.responseMap.set(response.id, response); - - // Store response -> entities mapping - this.responseToEntities.set(response.id, affectedEntities); - - // Update entity -> responses mapping - for (const entity of affectedEntities) { - const entityKey = `${entity.type}:${entity.id}`; - const existingResponses = this.entityToResponses.get(entityKey) || []; - if (!existingResponses.includes(response.id)) { - existingResponses.push(response.id); - this.entityToResponses.set(entityKey, existingResponses); - } - } - } - - /** - * Get all entities affected by a specific response - * @param responseId The agent response id - * @returns List of affected entities or empty array if not found - */ - getAffectedEntitiesByResponse(responseId: string): AffectedEntity[] { - return this.responseToEntities.get(responseId) || []; - } - - /** - * Get all responses that affected a specific operator - * @param operatorId The operator id - * @returns List of agent response ids sorted by timestamp (oldest first) - */ - getResponsesByOperator(operatorId: string): AgentResponse[] { - const entityKey = `operator:${operatorId}`; - const responseIds = this.entityToResponses.get(entityKey) || []; - return this.getResponsesById(responseIds); - } - - /** - * Get all responses that affected a specific link - * @param linkId The link id - * @returns List of agent response ids sorted by timestamp (oldest first) - */ - getResponsesByLink(linkId: string): AgentResponse[] { - const entityKey = `link:${linkId}`; - const responseIds = this.entityToResponses.get(entityKey) || []; - return this.getResponsesById(responseIds); - } - - /** - * Get all responses that affected a specific entity (operator or link) - * @param entityType Type of entity ("operator" or "link") - * @param entityId The entity id - * @returns List of agent responses sorted by timestamp (oldest first) - */ - getResponsesByEntity(entityType: "operator" | "link", entityId: string): AgentResponse[] { - const entityKey = `${entityType}:${entityId}`; - const responseIds = this.entityToResponses.get(entityKey) || []; - return this.getResponsesById(responseIds); - } - - /** - * Get response objects by their ids, sorted by timestamp - * @param responseIds List of response ids - * @returns List of agent responses sorted by timestamp (oldest first) - */ - private getResponsesById(responseIds: string[]): AgentResponse[] { - const responses: AgentResponse[] = []; - for (const id of responseIds) { - const response = this.responseMap.get(id); - if (response) { - responses.push(response); - } - } - // Sort by timestamp (oldest first) - return responses.sort((a, b) => a.timestamp - b.timestamp); - } - - /** - * Get a specific response by id - * @param responseId The response id - * @returns The agent response or undefined if not found - */ - getResponseById(responseId: string): AgentResponse | undefined { - return this.responseMap.get(responseId); - } - - /** - * Remove a response from the lineage tracking - * @param responseId The response id to remove - */ - removeResponse(responseId: string): void { - // Get affected entities before removing - const affectedEntities = this.responseToEntities.get(responseId) || []; - - // Remove from response -> entities mapping - this.responseToEntities.delete(responseId); - - // Remove from response map - this.responseMap.delete(responseId); - - // Remove from entity -> responses mapping - for (const entity of affectedEntities) { - const entityKey = `${entity.type}:${entity.id}`; - const responses = this.entityToResponses.get(entityKey); - if (responses) { - const filtered = responses.filter(id => id !== responseId); - if (filtered.length > 0) { - this.entityToResponses.set(entityKey, filtered); - } else { - this.entityToResponses.delete(entityKey); - } - } - } - } - - /** - * Remove all responses associated with a specific entity - * @param entityType Type of entity ("operator" or "link") - * @param entityId The entity id - */ - removeEntity(entityType: "operator" | "link", entityId: string): void { - const entityKey = `${entityType}:${entityId}`; - const responseIds = this.entityToResponses.get(entityKey) || []; - - // Remove the entity from all its associated responses - for (const responseId of responseIds) { - const affectedEntities = this.responseToEntities.get(responseId); - if (affectedEntities) { - const filtered = affectedEntities.filter(e => !(e.type === entityType && e.id === entityId)); - this.responseToEntities.set(responseId, filtered); - } - } - - // Remove the entity key - this.entityToResponses.delete(entityKey); - } - - /** - * Clear all lineage data - */ - clearAll(): void { - this.responseToEntities.clear(); - this.entityToResponses.clear(); - this.responseMap.clear(); - } - - /** - * Get all responses (sorted by timestamp) - * @returns List of all agent responses sorted by timestamp (oldest first) - */ - getAllResponses(): AgentResponse[] { - const responses = Array.from(this.responseMap.values()); - return responses.sort((a, b) => a.timestamp - b.timestamp); - } -} diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index de87bcca80a..a9140439bcc 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -21,7 +21,6 @@ import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp"; -import { v4 as uuid } from "uuid"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { createAddOperatorTool, @@ -69,7 +68,6 @@ import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling import { ValidationWorkflowService } from "../validation/validation-workflow.service"; import { COPILOT_SYSTEM_PROMPT } from "./copilot-prompts"; import { DataInconsistencyService } from "../data-inconsistency/data-inconsistency.service"; -import { ActionLineageService, AffectedEntity } from "./action-lineage.service"; // API endpoints as constants export const COPILOT_MCP_URL = "mcp"; @@ -89,8 +87,6 @@ export enum CopilotState { * Agent response structure for streaming intermediate and final results */ export interface AgentResponse { - id: string; // Unique identifier for this response - timestamp: number; // Unix timestamp in milliseconds type: "trace" | "response"; content: string; isDone: boolean; @@ -120,9 +116,6 @@ export class TexeraCopilot { // Message history using AI SDK's ModelMessage type private messages: ModelMessage[] = []; - // Map from message UUID to ModelMessage for tracking - private messageMap: Map = new Map(); - // Copilot state management private state: CopilotState = CopilotState.UNAVAILABLE; @@ -136,8 +129,7 @@ export class TexeraCopilot { private copilotCoeditorService: CopilotCoeditorService, private workflowCompilingService: WorkflowCompilingService, private validationWorkflowService: ValidationWorkflowService, - private dataInconsistencyService: DataInconsistencyService, - private actionLineageService: ActionLineageService + private dataInconsistencyService: DataInconsistencyService ) { // Don't auto-initialize, wait for user to enable } @@ -238,9 +230,7 @@ export class TexeraCopilot { // 1) push the user message (don't emit to stream - already handled by UI) const userMessage: UserModelMessage = { role: "user", content: message }; - const userMessageId = uuid(); this.messages.push(userMessage); - this.messageMap.set(userMessageId, userMessage); // 2) define tools (your existing helpers) const tools = this.createWorkflowTools(); @@ -269,8 +259,6 @@ export class TexeraCopilot { // If there are tool calls, emit raw trace data if (toolCalls && toolCalls.length > 0) { const traceResponse: AgentResponse = { - id: uuid(), - timestamp: Date.now(), type: "trace", content: text || "", isDone: false, @@ -279,15 +267,6 @@ export class TexeraCopilot { usage, }; - // Extract and record affected entities - try { - const affectedEntities = this.extractAffectedEntities(toolCalls, toolResults); - this.actionLineageService.recordResponseAction(traceResponse, affectedEntities); - } catch (error) { - console.error("Failed to record action lineage:", error); - // Continue execution even if lineage recording fails - } - // Emit raw trace data observer.next(traceResponse); } @@ -312,21 +291,10 @@ export class TexeraCopilot { // Emit final response with raw data const finalResponse: AgentResponse = { - id: uuid(), - timestamp: Date.now(), type: "response", content: text, isDone: true, }; - - // Record final response (may not have specific affected entities) - try { - this.actionLineageService.recordResponseAction(finalResponse, []); - } catch (error) { - console.error("Failed to record final response lineage:", error); - // Continue execution even if lineage recording fails - } - observer.next(finalResponse); observer.complete(); }) @@ -482,70 +450,6 @@ export class TexeraCopilot { }; } - /** - * Extract affected entities (operators and links) from tool calls and results - */ - private extractAffectedEntities(toolCalls: any[], toolResults?: any[]): AffectedEntity[] { - const affected: AffectedEntity[] = []; - - for (const toolCall of toolCalls) { - // Safely access toolName and args - if (!toolCall || typeof toolCall !== "object") { - continue; - } - - const toolName = toolCall.toolName; - const args = toolCall.args; - - // Skip if toolName or args are missing - if (!toolName || !args || typeof args !== "object") { - continue; - } - - // Extract based on tool type - switch (toolName) { - case "addOperator": - case "deleteOperator": - case "getOperator": - case "setOperatorProperty": - case "setPortProperty": - case "getOperatorPropertiesSchema": - case "getOperatorPortsInfo": - case "getOperatorMetadata": - case "getOperatorInputSchema": - case "hasOperatorResult": - case "getOperatorResult": - case "getOperatorResultInfo": - case "validateOperator": - if (args.operatorId) { - affected.push({ type: "operator", id: args.operatorId }); - } - break; - - case "addLink": - case "deleteLink": - if (args.linkId) { - affected.push({ type: "link", id: args.linkId }); - } - // Links also involve source and target operators - if (args.source?.operatorId) { - affected.push({ type: "operator", id: args.source.operatorId }); - } - if (args.target?.operatorId) { - affected.push({ type: "operator", id: args.target.operatorId }); - } - break; - - default: - // For other tools, we don't track specific entities - break; - } - } - - // Remove duplicates - return Array.from(new Map(affected.map(item => [`${item.type}:${item.id}`, item])).values()); - } - /** * Get conversation history as ModelMessage array */ From 41d0abf02ecc8703b9a6f6c72416b1eeb174a447 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Wed, 29 Oct 2025 00:36:10 -0700 Subject: [PATCH 039/158] add action plan --- .../inconsistency-list.component.html | 2 +- .../service/copilot/copilot-prompts.ts | 13 +- .../service/copilot/texera-copilot.ts | 10 + .../service/copilot/workflow-tools.ts | 175 ++++++++++++++++++ 4 files changed, 193 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html index 802afbf6059..5da008b07bd 100644 --- a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html +++ b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html @@ -56,7 +56,7 @@

    Data Inconsistencies

    [nzExtra]="extraTemplate" class="inconsistency-card" (click)="onInconsistencyClick(inconsistency)" - style="cursor: pointer;"> + style="cursor: pointer">

    {{ inconsistency.description }}

    Operator: {{ inconsistency.operatorId }}

    diff --git a/frontend/src/app/workspace/service/copilot/copilot-prompts.ts b/frontend/src/app/workspace/service/copilot/copilot-prompts.ts index c0199371cb5..a905e48a98a 100644 --- a/frontend/src/app/workspace/service/copilot/copilot-prompts.ts +++ b/frontend/src/app/workspace/service/copilot/copilot-prompts.ts @@ -44,12 +44,13 @@ DO NOT USE View Result Operator ### Generation Strategy A good generation style follows these steps: -1. After adding an operator, check the properties of that operator in order to properly configure it. -2. After configure it, validate the workflow to make sure your modification is valid. -3. If workflow is invalid, use the corresponding tools to check the validity and see how to fix it. -4. Run the workflow to see the operator's result -5. ONLY EXECUTE THE WORKFLOW when workflow is invalid. -6. After you identify a data inconsistency, please use the corresponding tool to record the finding +1. Use the corresponding tool to generate an action plan +2. After adding operator(s), check the properties of that operator in order to properly configure it. +3. After configure it, validate the workflow to make sure your modification is valid. +4. If workflow is invalid, use the corresponding tools to check the validity and see how to fix it. +5. Run the workflow to see the operator's result +6. ONLY EXECUTE THE WORKFLOW when workflow is invalid. +7. After you identify a data inconsistency, please use the corresponding tool to record the finding --- ## PythonUDFV2 Operator diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index a9140439bcc..eed2d90f413 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -25,6 +25,7 @@ import { WorkflowActionService } from "../workflow-graph/model/workflow-action.s import { createAddOperatorTool, createAddLinkTool, + createActionPlanTool, createListOperatorsTool, createListLinksTool, createListOperatorTypesTool, @@ -327,6 +328,14 @@ export class TexeraCopilot { ) ); const addLinkTool = toolWithTimeout(createAddLinkTool(this.workflowActionService)); + const actionPlanTool = toolWithTimeout( + createActionPlanTool( + this.workflowActionService, + this.workflowUtilService, + this.operatorMetadataService, + this.copilotCoeditorService + ) + ); const listOperatorsTool = toolWithTimeout( createListOperatorsTool(this.workflowActionService, this.copilotCoeditorService) ); @@ -419,6 +428,7 @@ export class TexeraCopilot { // ...mcpToolsForAI, addOperator: addOperatorTool, addLink: addLinkTool, + actionPlan: actionPlanTool, listOperators: listOperatorsTool, listLinks: listLinksTool, listOperatorTypes: listOperatorTypesTool, diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index 4933681b451..d0eabd15d7d 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -38,6 +38,17 @@ const TOOL_TIMEOUT_MS = 120000; // Estimated as characters / 4 (common approximation for token counting) const MAX_OPERATOR_RESULT_TOKEN_LIMIT = 1000; +export interface ActionPlan { + summary: string; + operators: Array<{ operatorType: string; customDisplayName?: string; description?: string }>; + links: Array<{ + sourceOperatorId: string; + targetOperatorId: string; + sourcePortId?: string; + targetPortId?: string; + }>; +} + /** * Estimates the number of tokens in a JSON-serializable object * Uses a common approximation: tokens ≈ characters / 4 @@ -204,6 +215,170 @@ export function createAddLinkTool(workflowActionService: WorkflowActionService) }); } +/** + * Create actionPlan tool for adding batch operators and links + */ +export function createActionPlanTool( + workflowActionService: WorkflowActionService, + workflowUtilService: WorkflowUtilService, + operatorMetadataService: OperatorMetadataService, + copilotCoeditor: CopilotCoeditorService +) { + return tool({ + name: "actionPlan", + description: + "Add a batch of operators and links to the workflow as part of an action plan. This tool is used to show the structure of what you plan to add without filling in detailed operator properties. It creates a workflow skeleton that demonstrates the planned data flow.", + inputSchema: z.object({ + summary: z.string().describe("A brief summary of what this action plan does"), + operators: z + .array( + z.object({ + operatorType: z.string().describe("Type of operator (e.g., 'CSVSource', 'Filter', 'Aggregate')"), + customDisplayName: z + .string() + .optional() + .describe("Brief custom name summarizing what this operator does in one sentence"), + description: z.string().optional().describe("Detailed description of what this operator will do"), + }) + ) + .describe("List of operators to add to the workflow"), + links: z + .array( + z.object({ + sourceOperatorId: z + .string() + .describe( + "ID of the source operator - can be either an existing operator ID from the workflow, or an index (e.g., '0', '1', '2') referring to operators in the plan array (0-based)" + ), + targetOperatorId: z + .string() + .describe( + "ID of the target operator - can be either an existing operator ID from the workflow, or an index (e.g., '0', '1', '2') referring to operators in the plan array (0-based)" + ), + sourcePortId: z.string().optional().describe("Port ID on source operator (e.g., 'output-0')"), + targetPortId: z.string().optional().describe("Port ID on target operator (e.g., 'input-0')"), + }) + ) + .describe("List of links to connect the operators"), + }), + execute: async (args: { summary: string; operators: ActionPlan["operators"]; links: ActionPlan["links"] }) => { + try { + // Clear previous highlights at start of tool execution + copilotCoeditor.clearAll(); + + // Validate all operator types exist + for (let i = 0; i < args.operators.length; i++) { + const operatorSpec = args.operators[i]; + if (!operatorMetadataService.operatorTypeExists(operatorSpec.operatorType)) { + return { + success: false, + error: `Unknown operator type at index ${i}: ${operatorSpec.operatorType}. Use listOperatorTypes tool to see available types.`, + }; + } + } + + // Helper function to resolve operator ID (can be existing ID or index string) + const resolveOperatorId = (idOrIndex: string, createdIds: string[]): string | null => { + // Check if it's a numeric index (referring to operators array) + const indexMatch = idOrIndex.match(/^(\d+)$/); + if (indexMatch) { + const index = parseInt(indexMatch[1], 10); + if (index >= 0 && index < createdIds.length) { + return createdIds[index]; + } + return null; // Invalid index + } + + // Otherwise, treat as existing operator ID + const existingOp = workflowActionService.getTexeraGraph().getOperator(idOrIndex); + return existingOp ? idOrIndex : null; + }; + + // Create all operators and store their IDs + const createdOperatorIds: string[] = []; + const existingOperators = workflowActionService.getTexeraGraph().getAllOperators(); + const startIndex = existingOperators.length; + + for (let i = 0; i < args.operators.length; i++) { + const operatorSpec = args.operators[i]; + + // Get a new operator predicate with default settings and optional custom display name + const operator = workflowUtilService.getNewOperatorPredicate( + operatorSpec.operatorType, + operatorSpec.customDisplayName + ); + + // Calculate a default position with better spacing for batch operations + const defaultX = 100 + ((startIndex + i) % 5) * 200; + const defaultY = 100 + Math.floor((startIndex + i) / 5) * 150; + const position = { x: defaultX, y: defaultY }; + + // Add the operator to the workflow + workflowActionService.addOperator(operator, position); + createdOperatorIds.push(operator.operatorID); + } + + // Create all links using the operator IDs + const createdLinkIds: string[] = []; + for (let i = 0; i < args.links.length; i++) { + const linkSpec = args.links[i]; + + // Resolve source and target operator IDs + const sourceOperatorId = resolveOperatorId(linkSpec.sourceOperatorId, createdOperatorIds); + const targetOperatorId = resolveOperatorId(linkSpec.targetOperatorId, createdOperatorIds); + + if (!sourceOperatorId) { + return { + success: false, + error: `Invalid source operator ID at link ${i}: '${linkSpec.sourceOperatorId}'. Must be either an existing operator ID or a valid index (0-${createdOperatorIds.length - 1}).`, + }; + } + + if (!targetOperatorId) { + return { + success: false, + error: `Invalid target operator ID at link ${i}: '${linkSpec.targetOperatorId}'. Must be either an existing operator ID or a valid index (0-${createdOperatorIds.length - 1}).`, + }; + } + + const sourcePId = linkSpec.sourcePortId || "output-0"; + const targetPId = linkSpec.targetPortId || "input-0"; + + const link: OperatorLink = { + linkID: `link_${Date.now()}_${Math.random()}`, + source: { + operatorID: sourceOperatorId, + portID: sourcePId, + }, + target: { + operatorID: targetOperatorId, + portID: targetPId, + }, + }; + + workflowActionService.addLink(link); + createdLinkIds.push(link.linkID); + } + + // Show copilot is adding these operators (after they're added to graph) + setTimeout(() => { + copilotCoeditor.highlightOperators(createdOperatorIds); + }, 100); + + return { + success: true, + summary: args.summary, + operatorIds: createdOperatorIds, + linkIds: createdLinkIds, + message: `Action Plan: ${args.summary}. Added ${args.operators.length} operator(s) and ${args.links.length} link(s) to workflow.`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} + /** * Create listOperators tool for getting all operators in the workflow */ From 5bca12028a9b305397713ab87c2edd917ae3143a Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Wed, 29 Oct 2025 00:45:43 -0700 Subject: [PATCH 040/158] introduce message window --- .../service/copilot/texera-copilot.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index eed2d90f413..1bf04aa0b3d 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -74,6 +74,9 @@ import { DataInconsistencyService } from "../data-inconsistency/data-inconsisten export const COPILOT_MCP_URL = "mcp"; export const AGENT_MODEL_ID = "claude-3.7"; +// Message window size: -1 means no limit, positive value keeps only latest N messages +export const MESSAGE_WINDOW_SIZE = 3; + /** * Copilot state enum */ @@ -233,6 +236,11 @@ export class TexeraCopilot { const userMessage: UserModelMessage = { role: "user", content: message }; this.messages.push(userMessage); + // Trim messages array to keep only the latest MESSAGE_WINDOW_SIZE messages if window size is set + if (MESSAGE_WINDOW_SIZE > 0 && this.messages.length > MESSAGE_WINDOW_SIZE) { + this.messages = this.messages.slice(-MESSAGE_WINDOW_SIZE); + } + // 2) define tools (your existing helpers) const tools = this.createWorkflowTools(); @@ -278,6 +286,11 @@ export class TexeraCopilot { // This keeps your history perfectly aligned with the SDK's internal state. this.messages.push(...response.messages); + // Trim messages array to keep only the latest MESSAGE_WINDOW_SIZE messages if window size is set + if (MESSAGE_WINDOW_SIZE > 0 && this.messages.length > MESSAGE_WINDOW_SIZE) { + this.messages = this.messages.slice(-MESSAGE_WINDOW_SIZE); + } + // 5) optional diagnostics if (steps?.length) { const totalToolCalls = steps.flatMap(s => s.toolCalls || []).length; @@ -310,6 +323,12 @@ export class TexeraCopilot { const errorText = `Error: ${err?.message ?? String(err)}`; const assistantError: AssistantModelMessage = { role: "assistant", content: errorText }; this.messages.push(assistantError); + + // Trim messages array to keep only the latest MESSAGE_WINDOW_SIZE messages if window size is set + if (MESSAGE_WINDOW_SIZE > 0 && this.messages.length > MESSAGE_WINDOW_SIZE) { + this.messages = this.messages.slice(-MESSAGE_WINDOW_SIZE); + } + observer.error(err); }); }); @@ -472,6 +491,11 @@ export class TexeraCopilot { */ public addMessage(message: ModelMessage): void { this.messages.push(message); + + // Trim messages array to keep only the latest MESSAGE_WINDOW_SIZE messages if window size is set + if (MESSAGE_WINDOW_SIZE > 0 && this.messages.length > MESSAGE_WINDOW_SIZE) { + this.messages = this.messages.slice(-MESSAGE_WINDOW_SIZE); + } } /** From f77281270362713d2f360e13caef6fa0a1631b42 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Wed, 29 Oct 2025 10:08:44 -0700 Subject: [PATCH 041/158] showing the action plan highlight --- .../workflow-editor.component.scss | 6 ++ .../workflow-editor.component.ts | 77 ++++++++++++++++++- .../action-plan/action-plan.service.ts | 56 ++++++++++++++ .../service/copilot/texera-copilot.ts | 7 +- .../service/copilot/workflow-tools.ts | 9 ++- 5 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 frontend/src/app/workspace/service/action-plan/action-plan.service.ts diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss index 72caad0296d..d220c8f51ff 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss @@ -29,6 +29,12 @@ display: none; } +::ng-deep .action-plan { + // Action plan highlights - temporary 5-second visual indicators + // Styles are defined inline in JointJS element, but this provides a hook for customization + pointer-events: none; +} + ::ng-deep .hide-worker-count .operator-worker-count { display: none; } diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts index ca76a5d2f22..438768839b9 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts @@ -43,6 +43,7 @@ import { isDefined } from "../../../common/util/predicate"; import { GuiConfigService } from "../../../common/service/gui-config.service"; import { line, curveCatmullRomClosed } from "d3-shape"; import concaveman from "concaveman"; +import { ActionPlanService } from "../../service/action-plan/action-plan.service"; // jointjs interactive options for enabling and disabling interactivity // https://resources.jointjs.com/docs/jointjs/v3.2/joint.html#dia.Paper.prototype.options.interactive @@ -112,7 +113,8 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy private router: Router, public nzContextMenu: NzContextMenuService, private elementRef: ElementRef, - private config: GuiConfigService + private config: GuiConfigService, + private actionPlanService: ActionPlanService ) { this.wrapper = this.workflowActionService.getJointGraphWrapper(); } @@ -164,6 +166,7 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy this.registerPortDisplayNameChangeHandler(); this.handleOperatorStatisticsUpdate(); this.handleRegionEvents(); + this.handleActionPlanHighlight(); this.handleOperatorSuggestionHighlightEvent(); this.handleElementDelete(); this.handleElementSelectAll(); @@ -405,6 +408,78 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy regionElement.attr("body/d", line().curve(curveCatmullRomClosed)(concaveman(points, 2, 0) as [number, number][])); } + private handleActionPlanHighlight(): void { + // Define ActionPlan JointJS element with blue color + const ActionPlan = joint.dia.Element.define( + "action-plan", + { + attrs: { + body: { + fill: "rgba(79,195,255,0.2)", + stroke: "rgba(79,195,255,0.6)", + strokeWidth: 2, + strokeDasharray: "5,5", + pointerEvents: "none", + class: "action-plan", + }, + }, + }, + { + markup: [{ tagName: "path", selector: "body" }], + } + ); + + // Subscribe to action plan highlight events + this.actionPlanService + .getActionPlanHighlightStream() + .pipe(untilDestroyed(this)) + .subscribe(actionPlan => { + // Get operator elements from IDs + const operators = actionPlan.operatorIds + .map(id => this.paper.getModelById(id)) + .filter(op => op !== undefined); + + if (operators.length === 0) { + return; // No valid operators found + } + + // Create action plan highlight element + const element = new ActionPlan(); + this.paper.model.addCell(element); + + // Update the highlight to wrap around operators + this.updateActionPlanElement(element, operators); + + // Listen to operator position changes to update the highlight + const positionHandler = (operator: joint.dia.Cell) => { + if (operators.includes(operator)) { + this.updateActionPlanElement(element, operators); + } + }; + this.paper.model.on("change:position", positionHandler); + + // Remove highlight after 5 seconds + setTimeout(() => { + element.remove(); + this.paper.model.off("change:position", positionHandler); + }, 5000); + }); + } + + private updateActionPlanElement(element: joint.dia.Element, operators: joint.dia.Cell[]) { + const points = operators.flatMap(op => { + const { x, y, width, height } = op.getBBox(), + padding = 20; // Slightly larger padding than regions + return [ + [x - padding, y - padding], + [x + width + padding, y - padding], + [x - padding, y + height + padding + 10], + [x + width + padding, y + height + padding + 10], + ]; + }); + element.attr("body/d", line().curve(curveCatmullRomClosed)(concaveman(points, 2, 0) as [number, number][])); + } + /** * Handles restore offset default event by translating jointJS paper * back to original position diff --git a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts new file mode 100644 index 00000000000..03b506b13db --- /dev/null +++ b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts @@ -0,0 +1,56 @@ +/** + * 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 { Subject } from "rxjs"; + +/** + * Interface for an action plan highlight event + */ +export interface ActionPlanHighlight { + operatorIds: string[]; + summary: string; +} + +/** + * Service to manage temporary action plan highlights + * Emits events when action plans are created, which trigger 5-second visual highlights + */ +@Injectable({ + providedIn: "root", +}) +export class ActionPlanService { + private actionPlanHighlightSubject = new Subject(); + + constructor() {} + + /** + * Get action plan highlight stream + */ + public getActionPlanHighlightStream() { + return this.actionPlanHighlightSubject.asObservable(); + } + + /** + * Show a temporary highlight for an action plan (5 seconds) + */ + public showActionPlanHighlight(operatorIds: string[], summary: string): void { + this.actionPlanHighlightSubject.next({ operatorIds, summary }); + } +} diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 1bf04aa0b3d..7fdcb5450cb 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -69,6 +69,7 @@ import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling import { ValidationWorkflowService } from "../validation/validation-workflow.service"; import { COPILOT_SYSTEM_PROMPT } from "./copilot-prompts"; import { DataInconsistencyService } from "../data-inconsistency/data-inconsistency.service"; +import { ActionPlanService } from "../action-plan/action-plan.service"; // API endpoints as constants export const COPILOT_MCP_URL = "mcp"; @@ -133,7 +134,8 @@ export class TexeraCopilot { private copilotCoeditorService: CopilotCoeditorService, private workflowCompilingService: WorkflowCompilingService, private validationWorkflowService: ValidationWorkflowService, - private dataInconsistencyService: DataInconsistencyService + private dataInconsistencyService: DataInconsistencyService, + private actionPlanService: ActionPlanService ) { // Don't auto-initialize, wait for user to enable } @@ -352,7 +354,8 @@ export class TexeraCopilot { this.workflowActionService, this.workflowUtilService, this.operatorMetadataService, - this.copilotCoeditorService + this.copilotCoeditorService, + this.actionPlanService ) ); const listOperatorsTool = toolWithTimeout( diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index d0eabd15d7d..c6043412274 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -30,6 +30,7 @@ import { CopilotCoeditorService } from "./copilot-coeditor.service"; import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; import { ValidationWorkflowService } from "../validation/validation-workflow.service"; import { DataInconsistencyService } from "../data-inconsistency/data-inconsistency.service"; +import { ActionPlanService } from "../action-plan/action-plan.service"; // Tool execution timeout in milliseconds (5 seconds) const TOOL_TIMEOUT_MS = 120000; @@ -222,7 +223,8 @@ export function createActionPlanTool( workflowActionService: WorkflowActionService, workflowUtilService: WorkflowUtilService, operatorMetadataService: OperatorMetadataService, - copilotCoeditor: CopilotCoeditorService + copilotCoeditor: CopilotCoeditorService, + actionPlanService: ActionPlanService ) { return tool({ name: "actionPlan", @@ -365,6 +367,11 @@ export function createActionPlanTool( copilotCoeditor.highlightOperators(createdOperatorIds); }, 100); + // Trigger action plan highlight (5-second visual indicator) + setTimeout(() => { + actionPlanService.showActionPlanHighlight(createdOperatorIds, args.summary); + }, 150); + return { success: true, summary: args.summary, From 5d38849ef119191c161e3dc3ea5aaed0d10aa6d0 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Wed, 29 Oct 2025 10:58:32 -0700 Subject: [PATCH 042/158] finish the action plan ui --- frontend/src/app/app.module.ts | 2 + .../action-plan-feedback.component.html | 44 ++++++ .../action-plan-feedback.component.scss | 81 +++++++++++ .../action-plan-feedback.component.ts | 45 +++++++ .../workflow-editor.component.html | 8 ++ .../workflow-editor.component.ts | 127 +++++++++++++++--- .../action-plan/action-plan.service.ts | 70 +++++++++- .../service/copilot/texera-copilot.ts | 20 +++ .../service/copilot/workflow-tools.ts | 31 ++++- 9 files changed, 400 insertions(+), 28 deletions(-) create mode 100644 frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.html create mode 100644 frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.scss create mode 100644 frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6213199d20f..63b15f6bc36 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -101,6 +101,7 @@ import { AdminGuardService } from "./dashboard/service/admin/guard/admin-guard.s import { ContextMenuComponent } from "./workspace/component/workflow-editor/context-menu/context-menu/context-menu.component"; import { CoeditorUserIconComponent } from "./workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component"; import { CopilotChatComponent } from "./workspace/component/copilot-chat/copilot-chat.component"; +import { ActionPlanFeedbackComponent } from "./workspace/component/action-plan-feedback/action-plan-feedback.component"; import { InputAutoCompleteComponent } from "./workspace/component/input-autocomplete/input-autocomplete.component"; import { CollabWrapperComponent } from "./common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; import { NzSwitchModule } from "ng-zorro-antd/switch"; @@ -249,6 +250,7 @@ registerLocaleData(en); ContextMenuComponent, CoeditorUserIconComponent, CopilotChatComponent, + ActionPlanFeedbackComponent, InputAutoCompleteComponent, FileSelectionComponent, CollabWrapperComponent, diff --git a/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.html b/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.html new file mode 100644 index 00000000000..92ca75e7eb3 --- /dev/null +++ b/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.html @@ -0,0 +1,44 @@ +
    +
    + Action Plan +
    +
    +
    + +

    {{ summary }}

    +
    + +
    + +
    diff --git a/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.scss b/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.scss new file mode 100644 index 00000000000..a5e61d70663 --- /dev/null +++ b/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.scss @@ -0,0 +1,81 @@ +.action-plan-feedback-panel { + position: absolute; + background: white; + border: 2px solid rgba(79, 195, 255, 0.8); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + min-width: 320px; + max-width: 400px; + z-index: 1000; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + .panel-header { + background: rgba(79, 195, 255, 0.1); + border-bottom: 1px solid rgba(79, 195, 255, 0.3); + padding: 12px 16px; + font-weight: 600; + font-size: 14px; + color: #1890ff; + } + + .panel-body { + padding: 16px; + + .summary-section { + margin-bottom: 16px; + + label { + display: block; + font-weight: 600; + font-size: 12px; + color: #595959; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + p { + margin: 0; + font-size: 14px; + color: #262626; + line-height: 1.6; + padding: 8px 12px; + background: #f5f5f5; + border-radius: 4px; + border-left: 3px solid #1890ff; + } + } + + .feedback-section { + label { + display: block; + font-weight: 600; + font-size: 12px; + color: #595959; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + textarea { + width: 100%; + resize: vertical; + font-size: 13px; + } + } + } + + .panel-footer { + padding: 12px 16px; + border-top: 1px solid #e8e8e8; + display: flex; + justify-content: flex-end; + gap: 8px; + + button { + display: flex; + align-items: center; + gap: 6px; + } + } +} diff --git a/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.ts b/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.ts new file mode 100644 index 00000000000..0b7f01bf2d9 --- /dev/null +++ b/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.ts @@ -0,0 +1,45 @@ +/** + * 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 { Component, Input } from "@angular/core"; +import { ActionPlanService } from "../../service/action-plan/action-plan.service"; + +@Component({ + selector: "texera-action-plan-feedback", + templateUrl: "./action-plan-feedback.component.html", + styleUrls: ["./action-plan-feedback.component.scss"], +}) +export class ActionPlanFeedbackComponent { + @Input() summary: string = ""; + @Input() left: number = 0; + @Input() top: number = 0; + + public rejectMessage: string = ""; + + constructor(private actionPlanService: ActionPlanService) {} + + public onAccept(): void { + this.actionPlanService.acceptPlan(); + } + + public onReject(): void { + const message = this.rejectMessage.trim() || "I don't want this action plan."; + this.actionPlanService.rejectPlan(message); + } +} diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html index 8f87c66c7a7..0dc058ad1b1 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html @@ -26,4 +26,12 @@ #menu="nzDropdownMenu">
    + + + +
  • diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts index 438768839b9..8ee35aa19f7 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts @@ -96,6 +96,12 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy private removeButton!: new () => joint.linkTools.Button; private breakpointButton!: new () => joint.linkTools.Button; + // Action plan feedback panel state + public showActionPlanFeedback: boolean = false; + public actionPlanSummary: string = ""; + public actionPlanPanelLeft: number = 0; + public actionPlanPanelTop: number = 0; + constructor( private workflowActionService: WorkflowActionService, private dynamicSchemaService: DynamicSchemaService, @@ -429,43 +435,130 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy } ); + // Track current highlight element and cleanup handler + let currentElement: joint.dia.Element | null = null; + let currentPositionHandler: ((operator: joint.dia.Cell) => void) | null = null; + // Subscribe to action plan highlight events this.actionPlanService .getActionPlanHighlightStream() .pipe(untilDestroyed(this)) .subscribe(actionPlan => { // Get operator elements from IDs - const operators = actionPlan.operatorIds - .map(id => this.paper.getModelById(id)) - .filter(op => op !== undefined); + const operators = actionPlan.operatorIds.map(id => this.paper.getModelById(id)).filter(op => op !== undefined); if (operators.length === 0) { return; // No valid operators found } // Create action plan highlight element - const element = new ActionPlan(); - this.paper.model.addCell(element); + currentElement = new ActionPlan(); + this.paper.model.addCell(currentElement); // Update the highlight to wrap around operators - this.updateActionPlanElement(element, operators); - - // Listen to operator position changes to update the highlight - const positionHandler = (operator: joint.dia.Cell) => { - if (operators.includes(operator)) { - this.updateActionPlanElement(element, operators); + this.updateActionPlanElement(currentElement, operators); + + // Calculate panel position (to the right of the highlighted area) + const bbox = this.getOperatorsBoundingBox(operators); + const panelPosition = this.calculatePanelPosition(bbox); + this.actionPlanPanelLeft = panelPosition.x; + this.actionPlanPanelTop = panelPosition.y; + this.actionPlanSummary = actionPlan.summary; + this.showActionPlanFeedback = true; + + // Listen to operator position changes to update the highlight and panel + currentPositionHandler = (operator: joint.dia.Cell) => { + if (operators.includes(operator) && currentElement) { + this.updateActionPlanElement(currentElement, operators); + // Update panel position when operators move + const newBbox = this.getOperatorsBoundingBox(operators); + const newPosition = this.calculatePanelPosition(newBbox); + this.actionPlanPanelLeft = newPosition.x; + this.actionPlanPanelTop = newPosition.y; + this.changeDetectorRef.detectChanges(); } }; - this.paper.model.on("change:position", positionHandler); + this.paper.model.on("change:position", currentPositionHandler); + }); - // Remove highlight after 5 seconds - setTimeout(() => { - element.remove(); - this.paper.model.off("change:position", positionHandler); - }, 5000); + // Subscribe to cleanup stream - triggered when user accepts/rejects + this.actionPlanService + .getCleanupStream() + .pipe(untilDestroyed(this)) + .subscribe(() => { + // Remove highlight element + if (currentElement) { + currentElement.remove(); + currentElement = null; + } + + // Remove position handler + if (currentPositionHandler) { + this.paper.model.off("change:position", currentPositionHandler); + currentPositionHandler = null; + } + + // Hide panel + this.showActionPlanFeedback = false; + this.changeDetectorRef.detectChanges(); }); } + /** + * Calculate bounding box that encompasses all operators + */ + private getOperatorsBoundingBox(operators: joint.dia.Cell[]): { + x: number; + y: number; + width: number; + height: number; + } { + const bboxes = operators.map(op => op.getBBox()); + const minX = Math.min(...bboxes.map(b => b.x)); + const minY = Math.min(...bboxes.map(b => b.y)); + const maxX = Math.max(...bboxes.map(b => b.x + b.width)); + const maxY = Math.max(...bboxes.map(b => b.y + b.height)); + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + + /** + * Calculate panel position to the right of the operators, considering canvas boundaries + */ + private calculatePanelPosition(bbox: { x: number; y: number; width: number; height: number }): { + x: number; + y: number; + } { + const panelWidth = 400; + const panelOffset = 40; // Space between operators and panel + + // Try to position to the right of operators + let x = bbox.x + bbox.width + panelOffset; + let y = bbox.y; + + // If panel would go off the right edge, position it to the left + const paperWidth = this.paper.getComputedSize().width; + if (x + panelWidth > paperWidth) { + x = bbox.x - panelWidth - panelOffset; + } + + // Ensure panel stays within vertical bounds + const paperHeight = this.paper.getComputedSize().height; + if (y < 20) { + y = 20; + } else if (y + 300 > paperHeight) { + // Approximate panel height + y = paperHeight - 320; + } + + return { x, y }; + } + private updateActionPlanElement(element: joint.dia.Element, operators: joint.dia.Cell[]) { const points = operators.flatMap(op => { const { x, y, width, height } = op.getBBox(), diff --git a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts index 03b506b13db..b63a6c90061 100644 --- a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts +++ b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts @@ -18,25 +18,36 @@ */ import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; +import { Subject, Observable } from "rxjs"; /** * Interface for an action plan highlight event */ export interface ActionPlanHighlight { operatorIds: string[]; + linkIds: string[]; summary: string; } /** - * Service to manage temporary action plan highlights - * Emits events when action plans are created, which trigger 5-second visual highlights + * User feedback for an action plan + */ +export interface ActionPlanFeedback { + accepted: boolean; + message?: string; // Optional message when rejecting +} + +/** + * Service to manage action plan highlights and user feedback + * Handles the interactive flow: show plan -> wait for user decision -> return feedback */ @Injectable({ providedIn: "root", }) export class ActionPlanService { private actionPlanHighlightSubject = new Subject(); + private feedbackSubject: Subject | null = null; + private cleanupSubject = new Subject(); constructor() {} @@ -48,9 +59,56 @@ export class ActionPlanService { } /** - * Show a temporary highlight for an action plan (5 seconds) + * Get cleanup stream - emits when user provides feedback (accept/reject) + */ + public getCleanupStream() { + return this.cleanupSubject.asObservable(); + } + + /** + * Show an action plan and wait for user feedback (accept/reject) + * Returns an Observable that emits when user makes a decision + */ + public showActionPlanAndWaitForFeedback( + operatorIds: string[], + linkIds: string[], + summary: string + ): Observable { + // Create new feedback subject for this plan + this.feedbackSubject = new Subject(); + + // Emit highlight event + this.actionPlanHighlightSubject.next({ operatorIds, linkIds, summary }); + + // Return observable that will emit when user accepts/rejects + return this.feedbackSubject.asObservable(); + } + + /** + * User accepted the action plan + */ + public acceptPlan(): void { + if (this.feedbackSubject) { + this.feedbackSubject.next({ accepted: true }); + this.feedbackSubject.complete(); + this.feedbackSubject = null; + + // Trigger cleanup (remove highlight and panel) + this.cleanupSubject.next(); + } + } + + /** + * User rejected the action plan with optional feedback message */ - public showActionPlanHighlight(operatorIds: string[], summary: string): void { - this.actionPlanHighlightSubject.next({ operatorIds, summary }); + public rejectPlan(message?: string): void { + if (this.feedbackSubject) { + this.feedbackSubject.next({ accepted: false, message }); + this.feedbackSubject.complete(); + this.feedbackSubject = null; + + // Trigger cleanup (remove highlight and panel) + this.cleanupSubject.next(); + } } } diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 7fdcb5450cb..ad2239e5408 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -267,6 +267,26 @@ export class TexeraCopilot { // Log each step for debugging console.debug("step finished", { text, toolCalls, toolResults, finishReason, usage }); + // Check if any tool result was an action plan rejection + if (toolResults && toolResults.length > 0) { + for (const result of toolResults) { + // Check if this was an actionPlan tool that was rejected + const toolCall = toolCalls?.find(tc => tc.toolCallId === result.toolCallId); + if (toolCall?.toolName === "actionPlan" && result.result) { + const parsedResult = typeof result.result === "string" ? JSON.parse(result.result) : result.result; + if (parsedResult.rejected) { + // Add user's rejection feedback as a user message + const userFeedbackMessage: UserModelMessage = { + role: "user", + content: parsedResult.userFeedback || "I rejected the action plan", + }; + this.messages.push(userFeedbackMessage); + console.log("Action plan rejected, added user feedback to messages"); + } + } + } + } + // If there are tool calls, emit raw trace data if (toolCalls && toolCalls.length > 0) { const traceResponse: AgentResponse = { diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index c6043412274..3e55015d202 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -367,17 +367,38 @@ export function createActionPlanTool( copilotCoeditor.highlightOperators(createdOperatorIds); }, 100); - // Trigger action plan highlight (5-second visual indicator) - setTimeout(() => { - actionPlanService.showActionPlanHighlight(createdOperatorIds, args.summary); - }, 150); + // Show action plan and wait for user feedback + const feedback = await new Promise((resolve, reject) => { + setTimeout(() => { + actionPlanService + .showActionPlanAndWaitForFeedback(createdOperatorIds, createdLinkIds, args.summary) + .subscribe({ + next: feedback => resolve(feedback), + error: (err: unknown) => reject(err), + }); + }, 150); + }); + + // Handle user feedback + if (!feedback.accepted) { + // User rejected - remove the created operators and links + workflowActionService.deleteOperatorsAndLinks(createdOperatorIds); + + return { + success: false, + rejected: true, + userFeedback: feedback.message || "User rejected this action plan", + message: `Action plan rejected by user: ${feedback.message || "No reason provided"}`, + }; + } + // User accepted - return success return { success: true, summary: args.summary, operatorIds: createdOperatorIds, linkIds: createdLinkIds, - message: `Action Plan: ${args.summary}. Added ${args.operators.length} operator(s) and ${args.links.length} link(s) to workflow.`, + message: `Action Plan: ${args.summary}. Added ${args.operators.length} operator(s) and ${args.links.length} link(s) to workflow. User accepted the plan.`, }; } catch (error: any) { return { success: false, error: error.message }; From f6a3513a9650111001d33e175790d9484b954bcd Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Wed, 29 Oct 2025 15:20:50 -0700 Subject: [PATCH 043/158] finish refactoring as multi agents --- frontend/src/app/app.module.ts | 10 +- .../agent-chat/agent-chat.component.html | 138 +++++++++ .../agent-chat/agent-chat.component.scss | 169 +++++++++++ .../agent-chat/agent-chat.component.spec.ts} | 0 .../agent-chat/agent-chat.component.ts | 262 +++++++++++++++++ .../agent-panel/agent-panel.component.html | 93 ++++++ .../agent-panel/agent-panel.component.scss | 143 ++++++++++ .../agent-panel/agent-panel.component.ts | 170 +++++++++++ .../agent-registration.component.html | 81 ++++++ .../agent-registration.component.scss | 148 ++++++++++ .../agent-registration.component.ts | 85 ++++++ .../copilot-chat/copilot-chat.component.html | 136 --------- .../copilot-chat/copilot-chat.component.scss | 241 ---------------- .../copilot-chat/copilot-chat.component.ts | 269 ------------------ .../component/menu/menu.component.html | 11 - .../component/menu/menu.component.ts | 19 -- .../component/workspace.component.html | 1 + .../copilot/texera-copilot-manager.service.ts | 178 ++++++++++++ .../service/copilot/texera-copilot.ts | 23 +- 19 files changed, 1492 insertions(+), 685 deletions(-) create mode 100644 frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html create mode 100644 frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss rename frontend/src/app/workspace/component/{copilot-chat/copilot-chat.component.spec.ts => agent-panel/agent-chat/agent-chat.component.spec.ts} (100%) create mode 100644 frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts create mode 100644 frontend/src/app/workspace/component/agent-panel/agent-panel.component.html create mode 100644 frontend/src/app/workspace/component/agent-panel/agent-panel.component.scss create mode 100644 frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts create mode 100644 frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.html create mode 100644 frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.scss create mode 100644 frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts delete mode 100644 frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html delete mode 100644 frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.scss delete mode 100644 frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts create mode 100644 frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 63b15f6bc36..02ffca30ae5 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -100,10 +100,13 @@ import { NzPopconfirmModule } from "ng-zorro-antd/popconfirm"; import { AdminGuardService } from "./dashboard/service/admin/guard/admin-guard.service"; import { ContextMenuComponent } from "./workspace/component/workflow-editor/context-menu/context-menu/context-menu.component"; import { CoeditorUserIconComponent } from "./workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component"; -import { CopilotChatComponent } from "./workspace/component/copilot-chat/copilot-chat.component"; +import { AgentPanelComponent } from "./workspace/component/agent-panel/agent-panel.component"; +import { AgentChatComponent } from "./workspace/component/agent-panel/agent-chat/agent-chat.component"; +import { AgentRegistrationComponent } from "./workspace/component/agent-panel/agent-registration/agent-registration.component"; import { ActionPlanFeedbackComponent } from "./workspace/component/action-plan-feedback/action-plan-feedback.component"; import { InputAutoCompleteComponent } from "./workspace/component/input-autocomplete/input-autocomplete.component"; import { CollabWrapperComponent } from "./common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; +import { TexeraCopilot } from "./workspace/service/copilot/texera-copilot"; import { NzSwitchModule } from "ng-zorro-antd/switch"; import { AboutComponent } from "./hub/component/about/about.component"; import { NzLayoutModule } from "ng-zorro-antd/layout"; @@ -249,7 +252,9 @@ registerLocaleData(en); LocalLoginComponent, ContextMenuComponent, CoeditorUserIconComponent, - CopilotChatComponent, + AgentPanelComponent, + AgentChatComponent, + AgentRegistrationComponent, ActionPlanFeedbackComponent, InputAutoCompleteComponent, FileSelectionComponent, @@ -354,6 +359,7 @@ registerLocaleData(en); GuiConfigService, FileSaverService, ReportGenerationService, + TexeraCopilot, { provide: HTTP_INTERCEPTORS, useClass: BlobErrorHttpInterceptor, diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html new file mode 100644 index 00000000000..01d4e795285 --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html @@ -0,0 +1,138 @@ + + + +
    + +
    +
    + + + Show Results +
    + + +
    +
    +
    + + +
    + +
    +
    +
    + + {{ message.role === 'user' ? 'You' : agentInfo.name }} +
    +
    +
    + + +
    +
    + + {{ agentInfo.name }} +
    +
    + + Thinking... +
    +
    +
    + + +
    + + + +
    +
    + + +
    + + Agent is disconnected. Please check your connection. +
    +
    diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss new file mode 100644 index 00000000000..32c293f14c4 --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss @@ -0,0 +1,169 @@ +/** + * 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. + */ + +.agent-chat-container { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + background: white; + border: 2px solid #1890ff; // Debug border to verify rendering +} + +.chat-toolbar { + display: flex; + justify-content: flex-end; + align-items: center; + padding: 8px 12px; + border-bottom: 1px solid #f0f0f0; + background: #fafafa; +} + +.chat-toolbar-controls { + display: flex; + gap: 8px; + align-items: center; + + .toggle-label { + font-size: 12px; + margin-left: 4px; + color: #666; + } +} + +.chat-toolbar-buttons { + display: flex; + gap: 4px; + align-items: center; + margin-left: 8px; +} + +.chat-content-wrapper { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + gap: 8px; +} + +.messages-container { + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.message { + display: flex; + flex-direction: column; + gap: 4px; + max-width: 85%; + + &.user-message { + align-self: flex-end; + + .message-content { + background: #1890ff; + color: white; + } + } + + &.ai-message { + align-self: flex-start; + + .message-content { + background: #f5f5f5; + color: #262626; + } + } + + &.loading-message { + .message-content { + background: #e6f7ff; + border: 1px solid #91d5ff; + } + } +} + +.message-header { + display: flex; + align-items: center; + gap: 6px; + padding: 0 8px; + font-size: 12px; + color: #8c8c8c; + + i { + font-size: 14px; + } +} + +.message-content { + padding: 10px 14px; + border-radius: 8px; + line-height: 1.6; + word-wrap: break-word; + + ::ng-deep { + code { + background: rgba(0, 0, 0, 0.06); + padding: 2px 6px; + border-radius: 3px; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 13px; + } + + strong { + font-weight: 600; + } + } +} + +.input-area { + display: flex; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid #f0f0f0; + background: #ffffff; + + textarea { + flex: 1; + } + + button { + align-self: flex-end; + } +} + +.connection-warning { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: #fff7e6; + border-top: 1px solid #ffd666; + color: #d46b08; + font-size: 12px; + + i { + color: #faad14; + } +} diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.spec.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.spec.ts similarity index 100% rename from frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.spec.ts rename to frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.spec.ts diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts new file mode 100644 index 00000000000..9490099fde2 --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -0,0 +1,262 @@ +// agent-chat.component.ts +import { Component, OnDestroy, ViewChild, ElementRef, Input, OnInit, AfterViewChecked } from "@angular/core"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { TexeraCopilot, AgentResponse, CopilotState } from "../../../service/copilot/texera-copilot"; +import { AgentInfo } from "../../../service/copilot/texera-copilot-manager.service"; +import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; + +interface ChatMessage { + role: "user" | "ai"; + text: string; +} + +@UntilDestroy() +@Component({ + selector: "texera-agent-chat", + templateUrl: "agent-chat.component.html", + styleUrls: ["agent-chat.component.scss"], +}) +export class AgentChatComponent implements OnInit, OnDestroy, AfterViewChecked { + @Input() agentInfo!: AgentInfo; + @ViewChild("messageContainer", { static: false }) messageContainer?: ElementRef; + @ViewChild("messageInput", { static: false }) messageInput?: ElementRef; + + public showToolResults = false; + public messages: ChatMessage[] = []; + public currentMessage = ""; + private copilotService!: TexeraCopilot; + private shouldScrollToBottom = false; + + constructor(private sanitizer: DomSanitizer) {} + + ngOnInit(): void { + console.log("AgentChatComponent ngOnInit - agentInfo:", this.agentInfo); + + if (!this.agentInfo) { + console.error("AgentInfo is not provided!"); + return; + } + + this.copilotService = this.agentInfo.instance; + + if (!this.copilotService) { + console.error("CopilotService instance is not available!"); + return; + } + + // Add initial greeting message + this.messages.push({ + role: "ai", + text: `Hi! I'm ${this.agentInfo.name}. I can help you build and modify workflows.`, + }); + console.log("AgentChatComponent initialized successfully"); + } + + ngAfterViewChecked(): void { + if (this.shouldScrollToBottom) { + this.scrollToBottom(); + this.shouldScrollToBottom = false; + } + } + + ngOnDestroy(): void { + // Cleanup when component is destroyed + } + + /** + * Format message content to display markdown-like text + */ + public formatMessageContent(text: string): SafeHtml { + // Simple markdown-like formatting + let formatted = text + // Bold: **text** + .replace(/\*\*(.+?)\*\*/g, "$1") + // Code: `code` + .replace(/`(.+?)`/g, "$1") + // Line breaks + .replace(/\n/g, "
    "); + + return this.sanitizer.sanitize(1, formatted) || ""; + } + + /** + * Send a message to the agent + */ + public sendMessage(): void { + if (!this.currentMessage.trim() || this.isGenerating()) { + return; + } + + const userMessage = this.currentMessage.trim(); + this.currentMessage = ""; + + // Add user message to chat + this.messages.push({ + role: "user", + text: userMessage, + }); + this.shouldScrollToBottom = true; + + // Send to copilot + let currentAiMessage = ""; + this.copilotService + .sendMessage(userMessage) + .pipe(untilDestroyed(this)) + .subscribe({ + next: (response: AgentResponse) => { + if (response.type === "trace") { + // Format and add trace message + const traceText = this.formatToolTrace(response); + if (traceText) { + this.messages.push({ + role: "ai", + text: traceText, + }); + this.shouldScrollToBottom = true; + } + } else if (response.type === "response") { + currentAiMessage = response.content; + if (response.isDone && currentAiMessage) { + this.messages.push({ + role: "ai", + text: currentAiMessage, + }); + this.shouldScrollToBottom = true; + } + } + }, + error: (error: unknown) => { + console.error("Error sending message:", error); + this.messages.push({ + role: "ai", + text: `Error: ${error || "Unknown error occurred"}`, + }); + this.shouldScrollToBottom = true; + }, + complete: () => { + const currentState = this.copilotService.getState(); + if (currentState === CopilotState.STOPPING) { + this.messages.push({ + role: "ai", + text: "_Generation stopped._", + }); + this.shouldScrollToBottom = true; + } + }, + }); + } + + /** + * Handle Enter key press in textarea + */ + public onEnterPress(event: KeyboardEvent): void { + if (!event.shiftKey) { + event.preventDefault(); + this.sendMessage(); + } + } + + /** + * Format tool trace for display + */ + private formatToolTrace(response: AgentResponse): string { + if (!response.toolCalls || response.toolCalls.length === 0) { + return ""; + } + + let output = ""; + if (response.content && response.content.trim()) { + output += `💭 **Agent:** ${response.content}\n\n`; + } + + const traces = response.toolCalls.map((tc: any, index: number) => { + const args = tc.args || tc.arguments || tc.parameters || tc.input || {}; + let argsDisplay = ""; + if (Object.keys(args).length > 0) { + argsDisplay = Object.entries(args) + .map(([key, value]) => ` **${key}:** \`${JSON.stringify(value)}\``) + .join("\n"); + } else { + argsDisplay = " *(no parameters)*"; + } + + let toolTrace = `🔧 **${tc.toolName}**\n${argsDisplay}`; + + if (this.showToolResults && response.toolResults && response.toolResults[index]) { + const result = response.toolResults[index]; + const resultOutput = result.output || result.result || {}; + + if (resultOutput.success === false) { + toolTrace += `\n ❌ **Error:** ${resultOutput.error || "Unknown error"}`; + } else if (resultOutput.success === true) { + toolTrace += `\n ✅ **Success:** ${resultOutput.message || "Operation completed"}`; + } else { + toolTrace += `\n **Result:** \`${JSON.stringify(resultOutput)}\``; + } + } + + return toolTrace; + }); + + output += traces.join("\n\n"); + return output; + } + + /** + * Scroll messages container to bottom + */ + private scrollToBottom(): void { + if (this.messageContainer) { + const element = this.messageContainer.nativeElement; + element.scrollTop = element.scrollHeight; + } + } + + /** + * Stop the current generation + */ + public stopGeneration(): void { + this.copilotService.stopGeneration(); + } + + /** + * Clear message history + */ + public clearMessages(): void { + this.copilotService.clearMessages(); + this.messages = []; + // Add greeting message back + this.messages.push({ + role: "ai", + text: "Hi! I'm Texera Agent. I can help you build and modify workflows.", + }); + } + + /** + * Check if copilot is currently generating + */ + public isGenerating(): boolean { + return this.copilotService.getState() === CopilotState.GENERATING; + } + + /** + * Check if copilot is currently stopping + */ + public isStopping(): boolean { + return this.copilotService.getState() === CopilotState.STOPPING; + } + + /** + * Check if copilot is available (can send messages) + */ + public isAvailable(): boolean { + return this.copilotService.getState() === CopilotState.AVAILABLE; + } + + /** + * Check if agent is connected + */ + public isConnected(): boolean { + return this.copilotService?.isConnected() ?? false; + } +} diff --git a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html new file mode 100644 index 00000000000..5d300911cff --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html @@ -0,0 +1,93 @@ + + +
    + +
    +
    + + AI Agents +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + {{ agents.length }} agent(s) +
    +
    diff --git a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.scss b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.scss new file mode 100644 index 00000000000..7d053be4acf --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.scss @@ -0,0 +1,143 @@ +/** + * 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. + */ + +:host { + display: block; + width: 100%; + height: 100%; + position: fixed; + z-index: 3; + pointer-events: none; +} + +.agent-panel-box { + position: absolute; + top: calc(-100% + 80px); + right: 0; + z-index: 3; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + pointer-events: auto; + overflow: hidden; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #1890ff; + color: white; + cursor: move; + user-select: none; +} + +.header-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 16px; + + i { + font-size: 18px; + } +} + +.header-actions { + display: flex; + gap: 4px; + + button { + color: white; + + &:hover { + background: rgba(255, 255, 255, 0.2); + } + } +} + +.agent-tabs { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + + ::ng-deep { + .ant-tabs { + height: 100%; + display: flex; + flex-direction: column; + } + + .ant-tabs-content-holder { + flex: 1; + overflow: hidden; + } + + .ant-tabs-content { + height: 100%; + } + + .ant-tabs-tabpane { + height: 100%; + overflow: hidden; + } + } +} + +.agent-tab-title { + display: flex; + align-items: center; + gap: 8px; +} + +.delete-agent-btn { + padding: 0; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.6; + + &:hover { + opacity: 1; + color: #ff4d4f; + } + + i { + font-size: 12px; + } +} + +.tab-bar-extra { + padding-right: 8px; +} + +.agent-count { + font-size: 12px; + color: #8c8c8c; + padding: 4px 8px; + background: #f0f0f0; + border-radius: 4px; +} diff --git a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts new file mode 100644 index 00000000000..b86d1c5bd19 --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts @@ -0,0 +1,170 @@ +/** + * 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 { Component, HostListener, OnDestroy, OnInit } from "@angular/core"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { NzResizeEvent } from "ng-zorro-antd/resizable"; +import { TexeraCopilotManagerService, AgentInfo } from "../../service/copilot/texera-copilot-manager.service"; + +@UntilDestroy() +@Component({ + selector: "texera-agent-panel", + templateUrl: "agent-panel.component.html", + styleUrls: ["agent-panel.component.scss"], +}) +export class AgentPanelComponent implements OnInit, OnDestroy { + protected readonly window = window; + private static readonly MIN_PANEL_WIDTH = 400; + private static readonly MIN_PANEL_HEIGHT = 450; + + // Panel dimensions and position + width = AgentPanelComponent.MIN_PANEL_WIDTH; + height = Math.max(AgentPanelComponent.MIN_PANEL_HEIGHT, window.innerHeight * 0.7); + id = -1; + dragPosition = { x: 0, y: 0 }; + returnPosition = { x: 0, y: 0 }; + isDocked = true; + + // Tab management + selectedTabIndex: number = 0; // 0 = registration tab, 1+ = agent tabs + agents: AgentInfo[] = []; + + constructor(private copilotManagerService: TexeraCopilotManagerService) {} + + ngOnInit(): void { + // Load saved panel dimensions and position + const savedWidth = localStorage.getItem("agent-panel-width"); + const savedHeight = localStorage.getItem("agent-panel-height"); + const savedStyle = localStorage.getItem("agent-panel-style"); + + if (savedWidth) this.width = Number(savedWidth); + if (savedHeight) this.height = Number(savedHeight); + + if (savedStyle) { + const container = document.getElementById("agent-container"); + if (container) { + container.style.cssText = savedStyle; + const translates = container.style.transform; + const [xOffset, yOffset] = this.calculateTotalTranslate3d(translates); + this.returnPosition = { x: -xOffset, y: -yOffset }; + this.isDocked = this.dragPosition.x === this.returnPosition.x && this.dragPosition.y === this.returnPosition.y; + } + } + + // Subscribe to agent changes + this.copilotManagerService.agentChange$.pipe(untilDestroyed(this)).subscribe(() => { + this.agents = this.copilotManagerService.getAllAgents(); + }); + + // Load initial agents + this.agents = this.copilotManagerService.getAllAgents(); + } + + @HostListener("window:beforeunload") + ngOnDestroy(): void { + // Save panel state + localStorage.setItem("agent-panel-width", String(this.width)); + localStorage.setItem("agent-panel-height", String(this.height)); + + const container = document.getElementById("agent-container"); + if (container) { + localStorage.setItem("agent-panel-style", container.style.cssText); + } + } + + /** + * Handle agent creation + */ + public onAgentCreated(agentId: string): void { + // The agent is already added to the agents array by the manager service + // Find the index of the newly created agent and switch to that tab + // Tab index 0 is registration, so agent tabs start at index 1 + const agentIndex = this.agents.findIndex(agent => agent.id === agentId); + if (agentIndex !== -1) { + this.selectedTabIndex = agentIndex + 1; // +1 because tab 0 is registration + } + } + + /** + * Delete an agent + */ + public deleteAgent(agentId: string, event: Event): void { + event.stopPropagation(); // Prevent tab switch + + if (confirm("Are you sure you want to delete this agent?")) { + const agentIndex = this.agents.findIndex(agent => agent.id === agentId); + this.copilotManagerService.deleteAgent(agentId); + + // If we're on the deleted agent's tab, switch to registration + if (agentIndex !== -1 && this.selectedTabIndex === agentIndex + 1) { + this.selectedTabIndex = 0; + } else if (this.selectedTabIndex > agentIndex + 1) { + // Adjust selected index if we deleted a tab before the current one + this.selectedTabIndex--; + } + } + } + + /** + * Handle panel resize + */ + onResize({ width, height }: NzResizeEvent): void { + cancelAnimationFrame(this.id); + this.id = requestAnimationFrame(() => { + this.width = width!; + this.height = height!; + }); + } + + /** + * Reset panel to docked position + */ + resetPanelPosition(): void { + this.dragPosition = { x: this.returnPosition.x, y: this.returnPosition.y }; + this.isDocked = true; + } + + /** + * Handle drag start + */ + handleDragStart(): void { + this.isDocked = false; + } + + /** + * Calculate total translate3d from transform string + */ + private calculateTotalTranslate3d(transformString: string): [number, number, number] { + if (!transformString) return [0, 0, 0]; + + const regex = /translate3d\(([^,]+),\s*([^,]+),\s*([^)]+)\)/g; + let match; + let totalX = 0, + totalY = 0, + totalZ = 0; + + while ((match = regex.exec(transformString)) !== null) { + totalX += parseFloat(match[1]); + totalY += parseFloat(match[2]); + totalZ += parseFloat(match[3]); + } + + return [totalX, totalY, totalZ]; + } +} diff --git a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.html b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.html new file mode 100644 index 00000000000..ed722565f36 --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.html @@ -0,0 +1,81 @@ + + +
    +
    +

    Create New Agent

    +

    Select a model type and create an AI agent to assist with your workflows

    +
    + +
    +

    Select Model Type

    +
    +
    +
    + +
    +
    +
    {{ modelType.name }}
    +

    {{ modelType.description }}

    +
    +
    + +
    +
    +
    +
    + +
    +

    Agent Name (Optional)

    + +
    + +
    + +
    +
    diff --git a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.scss b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.scss new file mode 100644 index 00000000000..ec49843a0c6 --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.scss @@ -0,0 +1,148 @@ +/** + * 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. + */ + +.agent-registration-container { + display: flex; + flex-direction: column; + gap: 24px; + padding: 20px; + height: 100%; + overflow-y: auto; +} + +.registration-header { + text-align: center; + + h3 { + margin: 0 0 8px 0; + font-size: 20px; + font-weight: 600; + color: #262626; + } + + p { + margin: 0; + font-size: 14px; + color: #8c8c8c; + } +} + +.model-type-selection { + h4 { + margin: 0 0 12px 0; + font-size: 16px; + font-weight: 500; + color: #262626; + } +} + +.model-cards { + display: flex; + flex-direction: column; + gap: 12px; +} + +.model-card { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + border: 2px solid #d9d9d9; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + + &:hover { + border-color: #1890ff; + box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2); + } + + &.selected { + border-color: #1890ff; + background: #e6f7ff; + box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3); + } +} + +.model-icon { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + background: #f0f0f0; + border-radius: 8px; + flex-shrink: 0; + + i { + font-size: 28px; + color: #1890ff; + } +} + +.model-info { + flex: 1; + + h5 { + margin: 0 0 4px 0; + font-size: 15px; + font-weight: 600; + color: #262626; + } + + p { + margin: 0; + font-size: 13px; + color: #8c8c8c; + line-height: 1.4; + } +} + +.selected-indicator { + flex-shrink: 0; + + i { + font-size: 24px; + color: #1890ff; + } +} + +.agent-name-input { + h4 { + margin: 0 0 8px 0; + font-size: 16px; + font-weight: 500; + color: #262626; + } + + input { + width: 100%; + } +} + +.action-buttons { + display: flex; + justify-content: center; + margin-top: auto; + + button { + min-width: 200px; + } +} diff --git a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts new file mode 100644 index 00000000000..5de21da611b --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts @@ -0,0 +1,85 @@ +/** + * 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 { Component, EventEmitter, Output } from "@angular/core"; +import { TexeraCopilotManagerService, ModelType } from "../../../service/copilot/texera-copilot-manager.service"; + +@Component({ + selector: "texera-agent-registration", + templateUrl: "agent-registration.component.html", + styleUrls: ["agent-registration.component.scss"], +}) +export class AgentRegistrationComponent { + @Output() agentCreated = new EventEmitter(); // Emit agent ID when created + + public modelTypes: ModelType[] = []; + public selectedModelType: string | null = null; + public customAgentName: string = ""; + + constructor(private copilotManagerService: TexeraCopilotManagerService) { + this.modelTypes = this.copilotManagerService.getModelTypes(); + } + + /** + * Select a model type + */ + public selectModelType(modelTypeId: string): void { + this.selectedModelType = modelTypeId; + } + + public isCreating: boolean = false; + + /** + * Create a new agent with the selected model type + */ + public async createAgent(): Promise { + if (!this.selectedModelType || this.isCreating) { + return; + } + + this.isCreating = true; + + try { + const agentInfo = await this.copilotManagerService.createAgent( + this.selectedModelType, + this.customAgentName || undefined + ); + + // Emit event with agent ID + this.agentCreated.emit(agentInfo.id); + + // Reset selection + this.selectedModelType = null; + this.customAgentName = ""; + } catch (error) { + console.error("Failed to create agent:", error); + // TODO: Show error notification + alert("Failed to create agent. Please check the console for details."); + } finally { + this.isCreating = false; + } + } + + /** + * Check if create button should be enabled + */ + public canCreate(): boolean { + return this.selectedModelType !== null && !this.isCreating; + } +} diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html deleted file mode 100644 index 29f675f165c..00000000000 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.html +++ /dev/null @@ -1,136 +0,0 @@ - - - -
    - -
    -
    - - Texera Copilot -
    -
    - - - Show Results -
    - - - - -
    -
    -
    - - -
    - - - - -
    - - {{ isStopping() ? 'Stopping...' : 'Processing...' }} - - -
    -
    - - -
    - - Copilot is disconnected. Please check your connection. -
    -
    diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.scss b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.scss deleted file mode 100644 index 94c0c219bd8..00000000000 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.scss +++ /dev/null @@ -1,241 +0,0 @@ -/** - * 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. - */ - -.copilot-avatar-container { - display: inline-flex; - align-items: center; - margin-right: 8px; -} - -.copilot-avatar { - transition: all 0.3s ease; - border: 2px solid transparent; - - &:hover { - transform: scale(1.1); - border-color: #1890ff; - } -} - -.chat-box { - position: fixed; - top: 80px; - right: 20px; - width: 500px; - height: 500px; - min-width: 350px; - min-height: 300px; - max-width: 90vw; - max-height: 90vh; - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - display: flex; - flex-direction: column; - z-index: 1000; - animation: slideIn 0.3s ease; - overflow: auto; - resize: both; - // Flip horizontally to move resize handle to bottom-left - transform: scaleX(-1); - - // Flip content back to normal - > * { - transform: scaleX(-1); - } - - &.minimized { - height: 60px; - resize: none; - } -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(-20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.chat-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - border-bottom: 1px solid #f0f0f0; - background: #1890ff; - color: white; - border-radius: 8px 8px 0 0; -} - -.chat-title { - display: flex; - align-items: center; - gap: 8px; - font-weight: 600; - font-size: 16px; - - i { - font-size: 18px; - } -} - -.chat-header-controls { - display: flex; - gap: 8px; - align-items: center; - - .toggle-label { - color: white; - font-size: 12px; - margin-left: 4px; - } -} - -.chat-header-buttons { - display: flex; - gap: 4px; - align-items: center; - margin-left: 8px; - - button { - color: white; - - &:hover { - background: rgba(255, 255, 255, 0.2); - } - } -} - -.chat-content-wrapper { - flex: 1; - overflow: hidden; - position: relative; - display: flex; - flex-direction: column; -} - -.deep-chat-container { - flex: 1; - overflow: hidden; - - &.collapsed { - display: none; - } -} - -.processing-indicator { - position: absolute; - bottom: 80px; - right: 20px; - display: flex; - align-items: center; - gap: 8px; - padding: 8px 16px; - background: rgba(24, 144, 255, 0.95); - color: white; - border-radius: 20px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - font-size: 13px; - z-index: 10; - animation: fadeIn 0.3s ease; - - nz-spin { - ::ng-deep .ant-spin-dot-item { - background-color: white; - } - } - - .stop-button { - color: white; - padding: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - margin-left: 4px; - - &:hover { - background: rgba(255, 255, 255, 0.2); - } - - i { - font-size: 14px; - } - } -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.connection-warning { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 16px; - background: #fff7e6; - border-top: 1px solid #ffd666; - color: #d46b08; - font-size: 12px; - - i { - color: #faad14; - } -} - -.tool-trace { - padding: 6px 8px; - background: #fafafa; - border: 1px solid #eee; - border-radius: 6px; -} -.tool-trace-title { - font-weight: 600; - margin-bottom: 6px; -} -.tool-trace-item + .tool-trace-item { - margin-top: 8px; -} -.tool-trace-head { - font-family: ui-monospace, SFMono-Regular, Menlo, monospace; - font-size: 12px; - margin-bottom: 4px; -} -.tool-trace-json { - margin: 0; - background: #fff; - border: 1px solid #f0f0f0; - padding: 8px; - border-radius: 4px; - overflow: auto; -} diff --git a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts b/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts deleted file mode 100644 index 48c38a365dc..00000000000 --- a/frontend/src/app/workspace/component/copilot-chat/copilot-chat.component.ts +++ /dev/null @@ -1,269 +0,0 @@ -// copilot-chat.component.ts -import { Component, OnDestroy, ViewChild, ElementRef } from "@angular/core"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { TexeraCopilot, AgentResponse, CopilotState } from "../../service/copilot/texera-copilot"; -import { CopilotCoeditorService } from "../../service/copilot/copilot-coeditor.service"; - -@UntilDestroy() -@Component({ - selector: "texera-copilot-chat", - templateUrl: "copilot-chat.component.html", - styleUrls: ["copilot-chat.component.scss"], -}) -export class CopilotChatComponent implements OnDestroy { - @ViewChild("deepChat", { static: false }) deepChatElement?: ElementRef; - - public isChatVisible = false; // Whether chat panel is shown at all - public isExpanded = true; // Whether chat content is expanded or minimized - public showToolResults = false; // Whether to show tool call results - public isConnected = false; - private isInitialized = false; - - // Deep-chat configuration - public deepChatConfig = { - connect: { - handler: (body: any, signals: any) => { - const last = body?.messages?.[body.messages.length - 1]; - const userText: string = typeof last?.text === "string" ? last.text : ""; - - // Send message to copilot and process AgentResponse - this.copilotService - .sendMessage(userText) - .pipe(untilDestroyed(this)) - .subscribe({ - next: (response: AgentResponse) => { - if (response.type === "trace") { - // Format tool traces - const displayText = this.formatToolTrace(response); - - // Add trace message via addMessage API - if (displayText && this.deepChatElement?.nativeElement?.addMessage) { - this.deepChatElement.nativeElement.addMessage({ role: "ai", text: displayText }); - } - - // Keep processing state true - loading indicator stays visible - } else if (response.type === "response") { - // For final response, signal completion with the content - // This will let deep-chat handle adding the message and clearing loading - if (response.isDone) { - signals.onResponse({ text: response.content }); - } - } - }, - error: (e: unknown) => { - signals.onResponse({ error: e ?? "Unknown error" }); - }, - complete: () => { - // Handle completion without final response (happens when generation is stopped) - const currentState = this.copilotService.getState(); - if (currentState === CopilotState.STOPPING) { - // Generation was stopped by user - show completion message - signals.onResponse({ text: "_Generation stopped._" }); - } else if (currentState === CopilotState.GENERATING) { - // Generation completed unexpectedly - signals.onResponse({ text: "_Generation completed._" }); - } - }, - }); - }, - }, - demo: false, - introMessage: { text: "Hi! I'm Texera Copilot. I can help you build and modify workflows." }, - textInput: { placeholder: { text: "Ask me anything about workflows..." } }, - requestBodyLimits: { maxMessages: -1 }, // Allow unlimited message history - }; - - /** - * Format tool trace for display with markdown - */ - private formatToolTrace(response: AgentResponse): string { - if (!response.toolCalls || response.toolCalls.length === 0) { - return ""; - } - - // Include agent's thinking/text if available - let output = ""; - if (response.content && response.content.trim()) { - output += `💭 **Agent:** ${response.content}\n\n`; - } - - // Format each tool call - show tool name, parameters, and optionally results - const traces = response.toolCalls.map((tc: any, index: number) => { - // Log the actual structure to debug - console.log("Tool call structure:", tc); - - // Try multiple possible property names for arguments - const args = tc.args || tc.arguments || tc.parameters || tc.input || {}; - - // Format args nicely - let argsDisplay = ""; - if (Object.keys(args).length > 0) { - argsDisplay = Object.entries(args) - .map(([key, value]) => ` **${key}:** \`${JSON.stringify(value)}\``) - .join("\n"); - } else { - argsDisplay = " *(no parameters)*"; - } - - let toolTrace = `🔧 **${tc.toolName}**\n${argsDisplay}`; - - // Add tool result if showToolResults is enabled - if (this.showToolResults && response.toolResults && response.toolResults[index]) { - const result = response.toolResults[index]; - const resultOutput = result.output || result.result || {}; - - // Format result based on success/error - if (resultOutput.success === false) { - toolTrace += `\n ❌ **Error:** ${resultOutput.error || "Unknown error"}`; - } else if (resultOutput.success === true) { - toolTrace += `\n ✅ **Success:** ${resultOutput.message || "Operation completed"}`; - // Include additional result details if present - const details = Object.entries(resultOutput) - .filter(([key]) => key !== "success" && key !== "message") - .map(([key, value]) => ` **${key}:** \`${JSON.stringify(value)}\``) - .join("\n"); - if (details) { - toolTrace += `\n${details}`; - } - } else { - // Show raw result if format is unexpected - toolTrace += `\n **Result:** \`${JSON.stringify(resultOutput)}\``; - } - } - - return toolTrace; - }); - - output += traces.join("\n\n"); - - // Add token usage information if available - if (response.usage) { - const inputTokens = response.usage.inputTokens || 0; - const outputTokens = response.usage.outputTokens || 0; - output += `\n\n📊 **Tokens:** ${inputTokens} input, ${outputTokens} output`; - } - - return output; - } - - constructor( - public copilotService: TexeraCopilot, - private copilotCoeditorService: CopilotCoeditorService - ) {} - - ngOnDestroy(): void { - // Cleanup when component is destroyed - this.disconnect(); - } - - /** - * Connect to copilot - called from menu button - * Registers copilot as coeditor and shows chat - */ - public async connect(): Promise { - if (this.isInitialized) return; - - try { - // Register copilot as virtual coeditor - this.copilotCoeditorService.register(); - - // Initialize copilot service - await this.copilotService.initialize(); - - this.isInitialized = true; - this.isChatVisible = true; // Show chat panel on connect - this.isExpanded = true; // Expand chat content by default - this.updateConnectionStatus(); - console.log("Copilot connected and registered as coeditor"); - } catch (error) { - console.error("Failed to connect copilot:", error); - this.copilotCoeditorService.unregister(); - } - } - - /** - * Disconnect from copilot - called from menu button - * Unregisters copilot and clears all messages - */ - public disconnect(): void { - if (!this.isInitialized) return; - - // Unregister copilot coeditor - this.copilotCoeditorService.unregister(); - - // Disconnect copilot service (this clears the connection) - this.copilotService.disconnect(); - - // Clear messages by resetting the message history - // The copilot service will need to be re-initialized next time - this.isInitialized = false; - this.isChatVisible = false; - this.updateConnectionStatus(); - console.log("Copilot disconnected and messages cleared"); - } - - /** - * Check if copilot is currently connected - */ - public isActive(): boolean { - return this.isInitialized; - } - - /** - * Toggle expand/collapse of chat content (keeps header visible) - */ - public toggleExpand(): void { - this.isExpanded = !this.isExpanded; - } - - /** - * Stop the current generation - */ - public stopGeneration(): void { - this.copilotService.stopGeneration(); - } - - /** - * Clear message history - */ - public clearMessages(): void { - this.copilotService.clearMessages(); - - // Clear deep-chat UI messages - if (this.deepChatElement?.nativeElement?.clearMessages) { - this.deepChatElement.nativeElement.clearMessages(true); - } - } - - /** - * Check if copilot is currently generating - */ - public isGenerating(): boolean { - return this.copilotService.getState() === CopilotState.GENERATING; - } - - /** - * Check if copilot is currently stopping - */ - public isStopping(): boolean { - return this.copilotService.getState() === CopilotState.STOPPING; - } - - /** - * Check if copilot is available (can send messages) - */ - public isAvailable(): boolean { - return this.copilotService.getState() === CopilotState.AVAILABLE; - } - - /** - * Get the copilot coeditor object (for displaying in UI) - */ - public getCopilot() { - return this.copilotCoeditorService.getCopilot(); - } - - private updateConnectionStatus(): void { - this.isConnected = this.copilotService.isConnected(); - } -} diff --git a/frontend/src/app/workspace/component/menu/menu.component.html b/frontend/src/app/workspace/component/menu/menu.component.html index 2f5045953bc..c9828e9d77e 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.html +++ b/frontend/src/app/workspace/component/menu/menu.component.html @@ -79,7 +79,6 @@ -
    @@ -142,16 +141,6 @@ nz-icon nzType="download"> - diff --git a/frontend/src/app/workspace/component/menu/menu.component.ts b/frontend/src/app/workspace/component/menu/menu.component.ts index 954b26f71b6..1096b3af0d4 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.ts @@ -56,7 +56,6 @@ import { ComputingUnitSelectionComponent } from "../power-button/computing-unit- import { GuiConfigService } from "../../../common/service/gui-config.service"; import { DashboardWorkflowComputingUnit } from "../../types/workflow-computing-unit"; import { Privilege } from "../../../dashboard/type/share-access.interface"; -import { CopilotChatComponent } from "../copilot-chat/copilot-chat.component"; /** * MenuComponent is the top level menu bar that shows @@ -120,7 +119,6 @@ export class MenuComponent implements OnInit, OnDestroy { public computingUnitStatus: ComputingUnitState = ComputingUnitState.NoComputingUnit; @ViewChild(ComputingUnitSelectionComponent) computingUnitSelectionComponent!: ComputingUnitSelectionComponent; - @ViewChild(CopilotChatComponent) copilotChat?: CopilotChatComponent; constructor( public executeWorkflowService: ExecuteWorkflowService, @@ -521,23 +519,6 @@ export class MenuComponent implements OnInit, OnDestroy { this.workflowActionService.deleteOperatorsAndLinks(allOperatorIDs); } - /** - * Toggle AI Copilot connection - * - If not connected: registers copilot as coeditor and shows chat - * - If connected: unregisters copilot, disconnects, and clears messages - */ - public async toggleCopilotChat(): Promise { - if (!this.copilotChat) return; - - if (this.copilotChat.isActive()) { - // Disconnect: remove from coeditors, clear messages - this.copilotChat.disconnect(); - } else { - // Connect: register as coeditor, show chat - await this.copilotChat.connect(); - } - } - public onClickImportWorkflow = (file: NzUploadFile): boolean => { const reader = new FileReader(); reader.readAsText(file as any); diff --git a/frontend/src/app/workspace/component/workspace.component.html b/frontend/src/app/workspace/component/workspace.component.html index 2e662831a33..20d6f18a3a8 100644 --- a/frontend/src/app/workspace/component/workspace.component.html +++ b/frontend/src/app/workspace/component/workspace.component.html @@ -33,5 +33,6 @@ + diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts new file mode 100644 index 00000000000..064ecb9bbae --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -0,0 +1,178 @@ +/** + * 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, Injector } from "@angular/core"; +import { TexeraCopilot } from "./texera-copilot"; +import { Subject } from "rxjs"; + +/** + * Agent info for tracking created agents + */ +export interface AgentInfo { + id: string; + name: string; + modelType: string; + instance: TexeraCopilot; + createdAt: Date; +} + +/** + * Available model types for agent creation + */ +export interface ModelType { + id: string; + name: string; + description: string; + icon: string; +} + +/** + * Service to manage multiple copilot agents + * Supports multi-agent workflows and agent lifecycle management + */ +@Injectable({ + providedIn: "root", +}) +export class TexeraCopilotManagerService { + // Map from agent ID to agent info + private agents = new Map(); + + // Counter for generating unique agent IDs + private agentCounter = 0; + + // Stream for agent creation/deletion events + private agentChangeSubject = new Subject(); + public agentChange$ = this.agentChangeSubject.asObservable(); + + // Available model types + private modelTypes: ModelType[] = [ + { + id: "claude-3.7", + name: "Claude 3.7 (Sonnet)", + description: "Balanced performance and speed for most workflow tasks", + icon: "robot", + }, + { + id: "claude-opus", + name: "Claude Opus", + description: "Most capable model for complex workflow operations", + icon: "star", + }, + { + id: "claude-haiku", + name: "Claude Haiku", + description: "Fastest model for simple and quick workflow edits", + icon: "thunderbolt", + }, + ]; + + constructor(private injector: Injector) {} + + /** + * Create a new agent with the specified model type + */ + public async createAgent(modelType: string, customName?: string): Promise { + const agentId = `agent-${++this.agentCounter}`; + const agentName = customName || `Agent ${this.agentCounter}`; + + try { + // Create new TexeraCopilot instance using Angular's Injector + const agentInstance = this.createCopilotInstance(modelType); + + // Initialize the agent + await agentInstance.initialize(); + + const agentInfo: AgentInfo = { + id: agentId, + name: agentName, + modelType, + instance: agentInstance, + createdAt: new Date(), + }; + + this.agents.set(agentId, agentInfo); + this.agentChangeSubject.next(); + + console.log(`Created agent: ${agentId} with model ${modelType}`); + return agentInfo; + } catch (error) { + console.error(`Failed to create agent with model ${modelType}:`, error); + throw error; + } + } + + /** + * Get agent by ID + */ + public getAgent(agentId: string): AgentInfo | undefined { + return this.agents.get(agentId); + } + + /** + * Get all agents + */ + public getAllAgents(): AgentInfo[] { + return Array.from(this.agents.values()); + } + + /** + * Delete agent by ID + */ + public deleteAgent(agentId: string): boolean { + const agent = this.agents.get(agentId); + if (agent) { + // Disconnect agent before deletion + agent.instance.disconnect(); + this.agents.delete(agentId); + this.agentChangeSubject.next(); + console.log(`Deleted agent: ${agentId}`); + return true; + } + return false; + } + + /** + * Get available model types + */ + public getModelTypes(): ModelType[] { + return this.modelTypes; + } + + /** + * Get agent count + */ + public getAgentCount(): number { + return this.agents.size; + } + + /** + * Create a copilot instance with proper dependency injection + * Uses Angular's Injector to dynamically create instances + */ + private createCopilotInstance(modelType: string): TexeraCopilot { + // Create a new instance using Angular's Injector + // This automatically injects all required dependencies + const copilotInstance = this.injector.get(TexeraCopilot); + + // Set the model type for this instance + copilotInstance.setModelType(modelType); + + return copilotInstance; + } +} diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index ad2239e5408..1629373477e 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -73,7 +73,7 @@ import { ActionPlanService } from "../action-plan/action-plan.service"; // API endpoints as constants export const COPILOT_MCP_URL = "mcp"; -export const AGENT_MODEL_ID = "claude-3.7"; +export const DEFAULT_AGENT_MODEL_ID = "claude-3.7"; // Message window size: -1 means no limit, positive value keeps only latest N messages export const MESSAGE_WINDOW_SIZE = 3; @@ -109,14 +109,15 @@ export interface AgentResponse { /** * Texera Copilot - An AI assistant for workflow manipulation * Uses Vercel AI SDK for chat completion and MCP SDK for tool discovery + * + * Note: Not a singleton - each agent has its own instance */ -@Injectable({ - providedIn: "root", -}) +@Injectable() export class TexeraCopilot { private mcpClient?: Client; private mcpTools: any[] = []; private model: any; + private modelType: string; // Message history using AI SDK's ModelMessage type private messages: ModelMessage[] = []; @@ -137,7 +138,15 @@ export class TexeraCopilot { private dataInconsistencyService: DataInconsistencyService, private actionPlanService: ActionPlanService ) { - // Don't auto-initialize, wait for user to enable + // Default model type + this.modelType = DEFAULT_AGENT_MODEL_ID; + } + + /** + * Set the model type for this agent + */ + public setModelType(modelType: string): void { + this.modelType = modelType; } /** @@ -148,11 +157,11 @@ export class TexeraCopilot { // 1. Connect to MCP server await this.connectMCP(); - // 2. Initialize OpenAI model + // 2. Initialize OpenAI model with the configured model type this.model = createOpenAI({ baseURL: new URL(`${AppSettings.getApiEndpoint()}`, document.baseURI).toString(), apiKey: "dummy", - }).chat(AGENT_MODEL_ID); + }).chat(this.modelType); // 3. Set state to Available this.state = CopilotState.AVAILABLE; From d99503d044d8df79c6f6141721f73d84c8add166 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Wed, 29 Oct 2025 19:03:24 -0700 Subject: [PATCH 044/158] improve the action plan --- .../service/copilot/workflow-tools.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index 3e55015d202..a7cad1f31c1 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -39,17 +39,6 @@ const TOOL_TIMEOUT_MS = 120000; // Estimated as characters / 4 (common approximation for token counting) const MAX_OPERATOR_RESULT_TOKEN_LIMIT = 1000; -export interface ActionPlan { - summary: string; - operators: Array<{ operatorType: string; customDisplayName?: string; description?: string }>; - links: Array<{ - sourceOperatorId: string; - targetOperatorId: string; - sourcePortId?: string; - targetPortId?: string; - }>; -} - /** * Estimates the number of tokens in a JSON-serializable object * Uses a common approximation: tokens ≈ characters / 4 @@ -231,7 +220,7 @@ export function createActionPlanTool( description: "Add a batch of operators and links to the workflow as part of an action plan. This tool is used to show the structure of what you plan to add without filling in detailed operator properties. It creates a workflow skeleton that demonstrates the planned data flow.", inputSchema: z.object({ - summary: z.string().describe("A brief summary of what this action plan does"), + summary: z.string().describe("A summary of what this action plan does"), operators: z .array( z.object({ @@ -263,7 +252,16 @@ export function createActionPlanTool( ) .describe("List of links to connect the operators"), }), - execute: async (args: { summary: string; operators: ActionPlan["operators"]; links: ActionPlan["links"] }) => { + execute: async (args: { + summary: string; + operators: Array<{ operatorType: string; customDisplayName?: string; description?: string }>; + links: Array<{ + sourceOperatorId: string; + targetOperatorId: string; + sourcePortId?: string; + targetPortId?: string; + }>; + }) => { try { // Clear previous highlights at start of tool execution copilotCoeditor.clearAll(); From 066084280f08cafe03be147bef92530f2eee72ac Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Thu, 30 Oct 2025 10:14:55 -0700 Subject: [PATCH 045/158] initial prototye of plan --- frontend/src/app/app.module.ts | 4 + .../action-plan-view.component.html | 135 ++++++++++++ .../action-plan-view.component.scss | 144 +++++++++++++ .../action-plan-view.component.ts | 127 +++++++++++ .../action-plans-tab.component.html | 75 +++++++ .../action-plans-tab.component.scss | 64 ++++++ .../action-plans-tab.component.ts | 68 ++++++ .../agent-chat/agent-chat.component.html | 18 ++ .../agent-chat/agent-chat.component.ts | 20 +- .../agent-panel/agent-panel.component.html | 7 + .../agent-panel/agent-panel.component.ts | 10 +- .../action-plan/action-plan.service.ts | 200 +++++++++++++++++- .../copilot/texera-copilot-manager.service.ts | 3 + .../service/copilot/texera-copilot.ts | 21 +- .../service/copilot/workflow-tools.ts | 74 ++++++- 15 files changed, 956 insertions(+), 14 deletions(-) create mode 100644 frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html create mode 100644 frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss create mode 100644 frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts create mode 100644 frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.html create mode 100644 frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.scss create mode 100644 frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 02ffca30ae5..1b9f9a8f6b6 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -104,6 +104,8 @@ import { AgentPanelComponent } from "./workspace/component/agent-panel/agent-pan import { AgentChatComponent } from "./workspace/component/agent-panel/agent-chat/agent-chat.component"; import { AgentRegistrationComponent } from "./workspace/component/agent-panel/agent-registration/agent-registration.component"; import { ActionPlanFeedbackComponent } from "./workspace/component/action-plan-feedback/action-plan-feedback.component"; +import { ActionPlanViewComponent } from "./workspace/component/action-plan-view/action-plan-view.component"; +import { ActionPlansTabComponent } from "./workspace/component/agent-panel/action-plans-tab/action-plans-tab.component"; import { InputAutoCompleteComponent } from "./workspace/component/input-autocomplete/input-autocomplete.component"; import { CollabWrapperComponent } from "./common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; import { TexeraCopilot } from "./workspace/service/copilot/texera-copilot"; @@ -256,6 +258,8 @@ registerLocaleData(en); AgentChatComponent, AgentRegistrationComponent, ActionPlanFeedbackComponent, + ActionPlanViewComponent, + ActionPlansTabComponent, InputAutoCompleteComponent, FileSelectionComponent, CollabWrapperComponent, diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html new file mode 100644 index 00000000000..1094d4bdd41 --- /dev/null +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html @@ -0,0 +1,135 @@ + + +
    + +
    +
    +

    {{ actionPlan.summary }}

    +
    + + + {{ actionPlan.agentName }} + + + + {{ actionPlan.createdAt | date : "short" }} + +
    +
    +
    + {{ getStatusLabel() }} +
    +
    + + +
    + +
    + + +
    +
    Tasks:
    + + +
    +
    + + +
    +
    +
    {{ task.description }}
    +
    Operator: {{ task.operatorId }}
    +
    +
    +
    +
    +
    + + + + + +
    + +
    + + +
    +
    +
    diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss new file mode 100644 index 00000000000..48c31d448a8 --- /dev/null +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss @@ -0,0 +1,144 @@ +/** + * 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. + */ + +.action-plan-view { + padding: 16px; + background: #fff; + border: 1px solid #d9d9d9; + border-radius: 8px; + margin-bottom: 12px; + + .plan-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; + + .plan-info { + flex: 1; + + h4 { + margin: 0 0 8px 0; + font-size: 16px; + font-weight: 600; + } + + .plan-meta { + display: flex; + gap: 16px; + font-size: 12px; + color: #8c8c8c; + + span { + display: flex; + align-items: center; + gap: 4px; + + i { + font-size: 12px; + } + } + } + } + + .plan-status { + flex-shrink: 0; + } + } + + .progress-section { + margin-bottom: 16px; + } + + .tasks-section { + margin-bottom: 16px; + + h5 { + margin: 0 0 8px 0; + font-size: 14px; + font-weight: 500; + } + + .task-item { + display: flex; + gap: 12px; + width: 100%; + + .task-checkbox { + flex-shrink: 0; + font-size: 18px; + + .task-completed { + color: #52c41a; + } + + .task-pending { + color: #d9d9d9; + } + } + + .task-content { + flex: 1; + + .task-description { + font-size: 14px; + margin-bottom: 4px; + } + + .task-operator-id { + font-size: 12px; + color: #8c8c8c; + } + } + } + } + + .feedback-section { + margin-bottom: 16px; + } + + .controls-section { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + + .feedback-input { + margin-bottom: 12px; + + label { + display: block; + margin-bottom: 4px; + font-size: 12px; + font-weight: 500; + } + } + + .action-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; + + button { + i { + margin-right: 4px; + } + } + } + } +} diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts new file mode 100644 index 00000000000..ae27c568a16 --- /dev/null +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts @@ -0,0 +1,127 @@ +/** + * 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 { Component, Input, OnInit, OnDestroy } from "@angular/core"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { + ActionPlan, + ActionPlanStatus, + ActionPlanService, +} from "../../service/action-plan/action-plan.service"; + +@UntilDestroy() +@Component({ + selector: "texera-action-plan-view", + templateUrl: "./action-plan-view.component.html", + styleUrls: ["./action-plan-view.component.scss"], +}) +export class ActionPlanViewComponent implements OnInit, OnDestroy { + @Input() actionPlan!: ActionPlan; + @Input() showFeedbackControls: boolean = false; // Show accept/reject buttons + + public rejectMessage: string = ""; + public ActionPlanStatus = ActionPlanStatus; // Expose enum to template + + // Track task completion states + public taskCompletionStates: { [operatorId: string]: boolean } = {}; + + constructor(private actionPlanService: ActionPlanService) {} + + ngOnInit(): void { + if (!this.actionPlan) { + console.error("ActionPlan is not provided!"); + return; + } + + // Subscribe to task completion changes + this.actionPlan.tasks.forEach(task => { + this.taskCompletionStates[task.operatorId] = task.completed$.value; + task.completed$.pipe(untilDestroyed(this)).subscribe(completed => { + this.taskCompletionStates[task.operatorId] = completed; + }); + }); + } + + ngOnDestroy(): void { + // Cleanup handled by UntilDestroy decorator + } + + /** + * User accepted the action plan + */ + public onAccept(): void { + this.actionPlanService.acceptPlan(this.actionPlan.id); + } + + /** + * User rejected the action plan with optional feedback + */ + public onReject(): void { + const message = this.rejectMessage.trim() || "I don't want this action plan."; + this.actionPlanService.rejectPlan(message, this.actionPlan.id); + this.rejectMessage = ""; + } + + /** + * Get status label for display + */ + public getStatusLabel(): string { + const status = this.actionPlan.status$.value; + switch (status) { + case ActionPlanStatus.PENDING: + return "Pending Approval"; + case ActionPlanStatus.ACCEPTED: + return "In Progress"; + case ActionPlanStatus.REJECTED: + return "Rejected"; + case ActionPlanStatus.COMPLETED: + return "Completed"; + default: + return status; + } + } + + /** + * Get status color for display + */ + public getStatusColor(): string { + const status = this.actionPlan.status$.value; + switch (status) { + case ActionPlanStatus.PENDING: + return "warning"; + case ActionPlanStatus.ACCEPTED: + return "processing"; + case ActionPlanStatus.REJECTED: + return "error"; + case ActionPlanStatus.COMPLETED: + return "success"; + default: + return "default"; + } + } + + /** + * Get progress percentage + */ + public getProgressPercentage(): number { + if (this.actionPlan.tasks.length === 0) return 0; + const completedCount = this.actionPlan.tasks.filter(t => t.completed$.value).length; + return Math.round((completedCount / this.actionPlan.tasks.length) * 100); + } +} diff --git a/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.html b/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.html new file mode 100644 index 00000000000..5c8346f6706 --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.html @@ -0,0 +1,75 @@ + + +
    +
    +

    Action Plans

    + +
    + + +
    + +
    + + +
    +
    +
    + +
    + +
    +
    +
    diff --git a/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.scss b/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.scss new file mode 100644 index 00000000000..5c8db580d25 --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.scss @@ -0,0 +1,64 @@ +/** + * 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. + */ + +.action-plans-tab { + height: 100%; + display: flex; + flex-direction: column; + padding: 16px; + + .tab-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid #f0f0f0; + + h3 { + margin: 0; + font-size: 18px; + font-weight: 600; + } + } + + .empty-state { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + } + + .action-plans-list { + flex: 1; + overflow-y: auto; + + .action-plan-container { + position: relative; + margin-bottom: 16px; + + .plan-header-bar { + position: absolute; + top: 8px; + right: 8px; + z-index: 10; + } + } + } +} diff --git a/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.ts b/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.ts new file mode 100644 index 00000000000..f5c6132a16c --- /dev/null +++ b/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.ts @@ -0,0 +1,68 @@ +/** + * 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 { Component, OnInit, OnDestroy } from "@angular/core"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { ActionPlan, ActionPlanService } from "../../../service/action-plan/action-plan.service"; + +@UntilDestroy() +@Component({ + selector: "texera-action-plans-tab", + templateUrl: "./action-plans-tab.component.html", + styleUrls: ["./action-plans-tab.component.scss"], +}) +export class ActionPlansTabComponent implements OnInit, OnDestroy { + public actionPlans: ActionPlan[] = []; + + constructor(private actionPlanService: ActionPlanService) {} + + ngOnInit(): void { + // Subscribe to action plans updates + this.actionPlanService + .getActionPlansStream() + .pipe(untilDestroyed(this)) + .subscribe(plans => { + // Sort plans by creation date (newest first) + this.actionPlans = [...plans].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + }); + } + + ngOnDestroy(): void { + // Cleanup handled by UntilDestroy decorator + } + + /** + * Delete an action plan + */ + public deleteActionPlan(planId: string, event: Event): void { + event.stopPropagation(); + if (confirm("Are you sure you want to delete this action plan?")) { + this.actionPlanService.deleteActionPlan(planId); + } + } + + /** + * Clear all action plans + */ + public clearAllActionPlans(): void { + if (confirm("Are you sure you want to clear all action plans?")) { + this.actionPlanService.clearAllActionPlans(); + } + } +} diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html index 01d4e795285..2ac3d17985c 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html @@ -89,6 +89,24 @@ Thinking...
    + + +
    +
    + + {{ agentInfo.name }} +
    +
    + +
    +
    diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts index 9490099fde2..0eeb9e07b2a 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -4,6 +4,7 @@ import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { TexeraCopilot, AgentResponse, CopilotState } from "../../../service/copilot/texera-copilot"; import { AgentInfo } from "../../../service/copilot/texera-copilot-manager.service"; import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; +import { ActionPlan, ActionPlanService } from "../../../service/action-plan/action-plan.service"; interface ChatMessage { role: "user" | "ai"; @@ -24,10 +25,11 @@ export class AgentChatComponent implements OnInit, OnDestroy, AfterViewChecked { public showToolResults = false; public messages: ChatMessage[] = []; public currentMessage = ""; + public pendingActionPlan: ActionPlan | null = null; private copilotService!: TexeraCopilot; private shouldScrollToBottom = false; - constructor(private sanitizer: DomSanitizer) {} + constructor(private sanitizer: DomSanitizer, private actionPlanService: ActionPlanService) {} ngOnInit(): void { console.log("AgentChatComponent ngOnInit - agentInfo:", this.agentInfo); @@ -49,6 +51,22 @@ export class AgentChatComponent implements OnInit, OnDestroy, AfterViewChecked { role: "ai", text: `Hi! I'm ${this.agentInfo.name}. I can help you build and modify workflows.`, }); + + // Subscribe to pending action plans + this.actionPlanService + .getPendingActionPlanStream() + .pipe(untilDestroyed(this)) + .subscribe(plan => { + // Only show plans from this agent + if (plan && plan.agentId === this.agentInfo.id) { + this.pendingActionPlan = plan; + this.shouldScrollToBottom = true; + } else if (plan === null || (plan && plan.agentId !== this.agentInfo.id)) { + // Clear pending plan if it's null or belongs to another agent + this.pendingActionPlan = null; + } + }); + console.log("AgentChatComponent initialized successfully"); } diff --git a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html index 5d300911cff..7386d162219 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html +++ b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html @@ -71,6 +71,13 @@ + + + + + + + agent.id === agentId); if (agentIndex !== -1) { - this.selectedTabIndex = agentIndex + 1; // +1 because tab 0 is registration + this.selectedTabIndex = agentIndex + 2; // +2 because tab 0 is registration, tab 1 is action plans } } @@ -112,9 +112,9 @@ export class AgentPanelComponent implements OnInit, OnDestroy { this.copilotManagerService.deleteAgent(agentId); // If we're on the deleted agent's tab, switch to registration - if (agentIndex !== -1 && this.selectedTabIndex === agentIndex + 1) { + if (agentIndex !== -1 && this.selectedTabIndex === agentIndex + 2) { this.selectedTabIndex = 0; - } else if (this.selectedTabIndex > agentIndex + 1) { + } else if (this.selectedTabIndex > agentIndex + 2) { // Adjust selected index if we deleted a tab before the current one this.selectedTabIndex--; } diff --git a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts index b63a6c90061..b13835e4a57 100644 --- a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts +++ b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts @@ -18,7 +18,7 @@ */ import { Injectable } from "@angular/core"; -import { Subject, Observable } from "rxjs"; +import { Subject, Observable, BehaviorSubject } from "rxjs"; /** * Interface for an action plan highlight event @@ -38,8 +38,43 @@ export interface ActionPlanFeedback { } /** - * Service to manage action plan highlights and user feedback - * Handles the interactive flow: show plan -> wait for user decision -> return feedback + * Status of an action plan + */ +export enum ActionPlanStatus { + PENDING = "pending", // Waiting for user approval + ACCEPTED = "accepted", // User accepted, being executed + REJECTED = "rejected", // User rejected + COMPLETED = "completed", // Execution completed +} + +/** + * Individual operator task within an action plan + */ +export interface ActionPlanTask { + operatorId: string; + description: string; + completed$: BehaviorSubject; +} + +/** + * Complete Action Plan data structure + */ +export interface ActionPlan { + id: string; // Unique identifier for the action plan + agentId: string; // ID of the agent that created this plan + agentName: string; // Name of the agent + summary: string; // Overall summary of the action plan + tasks: ActionPlanTask[]; // List of operator tasks + status$: BehaviorSubject; // Current status + createdAt: Date; // Creation timestamp + userFeedback?: string; // User's feedback message (if rejected) + operatorIds: string[]; // For highlighting + linkIds: string[]; // For highlighting +} + +/** + * Service to manage action plans, highlights, and user feedback + * Handles the interactive flow: show plan -> wait for user decision -> execute -> track progress */ @Injectable({ providedIn: "root", @@ -49,6 +84,11 @@ export class ActionPlanService { private feedbackSubject: Subject | null = null; private cleanupSubject = new Subject(); + // Action plan storage + private actionPlans = new Map(); + private actionPlansSubject = new BehaviorSubject([]); + private pendingActionPlanSubject = new BehaviorSubject(null); + constructor() {} /** @@ -65,6 +105,126 @@ export class ActionPlanService { return this.cleanupSubject.asObservable(); } + /** + * Get all action plans as observable + */ + public getActionPlansStream(): Observable { + return this.actionPlansSubject.asObservable(); + } + + /** + * Get pending action plan stream (for showing in agent chat) + */ + public getPendingActionPlanStream(): Observable { + return this.pendingActionPlanSubject.asObservable(); + } + + /** + * Get all action plans + */ + public getAllActionPlans(): ActionPlan[] { + return Array.from(this.actionPlans.values()); + } + + /** + * Get a specific action plan by ID + */ + public getActionPlan(id: string): ActionPlan | undefined { + return this.actionPlans.get(id); + } + + /** + * Create a new action plan + */ + public createActionPlan( + agentId: string, + agentName: string, + summary: string, + tasks: Array<{ operatorId: string; description: string }>, + operatorIds: string[], + linkIds: string[] + ): ActionPlan { + const id = this.generateId(); + const actionPlan: ActionPlan = { + id, + agentId, + agentName, + summary, + tasks: tasks.map(task => ({ + operatorId: task.operatorId, + description: task.description, + completed$: new BehaviorSubject(false), + })), + status$: new BehaviorSubject(ActionPlanStatus.PENDING), + createdAt: new Date(), + operatorIds, + linkIds, + }; + + this.actionPlans.set(id, actionPlan); + this.emitActionPlans(); + this.pendingActionPlanSubject.next(actionPlan); + + return actionPlan; + } + + /** + * Update action plan status + */ + public updateActionPlanStatus(id: string, status: ActionPlanStatus): void { + const plan = this.actionPlans.get(id); + if (plan) { + plan.status$.next(status); + this.emitActionPlans(); + } + } + + /** + * Update a task's completion status + */ + public updateTaskCompletion(planId: string, operatorId: string, completed: boolean): void { + const plan = this.actionPlans.get(planId); + if (plan) { + const task = plan.tasks.find(t => t.operatorId === operatorId); + if (task) { + task.completed$.next(completed); + this.emitActionPlans(); + + // Check if all tasks are completed + const allCompleted = plan.tasks.every(t => t.completed$.value); + if (allCompleted && plan.status$.value === ActionPlanStatus.ACCEPTED) { + this.updateActionPlanStatus(planId, ActionPlanStatus.COMPLETED); + } + } + } + } + + /** + * Delete an action plan + */ + public deleteActionPlan(id: string): void { + const plan = this.actionPlans.get(id); + if (plan) { + // Complete all subjects + plan.status$.complete(); + plan.tasks.forEach(task => task.completed$.complete()); + } + this.actionPlans.delete(id); + this.emitActionPlans(); + } + + /** + * Clear all action plans + */ + public clearAllActionPlans(): void { + this.actionPlans.forEach(plan => { + plan.status$.complete(); + plan.tasks.forEach(task => task.completed$.complete()); + }); + this.actionPlans.clear(); + this.emitActionPlans(); + } + /** * Show an action plan and wait for user feedback (accept/reject) * Returns an Observable that emits when user makes a decision @@ -87,7 +247,7 @@ export class ActionPlanService { /** * User accepted the action plan */ - public acceptPlan(): void { + public acceptPlan(planId?: string): void { if (this.feedbackSubject) { this.feedbackSubject.next({ accepted: true }); this.feedbackSubject.complete(); @@ -96,12 +256,18 @@ export class ActionPlanService { // Trigger cleanup (remove highlight and panel) this.cleanupSubject.next(); } + + // Update plan status if planId provided + if (planId) { + this.updateActionPlanStatus(planId, ActionPlanStatus.ACCEPTED); + this.pendingActionPlanSubject.next(null); + } } /** * User rejected the action plan with optional feedback message */ - public rejectPlan(message?: string): void { + public rejectPlan(message?: string, planId?: string): void { if (this.feedbackSubject) { this.feedbackSubject.next({ accepted: false, message }); this.feedbackSubject.complete(); @@ -110,5 +276,29 @@ export class ActionPlanService { // Trigger cleanup (remove highlight and panel) this.cleanupSubject.next(); } + + // Update plan status if planId provided + if (planId) { + const plan = this.actionPlans.get(planId); + if (plan) { + plan.userFeedback = message; + this.updateActionPlanStatus(planId, ActionPlanStatus.REJECTED); + } + this.pendingActionPlanSubject.next(null); + } + } + + /** + * Generate a unique ID for action plans + */ + private generateId(): string { + return `action-plan-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + } + + /** + * Emit the current list of action plans + */ + private emitActionPlans(): void { + this.actionPlansSubject.next(this.getAllActionPlans()); } } diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index 064ecb9bbae..6f341ccb6bb 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -95,6 +95,9 @@ export class TexeraCopilotManagerService { // Create new TexeraCopilot instance using Angular's Injector const agentInstance = this.createCopilotInstance(modelType); + // Set agent information + agentInstance.setAgentInfo(agentId, agentName); + // Initialize the agent await agentInstance.initialize(); diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 1629373477e..51e89282cfa 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -26,6 +26,7 @@ import { createAddOperatorTool, createAddLinkTool, createActionPlanTool, + createUpdateActionPlanProgressTool, createListOperatorsTool, createListLinksTool, createListOperatorTypesTool, @@ -119,6 +120,10 @@ export class TexeraCopilot { private model: any; private modelType: string; + // Agent identification + private agentId: string = ""; + private agentName: string = ""; + // Message history using AI SDK's ModelMessage type private messages: ModelMessage[] = []; @@ -142,6 +147,14 @@ export class TexeraCopilot { this.modelType = DEFAULT_AGENT_MODEL_ID; } + /** + * Set the agent identification + */ + public setAgentInfo(agentId: string, agentName: string): void { + this.agentId = agentId; + this.agentName = agentName; + } + /** * Set the model type for this agent */ @@ -384,9 +397,14 @@ export class TexeraCopilot { this.workflowUtilService, this.operatorMetadataService, this.copilotCoeditorService, - this.actionPlanService + this.actionPlanService, + this.agentId, + this.agentName ) ); + const updateActionPlanProgressTool = toolWithTimeout( + createUpdateActionPlanProgressTool(this.actionPlanService) + ); const listOperatorsTool = toolWithTimeout( createListOperatorsTool(this.workflowActionService, this.copilotCoeditorService) ); @@ -480,6 +498,7 @@ export class TexeraCopilot { addOperator: addOperatorTool, addLink: addLinkTool, actionPlan: actionPlanTool, + updateActionPlanProgress: updateActionPlanProgressTool, listOperators: listOperatorsTool, listLinks: listLinksTool, listOperatorTypes: listOperatorTypesTool, diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index a7cad1f31c1..ac469d7dbd4 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -213,7 +213,9 @@ export function createActionPlanTool( workflowUtilService: WorkflowUtilService, operatorMetadataService: OperatorMetadataService, copilotCoeditor: CopilotCoeditorService, - actionPlanService: ActionPlanService + actionPlanService: ActionPlanService, + agentId: string = "", + agentName: string = "" ) { return tool({ name: "actionPlan", @@ -318,6 +320,21 @@ export function createActionPlanTool( createdOperatorIds.push(operator.operatorID); } + // Create action plan with tasks + const tasks = args.operators.map((operatorSpec, index) => ({ + operatorId: createdOperatorIds[index], + description: operatorSpec.description || operatorSpec.customDisplayName || operatorSpec.operatorType, + })); + + const actionPlan = actionPlanService.createActionPlan( + agentId, + agentName || "AI Agent", + args.summary, + tasks, + createdOperatorIds, + [] // linkIds will be added after links are created + ); + // Create all links using the operator IDs const createdLinkIds: string[] = []; for (let i = 0; i < args.links.length; i++) { @@ -360,6 +377,9 @@ export function createActionPlanTool( createdLinkIds.push(link.linkID); } + // Update action plan with link IDs + actionPlan.linkIds = createdLinkIds; + // Show copilot is adding these operators (after they're added to graph) setTimeout(() => { copilotCoeditor.highlightOperators(createdOperatorIds); @@ -382,6 +402,9 @@ export function createActionPlanTool( // User rejected - remove the created operators and links workflowActionService.deleteOperatorsAndLinks(createdOperatorIds); + // Update action plan status to rejected + actionPlanService.rejectPlan(feedback.message, actionPlan.id); + return { success: false, rejected: true, @@ -390,12 +413,15 @@ export function createActionPlanTool( }; } - // User accepted - return success + // User accepted - update action plan status + actionPlanService.acceptPlan(actionPlan.id); + return { success: true, summary: args.summary, operatorIds: createdOperatorIds, linkIds: createdLinkIds, + actionPlanId: actionPlan.id, message: `Action Plan: ${args.summary}. Added ${args.operators.length} operator(s) and ${args.links.length} link(s) to workflow. User accepted the plan.`, }; } catch (error: any) { @@ -405,6 +431,50 @@ export function createActionPlanTool( }); } +/** + * Create updateActionPlanProgress tool for marking tasks as complete + */ +export function createUpdateActionPlanProgressTool(actionPlanService: ActionPlanService) { + return tool({ + name: "updateActionPlanProgress", + description: + "Mark a specific task in an action plan as completed. Use this after you've finished configuring an operator from an accepted action plan.", + inputSchema: z.object({ + actionPlanId: z.string().describe("ID of the action plan"), + operatorId: z.string().describe("ID of the operator task to mark as complete"), + completed: z.boolean().describe("Whether the task is completed (true) or not (false)"), + }), + execute: async (args: { actionPlanId: string; operatorId: string; completed: boolean }) => { + try { + const plan = actionPlanService.getActionPlan(args.actionPlanId); + if (!plan) { + return { + success: false, + error: `Action plan with ID ${args.actionPlanId} not found`, + }; + } + + const task = plan.tasks.find(t => t.operatorId === args.operatorId); + if (!task) { + return { + success: false, + error: `Task with operator ID ${args.operatorId} not found in action plan ${args.actionPlanId}`, + }; + } + + actionPlanService.updateTaskCompletion(args.actionPlanId, args.operatorId, args.completed); + + return { + success: true, + message: `Task for operator ${args.operatorId} marked as ${args.completed ? "completed" : "incomplete"}`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} + /** * Create listOperators tool for getting all operators in the workflow */ From dac1c916987b64768c247a94c6f3fc51b203a0e3 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Thu, 30 Oct 2025 11:12:20 -0700 Subject: [PATCH 046/158] a working prototype for planning agent --- .../action-plan-view.component.html | 5 +- .../action-plan-view.component.scss | 7 ++ .../action-plan-view.component.ts | 87 +++++++++++++++++-- .../agent-chat/agent-chat.component.html | 3 +- .../agent-chat/agent-chat.component.ts | 20 ++++- .../workflow-editor.component.html | 8 -- .../workflow-editor.component.ts | 26 +----- .../action-plan/action-plan.service.ts | 5 +- .../service/copilot/texera-copilot.ts | 4 +- .../service/copilot/workflow-tools.ts | 4 +- 10 files changed, 118 insertions(+), 51 deletions(-) diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html index 1094d4bdd41..f71dbe97a07 100644 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html @@ -60,7 +60,10 @@
    Tasks:
    [nzDataSource]="actionPlan.tasks" nzSize="small"> -
    +
    (); public rejectMessage: string = ""; public ActionPlanStatus = ActionPlanStatus; // Expose enum to template @@ -41,7 +40,10 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { // Track task completion states public taskCompletionStates: { [operatorId: string]: boolean } = {}; - constructor(private actionPlanService: ActionPlanService) {} + constructor( + private actionPlanService: ActionPlanService, + private workflowActionService: WorkflowActionService + ) {} ngOnInit(): void { if (!this.actionPlan) { @@ -66,6 +68,13 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { * User accepted the action plan */ public onAccept(): void { + // Emit user decision event for chat component to show as user message + this.userDecision.emit({ + accepted: true, + message: `✅ Accepted action plan: "${this.actionPlan.summary}"`, + }); + + // Trigger the feedback to resolve the tool's promise this.actionPlanService.acceptPlan(this.actionPlan.id); } @@ -73,11 +82,71 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { * User rejected the action plan with optional feedback */ public onReject(): void { - const message = this.rejectMessage.trim() || "I don't want this action plan."; - this.actionPlanService.rejectPlan(message, this.actionPlan.id); + const userFeedback = this.rejectMessage.trim() || "I don't want this action plan."; + + // Emit user decision event for chat component to show as user message + this.userDecision.emit({ + accepted: false, + message: `❌ Rejected action plan: "${this.actionPlan.summary}". Feedback: ${userFeedback}`, + }); + + // Trigger the feedback to resolve the tool's promise + // Note: Operators will be deleted by workflow-tools.ts when it receives the rejection + this.actionPlanService.rejectPlan(userFeedback, this.actionPlan.id); + this.rejectMessage = ""; } + /** + * Highlight an operator when clicking on its task + */ + public highlightOperator(operatorId: string): void { + // Get the operator from workflow + const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorId); + if (!operator) { + return; + } + + // Get the joint graph wrapper to access the paper + const jointGraphWrapper = this.workflowActionService.getJointGraphWrapper(); + if (!jointGraphWrapper) { + return; + } + + const paper = jointGraphWrapper.getMainJointPaper(); + const operatorElement = paper.getModelById(operatorId); + + if (operatorElement) { + // Create a temporary highlight using Joint.js highlight API + const operatorView = paper.findViewByModel(operatorElement); + if (operatorView) { + // Add light blue halo effect using joint.highlighters + const highlighterNamespace = joint.highlighters; + + // Remove any existing highlight with same name + highlighterNamespace.mask.remove(operatorView, "action-plan-click"); + + // Add new highlight with light blue color + highlighterNamespace.mask.add(operatorView, "body", "action-plan-click", { + padding: 10, + deep: true, + attrs: { + stroke: "#69b7ff", + "stroke-width": 3, + "stroke-opacity": 0.8, + fill: "#69b7ff", + "fill-opacity": 0.1, + }, + }); + + // Remove the highlight after 2 seconds + setTimeout(() => { + highlighterNamespace.mask.remove(operatorView, "action-plan-click"); + }, 2000); + } + } + } + /** * Get status label for display */ diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html index 2ac3d17985c..31c1277dd52 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html @@ -104,7 +104,8 @@
    + [showFeedbackControls]="true" + (userDecision)="onUserDecision($event)">
    diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts index 0eeb9e07b2a..0cda93626a0 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -29,7 +29,10 @@ export class AgentChatComponent implements OnInit, OnDestroy, AfterViewChecked { private copilotService!: TexeraCopilot; private shouldScrollToBottom = false; - constructor(private sanitizer: DomSanitizer, private actionPlanService: ActionPlanService) {} + constructor( + private sanitizer: DomSanitizer, + private actionPlanService: ActionPlanService + ) {} ngOnInit(): void { console.log("AgentChatComponent ngOnInit - agentInfo:", this.agentInfo); @@ -277,4 +280,19 @@ export class AgentChatComponent implements OnInit, OnDestroy, AfterViewChecked { public isConnected(): boolean { return this.copilotService?.isConnected() ?? false; } + + /** + * Handle user decision on action plan + */ + public onUserDecision(decision: { accepted: boolean; message: string }): void { + // Add the user's decision as a user message in the chat + this.messages.push({ + role: "user", + text: decision.message, + }); + this.shouldScrollToBottom = true; + + // Clear the pending action plan since user has made a decision + this.pendingActionPlan = null; + } } diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html index 0dc058ad1b1..8f87c66c7a7 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html @@ -26,12 +26,4 @@ #menu="nzDropdownMenu"> - - - -
    diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts index 8ee35aa19f7..7b967859c17 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts @@ -96,12 +96,6 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy private removeButton!: new () => joint.linkTools.Button; private breakpointButton!: new () => joint.linkTools.Button; - // Action plan feedback panel state - public showActionPlanFeedback: boolean = false; - public actionPlanSummary: string = ""; - public actionPlanPanelLeft: number = 0; - public actionPlanPanelTop: number = 0; - constructor( private workflowActionService: WorkflowActionService, private dynamicSchemaService: DynamicSchemaService, @@ -458,24 +452,10 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy // Update the highlight to wrap around operators this.updateActionPlanElement(currentElement, operators); - // Calculate panel position (to the right of the highlighted area) - const bbox = this.getOperatorsBoundingBox(operators); - const panelPosition = this.calculatePanelPosition(bbox); - this.actionPlanPanelLeft = panelPosition.x; - this.actionPlanPanelTop = panelPosition.y; - this.actionPlanSummary = actionPlan.summary; - this.showActionPlanFeedback = true; - - // Listen to operator position changes to update the highlight and panel + // Listen to operator position changes to update the highlight currentPositionHandler = (operator: joint.dia.Cell) => { if (operators.includes(operator) && currentElement) { this.updateActionPlanElement(currentElement, operators); - // Update panel position when operators move - const newBbox = this.getOperatorsBoundingBox(operators); - const newPosition = this.calculatePanelPosition(newBbox); - this.actionPlanPanelLeft = newPosition.x; - this.actionPlanPanelTop = newPosition.y; - this.changeDetectorRef.detectChanges(); } }; this.paper.model.on("change:position", currentPositionHandler); @@ -497,10 +477,6 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy this.paper.model.off("change:position", currentPositionHandler); currentPositionHandler = null; } - - // Hide panel - this.showActionPlanFeedback = false; - this.changeDetectorRef.detectChanges(); }); } diff --git a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts index b13835e4a57..8e225ea8c89 100644 --- a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts +++ b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts @@ -63,6 +63,7 @@ export interface ActionPlan { id: string; // Unique identifier for the action plan agentId: string; // ID of the agent that created this plan agentName: string; // Name of the agent + executorAgentId: string; // ID of the agent that will execute/handle feedback for this plan (can be different from creator) summary: string; // Overall summary of the action plan tasks: ActionPlanTask[]; // List of operator tasks status$: BehaviorSubject; // Current status @@ -142,13 +143,15 @@ export class ActionPlanService { summary: string, tasks: Array<{ operatorId: string; description: string }>, operatorIds: string[], - linkIds: string[] + linkIds: string[], + executorAgentId?: string // Optional: defaults to agentId if not specified ): ActionPlan { const id = this.generateId(); const actionPlan: ActionPlan = { id, agentId, agentName, + executorAgentId: executorAgentId || agentId, // Default to creator if not specified summary, tasks: tasks.map(task => ({ operatorId: task.operatorId, diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 51e89282cfa..4e507024b46 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -402,9 +402,7 @@ export class TexeraCopilot { this.agentName ) ); - const updateActionPlanProgressTool = toolWithTimeout( - createUpdateActionPlanProgressTool(this.actionPlanService) - ); + const updateActionPlanProgressTool = toolWithTimeout(createUpdateActionPlanProgressTool(this.actionPlanService)); const listOperatorsTool = toolWithTimeout( createListOperatorsTool(this.workflowActionService, this.copilotCoeditorService) ); diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index ac469d7dbd4..2bd7da27e0d 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -409,7 +409,7 @@ export function createActionPlanTool( success: false, rejected: true, userFeedback: feedback.message || "User rejected this action plan", - message: `Action plan rejected by user: ${feedback.message || "No reason provided"}`, + message: `User rejected the action plan. Feedback: "${feedback.message || "No specific feedback provided"}"`, }; } @@ -422,7 +422,7 @@ export function createActionPlanTool( operatorIds: createdOperatorIds, linkIds: createdLinkIds, actionPlanId: actionPlan.id, - message: `Action Plan: ${args.summary}. Added ${args.operators.length} operator(s) and ${args.links.length} link(s) to workflow. User accepted the plan.`, + message: "User accepted the action plan. Proceeding with execution.", }; } catch (error: any) { return { success: false, error: error.message }; From eb7bd9f7c756c736ca5899b709c962b64aca2583 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Thu, 30 Oct 2025 11:32:25 -0700 Subject: [PATCH 047/158] a working prototype for planning agent --- bin/litellm-config.yaml | 4 ++-- .../service/copilot/copilot-prompts.ts | 1 + .../copilot/texera-copilot-manager.service.ts | 18 ++++++------------ 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/bin/litellm-config.yaml b/bin/litellm-config.yaml index efcd4523fe6..34b9521b4fa 100644 --- a/bin/litellm-config.yaml +++ b/bin/litellm-config.yaml @@ -3,7 +3,7 @@ model_list: litellm_params: model: claude-3-7-sonnet-20250219 api_key: "os.environ/ANTHROPIC_API_KEY" - - model_name: claude-4 + - model_name: claude-sonnet-4-5 litellm_params: - model: claude-sonnet-4-20250514 + model: claude-sonnet-4-5-20250929 api_key: "os.environ/ANTHROPIC_API_KEY" \ No newline at end of file diff --git a/frontend/src/app/workspace/service/copilot/copilot-prompts.ts b/frontend/src/app/workspace/service/copilot/copilot-prompts.ts index a905e48a98a..b0d06fa92fc 100644 --- a/frontend/src/app/workspace/service/copilot/copilot-prompts.ts +++ b/frontend/src/app/workspace/service/copilot/copilot-prompts.ts @@ -141,6 +141,7 @@ class ProcessTableOperator(UDFTableOperator): 4. **Tuple field access** - Use \`tuple_["field"]\` ONLY. DO NOT use \`tuple_.get()\`, \`tuple_.set()\`, or \`tuple_.values()\` 5. **Think of types:** - \`Tuple\` = Python dict (key-value pairs) + For Tuple, DO NOT USE APIs like tuple.get, just use ["key"] to access/change the kv pairs - \`Table\` = pandas DataFrame 6. **Use yield** - Return results with \`yield\`; emit at most once per API call 7. **Handle None values** - \`tuple_["key"]\` or \`df["column"]\` can be None diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index 6f341ccb6bb..ca3becad12e 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -64,22 +64,16 @@ export class TexeraCopilotManagerService { private modelTypes: ModelType[] = [ { id: "claude-3.7", - name: "Claude 3.7 (Sonnet)", - description: "Balanced performance and speed for most workflow tasks", - icon: "robot", + name: "Claude Sonnet 3.7", + description: "Balanced performance for workflow editing", + icon: "thunderbolt", }, { - id: "claude-opus", - name: "Claude Opus", - description: "Most capable model for complex workflow operations", + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5", + description: "Most capable model for complex planning", icon: "star", }, - { - id: "claude-haiku", - name: "Claude Haiku", - description: "Fastest model for simple and quick workflow edits", - icon: "thunderbolt", - }, ]; constructor(private injector: Injector) {} From d56e5dd95cba9cd1c6bf49bff3e0cac5012fea53 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Thu, 30 Oct 2025 13:16:01 -0700 Subject: [PATCH 048/158] prototype for fork an agent --- .../action-plan-view.component.html | 8 ++ .../action-plan-view.component.scss | 17 ++++- .../action-plan-view.component.ts | 68 +++++++++++++++-- .../agent-chat/agent-chat.component.ts | 74 ++++++++++++------- .../copilot/texera-copilot-manager.service.ts | 10 +++ 5 files changed, 141 insertions(+), 36 deletions(-) diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html index f71dbe97a07..84b2341d2ba 100644 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html @@ -103,6 +103,14 @@
    Tasks:
    +
    + + Creates a dedicated agent to execute this action plan +
    diff --git a/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.scss b/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.scss deleted file mode 100644 index 9ccec70d955..00000000000 --- a/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.scss +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 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. - */ - -.add-inconsistency-form { - padding: 16px 0; - - nz-form-item { - margin-bottom: 16px; - } - - .modal-footer { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 24px; - padding-top: 16px; - border-top: 1px solid #f0f0f0; - } -} diff --git a/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.ts b/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.ts deleted file mode 100644 index 4ebe08b25ab..00000000000 --- a/frontend/src/app/workspace/component/add-inconsistency-modal/add-inconsistency-modal.component.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * 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 { Component, OnInit } from "@angular/core"; -import { NzModalRef } from "ng-zorro-antd/modal"; -import { DataInconsistencyService } from "../../service/data-inconsistency/data-inconsistency.service"; - -@Component({ - selector: "texera-add-inconsistency-modal", - templateUrl: "./add-inconsistency-modal.component.html", - styleUrls: ["./add-inconsistency-modal.component.scss"], -}) -export class AddInconsistencyModalComponent implements OnInit { - name: string = ""; - description: string = ""; - operatorId: string = ""; - - constructor( - private modalRef: NzModalRef, - private dataInconsistencyService: DataInconsistencyService - ) {} - - ngOnInit(): void { - // Get the operatorId passed from the modal - const data = this.modalRef.getConfig().nzData; - if (data && data.operatorId) { - this.operatorId = data.operatorId; - } - } - - onSubmit(): void { - if (this.name.trim() && this.description.trim()) { - this.dataInconsistencyService.addInconsistency(this.name.trim(), this.description.trim(), this.operatorId); - this.modalRef.close(); - } - } - - onCancel(): void { - this.modalRef.close(); - } -} diff --git a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html index d68ef3c024c..8bbff3d3c65 100644 --- a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html @@ -139,7 +139,7 @@
  • -
  • - - Add to Inconsistency -
  • -
  • Date: Sun, 2 Nov 2025 22:05:51 -0800 Subject: [PATCH 080/158] remove inconsistency list --- frontend/src/app/app.module.ts | 2 - .../inconsistency-list.component.html | 82 ----------------- .../inconsistency-list.component.scss | 84 ----------------- .../inconsistency-list.component.ts | 91 ------------------- .../left-panel/left-panel.component.ts | 7 -- 5 files changed, 266 deletions(-) delete mode 100644 frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html delete mode 100644 frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.scss delete mode 100644 frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 480b3bb39ad..c1518b1d70a 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -135,7 +135,6 @@ import { ErrorFrameComponent } from "./workspace/component/result-panel/error-fr import { NzResizableModule } from "ng-zorro-antd/resizable"; import { WorkflowRuntimeStatisticsComponent } from "./dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-runtime-statistics/workflow-runtime-statistics.component"; import { TimeTravelComponent } from "./workspace/component/left-panel/time-travel/time-travel.component"; -import { InconsistencyListComponent } from "./workspace/component/left-panel/inconsistency-list/inconsistency-list.component"; import { NzMessageModule } from "ng-zorro-antd/message"; import { NzModalModule } from "ng-zorro-antd/modal"; import { NzDescriptionsModule } from "ng-zorro-antd/descriptions"; @@ -202,7 +201,6 @@ registerLocaleData(en); PropertyEditorComponent, VersionsListComponent, TimeTravelComponent, - InconsistencyListComponent, WorkflowEditorComponent, ResultPanelComponent, ResultExportationComponent, diff --git a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html deleted file mode 100644 index 5da008b07bd..00000000000 --- a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.html +++ /dev/null @@ -1,82 +0,0 @@ - - -
    -
    -

    Data Inconsistencies

    - -
    - -
    - -
    - - - - - -

    {{ inconsistency.description }}

    -

    Operator: {{ inconsistency.operatorId }}

    - - - - -
    -
    -
    -
    -
    diff --git a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.scss b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.scss deleted file mode 100644 index 26d9d401e9b..00000000000 --- a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.scss +++ /dev/null @@ -1,84 +0,0 @@ -/** - * 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. - */ - -.inconsistency-container { - padding: 16px; - height: 100%; - overflow-y: auto; -} - -.inconsistency-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - - h3 { - margin: 0; - font-size: 16px; - font-weight: 600; - } -} - -.empty-state { - display: flex; - justify-content: center; - align-items: center; - min-height: 200px; -} - -.inconsistency-card { - margin-bottom: 12px; - width: 100%; - background-color: #fafafa; - - p { - margin-bottom: 8px; - } - - .operator-id { - color: #8c8c8c; - font-size: 12px; - margin-bottom: 0; - } -} - -::ng-deep { - .inconsistency-card { - background-color: #fafafa; - - .ant-card-head { - min-height: auto; - padding: 4px 12px; - background-color: #f0f0f0; - } - - .ant-card-head-title { - font-size: 13px; - font-weight: 600; - padding: 0; - line-height: 1.3; - } - - .ant-card-body { - padding: 8px 12px; - background-color: #fafafa; - } - } -} diff --git a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.ts b/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.ts deleted file mode 100644 index 85dc59cbfb9..00000000000 --- a/frontend/src/app/workspace/component/left-panel/inconsistency-list/inconsistency-list.component.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * 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 { Component, OnInit, OnDestroy } from "@angular/core"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { - DataInconsistencyService, - DataInconsistency, -} from "../../../service/data-inconsistency/data-inconsistency.service"; -import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; - -@UntilDestroy() -@Component({ - selector: "texera-inconsistency-list", - templateUrl: "./inconsistency-list.component.html", - styleUrls: ["./inconsistency-list.component.scss"], -}) -export class InconsistencyListComponent implements OnInit, OnDestroy { - inconsistencies: DataInconsistency[] = []; - - constructor( - private inconsistencyService: DataInconsistencyService, - private workflowActionService: WorkflowActionService - ) {} - - ngOnInit(): void { - // Subscribe to inconsistency updates - this.inconsistencyService - .getInconsistencies() - .pipe(untilDestroyed(this)) - .subscribe(inconsistencies => { - this.inconsistencies = inconsistencies; - }); - } - - ngOnDestroy(): void { - // Cleanup handled by @UntilDestroy - } - - /** - * Delete an inconsistency - */ - deleteInconsistency(id: string): void { - this.inconsistencyService.deleteInconsistency(id); - } - - /** - * Clear all inconsistencies - */ - clearAll(): void { - this.inconsistencyService.clearAll(); - } - - /** - * Handle click on inconsistency card to highlight the upstream path - */ - onInconsistencyClick(inconsistency: DataInconsistency): void { - if (!inconsistency.operatorId) { - return; - } - - // Clear any existing highlights first - const currentHighlights = this.workflowActionService.getJointGraphWrapper().getCurrentHighlights(); - this.workflowActionService.getJointGraphWrapper().unhighlightElements(currentHighlights); - - // Find all upstream operators and links leading to this operator - const pathResult = this.workflowActionService.findUpstreamPath(inconsistency.operatorId); - - if (pathResult.operators.length > 0 || pathResult.links.length > 0) { - // Highlight operators and links on the upstream path - this.workflowActionService.highlightOperators(false, ...pathResult.operators); - this.workflowActionService.highlightLinks(false, ...pathResult.links); - } - } -} diff --git a/frontend/src/app/workspace/component/left-panel/left-panel.component.ts b/frontend/src/app/workspace/component/left-panel/left-panel.component.ts index a5feef2542a..8dd9a5748b8 100644 --- a/frontend/src/app/workspace/component/left-panel/left-panel.component.ts +++ b/frontend/src/app/workspace/component/left-panel/left-panel.component.ts @@ -25,7 +25,6 @@ import { OperatorMenuComponent } from "./operator-menu/operator-menu.component"; import { VersionsListComponent } from "./versions-list/versions-list.component"; import { WorkflowExecutionHistoryComponent } from "../../../dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component"; import { TimeTravelComponent } from "./time-travel/time-travel.component"; -import { InconsistencyListComponent } from "./inconsistency-list/inconsistency-list.component"; import { SettingsComponent } from "./settings/settings.component"; import { calculateTotalTranslate3d } from "../../../common/util/panel-dock"; import { PanelService } from "../../service/panel/panel.service"; @@ -70,12 +69,6 @@ export class LeftPanelComponent implements OnDestroy, OnInit, AfterViewInit { icon: "clock-circle", enabled: false, }, - { - component: InconsistencyListComponent, - title: "Data Inconsistencies", - icon: "warning", - enabled: true, - }, ]; order = Array.from({ length: this.items.length - 1 }, (_, index) => index + 1); From 1358744100be73e2b3ff3da72303233e0994a1f3 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sun, 2 Nov 2025 22:10:11 -0800 Subject: [PATCH 081/158] remove inconsistencies --- .../copilot/texera-copilot-manager.service.ts | 4 +- .../service/copilot/texera-copilot.ts | 20 --- .../service/copilot/workflow-tools.ts | 161 ------------------ .../data-inconsistency.service.ts | 157 ----------------- 4 files changed, 2 insertions(+), 340 deletions(-) delete mode 100644 frontend/src/app/workspace/service/data-inconsistency/data-inconsistency.service.ts diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index 212ef2163dc..a07386794d6 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -28,8 +28,8 @@ import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.ser import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; import { ValidationWorkflowService } from "../validation/validation-workflow.service"; -import { DataInconsistencyService } from "../data-inconsistency/data-inconsistency.service"; import { ActionPlanService } from "../action-plan/action-plan.service"; +import { NotificationService } from "../../../common/service/notification/notification.service"; /** * Agent info for tracking created agents @@ -297,8 +297,8 @@ export class TexeraCopilotManagerService { WorkflowResultService, WorkflowCompilingService, ValidationWorkflowService, - DataInconsistencyService, ActionPlanService, + NotificationService, ], }, ], diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index fda4b9f5fc4..3084fab5a27 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -48,11 +48,6 @@ import { createGetOperatorResultInfoTool, createGetValidationInfoOfCurrentWorkflowTool, createValidateOperatorTool, - createAddInconsistencyTool, - createListInconsistenciesTool, - createUpdateInconsistencyTool, - createDeleteInconsistencyTool, - createClearInconsistenciesTool, toolWithTimeout, createListAllOperatorTypesTool, createListLinksTool, @@ -69,7 +64,6 @@ import { WorkflowResultService } from "../workflow-result/workflow-result.servic import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; import { ValidationWorkflowService } from "../validation/validation-workflow.service"; import { COPILOT_SYSTEM_PROMPT, PLANNING_MODE_PROMPT } from "./copilot-prompts"; -import { DataInconsistencyService } from "../data-inconsistency/data-inconsistency.service"; import { ActionPlanService } from "../action-plan/action-plan.service"; import { NotificationService } from "../../../common/service/notification/notification.service"; @@ -147,7 +141,6 @@ export class TexeraCopilot { private workflowResultService: WorkflowResultService, private workflowCompilingService: WorkflowCompilingService, private validationWorkflowService: ValidationWorkflowService, - private dataInconsistencyService: DataInconsistencyService, private actionPlanService: ActionPlanService, private notificationService: NotificationService ) { @@ -373,13 +366,6 @@ export class TexeraCopilot { ); const validateOperatorTool = toolWithTimeout(createValidateOperatorTool(this.validationWorkflowService)); - // Inconsistency tools - const addInconsistencyTool = toolWithTimeout(createAddInconsistencyTool(this.dataInconsistencyService)); - const listInconsistenciesTool = toolWithTimeout(createListInconsistenciesTool(this.dataInconsistencyService)); - const updateInconsistencyTool = toolWithTimeout(createUpdateInconsistencyTool(this.dataInconsistencyService)); - const deleteInconsistencyTool = toolWithTimeout(createDeleteInconsistencyTool(this.dataInconsistencyService)); - const clearInconsistenciesTool = toolWithTimeout(createClearInconsistenciesTool(this.dataInconsistencyService)); - // Base tools available in both modes const baseTools: Record = { // workflow editing @@ -409,12 +395,6 @@ export class TexeraCopilot { hasOperatorResult: hasOperatorResultTool, getOperatorResult: getOperatorResultTool, getOperatorResultInfo: getOperatorResultInfoTool, - // Data inconsistency tools - addInconsistency: addInconsistencyTool, - listInconsistencies: listInconsistenciesTool, - updateInconsistency: updateInconsistencyTool, - deleteInconsistency: deleteInconsistencyTool, - clearInconsistencies: clearInconsistenciesTool, }; // Conditionally add action plan tools based on planning mode diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index 3b0149c13d0..f107d4cebfc 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -23,12 +23,10 @@ import { WorkflowActionService } from "../workflow-graph/model/workflow-action.s import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { OperatorLink } from "../../types/workflow-common.interface"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; -import { DynamicSchemaService } from "../dynamic-schema/dynamic-schema.service"; import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; import { ValidationWorkflowService } from "../validation/validation-workflow.service"; -import { DataInconsistencyService } from "../data-inconsistency/data-inconsistency.service"; import { ActionPlanService } from "../action-plan/action-plan.service"; // Tool execution timeout in milliseconds (2 minutes) @@ -1500,162 +1498,3 @@ export function createValidateOperatorTool(validationWorkflowService: Validation }, }); } - -/** - * Tool to add a data inconsistency to the list - */ -export function createAddInconsistencyTool(service: DataInconsistencyService) { - return tool({ - name: "addInconsistency", - description: - "Add a data inconsistency finding to the inconsistency list. Use this when you find data errors or anomalies in the workflow results.", - inputSchema: z.object({ - name: z.string().describe("Short name for the inconsistency (e.g., 'Negative Prices', 'Missing Values')"), - description: z.string().describe("Detailed description of the inconsistency found"), - operatorId: z.string().describe("ID of the operator that revealed this inconsistency"), - }), - execute: async (args: { name: string; description: string; operatorId: string }) => { - try { - const inconsistency = service.addInconsistency(args.name, args.description, args.operatorId); - return { - success: true, - message: `Added inconsistency: ${args.name}`, - inconsistency, - }; - } catch (error: any) { - return { - success: false, - error: error.message || String(error), - }; - } - }, - }); -} - -/** - * Tool to list all data inconsistencies - */ -export function createListInconsistenciesTool(service: DataInconsistencyService) { - return tool({ - name: "listInconsistencies", - description: "Get all data inconsistencies found so far", - inputSchema: z.object({}), - execute: async (args: {}) => { - try { - const inconsistencies = service.getAllInconsistencies(); - return { - success: true, - count: inconsistencies.length, - inconsistencies, - }; - } catch (error: any) { - return { - success: false, - error: error.message || String(error), - }; - } - }, - }); -} - -/** - * Tool to update an existing data inconsistency - */ -export function createUpdateInconsistencyTool(service: DataInconsistencyService) { - return tool({ - name: "updateInconsistency", - description: "Update an existing data inconsistency", - inputSchema: z.object({ - id: z.string().describe("ID of the inconsistency to update"), - name: z.string().optional().describe("New name for the inconsistency"), - description: z.string().optional().describe("New description"), - operatorId: z.string().optional().describe("New operator ID"), - }), - execute: async (args: { id: string; name?: string; description?: string; operatorId?: string }) => { - try { - const updates: any = {}; - if (args.name !== undefined) updates.name = args.name; - if (args.description !== undefined) updates.description = args.description; - if (args.operatorId !== undefined) updates.operatorId = args.operatorId; - - const updated = service.updateInconsistency(args.id, updates); - if (!updated) { - return { - success: false, - error: `Inconsistency not found: ${args.id}`, - }; - } - - return { - success: true, - message: `Updated inconsistency: ${args.id}`, - inconsistency: updated, - }; - } catch (error: any) { - return { - success: false, - error: error.message || String(error), - }; - } - }, - }); -} - -/** - * Tool to delete a data inconsistency - */ -export function createDeleteInconsistencyTool(service: DataInconsistencyService) { - return tool({ - name: "deleteInconsistency", - description: "Delete a data inconsistency from the list", - inputSchema: z.object({ - id: z.string().describe("ID of the inconsistency to delete"), - }), - execute: async (args: { id: string }) => { - try { - const deleted = service.deleteInconsistency(args.id); - if (!deleted) { - return { - success: false, - error: `Inconsistency not found: ${args.id}`, - }; - } - - return { - success: true, - message: `Deleted inconsistency: ${args.id}`, - }; - } catch (error: any) { - return { - success: false, - error: error.message || String(error), - }; - } - }, - }); -} - -/** - * Tool to clear all data inconsistencies - */ -export function createClearInconsistenciesTool(service: DataInconsistencyService) { - return tool({ - name: "clearInconsistencies", - description: "Clear all data inconsistencies from the list", - inputSchema: z.object({}), - execute: async (args: {}) => { - try { - service.clearAll(); - return { - success: true, - message: "Cleared all inconsistencies", - }; - } catch (error: any) { - return { - success: false, - error: error.message || String(error), - }; - } - }, - }); -} diff --git a/frontend/src/app/workspace/service/data-inconsistency/data-inconsistency.service.ts b/frontend/src/app/workspace/service/data-inconsistency/data-inconsistency.service.ts deleted file mode 100644 index d5cb1b6defc..00000000000 --- a/frontend/src/app/workspace/service/data-inconsistency/data-inconsistency.service.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * 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 { BehaviorSubject, Observable } from "rxjs"; - -/** - * Interface for a data inconsistency item - */ -export interface DataInconsistency { - id: string; - name: string; - description: string; - operatorId: string; -} - -/** - * Service to manage data inconsistencies found in workflows - * Singleton service that maintains an in-memory list of inconsistencies - */ -@Injectable({ - providedIn: "root", -}) -export class DataInconsistencyService { - private inconsistencies: Map = new Map(); - private inconsistenciesSubject = new BehaviorSubject([]); - - constructor() {} - - /** - * Get all inconsistencies as an observable - */ - public getInconsistencies(): Observable { - return this.inconsistenciesSubject.asObservable(); - } - - /** - * Get all inconsistencies as an array - */ - public getAllInconsistencies(): DataInconsistency[] { - return Array.from(this.inconsistencies.values()); - } - - /** - * Get a specific inconsistency by ID - */ - public getInconsistency(id: string): DataInconsistency | undefined { - return this.inconsistencies.get(id); - } - - /** - * Add a new inconsistency - * Returns the created inconsistency - */ - public addInconsistency(name: string, description: string, operatorId: string): DataInconsistency { - const id = this.generateId(); - const inconsistency: DataInconsistency = { - id, - name, - description, - operatorId, - }; - - this.inconsistencies.set(id, inconsistency); - this.emitUpdate(); - - console.log(`Added inconsistency: ${name} (ID: ${id})`); - return inconsistency; - } - - /** - * Update an existing inconsistency - * Returns the updated inconsistency or undefined if not found - */ - public updateInconsistency( - id: string, - updates: Partial> - ): DataInconsistency | undefined { - const existing = this.inconsistencies.get(id); - if (!existing) { - console.warn(`Inconsistency not found: ${id}`); - return undefined; - } - - const updated: DataInconsistency = { - ...existing, - ...updates, - }; - - this.inconsistencies.set(id, updated); - this.emitUpdate(); - - console.log(`Updated inconsistency: ${id}`); - return updated; - } - - /** - * Delete an inconsistency by ID - * Returns true if deleted, false if not found - */ - public deleteInconsistency(id: string): boolean { - const existed = this.inconsistencies.delete(id); - if (existed) { - this.emitUpdate(); - console.log(`Deleted inconsistency: ${id}`); - } else { - console.warn(`Inconsistency not found: ${id}`); - } - return existed; - } - - /** - * Clear all inconsistencies - */ - public clearAll(): void { - this.inconsistencies.clear(); - this.emitUpdate(); - console.log("Cleared all inconsistencies"); - } - - /** - * Get inconsistencies for a specific operator - */ - public getInconsistenciesForOperator(operatorId: string): DataInconsistency[] { - return Array.from(this.inconsistencies.values()).filter(inc => inc.operatorId === operatorId); - } - - /** - * Generate a unique ID for an inconsistency - */ - private generateId(): string { - return `inc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - } - - /** - * Emit updated list to subscribers - */ - private emitUpdate(): void { - this.inconsistenciesSubject.next(this.getAllInconsistencies()); - } -} From 859c2739911441467eb3aea5d3b4f99a5ada29fc Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sun, 2 Nov 2025 23:43:45 -0800 Subject: [PATCH 082/158] add litellm configs --- bin/k8s/templates/litellm-config.yaml | 35 +++++++++++ bin/k8s/templates/litellm-deployment.yaml | 72 +++++++++++++++++++++++ bin/k8s/templates/litellm-secret.yaml | 27 +++++++++ bin/k8s/templates/litellm-service.yaml | 33 +++++++++++ bin/k8s/values.yaml | 27 +++++++++ 5 files changed, 194 insertions(+) create mode 100644 bin/k8s/templates/litellm-config.yaml create mode 100644 bin/k8s/templates/litellm-deployment.yaml create mode 100644 bin/k8s/templates/litellm-secret.yaml create mode 100644 bin/k8s/templates/litellm-service.yaml diff --git a/bin/k8s/templates/litellm-config.yaml b/bin/k8s/templates/litellm-config.yaml new file mode 100644 index 00000000000..1ac7075ce3e --- /dev/null +++ b/bin/k8s/templates/litellm-config.yaml @@ -0,0 +1,35 @@ +# 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. + +{{- if .Values.litellm.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: litellm-config + namespace: {{ .Release.Namespace }} +data: + config.yaml: | + model_list: + - model_name: claude-3.7 + litellm_params: + model: claude-3-7-sonnet-20250219 + api_key: "os.environ/ANTHROPIC_API_KEY" + - model_name: claude-sonnet-4-5 + litellm_params: + model: claude-sonnet-4-5-20250929 + api_key: "os.environ/ANTHROPIC_API_KEY" +{{- end }} diff --git a/bin/k8s/templates/litellm-deployment.yaml b/bin/k8s/templates/litellm-deployment.yaml new file mode 100644 index 00000000000..df51f0adc79 --- /dev/null +++ b/bin/k8s/templates/litellm-deployment.yaml @@ -0,0 +1,72 @@ +# 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. + +{{- if .Values.litellm.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.litellm.name }} + namespace: {{ .Release.Namespace }} +spec: + replicas: {{ .Values.litellm.replicaCount }} + selector: + matchLabels: + app: {{ .Values.litellm.name }} + template: + metadata: + labels: + app: {{ .Values.litellm.name }} + spec: + containers: + - name: litellm + image: {{ .Values.litellm.image.repository }}:{{ .Values.litellm.image.tag }} + imagePullPolicy: {{ .Values.litellm.image.pullPolicy }} + ports: + - containerPort: {{ .Values.litellm.service.port }} + name: http + envFrom: + - secretRef: + name: litellm-secret + command: + - litellm + - --config + - /etc/litellm/config.yaml + - --port + - "{{ .Values.litellm.service.port }}" + volumeMounts: + - name: config + mountPath: /etc/litellm + readOnly: true + resources: + {{- toYaml .Values.litellm.resources | nindent 12 }} + livenessProbe: + httpGet: + path: /health + port: {{ .Values.litellm.service.port }} + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: {{ .Values.litellm.service.port }} + initialDelaySeconds: 10 + periodSeconds: 5 + volumes: + - name: config + configMap: + name: litellm-config +{{- end }} diff --git a/bin/k8s/templates/litellm-secret.yaml b/bin/k8s/templates/litellm-secret.yaml new file mode 100644 index 00000000000..5f3ee863f1f --- /dev/null +++ b/bin/k8s/templates/litellm-secret.yaml @@ -0,0 +1,27 @@ +# 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. + +{{- if .Values.litellm.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: litellm-secret + namespace: {{ .Release.Namespace }} +type: Opaque +data: + ANTHROPIC_API_KEY: {{ .Values.litellm.apiKeys.anthropic | b64enc | quote }} +{{- end }} diff --git a/bin/k8s/templates/litellm-service.yaml b/bin/k8s/templates/litellm-service.yaml new file mode 100644 index 00000000000..982ffabbec9 --- /dev/null +++ b/bin/k8s/templates/litellm-service.yaml @@ -0,0 +1,33 @@ +# 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. + +{{- if .Values.litellm.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.litellm.name }}-svc + namespace: {{ .Release.Namespace }} +spec: + type: {{ .Values.litellm.service.type }} + selector: + app: {{ .Values.litellm.name }} + ports: + - protocol: TCP + port: {{ .Values.litellm.service.port }} + targetPort: {{ .Values.litellm.service.port }} + name: http +{{- end }} diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml index 95810913eb4..0b3775f8f3b 100644 --- a/bin/k8s/values.yaml +++ b/bin/k8s/values.yaml @@ -293,6 +293,30 @@ pythonLanguageServer: cpu: "100m" memory: "100Mi" +# LiteLLM Proxy configuration +litellm: + enabled: true + name: litellm + replicaCount: 1 + image: + repository: ghcr.io/berriai/litellm + tag: main-v1.30.3 + pullPolicy: IfNotPresent + service: + type: ClusterIP + port: 4000 + resources: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "200m" + memory: "256Mi" + apiKeys: + # Set your Anthropic API key here + # IMPORTANT: In production, use external secrets management (e.g., sealed-secrets, external-secrets) + anthropic: "" # Replace with your actual API key or use external secret + # Metrics Server configuration metrics-server: enabled: true # set to false if metrics-server is already installed @@ -338,6 +362,9 @@ ingressPaths: - path: /api/config serviceName: config-service-svc servicePort: 9094 + - path: /api/chat + serviceName: litellm-svc + servicePort: 4000 - path: /wsapi/workflow-websocket serviceName: envoy-svc servicePort: 10000 From 2bf76fca55b56ca79eea993f6e7696c88631ac1c Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Mon, 3 Nov 2025 15:05:34 -0800 Subject: [PATCH 083/158] add the planning mode getter --- .../service/copilot/texera-copilot-manager.service.ts | 11 +++++++++++ .../app/workspace/service/copilot/texera-copilot.ts | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index a07386794d6..d96cab92ecc 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -259,6 +259,17 @@ export class TexeraCopilotManagerService { agent.instance.setPlanningMode(planningMode); } + /** + * Get planning mode for a specific agent + */ + public getPlanningMode(agentId: string): boolean { + const agent = this.agents.get(agentId); + if (!agent) { + throw new Error(`Agent with ID ${agentId} not found`); + } + return agent.instance.getPlanningMode(); + } + /** * Get system information (prompt and available tools) for a specific agent */ diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 3084fab5a27..5c42f2123e9 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -171,6 +171,13 @@ export class TexeraCopilot { console.log(`[${this.agentId}] Planning mode set to: ${planningMode}`); } + /** + * Get the current planning mode + */ + public getPlanningMode(): boolean { + return this.planningMode; + } + /** * Initialize the copilot with MCP and AI model */ From 658e6c402b809cb5c987ab32fba57a937b0feb0c Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Mon, 3 Nov 2025 16:11:15 -0800 Subject: [PATCH 084/158] add output schema tool --- .../workflow-compiling.service.ts | 11 ++++++ .../service/copilot/texera-copilot.ts | 5 +++ .../service/copilot/workflow-tools.ts | 35 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/frontend/src/app/workspace/service/compile-workflow/workflow-compiling.service.ts b/frontend/src/app/workspace/service/compile-workflow/workflow-compiling.service.ts index 1b45f755994..9648d2b305f 100644 --- a/frontend/src/app/workspace/service/compile-workflow/workflow-compiling.service.ts +++ b/frontend/src/app/workspace/service/compile-workflow/workflow-compiling.service.ts @@ -148,6 +148,17 @@ export class WorkflowCompilingService { ); } + public getOperatorOutputSchemaMap(operatorID: string): OperatorPortSchemaMap | undefined { + if ( + this.currentCompilationStateInfo.state == CompilationState.Uninitialized || + !this.currentCompilationStateInfo.operatorOutputPortSchemaMap + ) { + return undefined; + } + + return this.currentCompilationStateInfo.operatorOutputPortSchemaMap[operatorID]; + } + public getPortInputSchema(operatorID: string, portIndex: number): PortSchema | undefined { return this.getOperatorInputSchemaMap(operatorID)?.[serializePortIdentity({ id: portIndex, internal: false })]; } diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 5c42f2123e9..7184e641775 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -39,6 +39,7 @@ import { createGetOperatorPortsInfoTool, createGetOperatorMetadataTool, createGetOperatorInputSchemaTool, + createGetOperatorOutputSchemaTool, createGetWorkflowCompilationStateTool, createExecuteWorkflowTool, createGetExecutionStateTool, @@ -355,6 +356,9 @@ export class TexeraCopilot { createGetOperatorMetadataTool(this.workflowActionService, this.operatorMetadataService) ); const getOperatorInputSchemaTool = toolWithTimeout(createGetOperatorInputSchemaTool(this.workflowCompilingService)); + const getOperatorOutputSchemaTool = toolWithTimeout( + createGetOperatorOutputSchemaTool(this.workflowCompilingService) + ); const getWorkflowCompilationStateTool = toolWithTimeout( createGetWorkflowCompilationStateTool(this.workflowCompilingService) ); @@ -394,6 +398,7 @@ export class TexeraCopilot { getOperatorPortsInfo: getOperatorPortsInfoTool, getOperatorMetadata: getOperatorMetadataTool, getOperatorInputSchema: getOperatorInputSchemaTool, + getOperatorOutputSchema: getOperatorOutputSchemaTool, getWorkflowCompilationState: getWorkflowCompilationStateTool, // workflow execution executeWorkflow: executeWorkflowTool, diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index f107d4cebfc..72f862472f2 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -1094,6 +1094,41 @@ export function createGetOperatorInputSchemaTool(workflowCompilingService: Workf }); } +/** + * Create getOperatorOutputSchema tool for getting operator's output schema from compilation + */ +export function createGetOperatorOutputSchemaTool(workflowCompilingService: WorkflowCompilingService) { + return tool({ + name: "getOperatorOutputSchema", + description: + "Get the output schema for an operator, which shows what columns/attributes this operator produces. This is determined by workflow compilation and shows the schema that will be available to downstream operators.", + inputSchema: z.object({ + operatorId: z.string().describe("ID of the operator to get output schema for"), + }), + execute: async (args: { operatorId: string }) => { + try { + const outputSchemaMap = workflowCompilingService.getOperatorOutputSchemaMap(args.operatorId); + + if (!outputSchemaMap) { + return { + success: true, + outputSchema: null, + message: `Operator ${args.operatorId} has no output schema (workflow may not be compiled yet or operator has errors)`, + }; + } + + return { + success: true, + outputSchema: outputSchemaMap, + message: `Retrieved output schema for operator ${args.operatorId}`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} + /** * Create getWorkflowCompilationState tool for checking compilation status and errors */ From 1999a70cf6ba38bfd3206d7d436ed23ad3146ecc Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Mon, 3 Nov 2025 16:51:00 -0800 Subject: [PATCH 085/158] revert redundant changes --- build.sbt | 5 -- common/config/src/main/resources/mcp.conf | 79 ------------------- .../org/apache/texera/config/McpConfig.scala | 59 -------------- common/workflow-core/build.sbt | 1 + .../org/apache/amber/core/tuple/Tuple.scala | 18 +---- 5 files changed, 3 insertions(+), 159 deletions(-) delete mode 100644 common/config/src/main/resources/mcp.conf delete mode 100644 common/config/src/main/scala/org/apache/texera/config/McpConfig.scala diff --git a/build.sbt b/build.sbt index 5515d8f0020..467febba5c6 100644 --- a/build.sbt +++ b/build.sbt @@ -88,11 +88,6 @@ lazy val WorkflowExecutionService = (project in file("amber")) ), libraryDependencies ++= Seq( "com.squareup.okhttp3" % "okhttp" % "4.10.0" force () // Force usage of OkHttp 4.10.0 - ), - // Java 17+ compatibility: Apache Arrow requires reflective access to java.nio internals - // See: https://arrow.apache.org/docs/java/install.html - Universal / javaOptions ++= Seq( - "--add-opens=java.base/java.nio=ALL-UNNAMED" ) ) .configs(Test) diff --git a/common/config/src/main/resources/mcp.conf b/common/config/src/main/resources/mcp.conf deleted file mode 100644 index ce78ab341a3..00000000000 --- a/common/config/src/main/resources/mcp.conf +++ /dev/null @@ -1,79 +0,0 @@ -# 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. - -# MCP (Model Context Protocol) Service Configuration -mcp { - # Server settings - server { - port = 9098 - port = ${?MCP_PORT} - name = "texera-mcp" - version = "1.0.0" - } - - # Transport protocol: stdio, http, or websocket - transport = "http" - transport = ${?MCP_TRANSPORT} - - # Authentication and database - auth { - enabled = false - enabled = ${?MCP_AUTH_ENABLED} - } - - database { - enabled = true - enabled = ${?MCP_DATABASE_ENABLED} - } - - # MCP Capabilities (following MCP specification) - capabilities { - tools = true - tools = ${?MCP_TOOLS_ENABLED} - - resources = true - resources = ${?MCP_RESOURCES_ENABLED} - - prompts = true - prompts = ${?MCP_PROMPTS_ENABLED} - - sampling = false - sampling = ${?MCP_SAMPLING_ENABLED} - - logging = true - logging = ${?MCP_LOGGING_ENABLED} - } - - # Performance settings - performance { - max-concurrent-requests = 100 - max-concurrent-requests = ${?MCP_MAX_CONCURRENT_REQUESTS} - - request-timeout-ms = 30000 - request-timeout-ms = ${?MCP_REQUEST_TIMEOUT_MS} - } - - # Enabled feature groups - enabled-tools = ["operators", "workflows", "datasets", "projects", "executions"] - enabled-tools = ${?MCP_ENABLED_TOOLS} - - enabled-resources = ["operator-schemas", "workflow-templates", "dataset-schemas"] - enabled-resources = ${?MCP_ENABLED_RESOURCES} - - enabled-prompts = ["create-workflow", "optimize-workflow", "explain-operator"] - enabled-prompts = ${?MCP_ENABLED_PROMPTS} -} diff --git a/common/config/src/main/scala/org/apache/texera/config/McpConfig.scala b/common/config/src/main/scala/org/apache/texera/config/McpConfig.scala deleted file mode 100644 index 75d13f0bfce..00000000000 --- a/common/config/src/main/scala/org/apache/texera/config/McpConfig.scala +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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. - */ - -package org.apache.texera.config - -import com.typesafe.config.{Config, ConfigFactory} -import scala.jdk.CollectionConverters._ - -/** - * Configuration for the MCP (Model Context Protocol) Service. - * Settings are loaded from mcp.conf. - */ -object McpConfig { - - private val conf: Config = ConfigFactory.parseResources("mcp.conf").resolve() - - // Server settings - val serverPort: Int = conf.getInt("mcp.server.port") - val serverName: String = conf.getString("mcp.server.name") - val serverVersion: String = conf.getString("mcp.server.version") - - // Transport protocol - val transport: String = conf.getString("mcp.transport") - - // Authentication and database - val authEnabled: Boolean = conf.getBoolean("mcp.auth.enabled") - val databaseEnabled: Boolean = conf.getBoolean("mcp.database.enabled") - - // MCP Capabilities - val toolsEnabled: Boolean = conf.getBoolean("mcp.capabilities.tools") - val resourcesEnabled: Boolean = conf.getBoolean("mcp.capabilities.resources") - val promptsEnabled: Boolean = conf.getBoolean("mcp.capabilities.prompts") - val samplingEnabled: Boolean = conf.getBoolean("mcp.capabilities.sampling") - val loggingEnabled: Boolean = conf.getBoolean("mcp.capabilities.logging") - - // Performance settings - val maxConcurrentRequests: Int = conf.getInt("mcp.performance.max-concurrent-requests") - val requestTimeoutMs: Long = conf.getLong("mcp.performance.request-timeout-ms") - - // Enabled features - val enabledTools: List[String] = conf.getStringList("mcp.enabled-tools").asScala.toList - val enabledResources: List[String] = conf.getStringList("mcp.enabled-resources").asScala.toList - val enabledPrompts: List[String] = conf.getStringList("mcp.enabled-prompts").asScala.toList -} diff --git a/common/workflow-core/build.sbt b/common/workflow-core/build.sbt index 0cb1630ce18..82a79e8e04b 100644 --- a/common/workflow-core/build.sbt +++ b/common/workflow-core/build.sbt @@ -176,6 +176,7 @@ libraryDependencies ++= Seq( libraryDependencies ++= Seq( "com.github.sisyphsu" % "dateparser" % "1.0.11", // DateParser "com.google.guava" % "guava" % "31.1-jre", // Guava + "org.ehcache" % "sizeof" % "0.4.3", // Ehcache SizeOf "org.jgrapht" % "jgrapht-core" % "1.4.0", // JGraphT Core "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", // Scala Logging "org.eclipse.jgit" % "org.eclipse.jgit" % "5.13.0.202109080827-r", // jgit diff --git a/common/workflow-core/src/main/scala/org/apache/amber/core/tuple/Tuple.scala b/common/workflow-core/src/main/scala/org/apache/amber/core/tuple/Tuple.scala index e9960fbd668..7bee0a0fc53 100644 --- a/common/workflow-core/src/main/scala/org/apache/amber/core/tuple/Tuple.scala +++ b/common/workflow-core/src/main/scala/org/apache/amber/core/tuple/Tuple.scala @@ -22,6 +22,7 @@ package org.apache.amber.core.tuple import com.fasterxml.jackson.annotation.{JsonCreator, JsonIgnore, JsonProperty} import com.google.common.base.Preconditions.checkNotNull import org.apache.amber.core.tuple.Tuple.checkSchemaMatchesFields +import org.ehcache.sizeof.SizeOf import java.util import scala.collection.mutable @@ -51,22 +52,7 @@ case class Tuple @JsonCreator() ( checkNotNull(fieldVals) checkSchemaMatchesFields(schema.getAttributes, fieldVals) - // Fast approximation of in-memory size for statistics tracking - // Avoids expensive reflection and Java 17 module system issues - override val inMemSize: Long = { - val fieldSize = fieldVals.map { - case s: String => 40 + (s.length * 2) // String overhead + UTF-16 chars - case _: Int | _: Float => 4 - case _: Long | _: Double => 8 - case _: Boolean | _: Byte => 1 - case _: Short => 2 - case arr: Array[Byte] => 24 + arr.length // Array overhead + data - case arr: Array[_] => 24 + (arr.length * 8) // Array overhead + references - case null => 4 // null reference - case _ => 16 // Generic object reference estimate - }.sum - fieldSize + 64 // Tuple object overhead (object header + schema reference + array reference) - } + override val inMemSize: Long = SizeOf.newInstance().deepSizeOf(this) @JsonIgnore def length: Int = fieldVals.length From d1c2bb8d4198d0d7f439a089e0ba6d26e89b1698 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Mon, 3 Nov 2025 21:04:08 -0800 Subject: [PATCH 086/158] code cleanup --- .../action-plan-view.component.html | 6 +- .../action-plan-view.component.scss | 5 +- .../action-plan-view.component.ts | 46 ++---- .../agent-chat/agent-chat.component.ts | 133 +++++------------- .../agent-registration.component.ts | 23 ++- .../copilot/texera-copilot-manager.service.ts | 84 +---------- .../service/copilot/texera-copilot.ts | 104 ++------------ 7 files changed, 76 insertions(+), 325 deletions(-) diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html index ceed1073648..6ecbdd7df11 100644 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html @@ -37,11 +37,11 @@

    {{ actionPlan.summary }}

    nzTheme="outline">
    {{ actionPlan.createdAt | date : "short" }} + + {{ getStatusLabel() }} +
  • -
    - {{ getStatusLabel() }} -
    diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss index 158e5b2a2e1..8ed50cc1867 100644 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss @@ -44,6 +44,7 @@ gap: 16px; font-size: 12px; color: #8c8c8c; + align-items: center; span { display: flex; @@ -56,10 +57,6 @@ } } } - - .plan-status { - flex-shrink: 0; - } } .progress-section { diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts index e0d383a93c5..1899fb6fab8 100644 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts @@ -29,9 +29,9 @@ import * as joint from "jointjs"; templateUrl: "./action-plan-view.component.html", styleUrls: ["./action-plan-view.component.scss"], }) -export class ActionPlanViewComponent implements OnInit, OnDestroy { +export class ActionPlanViewComponent implements OnInit { @Input() actionPlan!: ActionPlan; - @Input() showFeedbackControls: boolean = false; // Show accept/reject buttons + @Input() showFeedbackControls: boolean = false; @Output() userDecision = new EventEmitter<{ accepted: boolean; message: string; @@ -40,17 +40,14 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { }>(); public rejectMessage: string = ""; - public runInNewAgent: boolean = false; // Toggle for running in new agent - public ActionPlanStatus = ActionPlanStatus; // Expose enum to template - - // Track task completion states + public runInNewAgent: boolean = false; + public ActionPlanStatus = ActionPlanStatus; public taskCompletionStates: { [operatorId: string]: boolean } = {}; constructor(private workflowActionService: WorkflowActionService) {} ngOnInit(): void { if (!this.actionPlan) { - console.error("ActionPlan is not provided!"); return; } @@ -63,33 +60,28 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { }); } - ngOnDestroy(): void { - // Cleanup handled by UntilDestroy decorator - } - /** - * User accepted the action plan + * Handle user acceptance of the action plan. */ public onAccept(): void { - // Emit user decision with information about whether to create a new actor + const userFeedback = this.rejectMessage.trim(); this.userDecision.emit({ accepted: true, - message: `✅ Accepted action plan: "${this.actionPlan.summary}"${this.runInNewAgent ? " (will run in new agent)" : ""}`, + message: `Action plan ${this.actionPlan.id} accepted. Feedback: ${userFeedback}`, createNewActor: this.runInNewAgent, planId: this.actionPlan.id, }); } /** - * User rejected the action plan with optional feedback + * Handle user rejection with optional feedback message. */ public onReject(): void { - const userFeedback = this.rejectMessage.trim() || "I don't want this action plan."; + const userFeedback = this.rejectMessage.trim(); - // Emit user decision event for chat component to handle this.userDecision.emit({ accepted: false, - message: `❌ Rejected action plan: "${this.actionPlan.summary}". Feedback: ${userFeedback}`, + message: `Action plan ${this.actionPlan.id} rejected. Feedback: ${userFeedback}`, planId: this.actionPlan.id, }); @@ -97,16 +89,14 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { } /** - * Highlight an operator when clicking on its task + * Highlight an operator on the workflow canvas when its task is clicked. */ public highlightOperator(operatorId: string): void { - // Get the operator from workflow const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorId); if (!operator) { return; } - // Get the joint graph wrapper to access the paper const jointGraphWrapper = this.workflowActionService.getJointGraphWrapper(); if (!jointGraphWrapper) { return; @@ -116,16 +106,11 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { const operatorElement = paper.getModelById(operatorId); if (operatorElement) { - // Create a temporary highlight using Joint.js highlight API const operatorView = paper.findViewByModel(operatorElement); if (operatorView) { - // Add light blue halo effect using joint.highlighters const highlighterNamespace = joint.highlighters; - // Remove any existing highlight with same name highlighterNamespace.mask.remove(operatorView, "action-plan-click"); - - // Add new highlight with light blue color highlighterNamespace.mask.add(operatorView, "body", "action-plan-click", { padding: 10, deep: true, @@ -138,7 +123,6 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { }, }); - // Remove the highlight after 2 seconds setTimeout(() => { highlighterNamespace.mask.remove(operatorView, "action-plan-click"); }, 2000); @@ -147,7 +131,7 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { } /** - * Get status label for display + * Get display label for current plan status. */ public getStatusLabel(): string { const status = this.actionPlan.status$.value; @@ -166,7 +150,7 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { } /** - * Get status color for display + * Get display color for current plan status. */ public getStatusColor(): string { const status = this.actionPlan.status$.value; @@ -185,14 +169,14 @@ export class ActionPlanViewComponent implements OnInit, OnDestroy { } /** - * Get tasks as array for template iteration + * Get tasks as array for template iteration. */ public get tasksArray(): ActionPlanTask[] { return Array.from(this.actionPlan.tasks.values()); } /** - * Get progress percentage + * Calculate completion percentage based on finished tasks. */ public getProgressPercentage(): number { if (this.actionPlan.tasks.size === 0) return 0; diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts index 60ea52d7a72..85518de7d04 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -1,10 +1,29 @@ -// agent-chat.component.ts +/** + * 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 { Component, ViewChild, ElementRef, Input, OnInit, AfterViewChecked } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { CopilotState, AgentUIMessage } from "../../../service/copilot/texera-copilot"; import { AgentInfo, TexeraCopilotManagerService } from "../../../service/copilot/texera-copilot-manager.service"; import { ActionPlan, ActionPlanService } from "../../../service/action-plan/action-plan.service"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; +import { NotificationService } from "../../../../common/service/notification/notification.service"; @UntilDestroy() @Component({ @@ -17,18 +36,14 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { @ViewChild("messageContainer", { static: false }) messageContainer?: ElementRef; @ViewChild("messageInput", { static: false }) messageInput?: ElementRef; - public agentResponses: AgentUIMessage[] = []; // Populated from observable subscription + public agentResponses: AgentUIMessage[] = []; public currentMessage = ""; public pendingActionPlan: ActionPlan | null = null; private shouldScrollToBottom = false; - public planningMode = false; // Toggle for planning mode - - // Modal state for response details + public planningMode = false; public isDetailsModalVisible = false; public selectedResponse: AgentUIMessage | null = null; public hoveredMessageIndex: number | null = null; - - // Modal state for system info public isSystemInfoModalVisible = false; public systemPrompt: string = ""; public availableTools: Array<{ name: string; description: string; inputSchema: any }> = []; @@ -36,24 +51,20 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { constructor( private actionPlanService: ActionPlanService, private copilotManagerService: TexeraCopilotManagerService, - private workflowActionService: WorkflowActionService + private workflowActionService: WorkflowActionService, + private notificationService: NotificationService ) {} ngOnInit(): void { - console.log("AgentChatComponent ngOnInit - agentInfo:", this.agentInfo); - if (!this.agentInfo) { - console.error("AgentInfo is not provided!"); return; } - // Subscribe to agent responses stream from the manager service + // Subscribe to agent responses this.copilotManagerService .getAgentResponsesObservable(this.agentInfo.id) .pipe(untilDestroyed(this)) .subscribe(responses => { - console.log(`AgentChatComponent for ${this.agentInfo.id} received ${responses.length} responses`); - console.log(responses); this.agentResponses = responses; this.shouldScrollToBottom = true; }); @@ -63,17 +74,13 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { .getPendingActionPlanStream() .pipe(untilDestroyed(this)) .subscribe(plan => { - // Only show plans from this agent if (plan && plan.agentId === this.agentInfo.id) { this.pendingActionPlan = plan; this.shouldScrollToBottom = true; } else if (plan === null || (plan && plan.agentId !== this.agentInfo.id)) { - // Clear pending plan if it's null or belongs to another agent this.pendingActionPlan = null; } }); - - console.log("AgentChatComponent initialized successfully"); } ngAfterViewChecked(): void { @@ -83,32 +90,20 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { } } - /** - * Set the hovered message index - */ public setHoveredMessage(index: number | null): void { this.hoveredMessageIndex = index; } - /** - * Show response details modal - */ public showResponseDetails(response: AgentUIMessage): void { this.selectedResponse = response; this.isDetailsModalVisible = true; } - /** - * Close response details modal - */ public closeDetailsModal(): void { this.isDetailsModalVisible = false; this.selectedResponse = null; } - /** - * Show system info modal with current prompt and tools - */ public showSystemInfo(): void { const systemInfo = this.copilotManagerService.getSystemInfo(this.agentInfo.id); this.systemPrompt = systemInfo.systemPrompt; @@ -116,37 +111,23 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { this.isSystemInfoModalVisible = true; } - /** - * Close system info modal - */ public closeSystemInfoModal(): void { this.isSystemInfoModalVisible = false; } - /** - * Format any data as JSON string - */ public formatJson(data: any): string { return JSON.stringify(data, null, 2); } - /** - * Get tool result for a specific tool call index - */ public getToolResult(response: AgentUIMessage, toolCallIndex: number): any { if (!response.toolResults || toolCallIndex >= response.toolResults.length) { return null; } const toolResult = response.toolResults[toolCallIndex]; - // Extract the output field if it exists, otherwise return the whole result return toolResult.output || toolResult.result || toolResult; } - /** - * Get input tokens from the latest agent response with usage data - */ public getTotalInputTokens(): number { - // Find the last response with usage data (from most recent to oldest) for (let i = this.agentResponses.length - 1; i >= 0; i--) { const response = this.agentResponses[i]; if (response.usage?.inputTokens !== undefined) { @@ -156,11 +137,7 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { return 0; } - /** - * Get output tokens from the latest agent response with usage data - */ public getTotalOutputTokens(): number { - // Find the last response with usage data (from most recent to oldest) for (let i = this.agentResponses.length - 1; i >= 0; i--) { const response = this.agentResponses[i]; if (response.usage?.outputTokens !== undefined) { @@ -171,8 +148,7 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { } /** - * Send a message to the agent - * Messages are automatically updated via the messages$ observable + * Send a message to the agent via the copilot manager service. */ public sendMessage(): void { if (!this.currentMessage.trim() || this.isGenerating()) { @@ -183,20 +159,16 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { this.currentMessage = ""; // Send to copilot via manager service - // Messages are automatically updated via the observable subscription this.copilotManagerService .sendMessage(this.agentInfo.id, userMessage) .pipe(untilDestroyed(this)) .subscribe({ error: (error: unknown) => { - console.error("Error sending message:", error); + this.notificationService.error(`Error sending message: ${error}`); }, }); } - /** - * Handle Enter key press in textarea - */ public onEnterPress(event: KeyboardEvent): void { if (!event.shiftKey) { event.preventDefault(); @@ -204,9 +176,6 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { } } - /** - * Scroll messages container to bottom - */ private scrollToBottom(): void { if (this.messageContainer) { const element = this.messageContainer.nativeElement; @@ -214,57 +183,36 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { } } - /** - * Stop the current generation - */ public stopGeneration(): void { this.copilotManagerService.stopGeneration(this.agentInfo.id); } - /** - * Clear message history - */ public clearMessages(): void { this.copilotManagerService.clearMessages(this.agentInfo.id); } - /** - * Check if copilot is currently generating - */ public isGenerating(): boolean { return this.copilotManagerService.getAgentState(this.agentInfo.id) === CopilotState.GENERATING; } - /** - * Check if copilot is currently stopping - */ public isStopping(): boolean { return this.copilotManagerService.getAgentState(this.agentInfo.id) === CopilotState.STOPPING; } - /** - * Check if copilot is available (can send messages) - */ public isAvailable(): boolean { return this.copilotManagerService.getAgentState(this.agentInfo.id) === CopilotState.AVAILABLE; } - /** - * Check if agent is connected - */ public isConnected(): boolean { return this.copilotManagerService.isAgentConnected(this.agentInfo.id); } - /** - * Handle planning mode change - */ public onPlanningModeChange(value: boolean): void { this.copilotManagerService.setPlanningMode(this.agentInfo.id, value); } /** - * Handle user decision on action plan + * Handle user decision on action plan (acceptance or rejection). */ public onUserDecision(decision: { accepted: boolean; @@ -272,74 +220,61 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { createNewActor?: boolean; planId?: string; }): void { - // Clear the pending action plan since user has made a decision this.pendingActionPlan = null; - // Handle plan acceptance or rejection if (decision.planId) { if (decision.accepted) { - // Register plan acceptance this.actionPlanService.acceptPlan(decision.planId); - // If user chose to run in new agent, create one (non-blocking) if (decision.createNewActor) { - // Create new actor agent this.copilotManagerService .createAgent("claude-3.7", `Actor for Plan ${decision.planId}`) .then(newAgent => { - // Send the initial message to the new agent (also non-blocking) const initialMessage = `Please work on action plan with id: ${decision.planId}`; this.copilotManagerService .sendMessage(newAgent.id, initialMessage) .pipe(untilDestroyed(this)) .subscribe({ next: () => { - console.log(`Actor agent started for plan: ${decision.planId}`); + this.notificationService.info(`Actor agent started for plan: ${decision.planId}`); }, error: (error: unknown) => { - console.error("Error starting actor agent:", error); + this.notificationService.error(`Error starting actor agent: ${error}`); }, }); }) - .catch(error => { - console.error("Failed to create actor agent:", error); + .catch((error: unknown) => { + this.notificationService.error(`Failed to create actor agent: ${error}`); }); } else { - // If NOT creating new actor, send feedback and trigger execution on current agent const executionMessage = "I have accepted your action plan. Please proceed with executing it."; this.copilotManagerService .sendMessage(this.agentInfo.id, executionMessage) .pipe(untilDestroyed(this)) .subscribe({ error: (error: unknown) => { - console.error("Error sending acceptance message:", error); + this.notificationService.error(`Error sending acceptance message: ${error}`); }, }); } } else { - // Extract feedback from rejection message const feedbackMatch = decision.message.match(/Feedback: (.+)$/); const userFeedback = feedbackMatch ? feedbackMatch[1] : "I don't want this action plan."; - // Get the action plan to find operators to delete const actionPlan = this.actionPlanService.getActionPlan(decision.planId); if (actionPlan) { - // Delete the created operators and links this.workflowActionService.deleteOperatorsAndLinks(actionPlan.operatorIds); - console.log(`Deleted ${actionPlan.operatorIds.length} operators from rejected action plan`); } - // Register plan rejection this.actionPlanService.rejectPlan(userFeedback, decision.planId); - // Send rejection feedback to planner agent as a new message const rejectionMessage = `I have rejected your action plan. Feedback: ${userFeedback}`; this.copilotManagerService .sendMessage(this.agentInfo.id, rejectionMessage) .pipe(untilDestroyed(this)) .subscribe({ error: (error: unknown) => { - console.error("Error sending rejection feedback:", error); + this.notificationService.error(`Error sending rejection feedback: ${error}`); }, }); } diff --git a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts index 5de21da611b..cfac364caa0 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts @@ -19,6 +19,7 @@ import { Component, EventEmitter, Output } from "@angular/core"; import { TexeraCopilotManagerService, ModelType } from "../../../service/copilot/texera-copilot-manager.service"; +import { NotificationService } from "../../../../common/service/notification/notification.service"; @Component({ selector: "texera-agent-registration", @@ -26,19 +27,19 @@ import { TexeraCopilotManagerService, ModelType } from "../../../service/copilot styleUrls: ["agent-registration.component.scss"], }) export class AgentRegistrationComponent { - @Output() agentCreated = new EventEmitter(); // Emit agent ID when created + @Output() agentCreated = new EventEmitter(); public modelTypes: ModelType[] = []; public selectedModelType: string | null = null; public customAgentName: string = ""; - constructor(private copilotManagerService: TexeraCopilotManagerService) { + constructor( + private copilotManagerService: TexeraCopilotManagerService, + private notificationService: NotificationService + ) { this.modelTypes = this.copilotManagerService.getModelTypes(); } - /** - * Select a model type - */ public selectModelType(modelTypeId: string): void { this.selectedModelType = modelTypeId; } @@ -46,7 +47,7 @@ export class AgentRegistrationComponent { public isCreating: boolean = false; /** - * Create a new agent with the selected model type + * Create a new agent with the selected model type. */ public async createAgent(): Promise { if (!this.selectedModelType || this.isCreating) { @@ -61,24 +62,16 @@ export class AgentRegistrationComponent { this.customAgentName || undefined ); - // Emit event with agent ID this.agentCreated.emit(agentInfo.id); - - // Reset selection this.selectedModelType = null; this.customAgentName = ""; } catch (error) { - console.error("Failed to create agent:", error); - // TODO: Show error notification - alert("Failed to create agent. Please check the console for details."); + this.notificationService.error(`Failed to create agent: ${error}`); } finally { this.isCreating = false; } } - /** - * Check if create button should be enabled - */ public canCreate(): boolean { return this.selectedModelType !== null && !this.isCreating; } diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index d96cab92ecc..b7bc6e92c00 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -32,7 +32,7 @@ import { ActionPlanService } from "../action-plan/action-plan.service"; import { NotificationService } from "../../../common/service/notification/notification.service"; /** - * Agent info for tracking created agents + * Agent information for tracking created agents. */ export interface AgentInfo { id: string; @@ -43,7 +43,7 @@ export interface AgentInfo { } /** - * Available model types for agent creation + * Available model types for agent creation. */ export interface ModelType { id: string; @@ -53,24 +53,18 @@ export interface ModelType { } /** - * Service to manage multiple copilot agents - * Supports multi-agent workflows and agent lifecycle management + * Service to manage multiple copilot agents. + * Supports multi-agent workflows and agent lifecycle management. */ @Injectable({ providedIn: "root", }) export class TexeraCopilotManagerService { - // Map from agent ID to agent info private agents = new Map(); - - // Counter for generating unique agent IDs private agentCounter = 0; - - // Stream for agent creation/deletion events private agentChangeSubject = new Subject(); public agentChange$ = this.agentChangeSubject.asObservable(); - // Available model types private modelTypes: ModelType[] = [ { id: "claude-3.7", @@ -89,20 +83,15 @@ export class TexeraCopilotManagerService { constructor(private injector: Injector) {} /** - * Create a new agent with the specified model type + * Create a new agent with the specified model type. */ public async createAgent(modelType: string, customName?: string): Promise { const agentId = `agent-${++this.agentCounter}`; const agentName = customName || `Agent ${this.agentCounter}`; try { - // Create new TexeraCopilot instance using Angular's Injector const agentInstance = this.createCopilotInstance(modelType); - - // Set agent information agentInstance.setAgentInfo(agentId, agentName); - - // Initialize the agent await agentInstance.initialize(); const agentInfo: AgentInfo = { @@ -116,62 +105,39 @@ export class TexeraCopilotManagerService { this.agents.set(agentId, agentInfo); this.agentChangeSubject.next(); - console.log(`Created agent: ${agentId} with model ${modelType}`); return agentInfo; } catch (error) { - console.error(`Failed to create agent with model ${modelType}:`, error); throw error; } } - /** - * Get agent by ID - */ public getAgent(agentId: string): AgentInfo | undefined { return this.agents.get(agentId); } - /** - * Get all agents - */ public getAllAgents(): AgentInfo[] { return Array.from(this.agents.values()); } - /** - * Delete agent by ID - */ public deleteAgent(agentId: string): boolean { const agent = this.agents.get(agentId); if (agent) { - // Disconnect agent before deletion agent.instance.disconnect(); this.agents.delete(agentId); this.agentChangeSubject.next(); - console.log(`Deleted agent: ${agentId}`); return true; } return false; } - /** - * Get available model types - */ public getModelTypes(): ModelType[] { return this.modelTypes; } - /** - * Get agent count - */ public getAgentCount(): number { return this.agents.size; } - /** - * Send a message to a specific agent by ID - * Messages are automatically updated via the messages$ observable - */ public sendMessage(agentId: string, message: string): Observable { const agent = this.agents.get(agentId); if (!agent) { @@ -180,22 +146,14 @@ export class TexeraCopilotManagerService { return agent.instance.sendMessage(message); } - /** - * Get the agent responses observable for a specific agent - * Emits the agent response list on subscribe and updates with new responses - */ public getAgentResponsesObservable(agentId: string): Observable { const agent = this.agents.get(agentId); if (!agent) { throw new Error(`Agent with ID ${agentId} not found`); } - console.log(`getAgentResponsesObservable for agent ${agentId}`); return agent.instance.agentResponses$; } - /** - * Get current agent responses snapshot for a specific agent - */ public getAgentResponses(agentId: string): AgentUIMessage[] { const agent = this.agents.get(agentId); if (!agent) { @@ -204,9 +162,6 @@ export class TexeraCopilotManagerService { return agent.instance.getAgentResponses(); } - /** - * Clear message history for a specific agent - */ public clearMessages(agentId: string): void { const agent = this.agents.get(agentId); if (!agent) { @@ -215,9 +170,6 @@ export class TexeraCopilotManagerService { agent.instance.clearMessages(); } - /** - * Stop generation for a specific agent - */ public stopGeneration(agentId: string): void { const agent = this.agents.get(agentId); if (!agent) { @@ -226,9 +178,6 @@ export class TexeraCopilotManagerService { agent.instance.stopGeneration(); } - /** - * Get agent state - */ public getAgentState(agentId: string) { const agent = this.agents.get(agentId); if (!agent) { @@ -237,9 +186,6 @@ export class TexeraCopilotManagerService { return agent.instance.getState(); } - /** - * Check if agent is connected - */ public isAgentConnected(agentId: string): boolean { const agent = this.agents.get(agentId); if (!agent) { @@ -248,9 +194,6 @@ export class TexeraCopilotManagerService { return agent.instance.isConnected(); } - /** - * Set planning mode for a specific agent - */ public setPlanningMode(agentId: string, planningMode: boolean): void { const agent = this.agents.get(agentId); if (!agent) { @@ -259,9 +202,6 @@ export class TexeraCopilotManagerService { agent.instance.setPlanningMode(planningMode); } - /** - * Get planning mode for a specific agent - */ public getPlanningMode(agentId: string): boolean { const agent = this.agents.get(agentId); if (!agent) { @@ -270,9 +210,6 @@ export class TexeraCopilotManagerService { return agent.instance.getPlanningMode(); } - /** - * Get system information (prompt and available tools) for a specific agent - */ public getSystemInfo(agentId: string): { systemPrompt: string; tools: Array<{ name: string; description: string; inputSchema: any }>; @@ -288,13 +225,10 @@ export class TexeraCopilotManagerService { } /** - * Create a copilot instance with proper dependency injection - * Uses Angular's Injector to dynamically create instances - * Creates a child injector to ensure each agent gets a unique instance + * Create a copilot instance using Angular's dependency injection. + * Each agent receives a unique instance via a child injector. */ private createCopilotInstance(modelType: string): TexeraCopilot { - // Create a child injector that provides TexeraCopilot with all its dependencies - // This ensures each call creates a NEW instance instead of reusing a singleton const childInjector = Injector.create({ providers: [ { @@ -316,13 +250,9 @@ export class TexeraCopilotManagerService { parent: this.injector, }); - // Get a fresh instance from the child injector const copilotInstance = childInjector.get(TexeraCopilot); - - // Set the model type for this instance copilotInstance.setModelType(modelType); - console.log(`Created new TexeraCopilot instance for model ${modelType}`); return copilotInstance; } } diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 7184e641775..c7301323812 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -68,11 +68,10 @@ import { COPILOT_SYSTEM_PROMPT, PLANNING_MODE_PROMPT } from "./copilot-prompts"; import { ActionPlanService } from "../action-plan/action-plan.service"; import { NotificationService } from "../../../common/service/notification/notification.service"; -// API endpoints as constants export const DEFAULT_AGENT_MODEL_ID = "claude-3.7"; /** - * Copilot state enum + * Copilot state enum. */ export enum CopilotState { UNAVAILABLE = "Unavailable", @@ -82,15 +81,13 @@ export enum CopilotState { } /** - * Agent response for UI display - * Represents a step or final response from the agent + * Agent response for UI display. */ export interface AgentUIMessage { role: "user" | "agent"; content: string; isBegin: boolean; isEnd: boolean; - // Raw data for subscribers to process toolCalls?: any[]; toolResults?: any[]; usage?: { @@ -102,35 +99,22 @@ export interface AgentUIMessage { } /** - * Texera Copilot - An AI assistant for workflow manipulation - * Uses Vercel AI SDK for chat completion and MCP SDK for tool discovery - * - * Note: Not a singleton - each agent has its own instance + * Texera Copilot - An AI assistant for workflow manipulation. + * Uses Vercel AI SDK for chat completion. + * Note: Not a singleton - each agent has its own instance. */ @Injectable() export class TexeraCopilot { private model: any; private modelType: string; - - // Agent identification private agentId: string = ""; private agentName: string = ""; - - // PRIVATE message history for AI conversation (not exposed to UI) private messages: ModelMessage[] = []; - - // PUBLIC agent responses for UI display private agentResponses: AgentUIMessage[] = []; private agentResponsesSubject = new BehaviorSubject([]); public agentResponses$ = this.agentResponsesSubject.asObservable(); - - // Copilot state management private state: CopilotState = CopilotState.UNAVAILABLE; - - // Flag to stop generation after action plan is created private shouldStopAfterActionPlan: boolean = false; - - // Planning mode - controls which tools are available private planningMode: boolean = false; constructor( @@ -145,57 +129,38 @@ export class TexeraCopilot { private actionPlanService: ActionPlanService, private notificationService: NotificationService ) { - // Default model type this.modelType = DEFAULT_AGENT_MODEL_ID; } - /** - * Set the agent identification - */ public setAgentInfo(agentId: string, agentName: string): void { this.agentId = agentId; this.agentName = agentName; } - /** - * Set the model type for this agent - */ public setModelType(modelType: string): void { this.modelType = modelType; } - /** - * Set the planning mode for this agent - */ public setPlanningMode(planningMode: boolean): void { this.planningMode = planningMode; - console.log(`[${this.agentId}] Planning mode set to: ${planningMode}`); } - /** - * Get the current planning mode - */ public getPlanningMode(): boolean { return this.planningMode; } /** - * Initialize the copilot with MCP and AI model + * Initialize the copilot with the AI model. */ public async initialize(): Promise { try { - // Initialize OpenAI model with the configured model type this.model = createOpenAI({ baseURL: new URL(`${AppSettings.getApiEndpoint()}`, document.baseURI).toString(), apiKey: "dummy", }).chat(this.modelType); - // Set state to Available this.state = CopilotState.AVAILABLE; - - console.log("Texera Copilot initialized successfully"); } catch (error: unknown) { - console.error("Failed to initialize copilot:", error); this.state = CopilotState.UNAVAILABLE; throw error; } @@ -208,17 +173,12 @@ export class TexeraCopilot { throw new Error("Copilot not initialized"); } - // Set state to Generating this.state = CopilotState.GENERATING; - - // Reset action plan stop flag for this generation this.shouldStopAfterActionPlan = false; - // 1) push the user message to PRIVATE history const userMessage: UserModelMessage = { role: "user", content: message }; this.messages.push(userMessage); - // Add user message to UI responses const userUIMessage: AgentUIMessage = { role: "user", content: message, @@ -230,46 +190,36 @@ export class TexeraCopilot { try { const tools = this.createWorkflowTools(); - let isFirstStep = true; - // Conditionally append planning mode instructions to system prompt const systemPrompt = this.planningMode ? COPILOT_SYSTEM_PROMPT + "\n\n" + PLANNING_MODE_PROMPT : COPILOT_SYSTEM_PROMPT; const { text, steps, response } = await generateText({ model: this.model, - messages: this.messages, // full history + messages: this.messages, tools, system: systemPrompt, - // Stop when: user requested stop OR action plan created OR reached 50 steps stopWhen: ({ steps }) => { - // Check if user requested stop if (this.state === CopilotState.STOPPING) { this.notificationService.info(`Agent ${this.agentName} has stopped generation`); return true; } - // Check if action plan was just created if (this.shouldStopAfterActionPlan) { return true; } - // Otherwise use the default step count limit return stepCountIs(50)({ steps }); }, - // optional: observe every completed step (tool calls + results available) onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage }) => { - // If stopped by user, skip processing this step if (this.state === CopilotState.STOPPING) { return; } - // Check if actionPlan tool was called in this step if (toolCalls && toolCalls.some((call: any) => call.toolName === "actionPlan")) { this.shouldStopAfterActionPlan = true; } - // Emit AgentResponse for this step (not done yet) const stepResponse: AgentUIMessage = { role: "agent", content: text || "", @@ -282,7 +232,6 @@ export class TexeraCopilot { this.agentResponses.push(stepResponse); this.agentResponsesSubject.next([...this.agentResponses]); - // Mark that we've processed the first step isFirstStep = false; }, }); @@ -312,7 +261,7 @@ export class TexeraCopilot { } /** - * Create workflow manipulation tools with timeout protection + * Create workflow manipulation tools with timeout protection. */ private createWorkflowTools(): Record { const addOperatorTool = toolWithTimeout( @@ -377,19 +326,15 @@ export class TexeraCopilot { ); const validateOperatorTool = toolWithTimeout(createValidateOperatorTool(this.validationWorkflowService)); - // Base tools available in both modes const baseTools: Record = { - // workflow editing addOperator: addOperatorTool, addLink: addLinkTool, deleteOperator: deleteOperatorTool, deleteLink: deleteLinkTool, setOperatorProperty: setOperatorPropertyTool, setPortProperty: setPortPropertyTool, - // workflow validation getValidationInfoOfCurrentWorkflow: getValidationInfoOfCurrentWorkflowTool, validateOperator: validateOperatorTool, - // workflow inspecting listOperatorIds: listOperatorIdsTool, listLinks: listLinksTool, listAllOperatorTypes: listAllOperatorTypesTool, @@ -400,7 +345,6 @@ export class TexeraCopilot { getOperatorInputSchema: getOperatorInputSchemaTool, getOperatorOutputSchema: getOperatorOutputSchemaTool, getWorkflowCompilationState: getWorkflowCompilationStateTool, - // workflow execution executeWorkflow: executeWorkflowTool, getExecutionStateTool: getExecutionStateTool, killWorkflow: killWorkflowTool, @@ -409,10 +353,7 @@ export class TexeraCopilot { getOperatorResultInfo: getOperatorResultInfoTool, }; - // Conditionally add action plan tools based on planning mode if (this.planningMode) { - // In planning mode: include action plan tools - console.log(`[${this.agentId}] Creating tools WITH action plan tools (planning mode ON)`); return { ...baseTools, actionPlan: actionPlanTool, @@ -423,22 +364,14 @@ export class TexeraCopilot { updateActionPlan: updateActionPlanTool, }; } else { - // Not in planning mode: exclude action plan tools - console.log(`[${this.agentId}] Creating tools WITHOUT action plan tools (planning mode OFF)`); return baseTools; } } - /** - * Get agent responses for UI display - */ public getAgentResponses(): AgentUIMessage[] { return [...this.agentResponses]; } - /** - * Stop the current generation (async - waits for generation to actually stop) - */ public stopGeneration(): void { if (this.state !== CopilotState.GENERATING) { return; @@ -446,55 +379,34 @@ export class TexeraCopilot { this.state = CopilotState.STOPPING; } - /** - * Clear message history and agent responses - */ public clearMessages(): void { this.messages = []; this.agentResponses = []; this.agentResponsesSubject.next([...this.agentResponses]); } - /** - * Get current copilot state - */ public getState(): CopilotState { return this.state; } - /** - * Disconnect and cleanup copilot resources - */ public async disconnect(): Promise { - // Stop any ongoing generation if (this.state === CopilotState.GENERATING) { this.stopGeneration(); } - // Clear message history this.clearMessages(); - // Set state to Unavailable this.state = CopilotState.UNAVAILABLE; this.notificationService.info(`Agent ${this.agentName} is removed successfully`); } - /** - * Check if copilot is connected - */ public isConnected(): boolean { return this.state !== CopilotState.UNAVAILABLE; } - /** - * Get system prompt based on current planning mode - */ public getSystemPrompt(): string { return this.planningMode ? COPILOT_SYSTEM_PROMPT + "\n\n" + PLANNING_MODE_PROMPT : COPILOT_SYSTEM_PROMPT; } - /** - * Get available tools information (name, description, input schema) - */ public getToolsInfo(): Array<{ name: string; description: string; inputSchema: any }> { const tools = this.createWorkflowTools(); return Object.entries(tools).map(([name, tool]) => ({ From 5d54f3654314a2da6c8835da48bfd92857f1c4d6 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 4 Nov 2025 00:58:18 -0800 Subject: [PATCH 087/158] add validation on the input model message --- .../service/copilot/texera-copilot.ts | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index c7301323812..730b6593930 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -149,6 +149,66 @@ export class TexeraCopilot { return this.planningMode; } + /** + * Type guard to check if a message is a valid ModelMessage. + * Uses TypeScript's type predicate for compile-time type safety. + */ + private isValidModelMessage(message: unknown): message is ModelMessage { + if (!message || typeof message !== "object") { + return false; + } + + const msg = message as Record; + + // Check if role property exists and is a string + if (typeof msg.role !== "string") { + return false; + } + + // Validate based on role using type narrowing + switch (msg.role) { + case "user": + // UserModelMessage: { role: "user", content: string } + return typeof msg.content === "string"; + + case "assistant": + // AssistantModelMessage: { role: "assistant", content: string | array } + return typeof msg.content === "string" || Array.isArray(msg.content); + + case "tool": + // ToolModelMessage: { role: "tool", content: array } + return Array.isArray(msg.content); + + default: + return false; + } + } + + /** + * Validate all messages in the conversation history. + * Throws an error if any message doesn't conform to ModelMessage type. + */ + private validateMessages(): void { + const invalidMessages: Array<{ index: number; message: unknown }> = []; + + this.messages.forEach((message, index) => { + if (!this.isValidModelMessage(message)) { + invalidMessages.push({ index, message }); + } + }); + + if (invalidMessages.length > 0) { + const indices = invalidMessages.map(m => m.index).join(", "); + const details = invalidMessages.map(m => `[${m.index}]: ${JSON.stringify(m.message)}`).join("; "); + const errorMessage = `Invalid ModelMessage(s) found at indices: ${indices}. Details: ${details}`; + + this.notificationService.error( + `Message validation failed: ${invalidMessages.length} invalid message(s). Please disconnect current agent and create a new agent` + ); + throw new Error(errorMessage); + } + } + /** * Initialize the copilot with the AI model. */ @@ -196,7 +256,10 @@ export class TexeraCopilot { ? COPILOT_SYSTEM_PROMPT + "\n\n" + PLANNING_MODE_PROMPT : COPILOT_SYSTEM_PROMPT; - const { text, steps, response } = await generateText({ + // Validate messages before calling generateText + this.validateMessages(); + + const { response } = await generateText({ model: this.model, messages: this.messages, tools, @@ -211,7 +274,7 @@ export class TexeraCopilot { } return stepCountIs(50)({ steps }); }, - onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage }) => { + onStepFinish: ({ text, toolCalls, toolResults, usage }) => { if (this.state === CopilotState.STOPPING) { return; } From fc6db318c3738ac1997390a77f36d79c1c8edbbc Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 4 Nov 2025 00:58:48 -0800 Subject: [PATCH 088/158] try to improve the action plan plotting --- .../action-plan-view.component.html | 4 +-- .../action-plan-view.component.scss | 6 ---- .../action-plan-view.component.ts | 36 +++++++++---------- .../workflow-editor.component.ts | 4 +-- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html index 6ecbdd7df11..de39dfe5c3e 100644 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html @@ -62,7 +62,8 @@
    Tasks:
    Tasks:
    {{ task.description }}
    -
    Operator: {{ task.operatorId }}
    diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss index 8ed50cc1867..5a796865b03 100644 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss @@ -102,12 +102,6 @@ .task-description { font-size: 14px; - margin-bottom: 4px; - } - - .task-operator-id { - font-size: 12px; - color: #8c8c8c; } } } diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts index 1899fb6fab8..4018678d44a 100644 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts @@ -89,9 +89,9 @@ export class ActionPlanViewComponent implements OnInit { } /** - * Highlight an operator on the workflow canvas when its task is clicked. + * Show halo effect on operator when hovering over its task. */ - public highlightOperator(operatorId: string): void { + public onTaskHover(operatorId: string, isHovering: boolean): void { const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorId); if (!operator) { return; @@ -110,22 +110,22 @@ export class ActionPlanViewComponent implements OnInit { if (operatorView) { const highlighterNamespace = joint.highlighters; - highlighterNamespace.mask.remove(operatorView, "action-plan-click"); - highlighterNamespace.mask.add(operatorView, "body", "action-plan-click", { - padding: 10, - deep: true, - attrs: { - stroke: "#69b7ff", - "stroke-width": 3, - "stroke-opacity": 0.8, - fill: "#69b7ff", - "fill-opacity": 0.1, - }, - }); - - setTimeout(() => { - highlighterNamespace.mask.remove(operatorView, "action-plan-click"); - }, 2000); + if (isHovering) { + // Add halo effect on hover + highlighterNamespace.mask.add(operatorView, "body", "action-plan-hover", { + padding: 10, + deep: true, + attrs: { + stroke: "#69b7ff", + "stroke-width": 3, + "stroke-opacity": 0.8, + fill: "transparent", + }, + }); + } else { + // Remove halo effect when hover ends + highlighterNamespace.mask.remove(operatorView, "action-plan-hover"); + } } } } diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts index 7b967859c17..9d7a9db2359 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts @@ -409,13 +409,13 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy } private handleActionPlanHighlight(): void { - // Define ActionPlan JointJS element with blue color + // Define ActionPlan JointJS element with transparent fill and border only const ActionPlan = joint.dia.Element.define( "action-plan", { attrs: { body: { - fill: "rgba(79,195,255,0.2)", + fill: "transparent", stroke: "rgba(79,195,255,0.6)", strokeWidth: 2, strokeDasharray: "5,5", From 75fcdf7864b2c7f5a56d366887b38f1e8c3bbcba Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 4 Nov 2025 01:30:00 -0800 Subject: [PATCH 089/158] fix the operator highlight --- .../action-plan-view.component.ts | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts index 4018678d44a..102c3c594ae 100644 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts +++ b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts @@ -21,7 +21,6 @@ import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from "@angu import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { ActionPlan, ActionPlanStatus, ActionPlanTask } from "../../service/action-plan/action-plan.service"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; -import * as joint from "jointjs"; @UntilDestroy() @Component({ @@ -106,26 +105,22 @@ export class ActionPlanViewComponent implements OnInit { const operatorElement = paper.getModelById(operatorId); if (operatorElement) { - const operatorView = paper.findViewByModel(operatorElement); - if (operatorView) { - const highlighterNamespace = joint.highlighters; - - if (isHovering) { - // Add halo effect on hover - highlighterNamespace.mask.add(operatorView, "body", "action-plan-hover", { - padding: 10, - deep: true, - attrs: { - stroke: "#69b7ff", - "stroke-width": 3, - "stroke-opacity": 0.8, - fill: "transparent", - }, - }); - } else { - // Remove halo effect when hover ends - highlighterNamespace.mask.remove(operatorView, "action-plan-hover"); - } + if (isHovering) { + // Add highlight effect by changing stroke attributes + operatorElement.attr({ + "rect.body": { + stroke: "#69b7ff", + "stroke-width": 4, + }, + }); + } else { + // Restore default stroke attributes + operatorElement.attr({ + "rect.body": { + stroke: "#CFCFCF", + "stroke-width": 2, + }, + }); } } } From 8ce9ccc5142adaa1af6eec75d7ec195a78e347fb Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 4 Nov 2025 09:14:18 -0800 Subject: [PATCH 090/158] fix pannel switching issue --- .../component/agent-panel/agent-chat/agent-chat.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts index 85518de7d04..e527388f057 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -60,6 +60,8 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { return; } + this.planningMode = this.copilotManagerService.getPlanningMode(this.agentInfo.id); + // Subscribe to agent responses this.copilotManagerService .getAgentResponsesObservable(this.agentInfo.id) From f9548ce09e640bfb9cc8a7cce0fb9750bc9ff848 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 4 Nov 2025 15:34:14 -0800 Subject: [PATCH 091/158] initial display of tasks --- .../workflow-editor.component.html | 26 +++++++ .../workflow-editor.component.scss | 72 +++++++++++++++++++ .../workflow-editor.component.ts | 57 +++++++++++++++ .../action-plan/action-plan.service.ts | 30 ++++++++ 4 files changed, 185 insertions(+) diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html index 8f87c66c7a7..08701dc15d8 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html @@ -26,4 +26,30 @@ #menu="nzDropdownMenu"> + + +
    +
    Action Plan Tasks
    +
    +
    {{ taskInfo.task.description }}
    +
    + {{ taskInfo.agentName }} + +
    +
    +
    + No action plan tasks for this operator +
    +
    diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss index d220c8f51ff..d7d109e5070 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss @@ -38,3 +38,75 @@ ::ng-deep .hide-worker-count .operator-worker-count { display: none; } + +.operator-tasks-hover-panel { + position: absolute; + background: white; + border: 1px solid #d9d9d9; + border-radius: 4px; + padding: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 1000; + width: 180px; // Reduced width to 2/3 of original (~270px) + pointer-events: none; // Allow mouse to pass through to workflow elements + + .panel-header { + font-size: 12px; + font-weight: 600; + color: #595959; + margin-bottom: 8px; + padding-bottom: 4px; + border-bottom: 1px solid #f0f0f0; + } + + .task-item { + padding: 6px 0; + border-bottom: 1px solid #f5f5f5; + + &:last-child { + border-bottom: none; + } + + .task-description { + font-size: 13px; + color: #262626; + margin-bottom: 4px; + line-height: 1.4; + } + + .task-agent-info { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + + .agent-name { + font-size: 11px; + font-weight: 500; + font-family: + "Inter", + "SF Pro Display", + -apple-system, + sans-serif; + color: #595959; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .status-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + } + } + } + + .no-tasks { + font-size: 12px; + color: #8c8c8c; + font-style: italic; + padding: 4px 0; + } +} diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts index 9d7a9db2359..caf54701bd3 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts @@ -96,6 +96,13 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy private removeButton!: new () => joint.linkTools.Button; private breakpointButton!: new () => joint.linkTools.Button; + // Operator tasks hover panel state + public operatorTasksHoverInfo: { + x: number; + y: number; + tasks: Array<{ task: any; planId: string; agentName: string; isCompleted: boolean }>; + } | null = null; + constructor( private workflowActionService: WorkflowActionService, private dynamicSchemaService: DynamicSchemaService, @@ -178,6 +185,7 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy this.handleLinkBreakpoint(); } this.handlePointerEvents(); + this.handleOperatorTasksHover(); this.handleURLFragment(); this.invokeResize(); this.handleCenterEvent(); @@ -1486,6 +1494,55 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy }); } + /** + * Handles operator hover to show action plan tasks panel. + */ + private handleOperatorTasksHover(): void { + // Show panel on mouseenter + fromJointPaperEvent(this.paper, "cell:mouseenter") + .pipe( + filter(event => event[0].model.isElement()), + filter(event => this.workflowActionService.getTexeraGraph().hasOperator(event[0].model.id.toString())), + untilDestroyed(this) + ) + .subscribe(event => { + const operatorId = event[0].model.id.toString(); + const tasks = this.actionPlanService.getTasksByOperatorId(operatorId); + + // Only show panel if there are tasks + if (tasks.length > 0) { + // Get operator element to position panel directly below it + const operatorElement = event[0].model; + const bbox = operatorElement.getBBox(); + + // Panel width is 180px, center it below the operator + const panelWidth = 180; + const panelX = bbox.x + bbox.width / 2 - panelWidth / 2; // Center horizontally + const panelY = bbox.y + bbox.height + 20; // 10px gap below operator + + // Convert paper coordinates to client coordinates + const clientPoint = this.paper.localToClientPoint({ x: panelX, y: panelY }); + + this.operatorTasksHoverInfo = { + x: clientPoint.x, + y: clientPoint.y, + tasks: tasks, + }; + } + }); + + // Hide panel on mouseleave + fromJointPaperEvent(this.paper, "cell:mouseleave") + .pipe( + filter(event => event[0].model.isElement()), + filter(event => this.workflowActionService.getTexeraGraph().hasOperator(event[0].model.id.toString())), + untilDestroyed(this) + ) + .subscribe(() => { + this.operatorTasksHoverInfo = null; + }); + } + private setURLFragment(fragment: string | null): void { this.router.navigate([], { relativeTo: this.route, diff --git a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts index fb123a1e11f..3e2d2be5178 100644 --- a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts +++ b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts @@ -280,6 +280,36 @@ export class ActionPlanService { return `action-plan-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } + /** + * Get all tasks associated with a specific operator ID across all action plans + * Returns an array of tasks with their plan ID, agent name, and completion status + */ + public getTasksByOperatorId(operatorId: string): Array<{ + task: ActionPlanTask; + planId: string; + agentName: string; + isCompleted: boolean; + }> { + const results: Array<{ task: ActionPlanTask; planId: string; agentName: string; isCompleted: boolean }> = []; + + this.actionPlans.forEach((plan, planId) => { + const task = plan.tasks.get(operatorId); + if (task) { + // Check if all tasks in the plan are completed + const allCompleted = Array.from(plan.tasks.values()).every(t => t.completed$.value); + + results.push({ + task, + planId, + agentName: plan.agentName, + isCompleted: allCompleted, + }); + } + }); + + return results; + } + /** * Emit the current list of action plans */ From efe8ccccd21608704645972b90872a299560fedb Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 4 Nov 2025 15:42:53 -0800 Subject: [PATCH 092/158] fix panel location --- .../workflow-editor/workflow-editor.component.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts index caf54701bd3..84fe91e2601 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts @@ -1516,16 +1516,19 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy const bbox = operatorElement.getBBox(); // Panel width is 180px, center it below the operator - const panelWidth = 180; + const panelWidth = 250; const panelX = bbox.x + bbox.width / 2 - panelWidth / 2; // Center horizontally - const panelY = bbox.y + bbox.height + 20; // 10px gap below operator + const panelY = bbox.y + bbox.height + 20; // 20px gap below operator // Convert paper coordinates to client coordinates const clientPoint = this.paper.localToClientPoint({ x: panelX, y: panelY }); + // Get editor wrapper position to adjust for absolute positioning + const editorRect = this.editorWrapper.getBoundingClientRect(); + this.operatorTasksHoverInfo = { - x: clientPoint.x, - y: clientPoint.y, + x: clientPoint.x - editorRect.left, + y: clientPoint.y - editorRect.top, tasks: tasks, }; } From 7af5117fc24ff23c33d7deadebe8072846032ccd Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 4 Nov 2025 18:23:45 -0800 Subject: [PATCH 093/158] add tools for cu status --- .../copilot/texera-copilot-manager.service.ts | 2 + .../service/copilot/texera-copilot.ts | 7 ++- .../service/copilot/workflow-tools.ts | 47 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index b7bc6e92c00..993777782c4 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -30,6 +30,7 @@ import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling import { ValidationWorkflowService } from "../validation/validation-workflow.service"; import { ActionPlanService } from "../action-plan/action-plan.service"; import { NotificationService } from "../../../common/service/notification/notification.service"; +import { ComputingUnitStatusService } from "../computing-unit-status/computing-unit-status.service"; /** * Agent information for tracking created agents. @@ -244,6 +245,7 @@ export class TexeraCopilotManagerService { ValidationWorkflowService, ActionPlanService, NotificationService, + ComputingUnitStatusService, ], }, ], diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 730b6593930..88dc4eb9c7c 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -53,6 +53,7 @@ import { createListAllOperatorTypesTool, createListLinksTool, createListOperatorIdsTool, + createGetComputingUnitStatusTool, } from "./workflow-tools"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { createOpenAI } from "@ai-sdk/openai"; @@ -67,6 +68,7 @@ import { ValidationWorkflowService } from "../validation/validation-workflow.ser import { COPILOT_SYSTEM_PROMPT, PLANNING_MODE_PROMPT } from "./copilot-prompts"; import { ActionPlanService } from "../action-plan/action-plan.service"; import { NotificationService } from "../../../common/service/notification/notification.service"; +import { ComputingUnitStatusService } from "../computing-unit-status/computing-unit-status.service"; export const DEFAULT_AGENT_MODEL_ID = "claude-3.7"; @@ -127,7 +129,8 @@ export class TexeraCopilot { private workflowCompilingService: WorkflowCompilingService, private validationWorkflowService: ValidationWorkflowService, private actionPlanService: ActionPlanService, - private notificationService: NotificationService + private notificationService: NotificationService, + private computingUnitStatusService: ComputingUnitStatusService ) { this.modelType = DEFAULT_AGENT_MODEL_ID; } @@ -388,6 +391,7 @@ export class TexeraCopilot { createGetValidationInfoOfCurrentWorkflowTool(this.validationWorkflowService, this.workflowActionService) ); const validateOperatorTool = toolWithTimeout(createValidateOperatorTool(this.validationWorkflowService)); + const getComputingUnitStatusTool = toolWithTimeout(createGetComputingUnitStatusTool(this.computingUnitStatusService)); const baseTools: Record = { addOperator: addOperatorTool, @@ -414,6 +418,7 @@ export class TexeraCopilot { hasOperatorResult: hasOperatorResultTool, getOperatorResult: getOperatorResultTool, getOperatorResultInfo: getOperatorResultInfoTool, + getComputingUnitStatus: getComputingUnitStatusTool, }; if (this.planningMode) { diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index 72f862472f2..81cfe94db06 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -1533,3 +1533,50 @@ export function createValidateOperatorTool(validationWorkflowService: Validation }, }); } + +/** + * Create getComputingUnitStatus tool for checking computing unit connection status + */ +export function createGetComputingUnitStatusTool(computingUnitStatusService: any) { + return tool({ + name: "getComputingUnitStatus", + description: + "Check the status of the computing unit connection. This is important before workflow execution - if the unit is disconnected, workflows cannot be executed. Use this when execution fails or to verify readiness for execution.", + inputSchema: z.object({}), + execute: async () => { + try { + const selectedUnit = computingUnitStatusService.getSelectedComputingUnitValue(); + + if (!selectedUnit) { + return { + success: true, + status: "No Computing Unit", + isConnected: false, + message: + "No computing unit is selected. Workflow execution is not available. Please remind the user to connect to a computing unit.", + }; + } + + const unitStatus = selectedUnit.status; + const isConnected = unitStatus === "Running"; + + return { + success: true, + status: unitStatus, + isConnected: isConnected, + computingUnit: { + cuid: selectedUnit.computingUnit.cuid, + name: selectedUnit.computingUnit.name, + }, + message: isConnected + ? `Computing unit "${selectedUnit.computingUnit.name}" is running and ready for workflow execution` + : unitStatus === "Pending" + ? `Computing unit "${selectedUnit.computingUnit.name}" is pending/starting. Workflow execution may not be available yet.` + : `Computing unit is in state: ${unitStatus}. Workflow execution may not be available.`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} From 24e8f416880d70ace25cdd3c202ba1ee6252dd9f Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 4 Nov 2025 21:26:11 -0800 Subject: [PATCH 094/158] Revert "fix panel location" This reverts commit 84a2abd2a7ae1b0e934be8cca6699f2e92f6bd0b. --- .../workflow-editor/workflow-editor.component.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts index 84fe91e2601..caf54701bd3 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts @@ -1516,19 +1516,16 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy const bbox = operatorElement.getBBox(); // Panel width is 180px, center it below the operator - const panelWidth = 250; + const panelWidth = 180; const panelX = bbox.x + bbox.width / 2 - panelWidth / 2; // Center horizontally - const panelY = bbox.y + bbox.height + 20; // 20px gap below operator + const panelY = bbox.y + bbox.height + 20; // 10px gap below operator // Convert paper coordinates to client coordinates const clientPoint = this.paper.localToClientPoint({ x: panelX, y: panelY }); - // Get editor wrapper position to adjust for absolute positioning - const editorRect = this.editorWrapper.getBoundingClientRect(); - this.operatorTasksHoverInfo = { - x: clientPoint.x - editorRect.left, - y: clientPoint.y - editorRect.top, + x: clientPoint.x, + y: clientPoint.y, tasks: tasks, }; } From a651d532f684f9c0e7dc7035405c3f6748cd4d71 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 4 Nov 2025 21:26:11 -0800 Subject: [PATCH 095/158] Revert "initial display of tasks" This reverts commit 8db18e0b795d467ac67481cae1a3336c02340553. --- .../workflow-editor.component.html | 26 ------- .../workflow-editor.component.scss | 72 ------------------- .../workflow-editor.component.ts | 57 --------------- .../action-plan/action-plan.service.ts | 30 -------- 4 files changed, 185 deletions(-) diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html index 08701dc15d8..8f87c66c7a7 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html @@ -26,30 +26,4 @@ #menu="nzDropdownMenu"> - - -
    -
    Action Plan Tasks
    -
    -
    {{ taskInfo.task.description }}
    -
    - {{ taskInfo.agentName }} - -
    -
    -
    - No action plan tasks for this operator -
    -
    diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss index d7d109e5070..d220c8f51ff 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss @@ -38,75 +38,3 @@ ::ng-deep .hide-worker-count .operator-worker-count { display: none; } - -.operator-tasks-hover-panel { - position: absolute; - background: white; - border: 1px solid #d9d9d9; - border-radius: 4px; - padding: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - z-index: 1000; - width: 180px; // Reduced width to 2/3 of original (~270px) - pointer-events: none; // Allow mouse to pass through to workflow elements - - .panel-header { - font-size: 12px; - font-weight: 600; - color: #595959; - margin-bottom: 8px; - padding-bottom: 4px; - border-bottom: 1px solid #f0f0f0; - } - - .task-item { - padding: 6px 0; - border-bottom: 1px solid #f5f5f5; - - &:last-child { - border-bottom: none; - } - - .task-description { - font-size: 13px; - color: #262626; - margin-bottom: 4px; - line-height: 1.4; - } - - .task-agent-info { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - - .agent-name { - font-size: 11px; - font-weight: 500; - font-family: - "Inter", - "SF Pro Display", - -apple-system, - sans-serif; - color: #595959; - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .status-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - } - } - } - - .no-tasks { - font-size: 12px; - color: #8c8c8c; - font-style: italic; - padding: 4px 0; - } -} diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts index caf54701bd3..9d7a9db2359 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts @@ -96,13 +96,6 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy private removeButton!: new () => joint.linkTools.Button; private breakpointButton!: new () => joint.linkTools.Button; - // Operator tasks hover panel state - public operatorTasksHoverInfo: { - x: number; - y: number; - tasks: Array<{ task: any; planId: string; agentName: string; isCompleted: boolean }>; - } | null = null; - constructor( private workflowActionService: WorkflowActionService, private dynamicSchemaService: DynamicSchemaService, @@ -185,7 +178,6 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy this.handleLinkBreakpoint(); } this.handlePointerEvents(); - this.handleOperatorTasksHover(); this.handleURLFragment(); this.invokeResize(); this.handleCenterEvent(); @@ -1494,55 +1486,6 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy }); } - /** - * Handles operator hover to show action plan tasks panel. - */ - private handleOperatorTasksHover(): void { - // Show panel on mouseenter - fromJointPaperEvent(this.paper, "cell:mouseenter") - .pipe( - filter(event => event[0].model.isElement()), - filter(event => this.workflowActionService.getTexeraGraph().hasOperator(event[0].model.id.toString())), - untilDestroyed(this) - ) - .subscribe(event => { - const operatorId = event[0].model.id.toString(); - const tasks = this.actionPlanService.getTasksByOperatorId(operatorId); - - // Only show panel if there are tasks - if (tasks.length > 0) { - // Get operator element to position panel directly below it - const operatorElement = event[0].model; - const bbox = operatorElement.getBBox(); - - // Panel width is 180px, center it below the operator - const panelWidth = 180; - const panelX = bbox.x + bbox.width / 2 - panelWidth / 2; // Center horizontally - const panelY = bbox.y + bbox.height + 20; // 10px gap below operator - - // Convert paper coordinates to client coordinates - const clientPoint = this.paper.localToClientPoint({ x: panelX, y: panelY }); - - this.operatorTasksHoverInfo = { - x: clientPoint.x, - y: clientPoint.y, - tasks: tasks, - }; - } - }); - - // Hide panel on mouseleave - fromJointPaperEvent(this.paper, "cell:mouseleave") - .pipe( - filter(event => event[0].model.isElement()), - filter(event => this.workflowActionService.getTexeraGraph().hasOperator(event[0].model.id.toString())), - untilDestroyed(this) - ) - .subscribe(() => { - this.operatorTasksHoverInfo = null; - }); - } - private setURLFragment(fragment: string | null): void { this.router.navigate([], { relativeTo: this.route, diff --git a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts index 3e2d2be5178..fb123a1e11f 100644 --- a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts +++ b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts @@ -280,36 +280,6 @@ export class ActionPlanService { return `action-plan-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } - /** - * Get all tasks associated with a specific operator ID across all action plans - * Returns an array of tasks with their plan ID, agent name, and completion status - */ - public getTasksByOperatorId(operatorId: string): Array<{ - task: ActionPlanTask; - planId: string; - agentName: string; - isCompleted: boolean; - }> { - const results: Array<{ task: ActionPlanTask; planId: string; agentName: string; isCompleted: boolean }> = []; - - this.actionPlans.forEach((plan, planId) => { - const task = plan.tasks.get(operatorId); - if (task) { - // Check if all tasks in the plan are completed - const allCompleted = Array.from(plan.tasks.values()).every(t => t.completed$.value); - - results.push({ - task, - planId, - agentName: plan.agentName, - isCompleted: allCompleted, - }); - } - }); - - return results; - } - /** * Emit the current list of action plans */ From c6cfeb760e658972a893f1eff9edcf13b41323c3 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 4 Nov 2025 22:55:13 -0800 Subject: [PATCH 096/158] helm chart: add persistency --- bin/k8s/Chart.yaml | 6 ++ bin/k8s/templates/external-names.yaml | 12 ++- bin/k8s/templates/litellm-deployment.yaml | 21 ++++- bin/k8s/templates/litellm-secret.yaml | 4 + .../postgresql-litellm-persistence.yaml | 78 +++++++++++++++++++ bin/k8s/values.yaml | 36 ++++++++- 6 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 bin/k8s/templates/postgresql-litellm-persistence.yaml diff --git a/bin/k8s/Chart.yaml b/bin/k8s/Chart.yaml index ee57a2b8427..b212d229e9c 100644 --- a/bin/k8s/Chart.yaml +++ b/bin/k8s/Chart.yaml @@ -51,6 +51,12 @@ dependencies: version: 16.5.6 repository: https://charts.bitnami.com/bitnami + - name: postgresql + version: 16.5.6 + repository: https://charts.bitnami.com/bitnami + alias: postgresql-litellm + condition: litellm.persistence.enabled + - name: minio version: 15.0.7 repository: https://charts.bitnami.com/bitnami diff --git a/bin/k8s/templates/external-names.yaml b/bin/k8s/templates/external-names.yaml index 259c5fa6956..2c63b45463d 100644 --- a/bin/k8s/templates/external-names.yaml +++ b/bin/k8s/templates/external-names.yaml @@ -59,13 +59,23 @@ to access services in the main namespace using the same service names. --- {{/* PostgreSQL ExternalName */}} -{{- include "external-name-service" (dict +{{- include "external-name-service" (dict "name" (printf "%s-postgresql" .Release.Name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-postgresql.%s.svc.cluster.local" .Release.Name $namespace) ) | nindent 0 }} --- +{{/* PostgreSQL LiteLLM ExternalName */}} +{{- if .Values.litellm.persistence.enabled }} +{{- include "external-name-service" (dict + "name" (printf "%s-postgresql-litellm" .Release.Name) + "namespace" $workflowComputingUnitPoolNamespace + "externalName" (printf "%s-postgresql-litellm.%s.svc.cluster.local" .Release.Name $namespace) +) | nindent 0 }} + +--- +{{- end }} {{/* Webserver ExternalName */}} {{- include "external-name-service" (dict "name" (printf "%s-svc" .Values.webserver.name) diff --git a/bin/k8s/templates/litellm-deployment.yaml b/bin/k8s/templates/litellm-deployment.yaml index df51f0adc79..176d6292703 100644 --- a/bin/k8s/templates/litellm-deployment.yaml +++ b/bin/k8s/templates/litellm-deployment.yaml @@ -38,9 +38,24 @@ spec: ports: - containerPort: {{ .Values.litellm.service.port }} name: http - envFrom: - - secretRef: - name: litellm-secret + env: + {{- if .Values.litellm.persistence.enabled }} + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: litellm-secret + key: DATABASE_URL + - name: LITELLM_MASTER_KEY + valueFrom: + secretKeyRef: + name: litellm-secret + key: LITELLM_MASTER_KEY + {{- end }} + - name: ANTHROPIC_API_KEY + valueFrom: + secretKeyRef: + name: litellm-secret + key: ANTHROPIC_API_KEY command: - litellm - --config diff --git a/bin/k8s/templates/litellm-secret.yaml b/bin/k8s/templates/litellm-secret.yaml index 5f3ee863f1f..16f55259593 100644 --- a/bin/k8s/templates/litellm-secret.yaml +++ b/bin/k8s/templates/litellm-secret.yaml @@ -24,4 +24,8 @@ metadata: type: Opaque data: ANTHROPIC_API_KEY: {{ .Values.litellm.apiKeys.anthropic | b64enc | quote }} + {{- if .Values.litellm.persistence.enabled }} + DATABASE_URL: {{ printf "postgresql://%s:%s@%s-postgresql-litellm:5432/%s" .Values.litellm.persistence.database.username .Values.litellm.persistence.database.password .Release.Name .Values.litellm.persistence.database.name | b64enc | quote }} + LITELLM_MASTER_KEY: {{ .Values.litellm.masterKey | b64enc | quote }} + {{- end }} {{- end }} diff --git a/bin/k8s/templates/postgresql-litellm-persistence.yaml b/bin/k8s/templates/postgresql-litellm-persistence.yaml new file mode 100644 index 00000000000..027ae758b9e --- /dev/null +++ b/bin/k8s/templates/postgresql-litellm-persistence.yaml @@ -0,0 +1,78 @@ +# 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. + +{{/* Define storage path configuration, please change it to your own path and make sure the path exists with the right permission*/}} +{{/* This path only works for local-path storage class */}} +{{- $hostBasePath := .Values.persistence.postgresqlHostLocalPath }} + +{{- if .Values.litellm.persistence.enabled }} +{{- $name := "postgresql-litellm" }} +{{- $persistence := (index .Values "postgresql-litellm").primary.persistence }} +{{- $volumeName := printf "%s-data-pv" $name }} +{{- $claimName := printf "%s-data-pvc" $name }} +{{- $storageClass := $persistence.storageClass | default "local-path" }} +{{- $size := $persistence.size | default "5Gi" }} +{{- $hostPath := printf "%s/%s/%s" $hostBasePath $.Release.Name $name }} + +{{/* Only create PV for local-path storage class */}} +{{- if and (eq $storageClass "local-path") (ne $hostBasePath "") }} +apiVersion: v1 +kind: PersistentVolume +metadata: + name: {{ $volumeName }} + {{- if not $.Values.persistence.removeAfterUninstall }} + annotations: + "helm.sh/resource-policy": keep + {{- end }} + labels: + type: local + app: {{ $.Release.Name }} + component: {{ $name }} +spec: + storageClassName: {{ $storageClass }} + capacity: + storage: {{ $size }} + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + hostPath: + path: {{ $hostPath }} +--- +{{- end }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ $claimName }} + namespace: {{ $.Release.Namespace }} + {{- if not $.Values.persistence.removeAfterUninstall }} + annotations: + "helm.sh/resource-policy": keep + {{- end }} + labels: + app: {{ $.Release.Name }} + component: {{ $name }} +spec: + storageClassName: {{ $storageClass }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ $size }} + {{- if and (eq $storageClass "local-path") (ne $hostBasePath "") }} + volumeName: {{ $volumeName }} + {{- end }} +{{- end }} diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml index 0b3775f8f3b..df2bb81aad2 100644 --- a/bin/k8s/values.yaml +++ b/bin/k8s/values.yaml @@ -300,7 +300,7 @@ litellm: replicaCount: 1 image: repository: ghcr.io/berriai/litellm - tag: main-v1.30.3 + tag: main-stable pullPolicy: IfNotPresent service: type: ClusterIP @@ -312,11 +312,45 @@ litellm: requests: cpu: "200m" memory: "256Mi" + # Database persistence configuration + persistence: + enabled: true + database: + name: litellm + username: litellm + password: litellm_password # Change this in production + # Master key for LiteLLM admin API (must start with "sk-") + masterKey: "sk-texera-litellm-masterkey" # Change this in production apiKeys: # Set your Anthropic API key here # IMPORTANT: In production, use external secrets management (e.g., sealed-secrets, external-secrets) anthropic: "" # Replace with your actual API key or use external secret +# PostgreSQL database for LiteLLM persistence +postgresql-litellm: + auth: + postgresPassword: litellm_root_password # Change this in production + username: litellm + password: litellm_password # Should match litellm.persistence.database.password + database: litellm + primary: + livenessProbe: + initialDelaySeconds: 30 + readinessProbe: + initialDelaySeconds: 30 + resources: + requests: + cpu: "500m" + memory: "512Mi" + limits: + cpu: "1" + memory: "1Gi" + persistence: + enabled: true + size: 5Gi + storageClass: local-path + existingClaim: "postgresql-litellm-data-pvc" + # Metrics Server configuration metrics-server: enabled: true # set to false if metrics-server is already installed From af7ef442e8e03a2f3d3a1c5d196849b9a85ec6dd Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Wed, 5 Nov 2025 01:23:15 -0800 Subject: [PATCH 097/158] helm chart: finish initial k8s litellm --- .../texera/service/AccessControlService.scala | 7 +- .../resource/AccessControlResource.scala | 71 ++++++++++++++++++- .../access-control-service-deployment.yaml | 9 +++ bin/k8s/templates/external-names.yaml | 10 +++ bin/k8s/templates/litellm-config.yaml | 7 ++ bin/k8s/templates/litellm-deployment.yaml | 4 +- bin/k8s/values.yaml | 20 +++--- 7 files changed, 116 insertions(+), 12 deletions(-) diff --git a/access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala b/access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala index bed201c713b..8554acd743a 100644 --- a/access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala +++ b/access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala @@ -25,7 +25,11 @@ import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.amber.config.StorageConfig import org.apache.texera.auth.{JwtAuthFilter, SessionUser} import org.apache.texera.dao.SqlServer -import org.apache.texera.service.resource.{AccessControlResource, HealthCheckResource} +import org.apache.texera.service.resource.{ + AccessControlResource, + HealthCheckResource, + LiteLLMProxyResource +} import org.eclipse.jetty.server.session.SessionHandler import java.nio.file.Path @@ -54,6 +58,7 @@ class AccessControlService extends Application[AccessControlServiceConfiguration environment.jersey.register(classOf[HealthCheckResource]) environment.jersey.register(classOf[AccessControlResource]) + environment.jersey.register(classOf[LiteLLMProxyResource]) // Register JWT authentication filter environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) diff --git a/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala b/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala index 68c278dc71e..f1ffffcaffe 100644 --- a/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala +++ b/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala @@ -20,8 +20,9 @@ package org.apache.texera.service.resource import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.typesafe.scalalogging.LazyLogging +import jakarta.ws.rs.client.{Client, ClientBuilder, Entity} import jakarta.ws.rs.core._ -import jakarta.ws.rs.{GET, POST, Path, Produces} +import jakarta.ws.rs.{Consumes, GET, POST, Path, Produces} import org.apache.texera.auth.JwtParser.parseToken import org.apache.texera.auth.SessionUser import org.apache.texera.auth.util.{ComputingUnitAccess, HeaderField} @@ -203,3 +204,71 @@ class AccessControlResource extends LazyLogging { AccessControlResource.authorize(uriInfo, headers, Option(body).map(_.trim).filter(_.nonEmpty)) } } + +@Path("/chat") +@Produces(Array(MediaType.APPLICATION_JSON)) +@Consumes(Array(MediaType.APPLICATION_JSON)) +class LiteLLMProxyResource extends LazyLogging { + + private val client: Client = ClientBuilder.newClient() + private val litellmBaseUrl: String = sys.env.getOrElse( + "LITELLM_BASE_URL", + "http://litellm-svc:4000" + ) + private val litellmApiKey: String = sys.env.getOrElse("LITELLM_MASTER_KEY", "") + + @POST + @Path("/{path:.*}") + def proxyPost( + @Context uriInfo: UriInfo, + @Context headers: HttpHeaders, + body: String + ): Response = { + // uriInfo.getPath returns "chat/completions" for /api/chat/completions + // We want to forward as "/chat/completions" to LiteLLM + val fullPath = uriInfo.getPath + val targetUrl = s"$litellmBaseUrl/$fullPath" + + logger.info(s"Proxying POST request to LiteLLM: $targetUrl") + + try { + val requestBuilder = client + .target(targetUrl) + .request(MediaType.APPLICATION_JSON) + .header("Authorization", s"Bearer $litellmApiKey") + + // Forward other relevant headers from the original request + headers.getRequestHeaders.asScala.foreach { + case (key, values) + if !key.equalsIgnoreCase("Authorization") && + !key.equalsIgnoreCase("Host") && + !key.equalsIgnoreCase("Content-Length") => + values.asScala.foreach(value => requestBuilder.header(key, value)) + case _ => // Skip Authorization, Host, and Content-Length headers + } + + val response = requestBuilder.post(Entity.json(body)) + + // Build response with same status and body from LiteLLM + val responseBody = response.readEntity(classOf[String]) + val responseBuilder = Response + .status(response.getStatus) + .entity(responseBody) + + // Forward response headers + response.getHeaders.asScala.foreach { + case (key, values) => + values.asScala.foreach(value => responseBuilder.header(key, value)) + } + + responseBuilder.build() + } catch { + case e: Exception => + logger.error(s"Error proxying request to LiteLLM: ${e.getMessage}", e) + Response + .status(Response.Status.BAD_GATEWAY) + .entity(s"""{"error": "Failed to proxy request to LiteLLM: ${e.getMessage}"}""") + .build() + } + } +} diff --git a/bin/k8s/templates/access-control-service-deployment.yaml b/bin/k8s/templates/access-control-service-deployment.yaml index a332c65bd39..fc6784c5189 100644 --- a/bin/k8s/templates/access-control-service-deployment.yaml +++ b/bin/k8s/templates/access-control-service-deployment.yaml @@ -46,6 +46,15 @@ spec: secretKeyRef: name: {{ .Release.Name }}-postgresql key: postgres-password +{{- if .Values.litellm.enabled }} + - name: LITELLM_MASTER_KEY + valueFrom: + secretKeyRef: + name: litellm-secret + key: LITELLM_MASTER_KEY + - name: LITELLM_BASE_URL + value: "http://{{ .Values.litellm.name }}-svc:{{ .Values.litellm.service.port }}" +{{- end }} livenessProbe: httpGet: path: /api/healthcheck diff --git a/bin/k8s/templates/external-names.yaml b/bin/k8s/templates/external-names.yaml index 2c63b45463d..72af59eded1 100644 --- a/bin/k8s/templates/external-names.yaml +++ b/bin/k8s/templates/external-names.yaml @@ -74,6 +74,16 @@ to access services in the main namespace using the same service names. "externalName" (printf "%s-postgresql-litellm.%s.svc.cluster.local" .Release.Name $namespace) ) | nindent 0 }} +--- +{{- end }} +{{/* LiteLLM service ExternalName */}} +{{- if .Values.litellm.enabled }} +{{- include "external-name-service" (dict + "name" (printf "%s-svc" .Values.litellm.name) + "namespace" $workflowComputingUnitPoolNamespace + "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.litellm.name $namespace) +) | nindent 0 }} + --- {{- end }} {{/* Webserver ExternalName */}} diff --git a/bin/k8s/templates/litellm-config.yaml b/bin/k8s/templates/litellm-config.yaml index 1ac7075ce3e..5b6184cc258 100644 --- a/bin/k8s/templates/litellm-config.yaml +++ b/bin/k8s/templates/litellm-config.yaml @@ -32,4 +32,11 @@ data: litellm_params: model: claude-sonnet-4-5-20250929 api_key: "os.environ/ANTHROPIC_API_KEY" + + general_settings: + {{- if .Values.litellm.persistence.enabled }} + master_key: "os.environ/LITELLM_MASTER_KEY" + {{- end }} + # Disable spend tracking and key management for simpler setup + disable_spend_logs: {{ not .Values.litellm.persistence.enabled }} {{- end }} diff --git a/bin/k8s/templates/litellm-deployment.yaml b/bin/k8s/templates/litellm-deployment.yaml index 176d6292703..51f05a6b886 100644 --- a/bin/k8s/templates/litellm-deployment.yaml +++ b/bin/k8s/templates/litellm-deployment.yaml @@ -70,13 +70,13 @@ spec: {{- toYaml .Values.litellm.resources | nindent 12 }} livenessProbe: httpGet: - path: /health + path: /health/liveliness port: {{ .Values.litellm.service.port }} initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: - path: /health + path: /health/readiness port: {{ .Values.litellm.service.port }} initialDelaySeconds: 10 periodSeconds: 5 diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml index df2bb81aad2..95709cde85d 100644 --- a/bin/k8s/values.yaml +++ b/bin/k8s/values.yaml @@ -299,7 +299,7 @@ litellm: name: litellm replicaCount: 1 image: - repository: ghcr.io/berriai/litellm + repository: ghcr.io/berriai/litellm-database tag: main-stable pullPolicy: IfNotPresent service: @@ -307,11 +307,11 @@ litellm: port: 4000 resources: limits: - cpu: "500m" - memory: "512Mi" + cpu: "2" + memory: "2Gi" requests: - cpu: "200m" - memory: "256Mi" + cpu: "500m" + memory: "1Gi" # Database persistence configuration persistence: enabled: true @@ -328,6 +328,10 @@ litellm: # PostgreSQL database for LiteLLM persistence postgresql-litellm: + image: + repository: texera/postgres17-pgroonga + tag: latest + debug: true auth: postgresPassword: litellm_root_password # Change this in production username: litellm @@ -396,9 +400,6 @@ ingressPaths: - path: /api/config serviceName: config-service-svc servicePort: 9094 - - path: /api/chat - serviceName: litellm-svc - servicePort: 4000 - path: /wsapi/workflow-websocket serviceName: envoy-svc servicePort: 10000 @@ -410,6 +411,9 @@ ingressPaths: pathType: ImplementationSpecific serviceName: envoy-svc servicePort: 10000 + - path: /api/chat + serviceName: texera-access-control-service-svc + servicePort: 9096 - path: /api serviceName: webserver-svc servicePort: 8080 From 119e40b5d81bef5646c73b11a9101d0e319b400e Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Wed, 5 Nov 2025 09:26:51 -0800 Subject: [PATCH 098/158] helm chart: improve --- .../resource/AccessControlResource.scala | 8 ++--- bin/k8s/templates/litellm-secret.yaml | 3 +- bin/k8s/values.yaml | 4 --- common/config/src/main/resources/llm.conf | 27 +++++++++++++++++ .../org/apache/texera/config/LLMConfig.scala | 29 +++++++++++++++++++ 5 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 common/config/src/main/resources/llm.conf create mode 100644 common/config/src/main/scala/org/apache/texera/config/LLMConfig.scala diff --git a/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala b/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala index f1ffffcaffe..bc999cfe75e 100644 --- a/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala +++ b/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala @@ -26,6 +26,7 @@ import jakarta.ws.rs.{Consumes, GET, POST, Path, Produces} import org.apache.texera.auth.JwtParser.parseToken import org.apache.texera.auth.SessionUser import org.apache.texera.auth.util.{ComputingUnitAccess, HeaderField} +import org.apache.texera.config.LLMConfig import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum import java.net.URLDecoder @@ -211,11 +212,8 @@ class AccessControlResource extends LazyLogging { class LiteLLMProxyResource extends LazyLogging { private val client: Client = ClientBuilder.newClient() - private val litellmBaseUrl: String = sys.env.getOrElse( - "LITELLM_BASE_URL", - "http://litellm-svc:4000" - ) - private val litellmApiKey: String = sys.env.getOrElse("LITELLM_MASTER_KEY", "") + private val litellmBaseUrl: String = LLMConfig.baseUrl + private val litellmApiKey: String = LLMConfig.masterKey @POST @Path("/{path:.*}") diff --git a/bin/k8s/templates/litellm-secret.yaml b/bin/k8s/templates/litellm-secret.yaml index 16f55259593..f68c1d72ebc 100644 --- a/bin/k8s/templates/litellm-secret.yaml +++ b/bin/k8s/templates/litellm-secret.yaml @@ -25,7 +25,8 @@ type: Opaque data: ANTHROPIC_API_KEY: {{ .Values.litellm.apiKeys.anthropic | b64enc | quote }} {{- if .Values.litellm.persistence.enabled }} - DATABASE_URL: {{ printf "postgresql://%s:%s@%s-postgresql-litellm:5432/%s" .Values.litellm.persistence.database.username .Values.litellm.persistence.database.password .Release.Name .Values.litellm.persistence.database.name | b64enc | quote }} + {{- $postgresqlLitellm := index .Values "postgresql-litellm" }} + DATABASE_URL: {{ printf "postgresql://%s:%s@%s-postgresql-litellm:5432/%s" $postgresqlLitellm.auth.username $postgresqlLitellm.auth.password .Release.Name $postgresqlLitellm.auth.database | b64enc | quote }} LITELLM_MASTER_KEY: {{ .Values.litellm.masterKey | b64enc | quote }} {{- end }} {{- end }} diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml index 95709cde85d..f886d5627ab 100644 --- a/bin/k8s/values.yaml +++ b/bin/k8s/values.yaml @@ -315,10 +315,6 @@ litellm: # Database persistence configuration persistence: enabled: true - database: - name: litellm - username: litellm - password: litellm_password # Change this in production # Master key for LiteLLM admin API (must start with "sk-") masterKey: "sk-texera-litellm-masterkey" # Change this in production apiKeys: diff --git a/common/config/src/main/resources/llm.conf b/common/config/src/main/resources/llm.conf new file mode 100644 index 00000000000..05ab4c4b01d --- /dev/null +++ b/common/config/src/main/resources/llm.conf @@ -0,0 +1,27 @@ +# 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. + +# LLM Configuration +llm { + # Base URL for LiteLLM service + base-url = "http://litellm-svc:4000" + base-url = ${?LITELLM_BASE_URL} + + # Master key for LiteLLM authentication + master-key = "" + master-key = ${?LITELLM_MASTER_KEY} +} diff --git a/common/config/src/main/scala/org/apache/texera/config/LLMConfig.scala b/common/config/src/main/scala/org/apache/texera/config/LLMConfig.scala new file mode 100644 index 00000000000..a85b734bad6 --- /dev/null +++ b/common/config/src/main/scala/org/apache/texera/config/LLMConfig.scala @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package org.apache.texera.config + +import com.typesafe.config.{Config, ConfigFactory} + +object LLMConfig { + private val conf: Config = ConfigFactory.parseResources("llm.conf").resolve() + + // LLM Service Configuration + val baseUrl: String = conf.getString("llm.base-url") + val masterKey: String = conf.getString("llm.master-key") +} From 5af7fe05313883787bea962431732e11348dbd05 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Wed, 5 Nov 2025 11:30:03 -0800 Subject: [PATCH 099/158] fix the agent chat style --- .../agent-chat/agent-chat.component.html | 12 ++- .../agent-chat/agent-chat.component.scss | 2 +- .../agent-chat/agent-chat.component.ts | 44 ++++++++++- .../copilot/texera-copilot-manager.service.ts | 10 ++- .../service/copilot/texera-copilot.ts | 78 +++++++++++++++---- 5 files changed, 123 insertions(+), 23 deletions(-) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html index fd436ebedbf..0d27efb18a6 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html @@ -22,8 +22,14 @@
    - +
    + Model: {{ agentInfo.modelType }} - -
    -
    diff --git a/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.scss b/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.scss deleted file mode 100644 index a5e61d70663..00000000000 --- a/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.scss +++ /dev/null @@ -1,81 +0,0 @@ -.action-plan-feedback-panel { - position: absolute; - background: white; - border: 2px solid rgba(79, 195, 255, 0.8); - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - min-width: 320px; - max-width: 400px; - z-index: 1000; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - - .panel-header { - background: rgba(79, 195, 255, 0.1); - border-bottom: 1px solid rgba(79, 195, 255, 0.3); - padding: 12px 16px; - font-weight: 600; - font-size: 14px; - color: #1890ff; - } - - .panel-body { - padding: 16px; - - .summary-section { - margin-bottom: 16px; - - label { - display: block; - font-weight: 600; - font-size: 12px; - color: #595959; - margin-bottom: 8px; - text-transform: uppercase; - letter-spacing: 0.5px; - } - - p { - margin: 0; - font-size: 14px; - color: #262626; - line-height: 1.6; - padding: 8px 12px; - background: #f5f5f5; - border-radius: 4px; - border-left: 3px solid #1890ff; - } - } - - .feedback-section { - label { - display: block; - font-weight: 600; - font-size: 12px; - color: #595959; - margin-bottom: 8px; - text-transform: uppercase; - letter-spacing: 0.5px; - } - - textarea { - width: 100%; - resize: vertical; - font-size: 13px; - } - } - } - - .panel-footer { - padding: 12px 16px; - border-top: 1px solid #e8e8e8; - display: flex; - justify-content: flex-end; - gap: 8px; - - button { - display: flex; - align-items: center; - gap: 6px; - } - } -} diff --git a/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.ts b/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.ts deleted file mode 100644 index 0b7f01bf2d9..00000000000 --- a/frontend/src/app/workspace/component/action-plan-feedback/action-plan-feedback.component.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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 { Component, Input } from "@angular/core"; -import { ActionPlanService } from "../../service/action-plan/action-plan.service"; - -@Component({ - selector: "texera-action-plan-feedback", - templateUrl: "./action-plan-feedback.component.html", - styleUrls: ["./action-plan-feedback.component.scss"], -}) -export class ActionPlanFeedbackComponent { - @Input() summary: string = ""; - @Input() left: number = 0; - @Input() top: number = 0; - - public rejectMessage: string = ""; - - constructor(private actionPlanService: ActionPlanService) {} - - public onAccept(): void { - this.actionPlanService.acceptPlan(); - } - - public onReject(): void { - const message = this.rejectMessage.trim() || "I don't want this action plan."; - this.actionPlanService.rejectPlan(message); - } -} diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html deleted file mode 100644 index de39dfe5c3e..00000000000 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.html +++ /dev/null @@ -1,146 +0,0 @@ - - -
    - -
    -
    -

    {{ actionPlan.summary }}

    -
    - - - {{ actionPlan.agentName }} - - - - {{ actionPlan.createdAt | date : "short" }} - - - {{ getStatusLabel() }} - -
    -
    -
    - - -
    - -
    - - -
    -
    Tasks:
    - - -
    -
    - - -
    -
    -
    {{ task.description }}
    -
    -
    -
    -
    -
    - - - - - -
    -
    - - Creates a dedicated agent to execute this action plan -
    - -
    - - -
    -
    -
    diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss deleted file mode 100644 index 5a796865b03..00000000000 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.scss +++ /dev/null @@ -1,155 +0,0 @@ -/** - * 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. - */ - -.action-plan-view { - padding: 16px; - background: transparent; - border: 2px dashed #69b7ff; - border-radius: 8px; - margin-bottom: 12px; - - .plan-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 16px; - - .plan-info { - flex: 1; - - h4 { - margin: 0 0 8px 0; - font-size: 16px; - font-weight: 600; - } - - .plan-meta { - display: flex; - gap: 16px; - font-size: 12px; - color: #8c8c8c; - align-items: center; - - span { - display: flex; - align-items: center; - gap: 4px; - - i { - font-size: 12px; - } - } - } - } - } - - .progress-section { - margin-bottom: 16px; - } - - .tasks-section { - margin-bottom: 16px; - - h5 { - margin: 0 0 8px 0; - font-size: 14px; - font-weight: 500; - } - - .task-item { - display: flex; - gap: 12px; - width: 100%; - padding: 8px; - border-radius: 4px; - transition: background-color 0.2s; - - &:hover { - background-color: #f0f7ff; - } - - .task-checkbox { - flex-shrink: 0; - font-size: 18px; - - .task-completed { - color: #52c41a; - } - - .task-pending { - color: #d9d9d9; - } - } - - .task-content { - flex: 1; - - .task-description { - font-size: 14px; - } - } - } - } - - .feedback-section { - margin-bottom: 16px; - } - - .controls-section { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid #f0f0f0; - - .new-agent-toggle { - margin-bottom: 12px; - display: flex; - align-items: center; - gap: 12px; - - .toggle-hint { - font-size: 12px; - color: #8c8c8c; - font-style: italic; - } - } - - .feedback-input { - margin-bottom: 12px; - - label { - display: block; - margin-bottom: 4px; - font-size: 12px; - font-weight: 500; - } - } - - .action-buttons { - display: flex; - gap: 8px; - justify-content: flex-end; - - button { - i { - margin-right: 4px; - } - } - } - } -} diff --git a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts b/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts deleted file mode 100644 index 102c3c594ae..00000000000 --- a/frontend/src/app/workspace/component/action-plan-view/action-plan-view.component.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * 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 { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from "@angular/core"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { ActionPlan, ActionPlanStatus, ActionPlanTask } from "../../service/action-plan/action-plan.service"; -import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; - -@UntilDestroy() -@Component({ - selector: "texera-action-plan-view", - templateUrl: "./action-plan-view.component.html", - styleUrls: ["./action-plan-view.component.scss"], -}) -export class ActionPlanViewComponent implements OnInit { - @Input() actionPlan!: ActionPlan; - @Input() showFeedbackControls: boolean = false; - @Output() userDecision = new EventEmitter<{ - accepted: boolean; - message: string; - createNewActor?: boolean; - planId?: string; - }>(); - - public rejectMessage: string = ""; - public runInNewAgent: boolean = false; - public ActionPlanStatus = ActionPlanStatus; - public taskCompletionStates: { [operatorId: string]: boolean } = {}; - - constructor(private workflowActionService: WorkflowActionService) {} - - ngOnInit(): void { - if (!this.actionPlan) { - return; - } - - // Subscribe to task completion changes - this.actionPlan.tasks.forEach((task, operatorId) => { - this.taskCompletionStates[operatorId] = task.completed$.value; - task.completed$.pipe(untilDestroyed(this)).subscribe(completed => { - this.taskCompletionStates[operatorId] = completed; - }); - }); - } - - /** - * Handle user acceptance of the action plan. - */ - public onAccept(): void { - const userFeedback = this.rejectMessage.trim(); - this.userDecision.emit({ - accepted: true, - message: `Action plan ${this.actionPlan.id} accepted. Feedback: ${userFeedback}`, - createNewActor: this.runInNewAgent, - planId: this.actionPlan.id, - }); - } - - /** - * Handle user rejection with optional feedback message. - */ - public onReject(): void { - const userFeedback = this.rejectMessage.trim(); - - this.userDecision.emit({ - accepted: false, - message: `Action plan ${this.actionPlan.id} rejected. Feedback: ${userFeedback}`, - planId: this.actionPlan.id, - }); - - this.rejectMessage = ""; - } - - /** - * Show halo effect on operator when hovering over its task. - */ - public onTaskHover(operatorId: string, isHovering: boolean): void { - const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorId); - if (!operator) { - return; - } - - const jointGraphWrapper = this.workflowActionService.getJointGraphWrapper(); - if (!jointGraphWrapper) { - return; - } - - const paper = jointGraphWrapper.getMainJointPaper(); - const operatorElement = paper.getModelById(operatorId); - - if (operatorElement) { - if (isHovering) { - // Add highlight effect by changing stroke attributes - operatorElement.attr({ - "rect.body": { - stroke: "#69b7ff", - "stroke-width": 4, - }, - }); - } else { - // Restore default stroke attributes - operatorElement.attr({ - "rect.body": { - stroke: "#CFCFCF", - "stroke-width": 2, - }, - }); - } - } - } - - /** - * Get display label for current plan status. - */ - public getStatusLabel(): string { - const status = this.actionPlan.status$.value; - switch (status) { - case ActionPlanStatus.PENDING: - return "Pending Approval"; - case ActionPlanStatus.ACCEPTED: - return "In Progress"; - case ActionPlanStatus.REJECTED: - return "Rejected"; - case ActionPlanStatus.COMPLETED: - return "Completed"; - default: - return status; - } - } - - /** - * Get display color for current plan status. - */ - public getStatusColor(): string { - const status = this.actionPlan.status$.value; - switch (status) { - case ActionPlanStatus.PENDING: - return "warning"; - case ActionPlanStatus.ACCEPTED: - return "processing"; - case ActionPlanStatus.REJECTED: - return "error"; - case ActionPlanStatus.COMPLETED: - return "success"; - default: - return "default"; - } - } - - /** - * Get tasks as array for template iteration. - */ - public get tasksArray(): ActionPlanTask[] { - return Array.from(this.actionPlan.tasks.values()); - } - - /** - * Calculate completion percentage based on finished tasks. - */ - public getProgressPercentage(): number { - if (this.actionPlan.tasks.size === 0) return 0; - const tasksArray = Array.from(this.actionPlan.tasks.values()); - const completedCount = tasksArray.filter(t => t.completed$.value).length; - return Math.round((completedCount / this.actionPlan.tasks.size) * 100); - } -} diff --git a/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.html b/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.html deleted file mode 100644 index 17950d017b8..00000000000 --- a/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.html +++ /dev/null @@ -1,74 +0,0 @@ - - -
    -
    - -
    - - -
    - -
    - - -
    -
    -
    - -
    - -
    -
    -
    diff --git a/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.scss b/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.scss deleted file mode 100644 index 5c8db580d25..00000000000 --- a/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.scss +++ /dev/null @@ -1,64 +0,0 @@ -/** - * 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. - */ - -.action-plans-tab { - height: 100%; - display: flex; - flex-direction: column; - padding: 16px; - - .tab-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - padding-bottom: 12px; - border-bottom: 1px solid #f0f0f0; - - h3 { - margin: 0; - font-size: 18px; - font-weight: 600; - } - } - - .empty-state { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - } - - .action-plans-list { - flex: 1; - overflow-y: auto; - - .action-plan-container { - position: relative; - margin-bottom: 16px; - - .plan-header-bar { - position: absolute; - top: 8px; - right: 8px; - z-index: 10; - } - } - } -} diff --git a/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.ts b/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.ts deleted file mode 100644 index f5c6132a16c..00000000000 --- a/frontend/src/app/workspace/component/agent-panel/action-plans-tab/action-plans-tab.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 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 { Component, OnInit, OnDestroy } from "@angular/core"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { ActionPlan, ActionPlanService } from "../../../service/action-plan/action-plan.service"; - -@UntilDestroy() -@Component({ - selector: "texera-action-plans-tab", - templateUrl: "./action-plans-tab.component.html", - styleUrls: ["./action-plans-tab.component.scss"], -}) -export class ActionPlansTabComponent implements OnInit, OnDestroy { - public actionPlans: ActionPlan[] = []; - - constructor(private actionPlanService: ActionPlanService) {} - - ngOnInit(): void { - // Subscribe to action plans updates - this.actionPlanService - .getActionPlansStream() - .pipe(untilDestroyed(this)) - .subscribe(plans => { - // Sort plans by creation date (newest first) - this.actionPlans = [...plans].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); - }); - } - - ngOnDestroy(): void { - // Cleanup handled by UntilDestroy decorator - } - - /** - * Delete an action plan - */ - public deleteActionPlan(planId: string, event: Event): void { - event.stopPropagation(); - if (confirm("Are you sure you want to delete this action plan?")) { - this.actionPlanService.deleteActionPlan(planId); - } - } - - /** - * Clear all action plans - */ - public clearAllActionPlans(): void { - if (confirm("Are you sure you want to clear all action plans?")) { - this.actionPlanService.clearAllActionPlans(); - } - } -} diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html index 0d27efb18a6..8a52b28f578 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html @@ -46,16 +46,6 @@
    - -
    - Planning Mode - -
    -
    @@ -157,25 +147,6 @@ Thinking...
    - - -
    -
    - - {{ agentInfo.name }} -
    -
    - -
    -
    diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts index 0827a5bf95c..a187c09b13e 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -21,8 +21,6 @@ import { Component, ViewChild, ElementRef, Input, OnInit, AfterViewChecked } fro import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { CopilotState, AgentUIMessage } from "../../../service/copilot/texera-copilot"; import { AgentInfo, TexeraCopilotManagerService } from "../../../service/copilot/texera-copilot-manager.service"; -import { ActionPlan, ActionPlanService } from "../../../service/action-plan/action-plan.service"; -import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; @UntilDestroy() @@ -38,9 +36,7 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { public agentResponses: AgentUIMessage[] = []; public currentMessage = ""; - public pendingActionPlan: ActionPlan | null = null; private shouldScrollToBottom = false; - public planningMode = false; public isDetailsModalVisible = false; public selectedResponse: AgentUIMessage | null = null; public hoveredMessageIndex: number | null = null; @@ -50,9 +46,7 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { public agentState: CopilotState = CopilotState.UNAVAILABLE; constructor( - private actionPlanService: ActionPlanService, private copilotManagerService: TexeraCopilotManagerService, - private workflowActionService: WorkflowActionService, private notificationService: NotificationService ) {} @@ -61,8 +55,6 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { return; } - this.planningMode = this.copilotManagerService.getPlanningMode(this.agentInfo.id); - // Subscribe to agent responses this.copilotManagerService .getAgentResponsesObservable(this.agentInfo.id) @@ -72,19 +64,6 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { this.shouldScrollToBottom = true; }); - // Subscribe to pending action plans - this.actionPlanService - .getPendingActionPlanStream() - .pipe(untilDestroyed(this)) - .subscribe(plan => { - if (plan && plan.agentId === this.agentInfo.id) { - this.pendingActionPlan = plan; - this.shouldScrollToBottom = true; - } else if (plan === null || (plan && plan.agentId !== this.agentInfo.id)) { - this.pendingActionPlan = null; - } - }); - // Subscribe to agent state changes this.copilotManagerService .getAgentStateObservable(this.agentInfo.id) @@ -250,78 +229,4 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { public isConnected(): boolean { return this.copilotManagerService.isAgentConnected(this.agentInfo.id); } - - public onPlanningModeChange(value: boolean): void { - this.copilotManagerService.setPlanningMode(this.agentInfo.id, value); - } - - /** - * Handle user decision on action plan (acceptance or rejection). - */ - public onUserDecision(decision: { - accepted: boolean; - message: string; - createNewActor?: boolean; - planId?: string; - }): void { - this.pendingActionPlan = null; - - if (decision.planId) { - if (decision.accepted) { - this.actionPlanService.acceptPlan(decision.planId); - - if (decision.createNewActor) { - this.copilotManagerService - .createAgent("claude-3.7", `Actor for Plan ${decision.planId}`) - .then(newAgent => { - const initialMessage = `Please work on action plan with id: ${decision.planId}`; - this.copilotManagerService - .sendMessage(newAgent.id, initialMessage) - .pipe(untilDestroyed(this)) - .subscribe({ - next: () => { - this.notificationService.info(`Actor agent started for plan: ${decision.planId}`); - }, - error: (error: unknown) => { - this.notificationService.error(`Error starting actor agent: ${error}`); - }, - }); - }) - .catch((error: unknown) => { - this.notificationService.error(`Failed to create actor agent: ${error}`); - }); - } else { - const executionMessage = "I have accepted your action plan. Please proceed with executing it."; - this.copilotManagerService - .sendMessage(this.agentInfo.id, executionMessage) - .pipe(untilDestroyed(this)) - .subscribe({ - error: (error: unknown) => { - this.notificationService.error(`Error sending acceptance message: ${error}`); - }, - }); - } - } else { - const feedbackMatch = decision.message.match(/Feedback: (.+)$/); - const userFeedback = feedbackMatch ? feedbackMatch[1] : "I don't want this action plan."; - - const actionPlan = this.actionPlanService.getActionPlan(decision.planId); - if (actionPlan) { - this.workflowActionService.deleteOperatorsAndLinks(actionPlan.operatorIds); - } - - this.actionPlanService.rejectPlan(userFeedback, decision.planId); - - const rejectionMessage = `I have rejected your action plan. Feedback: ${userFeedback}`; - this.copilotManagerService - .sendMessage(this.agentInfo.id, rejectionMessage) - .pipe(untilDestroyed(this)) - .subscribe({ - error: (error: unknown) => { - this.notificationService.error(`Error sending rejection feedback: ${error}`); - }, - }); - } - } - } } diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts index 9d7a9db2359..ca76a5d2f22 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts @@ -43,7 +43,6 @@ import { isDefined } from "../../../common/util/predicate"; import { GuiConfigService } from "../../../common/service/gui-config.service"; import { line, curveCatmullRomClosed } from "d3-shape"; import concaveman from "concaveman"; -import { ActionPlanService } from "../../service/action-plan/action-plan.service"; // jointjs interactive options for enabling and disabling interactivity // https://resources.jointjs.com/docs/jointjs/v3.2/joint.html#dia.Paper.prototype.options.interactive @@ -113,8 +112,7 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy private router: Router, public nzContextMenu: NzContextMenuService, private elementRef: ElementRef, - private config: GuiConfigService, - private actionPlanService: ActionPlanService + private config: GuiConfigService ) { this.wrapper = this.workflowActionService.getJointGraphWrapper(); } @@ -166,7 +164,6 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy this.registerPortDisplayNameChangeHandler(); this.handleOperatorStatisticsUpdate(); this.handleRegionEvents(); - this.handleActionPlanHighlight(); this.handleOperatorSuggestionHighlightEvent(); this.handleElementDelete(); this.handleElementSelectAll(); @@ -408,147 +405,6 @@ export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy regionElement.attr("body/d", line().curve(curveCatmullRomClosed)(concaveman(points, 2, 0) as [number, number][])); } - private handleActionPlanHighlight(): void { - // Define ActionPlan JointJS element with transparent fill and border only - const ActionPlan = joint.dia.Element.define( - "action-plan", - { - attrs: { - body: { - fill: "transparent", - stroke: "rgba(79,195,255,0.6)", - strokeWidth: 2, - strokeDasharray: "5,5", - pointerEvents: "none", - class: "action-plan", - }, - }, - }, - { - markup: [{ tagName: "path", selector: "body" }], - } - ); - - // Track current highlight element and cleanup handler - let currentElement: joint.dia.Element | null = null; - let currentPositionHandler: ((operator: joint.dia.Cell) => void) | null = null; - - // Subscribe to action plan highlight events - this.actionPlanService - .getActionPlanHighlightStream() - .pipe(untilDestroyed(this)) - .subscribe(actionPlan => { - // Get operator elements from IDs - const operators = actionPlan.operatorIds.map(id => this.paper.getModelById(id)).filter(op => op !== undefined); - - if (operators.length === 0) { - return; // No valid operators found - } - - // Create action plan highlight element - currentElement = new ActionPlan(); - this.paper.model.addCell(currentElement); - - // Update the highlight to wrap around operators - this.updateActionPlanElement(currentElement, operators); - - // Listen to operator position changes to update the highlight - currentPositionHandler = (operator: joint.dia.Cell) => { - if (operators.includes(operator) && currentElement) { - this.updateActionPlanElement(currentElement, operators); - } - }; - this.paper.model.on("change:position", currentPositionHandler); - }); - - // Subscribe to cleanup stream - triggered when user accepts/rejects - this.actionPlanService - .getCleanupStream() - .pipe(untilDestroyed(this)) - .subscribe(() => { - // Remove highlight element - if (currentElement) { - currentElement.remove(); - currentElement = null; - } - - // Remove position handler - if (currentPositionHandler) { - this.paper.model.off("change:position", currentPositionHandler); - currentPositionHandler = null; - } - }); - } - - /** - * Calculate bounding box that encompasses all operators - */ - private getOperatorsBoundingBox(operators: joint.dia.Cell[]): { - x: number; - y: number; - width: number; - height: number; - } { - const bboxes = operators.map(op => op.getBBox()); - const minX = Math.min(...bboxes.map(b => b.x)); - const minY = Math.min(...bboxes.map(b => b.y)); - const maxX = Math.max(...bboxes.map(b => b.x + b.width)); - const maxY = Math.max(...bboxes.map(b => b.y + b.height)); - - return { - x: minX, - y: minY, - width: maxX - minX, - height: maxY - minY, - }; - } - - /** - * Calculate panel position to the right of the operators, considering canvas boundaries - */ - private calculatePanelPosition(bbox: { x: number; y: number; width: number; height: number }): { - x: number; - y: number; - } { - const panelWidth = 400; - const panelOffset = 40; // Space between operators and panel - - // Try to position to the right of operators - let x = bbox.x + bbox.width + panelOffset; - let y = bbox.y; - - // If panel would go off the right edge, position it to the left - const paperWidth = this.paper.getComputedSize().width; - if (x + panelWidth > paperWidth) { - x = bbox.x - panelWidth - panelOffset; - } - - // Ensure panel stays within vertical bounds - const paperHeight = this.paper.getComputedSize().height; - if (y < 20) { - y = 20; - } else if (y + 300 > paperHeight) { - // Approximate panel height - y = paperHeight - 320; - } - - return { x, y }; - } - - private updateActionPlanElement(element: joint.dia.Element, operators: joint.dia.Cell[]) { - const points = operators.flatMap(op => { - const { x, y, width, height } = op.getBBox(), - padding = 20; // Slightly larger padding than regions - return [ - [x - padding, y - padding], - [x + width + padding, y - padding], - [x - padding, y + height + padding + 10], - [x + width + padding, y + height + padding + 10], - ]; - }); - element.attr("body/d", line().curve(curveCatmullRomClosed)(concaveman(points, 2, 0) as [number, number][])); - } - /** * Handles restore offset default event by translating jointJS paper * back to original position diff --git a/frontend/src/app/workspace/component/workspace.component.ts b/frontend/src/app/workspace/component/workspace.component.ts index a11e38a6ef9..001dec456e3 100644 --- a/frontend/src/app/workspace/component/workspace.component.ts +++ b/frontend/src/app/workspace/component/workspace.component.ts @@ -42,7 +42,6 @@ import { WorkflowCompilingService } from "../service/compile-workflow/workflow-c import { DASHBOARD_USER_WORKSPACE } from "../../app-routing.constant"; import { GuiConfigService } from "../../common/service/gui-config.service"; import { checkIfWorkflowBroken } from "../../common/util/workflow-check"; -import { AgentActionProgressDisplayService } from "../service/copilot/agent-action-progress-display.service"; export const SAVE_DEBOUNCE_TIME_IN_MS = 5000; @@ -76,7 +75,6 @@ export class WorkspaceComponent implements AfterViewInit, OnInit, OnDestroy { private workflowCompilingService: WorkflowCompilingService, private workflowConsoleService: WorkflowConsoleService, private operatorReuseCacheStatusService: OperatorReuseCacheStatusService, - private agentActionProgressDisplayService: AgentActionProgressDisplayService, // end of additional services private undoRedoService: UndoRedoService, private workflowPersistService: WorkflowPersistService, diff --git a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts b/frontend/src/app/workspace/service/action-plan/action-plan.service.ts deleted file mode 100644 index fb123a1e11f..00000000000 --- a/frontend/src/app/workspace/service/action-plan/action-plan.service.ts +++ /dev/null @@ -1,289 +0,0 @@ -/** - * 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 { Subject, Observable, BehaviorSubject } from "rxjs"; - -/** - * Interface for an action plan highlight event - */ -export interface ActionPlanHighlight { - operatorIds: string[]; - linkIds: string[]; - summary: string; -} - -/** - * User feedback for an action plan - */ -export interface ActionPlanFeedback { - accepted: boolean; - message?: string; // Optional message when rejecting -} - -/** - * Status of an action plan - */ -export enum ActionPlanStatus { - PENDING = "pending", // Waiting for user approval - ACCEPTED = "accepted", // User accepted, being executed - REJECTED = "rejected", // User rejected - COMPLETED = "completed", // Execution completed -} - -/** - * Individual operator task within an action plan - */ -export interface ActionPlanTask { - operatorId: string; - description: string; - agentId: string | null; // ID of the agent assigned to this task, null if no agent assigned - completed$: BehaviorSubject; -} - -/** - * Complete Action Plan data structure - */ -export interface ActionPlan { - id: string; // Unique identifier for the action plan - agentId: string; // ID of the agent that created this plan - agentName: string; // Name of the agent - executorAgentId: string; // ID of the agent that will execute/handle feedback for this plan (can be different from creator) - summary: string; // Overall summary of the action plan - tasks: Map; // Map of operatorId to task - status$: BehaviorSubject; // Current status - createdAt: Date; // Creation timestamp - userFeedback?: string; // User's feedback message (if rejected) - operatorIds: string[]; // For highlighting - linkIds: string[]; // For highlighting -} - -/** - * Service to manage action plans, highlights, and user feedback - * Handles the interactive flow: show plan -> wait for user decision -> execute -> track progress - */ -@Injectable({ - providedIn: "root", -}) -export class ActionPlanService { - private actionPlanHighlightSubject = new Subject(); - private cleanupSubject = new Subject(); - - // Action plan storage - private actionPlans = new Map(); - private actionPlansSubject = new BehaviorSubject([]); - private pendingActionPlanSubject = new BehaviorSubject(null); - - constructor() {} - - /** - * Get action plan highlight stream - */ - public getActionPlanHighlightStream() { - return this.actionPlanHighlightSubject.asObservable(); - } - - /** - * Get cleanup stream - emits when user provides feedback (accept/reject) - */ - public getCleanupStream() { - return this.cleanupSubject.asObservable(); - } - - /** - * Get all action plans as observable - */ - public getActionPlansStream(): Observable { - return this.actionPlansSubject.asObservable(); - } - - /** - * Get pending action plan stream (for showing in agent chat) - */ - public getPendingActionPlanStream(): Observable { - return this.pendingActionPlanSubject.asObservable(); - } - - /** - * Get all action plans - */ - public getAllActionPlans(): ActionPlan[] { - return Array.from(this.actionPlans.values()); - } - - /** - * Get a specific action plan by ID - */ - public getActionPlan(id: string): ActionPlan | undefined { - return this.actionPlans.get(id); - } - - /** - * Create a new action plan - */ - public createActionPlan( - agentId: string, - agentName: string, - summary: string, - tasks: Array<{ operatorId: string; description: string; agentId?: string | null }>, - operatorIds: string[], - linkIds: string[], - executorAgentId?: string // Optional: defaults to agentId if not specified - ): ActionPlan { - const id = this.generateId(); - - // Create tasks map - const tasksMap = new Map(); - tasks.forEach(task => { - tasksMap.set(task.operatorId, { - operatorId: task.operatorId, - description: task.description, - agentId: task.agentId !== undefined ? task.agentId : agentId, // Default to plan's agentId if not specified - completed$: new BehaviorSubject(false), - }); - }); - - const actionPlan: ActionPlan = { - id, - agentId, - agentName, - executorAgentId: executorAgentId || agentId, // Default to creator if not specified - summary, - tasks: tasksMap, - status$: new BehaviorSubject(ActionPlanStatus.PENDING), - createdAt: new Date(), - operatorIds, - linkIds, - }; - - this.actionPlans.set(id, actionPlan); - this.emitActionPlans(); - this.pendingActionPlanSubject.next(actionPlan); - - // Emit highlight event for the workflow editor - this.actionPlanHighlightSubject.next({ operatorIds, linkIds, summary }); - - return actionPlan; - } - - /** - * Update action plan status - */ - public updateActionPlanStatus(id: string, status: ActionPlanStatus): void { - const plan = this.actionPlans.get(id); - if (plan) { - plan.status$.next(status); - this.emitActionPlans(); - } - } - - /** - * Update a task's completion status - */ - public updateTaskCompletion(planId: string, operatorId: string, completed: boolean): void { - const plan = this.actionPlans.get(planId); - if (plan) { - const task = plan.tasks.get(operatorId); - if (task) { - task.completed$.next(completed); - this.emitActionPlans(); - - // Check if all tasks are completed - const allCompleted = Array.from(plan.tasks.values()).every(t => t.completed$.value); - if (allCompleted && plan.status$.value === ActionPlanStatus.ACCEPTED) { - this.updateActionPlanStatus(planId, ActionPlanStatus.COMPLETED); - } - } - } - } - - /** - * Delete an action plan - */ - public deleteActionPlan(id: string): boolean { - const plan = this.actionPlans.get(id); - if (plan) { - // Complete all subjects - plan.status$.complete(); - plan.tasks.forEach(task => task.completed$.complete()); - this.actionPlans.delete(id); - this.emitActionPlans(); - return true; - } - return false; - } - - /** - * Clear all action plans - */ - public clearAllActionPlans(): void { - this.actionPlans.forEach(plan => { - plan.status$.complete(); - plan.tasks.forEach(task => task.completed$.complete()); - }); - this.actionPlans.clear(); - this.emitActionPlans(); - } - - /** - * User accepted the action plan - */ - public acceptPlan(planId?: string): void { - // Trigger cleanup (remove highlight) - this.cleanupSubject.next(); - - // Update plan status if planId provided - if (planId) { - this.updateActionPlanStatus(planId, ActionPlanStatus.ACCEPTED); - this.pendingActionPlanSubject.next(null); - } - } - - /** - * User rejected the action plan with optional feedback message - */ - public rejectPlan(message?: string, planId?: string): void { - // Trigger cleanup (remove highlight) - this.cleanupSubject.next(); - - // Update plan status if planId provided - if (planId) { - const plan = this.actionPlans.get(planId); - if (plan) { - plan.userFeedback = message; - this.updateActionPlanStatus(planId, ActionPlanStatus.REJECTED); - } - this.pendingActionPlanSubject.next(null); - } - } - - /** - * Generate a unique ID for action plans - */ - private generateId(): string { - return `action-plan-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - } - - /** - * Emit the current list of action plans - */ - private emitActionPlans(): void { - this.actionPlansSubject.next(this.getAllActionPlans()); - } -} diff --git a/frontend/src/app/workspace/service/copilot/agent-action-progress-display.service.ts b/frontend/src/app/workspace/service/copilot/agent-action-progress-display.service.ts deleted file mode 100644 index 42cc83d8ad4..00000000000 --- a/frontend/src/app/workspace/service/copilot/agent-action-progress-display.service.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * 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 { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; -import { ActionPlan, ActionPlanStatus, ActionPlanService } from "../action-plan/action-plan.service"; -import { Subscription } from "rxjs"; - -/** - * Stores subscriptions and operator IDs for a plan - */ -interface PlanProgressTracking { - subscriptions: Subscription[]; - operatorIds: string[]; -} - -/** - * Service to display agent action progress on operators - * Shows agent names and progress indicators on operators during action plan execution - */ -@Injectable({ - providedIn: "root", -}) -export class AgentActionProgressDisplayService { - // Track subscriptions and operator IDs per plan ID - private planTracking: Map = new Map(); - - constructor( - private workflowActionService: WorkflowActionService, - private actionPlanService: ActionPlanService - ) { - this.initializeMonitoring(); - } - - /** - * Initialize monitoring of all action plans - * Shows progress for all active plans - */ - private initializeMonitoring(): void { - this.actionPlanService.getActionPlansStream().subscribe(plans => { - // Get all plan IDs currently being tracked - const currentPlanIds = new Set(this.planTracking.keys()); - const newPlanIds = new Set(plans.map(p => p.id)); - - // Remove plans that no longer exist - currentPlanIds.forEach(planId => { - if (!newPlanIds.has(planId)) { - this.clearPlanProgress(planId); - } - }); - - // Update or add plans - plans.forEach(plan => { - // Only show progress for accepted plans - if (plan.status$.value === ActionPlanStatus.ACCEPTED) { - this.showPlanProgress(plan); - } else { - this.clearPlanProgress(plan.id); - } - }); - }); - } - - /** - * Show progress indicators for a specific action plan - */ - private showPlanProgress(plan: ActionPlan): void { - // Clear existing subscriptions for this plan if any - this.clearPlanProgress(plan.id); - - const jointWrapper = this.workflowActionService.getJointGraphWrapper(); - const subscriptions: Subscription[] = []; - const operatorIds: string[] = []; - - // Display progress for each task in the plan - plan.tasks.forEach((task, operatorId) => { - if (task.agentId) { - const agentName = this.getAgentName(task.agentId, plan); - - // Subscribe to task completion status - const subscription = task.completed$.subscribe(isCompleted => { - jointWrapper.setAgentActionProgress(operatorId, agentName, isCompleted); - }); - - subscriptions.push(subscription); - operatorIds.push(operatorId); - - // Set initial state - jointWrapper.setAgentActionProgress(operatorId, agentName, task.completed$.value); - } - }); - - // Store subscriptions and operator IDs for this plan - this.planTracking.set(plan.id, { subscriptions, operatorIds }); - } - - /** - * Clear progress indicators for a specific plan - */ - private clearPlanProgress(planId: string): void { - const tracking = this.planTracking.get(planId); - if (tracking) { - // Unsubscribe from all task completion observables - tracking.subscriptions.forEach(sub => sub.unsubscribe()); - - // Clear the visual indicators on operators using stored operator IDs - const jointWrapper = this.workflowActionService.getJointGraphWrapper(); - tracking.operatorIds.forEach(operatorId => { - jointWrapper.clearAgentActionProgress(operatorId); - }); - - // Remove tracking for this plan - this.planTracking.delete(planId); - } - } - - /** - * Get agent name from agent ID - */ - private getAgentName(agentId: string, plan: ActionPlan): string { - // If the agent is the plan's creator - if (agentId === plan.agentId) { - return plan.agentName; - } - - // Otherwise, use a default format - return `Agent ${agentId}`; - } -} diff --git a/frontend/src/app/workspace/service/copilot/copilot-prompts.ts b/frontend/src/app/workspace/service/copilot/copilot-prompts.ts index 87185c79e71..0d2411eca6b 100644 --- a/frontend/src/app/workspace/service/copilot/copilot-prompts.ts +++ b/frontend/src/app/workspace/service/copilot/copilot-prompts.ts @@ -137,12 +137,3 @@ class ProcessTableOperator(UDFTableOperator): - Do things in small steps, it is NOT recommended to have a giant UDF to contains lots of logic. - If users give a very specific requirement, stick to users' requirement strictly `; - -export const PLANNING_MODE_PROMPT = ` -## PLANNING MODE IS ENABLED - -**IMPORTANT:** You are currently in PLANNING MODE. This means: -1. **You MUST use the actionPlan tool to generate an action plan FIRST** before making any workflow modifications -2. Do NOT directly add, delete, or modify operators without creating an action plan first -3. The plan should be small and atomic, focusing on either user's request of certain dimension -`; diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index 798420923cd..72076ace884 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -29,7 +29,6 @@ import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.ser import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; import { ValidationWorkflowService } from "../validation/validation-workflow.service"; -import { ActionPlanService } from "../action-plan/action-plan.service"; import { NotificationService } from "../../../common/service/notification/notification.service"; import { ComputingUnitStatusService } from "../computing-unit-status/computing-unit-status.service"; import { WorkflowConsoleService } from "../workflow-console/workflow-console.service"; @@ -245,22 +244,6 @@ export class TexeraCopilotManagerService { return agent.instance.isConnected(); } - public setPlanningMode(agentId: string, planningMode: boolean): void { - const agent = this.agents.get(agentId); - if (!agent) { - throw new Error(`Agent with ID ${agentId} not found`); - } - agent.instance.setPlanningMode(planningMode); - } - - public getPlanningMode(agentId: string): boolean { - const agent = this.agents.get(agentId); - if (!agent) { - throw new Error(`Agent with ID ${agentId} not found`); - } - return agent.instance.getPlanningMode(); - } - public getSystemInfo(agentId: string): { systemPrompt: string; tools: Array<{ name: string; description: string; inputSchema: any }>; @@ -293,7 +276,6 @@ export class TexeraCopilotManagerService { WorkflowResultService, WorkflowCompilingService, ValidationWorkflowService, - ActionPlanService, NotificationService, ComputingUnitStatusService, WorkflowConsoleService, diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 2010677c84d..045095cb6df 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -23,12 +23,6 @@ import { WorkflowActionService } from "../workflow-graph/model/workflow-action.s import { createAddOperatorTool, createAddLinkTool, - createActionPlanTool, - createUpdateActionPlanProgressTool, - createGetActionPlanTool, - createListActionPlansTool, - createDeleteActionPlanTool, - createUpdateActionPlanTool, createGetOperatorTool, createDeleteOperatorTool, createDeleteLinkTool, @@ -65,8 +59,7 @@ import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.ser import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; import { ValidationWorkflowService } from "../validation/validation-workflow.service"; -import { COPILOT_SYSTEM_PROMPT, PLANNING_MODE_PROMPT } from "./copilot-prompts"; -import { ActionPlanService } from "../action-plan/action-plan.service"; +import { COPILOT_SYSTEM_PROMPT } from "./copilot-prompts"; import { NotificationService } from "../../../common/service/notification/notification.service"; import { ComputingUnitStatusService } from "../computing-unit-status/computing-unit-status.service"; import { WorkflowConsoleService } from "../workflow-console/workflow-console.service"; @@ -117,8 +110,6 @@ export class TexeraCopilot { private state: CopilotState = CopilotState.UNAVAILABLE; private stateSubject = new BehaviorSubject(CopilotState.UNAVAILABLE); public state$ = this.stateSubject.asObservable(); - private shouldStopAfterActionPlan: boolean = false; - private planningMode: boolean = false; constructor( private workflowActionService: WorkflowActionService, @@ -129,7 +120,6 @@ export class TexeraCopilot { private workflowResultService: WorkflowResultService, private workflowCompilingService: WorkflowCompilingService, private validationWorkflowService: ValidationWorkflowService, - private actionPlanService: ActionPlanService, private notificationService: NotificationService, private computingUnitStatusService: ComputingUnitStatusService, private workflowConsoleService: WorkflowConsoleService @@ -146,14 +136,6 @@ export class TexeraCopilot { this.modelType = modelType; } - public setPlanningMode(planningMode: boolean): void { - this.planningMode = planningMode; - } - - public getPlanningMode(): boolean { - return this.planningMode; - } - /** * Update the state and emit to the observable. */ @@ -283,7 +265,6 @@ export class TexeraCopilot { } this.setState(CopilotState.GENERATING); - this.shouldStopAfterActionPlan = false; const userMessage: UserModelMessage = { role: "user", content: message }; this.messages.push(userMessage); @@ -300,23 +281,16 @@ export class TexeraCopilot { const tools = this.createWorkflowTools(); let isFirstStep = true; - const systemPrompt = this.planningMode - ? COPILOT_SYSTEM_PROMPT + "\n\n" + PLANNING_MODE_PROMPT - : COPILOT_SYSTEM_PROMPT; - const { response } = await generateText({ model: this.model, messages: this.messages, tools, - system: systemPrompt, + system: COPILOT_SYSTEM_PROMPT, stopWhen: ({ steps }) => { if (this.state === CopilotState.STOPPING) { this.notificationService.info(`Agent ${this.agentName} has stopped generation`); return true; } - if (this.shouldStopAfterActionPlan) { - return true; - } return stepCountIs(50)({ steps }); }, onStepFinish: ({ text, toolCalls, toolResults, usage }) => { @@ -324,10 +298,6 @@ export class TexeraCopilot { return; } - if (toolCalls && toolCalls.some((call: any) => call.toolName === "actionPlan")) { - this.shouldStopAfterActionPlan = true; - } - const stepResponse: AgentUIMessage = { role: "agent", content: text || "", @@ -376,21 +346,6 @@ export class TexeraCopilot { createAddOperatorTool(this.workflowActionService, this.workflowUtilService, this.operatorMetadataService) ); const addLinkTool = toolWithTimeout(createAddLinkTool(this.workflowActionService)); - const actionPlanTool = toolWithTimeout( - createActionPlanTool( - this.workflowActionService, - this.workflowUtilService, - this.operatorMetadataService, - this.actionPlanService, - this.agentId, - this.agentName - ) - ); - const updateActionPlanProgressTool = toolWithTimeout(createUpdateActionPlanProgressTool(this.actionPlanService)); - const getActionPlanTool = toolWithTimeout(createGetActionPlanTool(this.actionPlanService)); - const listActionPlansTool = toolWithTimeout(createListActionPlansTool(this.actionPlanService)); - const deleteActionPlanTool = toolWithTimeout(createDeleteActionPlanTool(this.actionPlanService)); - const updateActionPlanTool = toolWithTimeout(createUpdateActionPlanTool(this.actionPlanService)); const listOperatorIdsTool = toolWithTimeout(createListOperatorIdsTool(this.workflowActionService)); const listLinksTool = toolWithTimeout(createListLinksTool(this.workflowActionService)); const listAllOperatorTypesTool = toolWithTimeout(createListAllOperatorTypesTool(this.workflowUtilService)); @@ -439,7 +394,7 @@ export class TexeraCopilot { createGetComputingUnitStatusTool(this.computingUnitStatusService) ); - const baseTools: Record = { + return { addOperator: addOperatorTool, addLink: addLinkTool, deleteOperator: deleteOperatorTool, @@ -466,20 +421,6 @@ export class TexeraCopilot { getOperatorResultInfo: getOperatorResultInfoTool, getComputingUnitStatus: getComputingUnitStatusTool, }; - - if (this.planningMode) { - return { - ...baseTools, - actionPlan: actionPlanTool, - updateActionPlanProgress: updateActionPlanProgressTool, - getActionPlan: getActionPlanTool, - listActionPlans: listActionPlansTool, - deleteActionPlan: deleteActionPlanTool, - updateActionPlan: updateActionPlanTool, - }; - } else { - return baseTools; - } } public getAgentResponses(): AgentUIMessage[] { @@ -518,7 +459,7 @@ export class TexeraCopilot { } public getSystemPrompt(): string { - return this.planningMode ? COPILOT_SYSTEM_PROMPT + "\n\n" + PLANNING_MODE_PROMPT : COPILOT_SYSTEM_PROMPT; + return COPILOT_SYSTEM_PROMPT; } public getToolsInfo(): Array<{ name: string; description: string; inputSchema: any }> { diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index 966cada2619..ec1fad4ebd1 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -27,7 +27,6 @@ import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.ser import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; import { ValidationWorkflowService } from "../validation/validation-workflow.service"; -import { ActionPlanService } from "../action-plan/action-plan.service"; import { WorkflowConsoleService } from "../workflow-console/workflow-console.service"; // Tool execution timeout in milliseconds (2 minutes) @@ -198,406 +197,6 @@ export function createAddLinkTool(workflowActionService: WorkflowActionService) }); } -/** - * Create actionPlan tool for adding batch operators and links - */ -export function createActionPlanTool( - workflowActionService: WorkflowActionService, - workflowUtilService: WorkflowUtilService, - operatorMetadataService: OperatorMetadataService, - actionPlanService: ActionPlanService, - agentId: string = "", - agentName: string = "" -) { - return tool({ - name: "actionPlan", - description: - "Add a batch of operators and links to the workflow as part of an action plan. This tool is used to show the structure of what you plan to add without filling in detailed operator properties. It creates a workflow skeleton that demonstrates the planned data flow.", - inputSchema: z.object({ - summary: z.string().describe("A summary of what this action plan does"), - operators: z - .array( - z.object({ - operatorType: z.string().describe("Type of operator (e.g., 'CSVSource', 'Filter', 'Aggregate')"), - customDisplayName: z - .string() - .optional() - .describe("Brief custom name summarizing what this operator does in one sentence"), - description: z.string().optional().describe("Detailed description of what this operator will do"), - }) - ) - .describe("List of operators to add to the workflow"), - links: z - .array( - z.object({ - sourceOperatorId: z - .string() - .describe( - "ID of the source operator - can be either an existing operator ID from the workflow, or an index (e.g., '0', '1', '2') referring to operators in the plan array (0-based)" - ), - targetOperatorId: z - .string() - .describe( - "ID of the target operator - can be either an existing operator ID from the workflow, or an index (e.g., '0', '1', '2') referring to operators in the plan array (0-based)" - ), - sourcePortId: z.string().optional().describe("Port ID on source operator (e.g., 'output-0')"), - targetPortId: z.string().optional().describe("Port ID on target operator (e.g., 'input-0')"), - }) - ) - .describe("List of links to connect the operators"), - }), - execute: async (args: { - summary: string; - operators: Array<{ operatorType: string; customDisplayName?: string; description?: string }>; - links: Array<{ - sourceOperatorId: string; - targetOperatorId: string; - sourcePortId?: string; - targetPortId?: string; - }>; - }) => { - try { - // Clear previous highlights at start of tool execution - - // Validate all operator types exist - for (let i = 0; i < args.operators.length; i++) { - const operatorSpec = args.operators[i]; - if (!operatorMetadataService.operatorTypeExists(operatorSpec.operatorType)) { - return { - success: false, - error: `Unknown operator type at index ${i}: ${operatorSpec.operatorType}. Use listOperatorTypes tool to see available types.`, - }; - } - } - - // Helper function to resolve operator ID (can be existing ID or index string) - const resolveOperatorId = (idOrIndex: string, createdIds: string[]): string | null => { - // Check if it's a numeric index (referring to operators array) - const indexMatch = idOrIndex.match(/^(\d+)$/); - if (indexMatch) { - const index = parseInt(indexMatch[1], 10); - if (index >= 0 && index < createdIds.length) { - return createdIds[index]; - } - return null; // Invalid index - } - - // Otherwise, treat as existing operator ID - const existingOp = workflowActionService.getTexeraGraph().getOperator(idOrIndex); - return existingOp ? idOrIndex : null; - }; - - // Create all operators and store their IDs - const createdOperatorIds: string[] = []; - const existingOperators = workflowActionService.getTexeraGraph().getAllOperators(); - const startIndex = existingOperators.length; - - for (let i = 0; i < args.operators.length; i++) { - const operatorSpec = args.operators[i]; - - // Get a new operator predicate with default settings and optional custom display name - const operator = workflowUtilService.getNewOperatorPredicate( - operatorSpec.operatorType, - operatorSpec.customDisplayName - ); - - // Calculate a default position with better spacing for batch operations - const defaultX = 100 + ((startIndex + i) % 5) * 200; - const defaultY = 100 + Math.floor((startIndex + i) / 5) * 150; - const position = { x: defaultX, y: defaultY }; - - // Add the operator to the workflow - workflowActionService.addOperator(operator, position); - createdOperatorIds.push(operator.operatorID); - } - - // Create action plan with tasks - const tasks = args.operators.map((operatorSpec, index) => ({ - operatorId: createdOperatorIds[index], - description: operatorSpec.description || operatorSpec.customDisplayName || operatorSpec.operatorType, - })); - - // Create all links using the operator IDs - const createdLinkIds: string[] = []; - for (let i = 0; i < args.links.length; i++) { - const linkSpec = args.links[i]; - - // Resolve source and target operator IDs - const sourceOperatorId = resolveOperatorId(linkSpec.sourceOperatorId, createdOperatorIds); - const targetOperatorId = resolveOperatorId(linkSpec.targetOperatorId, createdOperatorIds); - - if (!sourceOperatorId) { - return { - success: false, - error: `Invalid source operator ID at link ${i}: '${linkSpec.sourceOperatorId}'. Must be either an existing operator ID or a valid index (0-${createdOperatorIds.length - 1}).`, - }; - } - - if (!targetOperatorId) { - return { - success: false, - error: `Invalid target operator ID at link ${i}: '${linkSpec.targetOperatorId}'. Must be either an existing operator ID or a valid index (0-${createdOperatorIds.length - 1}).`, - }; - } - - const sourcePId = linkSpec.sourcePortId || "output-0"; - const targetPId = linkSpec.targetPortId || "input-0"; - - const link: OperatorLink = { - linkID: `link_${Date.now()}_${Math.random()}`, - source: { - operatorID: sourceOperatorId, - portID: sourcePId, - }, - target: { - operatorID: targetOperatorId, - portID: targetPId, - }, - }; - - workflowActionService.addLink(link); - createdLinkIds.push(link.linkID); - } - - const actionPlan = actionPlanService.createActionPlan( - agentId, - agentName || "AI Agent", - args.summary, - tasks, - createdOperatorIds, - createdLinkIds - ); - - // Show copilot is adding these operators (after they're added to graph) - setTimeout(() => {}, 100); - - // Return the action plan info - user feedback will be handled via messages - return { - success: true, - summary: args.summary, - operatorIds: createdOperatorIds, - linkIds: createdLinkIds, - actionPlanId: actionPlan.id, - message: `Created action plan with ${createdOperatorIds.length} operators and ${createdLinkIds.length} links. Waiting for user feedback.`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create updateActionPlanProgress tool for marking tasks as complete - */ -export function createUpdateActionPlanProgressTool(actionPlanService: ActionPlanService) { - return tool({ - name: "updateActionPlanProgress", - description: - "Mark a specific task in an action plan as completed. Use this after you've finished configuring an operator from an accepted action plan.", - inputSchema: z.object({ - actionPlanId: z.string().describe("ID of the action plan"), - operatorId: z.string().describe("ID of the operator task to mark as complete"), - completed: z.boolean().describe("Whether the task is completed (true) or not (false)"), - }), - execute: async (args: { actionPlanId: string; operatorId: string; completed: boolean }) => { - try { - const plan = actionPlanService.getActionPlan(args.actionPlanId); - if (!plan) { - return { - success: false, - error: `Action plan with ID ${args.actionPlanId} not found`, - }; - } - - const task = plan.tasks.get(args.operatorId); - if (!task) { - return { - success: false, - error: `Task with operator ID ${args.operatorId} not found in action plan ${args.actionPlanId}`, - }; - } - - actionPlanService.updateTaskCompletion(args.actionPlanId, args.operatorId, args.completed); - - return { - success: true, - message: `Task for operator ${args.operatorId} marked as ${args.completed ? "completed" : "incomplete"}`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create getActionPlan tool for retrieving a specific action plan by ID - */ -export function createGetActionPlanTool(actionPlanService: ActionPlanService) { - return tool({ - name: "getActionPlan", - description: "Retrieve a specific action plan by its ID", - inputSchema: z.object({ - actionPlanId: z.string().describe("The ID of the action plan to retrieve"), - }), - execute: async (args: { actionPlanId: string }) => { - try { - const plan = actionPlanService.getActionPlan(args.actionPlanId); - if (!plan) { - return { success: false, error: "Action plan not found" }; - } - - // Convert to a serializable format - return { - success: true, - actionPlan: { - id: plan.id, - agentId: plan.agentId, - agentName: plan.agentName, - executorAgentId: plan.executorAgentId, - summary: plan.summary, - status: plan.status$.value, - createdAt: plan.createdAt.toISOString(), - userFeedback: plan.userFeedback, - operatorIds: plan.operatorIds, - linkIds: plan.linkIds, - tasks: Array.from(plan.tasks.values()).map(task => ({ - operatorId: task.operatorId, - description: task.description, - completed: task.completed$.value, - })), - }, - }; - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : "Failed to retrieve action plan" }; - } - }, - }); -} - -/** - * Create listActionPlans tool for retrieving all action plans - */ -export function createListActionPlansTool(actionPlanService: ActionPlanService) { - return tool({ - name: "listActionPlans", - description: "List all action plans in the system", - inputSchema: z.object({ - filterByAgent: z.string().optional().describe("Optional: Filter by agent ID"), - filterByStatus: z - .string() - .optional() - .describe("Optional: Filter by status (pending, accepted, rejected, completed)"), - }), - execute: async (args: { filterByAgent?: string; filterByStatus?: string }) => { - try { - const allPlans = actionPlanService.getAllActionPlans(); - - // Apply filters if provided - let filteredPlans = allPlans; - if (args.filterByAgent) { - filteredPlans = filteredPlans.filter(plan => plan.agentId === args.filterByAgent); - } - if (args.filterByStatus) { - filteredPlans = filteredPlans.filter(plan => plan.status$.value === args.filterByStatus); - } - - // Convert to serializable format - const plans = filteredPlans.map(plan => ({ - id: plan.id, - agentId: plan.agentId, - agentName: plan.agentName, - executorAgentId: plan.executorAgentId, - summary: plan.summary, - status: plan.status$.value, - createdAt: plan.createdAt.toISOString(), - taskCount: plan.tasks.size, - completedTasks: Array.from(plan.tasks.values()).filter(t => t.completed$.value).length, - })); - - return { - success: true, - actionPlans: plans, - totalCount: plans.length, - }; - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : "Failed to list action plans" }; - } - }, - }); -} - -/** - * Create deleteActionPlan tool for deleting an action plan - */ -export function createDeleteActionPlanTool(actionPlanService: ActionPlanService) { - return tool({ - name: "deleteActionPlan", - description: "Delete an action plan by its ID", - inputSchema: z.object({ - actionPlanId: z.string().describe("The ID of the action plan to delete"), - }), - execute: async (args: { actionPlanId: string }) => { - try { - const success = actionPlanService.deleteActionPlan(args.actionPlanId); - if (!success) { - return { success: false, error: "Action plan not found or could not be deleted" }; - } - return { success: true, message: `Action plan ${args.actionPlanId} deleted successfully` }; - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : "Failed to delete action plan" }; - } - }, - }); -} - -/** - * Create updateActionPlan tool for updating an action plan - */ -export function createUpdateActionPlanTool(actionPlanService: ActionPlanService) { - return tool({ - name: "updateActionPlan", - description: "Update an action plan's properties", - inputSchema: z.object({ - actionPlanId: z.string().describe("The ID of the action plan to update"), - summary: z.string().optional().describe("New summary for the action plan"), - status: z - .enum(["pending", "accepted", "rejected", "completed"]) - .optional() - .describe("New status for the action plan"), - userFeedback: z.string().optional().describe("User feedback to add"), - }), - execute: async (args: { actionPlanId: string; summary?: string; status?: string; userFeedback?: string }) => { - try { - const plan = actionPlanService.getActionPlan(args.actionPlanId); - if (!plan) { - return { success: false, error: "Action plan not found" }; - } - - // Update fields if provided - if (args.summary !== undefined) { - plan.summary = args.summary; - } - if (args.status !== undefined) { - plan.status$.next(args.status as any); - } - if (args.userFeedback !== undefined) { - plan.userFeedback = args.userFeedback; - } - - return { - success: true, - message: `Action plan ${args.actionPlanId} updated successfully`, - updatedFields: Object.keys(args).filter(k => k !== "actionPlanId"), - }; - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : "Failed to update action plan" }; - } - }, - }); -} - /** * Create listOperatorIds tool for getting all operator IDs in the workflow */ From aab58378a515799d2b949d29a119c12abbf9fe7a Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 10:20:01 -0800 Subject: [PATCH 110/158] one more cleanup on action plan --- .../component/agent-panel/agent-panel.component.html | 7 ------- 1 file changed, 7 deletions(-) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html index 631bf103552..4d419bd5764 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html +++ b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.html @@ -96,13 +96,6 @@
    - - - - - - - Date: Fri, 7 Nov 2025 10:27:24 -0800 Subject: [PATCH 111/158] revert changes --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 467febba5c6..60efe697ce7 100644 --- a/build.sbt +++ b/build.sbt @@ -114,4 +114,4 @@ lazy val TexeraProject = (project in file(".")) organization := "org.apache", scalaVersion := "2.13.12", publishMavenStyle := true - ) \ No newline at end of file + ) From f603458679356afadcf7b5fc978609ce80d526d7 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 10:32:27 -0800 Subject: [PATCH 112/158] revert irrelevant changes --- bin/build-services.sh | 3 -- bin/mcp-service.sh | 25 --------- frontend/custom-webpack.config.js | 6 --- .../workflow-editor.component.scss | 6 --- .../model/workflow-action.service.ts | 52 ------------------- 5 files changed, 92 deletions(-) delete mode 100755 bin/mcp-service.sh diff --git a/bin/build-services.sh b/bin/build-services.sh index f03ca6c8fea..a28e7e2cb0a 100755 --- a/bin/build-services.sh +++ b/bin/build-services.sh @@ -28,8 +28,5 @@ rm config-service/target/universal/config-service-*.zip unzip computing-unit-managing-service/target/universal/computing-unit-managing-service-*.zip -d target/ rm computing-unit-managing-service/target/universal/computing-unit-managing-service-*.zip -unzip mcp-service/target/universal/mcp-service-*.zip -d target/ -rm mcp-service/target/universal/mcp-service-*.zip - unzip amber/target/universal/texera-*.zip -d amber/target/ rm amber/target/universal/texera-*.zip diff --git a/bin/mcp-service.sh b/bin/mcp-service.sh deleted file mode 100755 index 7d52cd1bfbc..00000000000 --- a/bin/mcp-service.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -# 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. - -# Start the Texera MCP Service -# Use TEXERA_HOME environment variable, or default to parent directory of script -TEXERA_HOME="${TEXERA_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" - -# Start the MCP service -cd "$TEXERA_HOME/mcp-service" && target/universal/stage/bin/mcp-service server src/main/resources/mcp-service-config.yaml diff --git a/frontend/custom-webpack.config.js b/frontend/custom-webpack.config.js index 2e099a79fff..df1d742b920 100644 --- a/frontend/custom-webpack.config.js +++ b/frontend/custom-webpack.config.js @@ -18,12 +18,6 @@ */ module.exports = { - resolve: { - fallback: { - // Minimal polyfill for path (needed by some dependencies) - "path": require.resolve("path-browserify"), - } - }, module: { rules: [ { diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss index d220c8f51ff..72caad0296d 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss @@ -29,12 +29,6 @@ display: none; } -::ng-deep .action-plan { - // Action plan highlights - temporary 5-second visual indicators - // Styles are defined inline in JointJS element, but this provides a hook for customization - pointer-events: none; -} - ::ng-deep .hide-worker-count .operator-worker-count { display: none; } diff --git a/frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts b/frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts index 348c5cc5663..40092419fd2 100644 --- a/frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts +++ b/frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts @@ -919,56 +919,4 @@ export class WorkflowActionService { public getHighlightingEnabled() { return this.highlightingEnabled; } - - /** - * Find all operators and links on any upstream path leading to the given destination operator. - * Uses BFS to traverse backwards from the destination to find all contributing operators. - * @param destinationOperatorId The operator ID to find upstream paths to - * @returns Object containing arrays of operator IDs and link IDs on upstream paths - */ - public findUpstreamPath(destinationOperatorId: string): { operators: string[]; links: string[] } { - const allLinks = this.getTexeraGraph().getAllLinks(); - - // Build reverse adjacency list (from target to source) - const reverseAdjacencyMap = new Map>(); - allLinks.forEach(link => { - const source = link.source.operatorID; - const target = link.target.operatorID; - if (!reverseAdjacencyMap.has(target)) { - reverseAdjacencyMap.set(target, []); - } - reverseAdjacencyMap.get(target)!.push({ neighbor: source, linkId: link.linkID }); - }); - - // BFS to find all upstream operators and links - const queue: string[] = [destinationOperatorId]; - const visitedOperators = new Set(); - const allOperatorsOnPaths = new Set(); - const allLinksOnPaths = new Set(); - - allOperatorsOnPaths.add(destinationOperatorId); // Include the destination operator - - while (queue.length > 0) { - const current = queue.shift()!; - - if (visitedOperators.has(current)) { - continue; - } - visitedOperators.add(current); - - const upstreamNeighbors = reverseAdjacencyMap.get(current) || []; - for (const { neighbor, linkId } of upstreamNeighbors) { - allOperatorsOnPaths.add(neighbor); - allLinksOnPaths.add(linkId); - if (!visitedOperators.has(neighbor)) { - queue.push(neighbor); - } - } - } - - return { - operators: Array.from(allOperatorsOnPaths), - links: Array.from(allLinksOnPaths), - }; - } } From aa8b70fc5859acbea376ab0193c349782f32ee0d Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 10:56:01 -0800 Subject: [PATCH 113/158] avoid injecting redundant code --- .../copilot/texera-copilot-manager.service.ts | 13 ------------- .../app/workspace/service/copilot/texera-copilot.ts | 1 - 2 files changed, 14 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index 72076ace884..2377a030acc 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -267,19 +267,6 @@ export class TexeraCopilotManagerService { providers: [ { provide: TexeraCopilot, - deps: [ - WorkflowActionService, - WorkflowUtilService, - OperatorMetadataService, - DynamicSchemaService, - ExecuteWorkflowService, - WorkflowResultService, - WorkflowCompilingService, - ValidationWorkflowService, - NotificationService, - ComputingUnitStatusService, - WorkflowConsoleService, - ], }, ], parent: this.injector, diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 045095cb6df..f8222ce38ef 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -115,7 +115,6 @@ export class TexeraCopilot { private workflowActionService: WorkflowActionService, private workflowUtilService: WorkflowUtilService, private operatorMetadataService: OperatorMetadataService, - private dynamicSchemaService: DynamicSchemaService, private executeWorkflowService: ExecuteWorkflowService, private workflowResultService: WorkflowResultService, private workflowCompilingService: WorkflowCompilingService, From 00d349f372a193fda97af07b25ab3188b94af29c Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 13:15:35 -0800 Subject: [PATCH 114/158] add openai key support to helm chart --- bin/k8s/templates/litellm-deployment.yaml | 5 +++++ bin/k8s/templates/litellm-secret.yaml | 1 + bin/k8s/values.yaml | 2 ++ 3 files changed, 8 insertions(+) diff --git a/bin/k8s/templates/litellm-deployment.yaml b/bin/k8s/templates/litellm-deployment.yaml index 51f05a6b886..6fcd9afd794 100644 --- a/bin/k8s/templates/litellm-deployment.yaml +++ b/bin/k8s/templates/litellm-deployment.yaml @@ -56,6 +56,11 @@ spec: secretKeyRef: name: litellm-secret key: ANTHROPIC_API_KEY + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: litellm-secret + key: OPENAI_API_KEY command: - litellm - --config diff --git a/bin/k8s/templates/litellm-secret.yaml b/bin/k8s/templates/litellm-secret.yaml index f68c1d72ebc..1cb5d2a68b5 100644 --- a/bin/k8s/templates/litellm-secret.yaml +++ b/bin/k8s/templates/litellm-secret.yaml @@ -24,6 +24,7 @@ metadata: type: Opaque data: ANTHROPIC_API_KEY: {{ .Values.litellm.apiKeys.anthropic | b64enc | quote }} + OPENAI_API_KEY: {{ .Values.litellm.apiKeys.openai | b64enc | quote }} {{- if .Values.litellm.persistence.enabled }} {{- $postgresqlLitellm := index .Values "postgresql-litellm" }} DATABASE_URL: {{ printf "postgresql://%s:%s@%s-postgresql-litellm:5432/%s" $postgresqlLitellm.auth.username $postgresqlLitellm.auth.password .Release.Name $postgresqlLitellm.auth.database | b64enc | quote }} diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml index c28dcbe6dc1..d54f9e08239 100644 --- a/bin/k8s/values.yaml +++ b/bin/k8s/values.yaml @@ -321,6 +321,8 @@ litellm: # Set your Anthropic API key here # IMPORTANT: In production, use external secrets management (e.g., sealed-secrets, external-secrets) anthropic: "" # Replace with your actual API key or use external secret + # Set your OpenAI API key here + openai: "" # Replace with your actual API key or use external secret # PostgreSQL database for LiteLLM persistence postgresql-litellm: From 1f60d949b42320f6022dd3b7bfb4c355bab89c61 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 17:10:00 -0800 Subject: [PATCH 115/158] add open ai to the litellm --- bin/litellm-config.yaml | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/bin/litellm-config.yaml b/bin/litellm-config.yaml index 9d10d640acb..6a75d7e0013 100644 --- a/bin/litellm-config.yaml +++ b/bin/litellm-config.yaml @@ -1,5 +1,35 @@ +# 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. + +# The default configuration file for starting litellm (https://docs.litellm.ai/docs/proxy/quick_start) +# To start the litellm service: +# 1. Install litellm by: +# pip install 'litellm[proxy]' +# 2. Set your API keys as environment variable, e.g. +# export ANTHROPIC_API_KEY= +# 3. Start litellm by: +# litellm --config bin/litellm-config.yaml +# By default, litellm is running on http://0.0.0.0:4000 model_list: - model_name: claude-haiku-4.5 litellm_params: model: claude-haiku-4-5-20251001 - api_key: "os.environ/ANTHROPIC_API_KEY" \ No newline at end of file + api_key: "os.environ/ANTHROPIC_API_KEY" + - model_name: gpt-5-mini + litellm_params: + model: gpt-5-mini-2025-08-07 + api_key: "os.environ/OPENAI_API_KEY" \ No newline at end of file From dbd4927bd6f168b65cb6d173c2a2a3fd9fe739fc Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 18:00:35 -0800 Subject: [PATCH 116/158] shrink the number of tools --- .../service/copilot/texera-copilot.ts | 173 +--- .../service/copilot/workflow-tools.ts | 886 +----------------- 2 files changed, 5 insertions(+), 1054 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index f8222ce38ef..88f44cfaf36 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -21,48 +21,25 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, Observable, from } from "rxjs"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { - createAddOperatorTool, - createAddLinkTool, createGetOperatorTool, - createDeleteOperatorTool, - createDeleteLinkTool, - createSetOperatorPropertyTool, - createSetPortPropertyTool, - createGetOperatorSchemaTool, createGetOperatorPropertiesSchemaTool, createGetOperatorPortsInfoTool, createGetOperatorMetadataTool, createGetOperatorInputSchemaTool, createGetOperatorOutputSchemaTool, - createGetWorkflowCompilationStateTool, - createExecuteWorkflowTool, - createGetExecutionStateTool, - createKillWorkflowTool, - createHasOperatorResultTool, - createGetOperatorResultTool, - createGetOperatorResultInfoTool, - createGetValidationInfoOfCurrentWorkflowTool, - createValidateOperatorTool, toolWithTimeout, createListAllOperatorTypesTool, createListLinksTool, createListOperatorIdsTool, - createGetComputingUnitStatusTool, } from "./workflow-tools"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { createOpenAI } from "@ai-sdk/openai"; import { AssistantModelMessage, generateText, type ModelMessage, stepCountIs, UIMessage, UserModelMessage } from "ai"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { AppSettings } from "../../../common/app-setting"; -import { DynamicSchemaService } from "../dynamic-schema/dynamic-schema.service"; -import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; -import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; -import { ValidationWorkflowService } from "../validation/validation-workflow.service"; import { COPILOT_SYSTEM_PROMPT } from "./copilot-prompts"; import { NotificationService } from "../../../common/service/notification/notification.service"; -import { ComputingUnitStatusService } from "../computing-unit-status/computing-unit-status.service"; -import { WorkflowConsoleService } from "../workflow-console/workflow-console.service"; /** * Copilot state enum. @@ -115,13 +92,8 @@ export class TexeraCopilot { private workflowActionService: WorkflowActionService, private workflowUtilService: WorkflowUtilService, private operatorMetadataService: OperatorMetadataService, - private executeWorkflowService: ExecuteWorkflowService, - private workflowResultService: WorkflowResultService, private workflowCompilingService: WorkflowCompilingService, - private validationWorkflowService: ValidationWorkflowService, - private notificationService: NotificationService, - private computingUnitStatusService: ComputingUnitStatusService, - private workflowConsoleService: WorkflowConsoleService + private notificationService: NotificationService ) { this.modelType = ""; } @@ -143,97 +115,6 @@ export class TexeraCopilot { this.stateSubject.next(newState); } - /** - * Type guard to check if a message is a valid ModelMessage. - * Uses TypeScript's type predicate for compile-time type safety. - * - * Validates messages according to Vercel AI SDK ModelMessage types: - * - UserModelMessage: { role: "user", content: string | ContentPart[] } - * - AssistantModelMessage: { role: "assistant", content: string | ContentPart[] } - * - ToolModelMessage: { role: "tool", content: ToolResultPart[] } - * - SystemModelMessage: { role: "system", content: string } - */ - private isValidModelMessage(message: unknown): message is ModelMessage { - if (!message || typeof message !== "object") { - return false; - } - - const msg = message as Record; - - // Check if role property exists and is a string - if (typeof msg.role !== "string") { - return false; - } - - // Validate based on role using type narrowing - switch (msg.role) { - case "user": - case "system": - // UserModelMessage/SystemModelMessage: { role: "user"/"system", content: string | array } - return typeof msg.content === "string" || Array.isArray(msg.content); - - case "assistant": - // AssistantModelMessage: { role: "assistant", content: string | array } - // Array content must contain valid content parts (text, tool-call, tool-result, etc.) - if (typeof msg.content === "string") { - return true; - } - if (Array.isArray(msg.content)) { - // Verify all parts have the required 'type' field - return msg.content.every((part: any) => part && typeof part === "object" && typeof part.type === "string"); - } - return false; - - case "tool": - // ToolModelMessage: { role: "tool", content: ToolResultPart[] } - // Content must be array of tool result parts - if (!Array.isArray(msg.content)) { - return false; - } - // Each part must have type='tool-result', toolCallId, toolName, and output with type/value - return msg.content.every( - (part: any) => - part && - typeof part === "object" && - part.type === "tool-result" && - typeof part.toolCallId === "string" && - typeof part.toolName === "string" && - part.output && - typeof part.output === "object" && - typeof part.output.type === "string" && - "value" in part.output - ); - - default: - return false; - } - } - - /** - * Validate all messages in the conversation history. - * Throws an error if any message doesn't conform to ModelMessage type. - */ - private validateMessages(): void { - const invalidMessages: Array<{ index: number; message: unknown }> = []; - - this.messages.forEach((message, index) => { - if (!this.isValidModelMessage(message)) { - invalidMessages.push({ index, message }); - } - }); - - if (invalidMessages.length > 0) { - const indices = invalidMessages.map(m => m.index).join(", "); - const details = invalidMessages.map(m => `[${m.index}]: ${JSON.stringify(m.message)}`).join("; "); - const errorMessage = `Invalid ModelMessage(s) found at indices: ${indices}. Details: ${details}`; - - this.notificationService.error( - `Message validation failed: ${invalidMessages.length} invalid message(s). Please disconnect current agent and create a new agent` - ); - throw new Error(errorMessage); - } - } - /** * Initialize the copilot with the AI model. */ @@ -341,22 +222,10 @@ export class TexeraCopilot { * Create workflow manipulation tools with timeout protection. */ private createWorkflowTools(): Record { - const addOperatorTool = toolWithTimeout( - createAddOperatorTool(this.workflowActionService, this.workflowUtilService, this.operatorMetadataService) - ); - const addLinkTool = toolWithTimeout(createAddLinkTool(this.workflowActionService)); const listOperatorIdsTool = toolWithTimeout(createListOperatorIdsTool(this.workflowActionService)); const listLinksTool = toolWithTimeout(createListLinksTool(this.workflowActionService)); const listAllOperatorTypesTool = toolWithTimeout(createListAllOperatorTypesTool(this.workflowUtilService)); const getOperatorTool = toolWithTimeout(createGetOperatorTool(this.workflowActionService)); - const deleteOperatorTool = toolWithTimeout(createDeleteOperatorTool(this.workflowActionService)); - const deleteLinkTool = toolWithTimeout(createDeleteLinkTool(this.workflowActionService)); - const setOperatorPropertyTool = toolWithTimeout( - createSetOperatorPropertyTool(this.workflowActionService, this.validationWorkflowService) - ); - const setPortPropertyTool = toolWithTimeout( - createSetPortPropertyTool(this.workflowActionService, this.validationWorkflowService) - ); const getOperatorPropertiesSchemaTool = toolWithTimeout( createGetOperatorPropertiesSchemaTool(this.workflowActionService, this.operatorMetadataService) ); @@ -370,55 +239,17 @@ export class TexeraCopilot { const getOperatorOutputSchemaTool = toolWithTimeout( createGetOperatorOutputSchemaTool(this.workflowCompilingService) ); - const getWorkflowCompilationStateTool = toolWithTimeout( - createGetWorkflowCompilationStateTool(this.workflowCompilingService) - ); - const executeWorkflowTool = toolWithTimeout(createExecuteWorkflowTool(this.executeWorkflowService)); - const getExecutionStateTool = toolWithTimeout( - createGetExecutionStateTool(this.executeWorkflowService, this.workflowActionService, this.workflowConsoleService) - ); - const killWorkflowTool = toolWithTimeout(createKillWorkflowTool(this.executeWorkflowService)); - const hasOperatorResultTool = toolWithTimeout( - createHasOperatorResultTool(this.workflowResultService, this.workflowActionService) - ); - const getOperatorResultTool = toolWithTimeout(createGetOperatorResultTool(this.workflowResultService)); - const getOperatorResultInfoTool = toolWithTimeout( - createGetOperatorResultInfoTool(this.workflowResultService, this.workflowActionService) - ); - const getValidationInfoOfCurrentWorkflowTool = toolWithTimeout( - createGetValidationInfoOfCurrentWorkflowTool(this.validationWorkflowService, this.workflowActionService) - ); - const validateOperatorTool = toolWithTimeout(createValidateOperatorTool(this.validationWorkflowService)); - const getComputingUnitStatusTool = toolWithTimeout( - createGetComputingUnitStatusTool(this.computingUnitStatusService) - ); return { - addOperator: addOperatorTool, - addLink: addLinkTool, - deleteOperator: deleteOperatorTool, - deleteLink: deleteLinkTool, - setOperatorProperty: setOperatorPropertyTool, - setPortProperty: setPortPropertyTool, - getValidationInfoOfCurrentWorkflow: getValidationInfoOfCurrentWorkflowTool, - validateOperator: validateOperatorTool, + listAllOperatorTypes: listAllOperatorTypesTool, listOperatorIds: listOperatorIdsTool, listLinks: listLinksTool, - listAllOperatorTypes: listAllOperatorTypesTool, getOperator: getOperatorTool, getOperatorPropertiesSchema: getOperatorPropertiesSchemaTool, getOperatorPortsInfo: getOperatorPortsInfoTool, getOperatorMetadata: getOperatorMetadataTool, getOperatorInputSchema: getOperatorInputSchemaTool, getOperatorOutputSchema: getOperatorOutputSchemaTool, - getWorkflowCompilationState: getWorkflowCompilationStateTool, - executeWorkflow: executeWorkflowTool, - getExecutionStateTool: getExecutionStateTool, - killWorkflow: killWorkflowTool, - hasOperatorResult: hasOperatorResultTool, - getOperatorResult: getOperatorResultTool, - getOperatorResultInfo: getOperatorResultInfoTool, - getComputingUnitStatus: getComputingUnitStatusTool, }; } diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index ec1fad4ebd1..f9829fde233 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -21,185 +21,42 @@ import { z } from "zod"; import { tool } from "ai"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; -import { OperatorLink } from "../../types/workflow-common.interface"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; -import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; -import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; -import { ValidationWorkflowService } from "../validation/validation-workflow.service"; -import { WorkflowConsoleService } from "../workflow-console/workflow-console.service"; -// Tool execution timeout in milliseconds (2 minutes) const TOOL_TIMEOUT_MS = 120000; -// Maximum token limit for operator result data to prevent overwhelming LLM context -// Estimated as characters / 4 (common approximation for token counting) -const MAX_OPERATOR_RESULT_TOKEN_LIMIT = 1000; - -/** - * Estimates the number of tokens in a JSON-serializable object - * Uses a common approximation: tokens ≈ characters / 4 - */ -function estimateTokenCount(data: any): number { - try { - const jsonString = JSON.stringify(data); - return Math.ceil(jsonString.length / 4); - } catch (error) { - // Fallback if JSON.stringify fails - return 0; - } -} - -/** - * Wraps a tool definition to add timeout protection to its execute function - * Uses AbortController to properly cancel operations on timeout - */ export function toolWithTimeout(toolConfig: any): any { const originalExecute = toolConfig.execute; return { ...toolConfig, execute: async (args: any) => { - // Create an AbortController for this execution const abortController = new AbortController(); - // Create a timeout promise that will abort the controller const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { - abortController.abort(); // Signal cancellation to the operation + abortController.abort(); reject(new Error("timeout")); }, TOOL_TIMEOUT_MS); }); try { - // Pass the abort signal in args so tools can check it const argsWithSignal = { ...args, signal: abortController.signal }; return await Promise.race([originalExecute(argsWithSignal), timeoutPromise]); } catch (error: any) { - // If it's a timeout error, return a properly formatted error response if (error.message === "timeout") { return { success: false, error: "Tool execution timeout - operation took longer than 2 minutes. Please try again later.", }; } - // Re-throw other errors to be handled by the original error handler throw error; } }, }; } -/** - * Create addOperator tool for adding a new operator to the workflow - */ -export function createAddOperatorTool( - workflowActionService: WorkflowActionService, - workflowUtilService: WorkflowUtilService, - operatorMetadataService: OperatorMetadataService -) { - return tool({ - name: "addOperator", - description: "Add a new operator to the workflow", - inputSchema: z.object({ - operatorType: z.string().describe("Type of operator (e.g., 'CSVSource', 'Filter', 'Aggregate')"), - customDisplayName: z - .string() - .optional() - .describe("Brief custom name summarizing what this operator does in one sentence"), - }), - execute: async (args: { operatorType: string; customDisplayName?: string }) => { - try { - // Clear previous highlights at start of tool execution - - // Validate operator type exists - if (!operatorMetadataService.operatorTypeExists(args.operatorType)) { - return { - success: false, - error: `Unknown operator type: ${args.operatorType}.Use tools to see available types.`, - }; - } - - // Get a new operator predicate with default settings and optional custom display name - const operator = workflowUtilService.getNewOperatorPredicate(args.operatorType, args.customDisplayName); - - // Calculate a default position (can be adjusted by auto-layout later) - const existingOperators = workflowActionService.getTexeraGraph().getAllOperators(); - const defaultX = 100 + (existingOperators.length % 5) * 200; - const defaultY = 100 + Math.floor(existingOperators.length / 5) * 150; - const position = { x: defaultX, y: defaultY }; - - // Add the operator to the workflow first - workflowActionService.addOperator(operator, position); - - // Show copilot is adding this operator (after it's added to graph) - setTimeout(() => {}, 100); - - return { - success: true, - operatorId: operator.operatorID, - message: `Added ${args.operatorType} operator to workflow`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create addLink tool for connecting two operators - */ -export function createAddLinkTool(workflowActionService: WorkflowActionService) { - return tool({ - name: "addLink", - description: "Connect two operators with a link", - inputSchema: z.object({ - sourceOperatorId: z.string().describe("ID of the source operator"), - sourcePortId: z.string().optional().describe("Port ID on source operator (e.g., 'output-0')"), - targetOperatorId: z.string().describe("ID of the target operator"), - targetPortId: z.string().optional().describe("Port ID on target operator (e.g., 'input-0')"), - }), - execute: async (args: { - sourceOperatorId: string; - sourcePortId?: string; - targetOperatorId: string; - targetPortId?: string; - }) => { - try { - // Default port IDs if not specified - const sourcePId = args.sourcePortId || "output-0"; - const targetPId = args.targetPortId || "input-0"; - - const link: OperatorLink = { - linkID: `link_${Date.now()}`, - source: { - operatorID: args.sourceOperatorId, - portID: sourcePId, - }, - target: { - operatorID: args.targetOperatorId, - portID: targetPId, - }, - }; - - workflowActionService.addLink(link); - - return { - success: true, - linkId: link.linkID, - message: `Connected ${args.sourceOperatorId}:${sourcePId} to ${args.targetOperatorId}:${targetPId}`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create listOperatorIds tool for getting all operator IDs in the workflow - */ export function createListOperatorIdsTool(workflowActionService: WorkflowActionService) { return tool({ name: "listOperatorIds", @@ -222,9 +79,6 @@ export function createListOperatorIdsTool(workflowActionService: WorkflowActionS }); } -/** - * Create listLinks tool for getting all links in the workflow - */ export function createListLinksTool(workflowActionService: WorkflowActionService) { return tool({ name: "listLinks", @@ -245,9 +99,6 @@ export function createListLinksTool(workflowActionService: WorkflowActionService }); } -/** - * Create listAllOperatorTypes tool for getting all available operator types - */ export function createListAllOperatorTypesTool(workflowUtilService: WorkflowUtilService) { return tool({ name: "listAllOperatorTypes", @@ -268,9 +119,6 @@ export function createListAllOperatorTypesTool(workflowUtilService: WorkflowUtil }); } -/** - * Create getOperator tool for getting detailed information about a specific operator - */ export function createGetOperatorTool(workflowActionService: WorkflowActionService) { return tool({ name: "getOperator", @@ -280,10 +128,6 @@ export function createGetOperatorTool(workflowActionService: WorkflowActionServi }), execute: async (args: { operatorId: string }) => { try { - // Clear previous highlights at start of tool execution - - // Show copilot is viewing this operator - const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); return { @@ -301,214 +145,6 @@ export function createGetOperatorTool(workflowActionService: WorkflowActionServi }); } -/** - * Create deleteOperator tool for removing an operator from the workflow - */ -export function createDeleteOperatorTool(workflowActionService: WorkflowActionService) { - return tool({ - name: "deleteOperator", - description: "Delete an operator from the workflow", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to delete"), - }), - execute: async (args: { operatorId: string }) => { - try { - // Clear previous highlights at start of tool execution - - // Show copilot is editing this operator before deletion - - workflowActionService.deleteOperator(args.operatorId); - - return { - success: true, - message: `Deleted operator ${args.operatorId}`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create deleteLink tool for removing a link from the workflow - */ -export function createDeleteLinkTool(workflowActionService: WorkflowActionService) { - return tool({ - name: "deleteLink", - description: "Delete a link between two operators in the workflow by link ID", - inputSchema: z.object({ - linkId: z.string().describe("ID of the link to delete"), - }), - execute: async (args: { linkId: string }) => { - try { - workflowActionService.deleteLinkWithID(args.linkId); - return { - success: true, - message: `Deleted link ${args.linkId}`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create setOperatorProperty tool for modifying operator properties - */ -export function createSetOperatorPropertyTool( - workflowActionService: WorkflowActionService, - validationWorkflowService: ValidationWorkflowService -) { - return tool({ - name: "setOperatorProperty", - description: - "Set or update properties of an operator in the workflow. Properties must match the operator's schema. Use getOperatorPropertiesSchema first to understand required properties and their types.", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to modify"), - properties: z.record(z.any()).describe("Properties object to set on the operator"), - }), - execute: async (args: { operatorId: string; properties: Record }) => { - try { - // Clear previous highlights at start of tool execution - - // Show copilot is editing this operator - - // Set the properties first - workflowActionService.setOperatorProperty(args.operatorId, args.properties); - - // Validate the operator after setting properties - const validation = validationWorkflowService.validateOperator(args.operatorId); - - if (!validation.isValid) { - // Properties are set but invalid - return error with details - return { - success: false, - error: "Property validation failed", - validationErrors: validation.messages, - hint: "Use getOperatorPropertiesSchema tool to see the expected schema structure for this operator", - }; - } - - // Show property was changed - - return { - success: true, - message: `Updated properties for operator ${args.operatorId}`, - properties: args.properties, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create setPortProperty tool for modifying port properties - */ -export function createSetPortPropertyTool( - workflowActionService: WorkflowActionService, - validationWorkflowService: ValidationWorkflowService -) { - return tool({ - name: "setPortProperty", - description: - "Set or update properties of a port on an operator (e.g., partition information, dependencies). Use getOperatorPortsInfo first to see available ports.", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator that owns the port"), - portId: z.string().describe("ID of the port to modify (e.g., 'input-0', 'output-0')"), - properties: z.record(z.any()).describe("Port properties to set (partitionInfo, dependencies)"), - }), - execute: async (args: { operatorId: string; portId: string; properties: Record }) => { - try { - // Clear previous highlights at start of tool execution - - // Show copilot is editing this operator - - // Create LogicalPort object - const logicalPort = { - operatorID: args.operatorId, - portID: args.portId, - }; - - // Set the port properties using the high-level service method - workflowActionService.setPortProperty(logicalPort, args.properties); - - // Validate the operator after setting port properties - const validation = validationWorkflowService.validateOperator(args.operatorId); - - if (!validation.isValid) { - // Properties are set but invalid - return error with details - return { - success: false, - error: "Port property validation failed", - validationErrors: validation.messages, - hint: "Use getOperatorPortsInfo tool to see the available ports and their current configuration", - }; - } - - // Show property was changed - - return { - success: true, - message: `Updated port ${args.portId} properties for operator ${args.operatorId}`, - properties: args.properties, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create getOperatorSchema tool for getting operator schema information - * Returns the original operator schema (not the dynamic one) to save tokens - */ -export function createGetOperatorSchemaTool( - workflowActionService: WorkflowActionService, - operatorMetadataService: OperatorMetadataService -) { - return tool({ - name: "getOperatorSchema", - description: - "Get the original schema of an operator, which includes properties of this operator. Use this to understand what properties can be edited on an operator before modifying it.", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to get schema for"), - }), - execute: async (args: { operatorId: string }) => { - try { - // Clear previous highlights at start of tool execution - - // Highlight the operator being inspected - - // Get the operator to find its type - const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); - if (!operator) { - return { success: false, error: `Operator ${args.operatorId} not found` }; - } - - // Get the original operator schema from metadata - const schema = operatorMetadataService.getOperatorSchema(operator.operatorType); - - return { - success: true, - schema: schema, - message: `Retrieved original schema for operator ${args.operatorId} (type: ${operator.operatorType})`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create getOperatorPropertiesSchema tool for getting just the properties schema - * More token-efficient than getOperatorSchema for property-focused queries - */ export function createGetOperatorPropertiesSchemaTool( workflowActionService: WorkflowActionService, operatorMetadataService: OperatorMetadataService @@ -516,30 +152,22 @@ export function createGetOperatorPropertiesSchemaTool( return tool({ name: "getOperatorPropertiesSchema", description: - "Get just the properties schema for an operator. This is more token-efficient than getOperatorSchema and returns only the properties structure and required fields. Use this before setting operator properties.", + "Get only the properties schema for an operator. Use this before setting operator properties.", inputSchema: z.object({ operatorId: z.string().describe("ID of the operator to get properties schema for"), }), execute: async (args: { operatorId: string }) => { try { - // Clear previous highlights at start of tool execution - - // Highlight the operator being inspected - - // Get the operator to find its type const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); if (!operator) { return { success: false, error: `Operator ${args.operatorId} not found` }; } - // Get the original operator schema from metadata const schema = operatorMetadataService.getOperatorSchema(operator.operatorType); - - // Extract just the properties and required fields from the JSON schema const propertiesSchema = { properties: schema.jsonSchema.properties, required: schema.jsonSchema.required, - definitions: schema.jsonSchema.definitions, // Include definitions for $ref resolution + definitions: schema.jsonSchema.definitions, }; return { @@ -555,10 +183,6 @@ export function createGetOperatorPropertiesSchemaTool( }); } -/** - * Create getOperatorPortsInfo tool for getting just the port information - * More token-efficient than getOperatorSchema for port-focused queries - */ export function createGetOperatorPortsInfoTool( workflowActionService: WorkflowActionService, operatorMetadataService: OperatorMetadataService @@ -572,20 +196,12 @@ export function createGetOperatorPortsInfoTool( }), execute: async (args: { operatorId: string }) => { try { - // Clear previous highlights at start of tool execution - - // Highlight the operator being inspected - - // Get the operator to find its type const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); if (!operator) { return { success: false, error: `Operator ${args.operatorId} not found` }; } - // Get the original operator schema from metadata const schema = operatorMetadataService.getOperatorSchema(operator.operatorType); - - // Extract just the port information from the additional metadata const portsInfo = { inputPorts: schema.additionalMetadata.inputPorts, outputPorts: schema.additionalMetadata.outputPorts, @@ -606,10 +222,6 @@ export function createGetOperatorPortsInfoTool( }); } -/** - * Create getOperatorMetadata tool for getting operator's semantic metadata - * Returns information about what the operator does, its description, and capabilities - */ export function createGetOperatorMetadataTool( workflowActionService: WorkflowActionService, operatorMetadataService: OperatorMetadataService @@ -623,24 +235,13 @@ export function createGetOperatorMetadataTool( }), execute: async (args: { operatorId: string; signal?: AbortSignal }) => { try { - // Clear previous highlights at start of tool execution - // copilotCoeditor.clearAll(); - - // Highlight the operator being inspected - // copilotCoeditor.highlightOperators([args.operatorId]); - - // Get the operator to find its type const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); if (!operator) { return { success: false, error: `Operator ${args.operatorId} not found` }; } - - // Get the original operator schema from metadata const schema = operatorMetadataService.getOperatorSchema(operator.operatorType); - // Return the additional metadata which contains semantic information const metadata = schema.additionalMetadata; - return { success: true, metadata: metadata, @@ -655,9 +256,6 @@ export function createGetOperatorMetadataTool( }); } -/** - * Create getOperatorInputSchema tool for getting operator's input schema from compilation - */ export function createGetOperatorInputSchemaTool(workflowCompilingService: WorkflowCompilingService) { return tool({ name: "getOperatorInputSchema", @@ -668,10 +266,6 @@ export function createGetOperatorInputSchemaTool(workflowCompilingService: Workf }), execute: async (args: { operatorId: string }) => { try { - // Clear previous highlights at start of tool execution - - // Highlight the operator being inspected - const inputSchemaMap = workflowCompilingService.getOperatorInputSchemaMap(args.operatorId); if (!inputSchemaMap) { @@ -694,9 +288,6 @@ export function createGetOperatorInputSchemaTool(workflowCompilingService: Workf }); } -/** - * Create getOperatorOutputSchema tool for getting operator's output schema from compilation - */ export function createGetOperatorOutputSchemaTool(workflowCompilingService: WorkflowCompilingService) { return tool({ name: "getOperatorOutputSchema", @@ -729,474 +320,3 @@ export function createGetOperatorOutputSchemaTool(workflowCompilingService: Work }); } -/** - * Create getWorkflowCompilationState tool for checking compilation status and errors - */ -export function createGetWorkflowCompilationStateTool(workflowCompilingService: WorkflowCompilingService) { - return tool({ - name: "getWorkflowCompilationState", - description: - "Get the current workflow compilation state and any compilation errors. Use this to check if the workflow is valid and identify any operator configuration issues.", - inputSchema: z.object({}), - execute: async () => { - try { - const compilationState = workflowCompilingService.getWorkflowCompilationState(); - const compilationErrors = workflowCompilingService.getWorkflowCompilationErrors(); - - const hasErrors = Object.keys(compilationErrors).length > 0; - - return { - success: true, - state: compilationState, - hasErrors: hasErrors, - errors: hasErrors ? compilationErrors : undefined, - message: hasErrors - ? `Workflow compilation failed with ${Object.keys(compilationErrors).length} error(s)` - : `Workflow compilation state: ${compilationState}`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create executeWorkflow tool for running the workflow - */ -export function createExecuteWorkflowTool(executeWorkflowService: ExecuteWorkflowService) { - return tool({ - name: "executeWorkflow", - description: "Execute the current workflow", - inputSchema: z.object({ - executionName: z.string().optional().describe("Name for this execution (default: 'Copilot Execution')"), - targetOperatorId: z - .string() - .optional() - .describe("Optional operator ID to execute up to (executes entire workflow if not specified)"), - }), - execute: async (args: { executionName?: string; targetOperatorId?: string }) => { - try { - const name = args.executionName || "Copilot Execution"; - executeWorkflowService.executeWorkflow(name, args.targetOperatorId); - return { - success: true, - message: args.targetOperatorId - ? `Started workflow execution up to operator ${args.targetOperatorId}` - : "Started workflow execution", - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create getExecutionState tool for checking workflow execution status - */ -export function createGetExecutionStateTool( - executeWorkflowService: ExecuteWorkflowService, - workflowActionService: WorkflowActionService, - workflowConsoleService: WorkflowConsoleService -) { - return tool({ - name: "getExecutionState", - description: "Get the current execution state of the workflow, including console logs from operators", - inputSchema: z.object({}), - execute: async () => { - try { - const stateInfo = executeWorkflowService.getExecutionState(); - - // Get console logs for all operators in the workflow - const consoleLogs: { [operatorId: string]: ReadonlyArray } = {}; - const allOperators = workflowActionService.getTexeraGraph().getAllOperators(); - - for (const operator of allOperators) { - const operatorId = operator.operatorID; - if (workflowConsoleService.hasConsoleMessages(operatorId)) { - const messages = workflowConsoleService.getConsoleMessages(operatorId); - if (messages && messages.length > 0) { - consoleLogs[operatorId] = messages; - } - } - } - - // Only include essential information, not the entire stateInfo which can be very large - const result: any = { - success: true, - state: stateInfo, - consoleLogs: consoleLogs, - }; - return result; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create killWorkflow tool for stopping workflow execution - */ -export function createKillWorkflowTool(executeWorkflowService: ExecuteWorkflowService) { - return tool({ - name: "killWorkflow", - description: - "Kill the currently running workflow execution. Use this when the workflow is stuck or you need to stop it. Cannot kill if workflow is uninitialized or already completed.", - inputSchema: z.object({}), - execute: async () => { - try { - executeWorkflowService.killWorkflow(); - return { - success: true, - message: "Workflow execution killed successfully", - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create hasOperatorResult tool for checking if an operator has results - */ -export function createHasOperatorResultTool( - workflowResultService: WorkflowResultService, - workflowActionService: WorkflowActionService -) { - return tool({ - name: "hasOperatorResult", - description: "Check if an operator has any execution results available", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to check"), - }), - execute: async (args: { operatorId: string }) => { - try { - // Clear previous highlights at start of tool execution - - // Highlight operator being checked - - const hasResult = workflowResultService.hasAnyResult(args.operatorId); - - return { - success: true, - hasResult: hasResult, - message: hasResult - ? `Operator ${args.operatorId} has results available` - : `Operator ${args.operatorId} has no results`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create unified getOperatorResult tool that automatically handles both pagination and snapshot modes - */ -export function createGetOperatorResultTool(workflowResultService: WorkflowResultService) { - return tool({ - name: "getOperatorResult", - description: - "Get result data for an operator. Automatically detects and uses the appropriate mode (pagination for tables, snapshot for visualizations). Returns rows limited by token count (~3000 tokens) to avoid overwhelming LLM context.", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to get results for"), - }), - execute: async (args: { operatorId: string; signal?: AbortSignal }) => { - try { - // Clear previous highlights at start of tool execution - - // Highlight operator being inspected - - // First, try pagination mode (for table results) - const paginatedResultService = workflowResultService.getPaginatedResultService(args.operatorId); - if (paginatedResultService) { - try { - // Request first page with reasonable size (200 rows) - // We'll filter by token limit after receiving - const pageSize = 200; - const resultEvent: any = await new Promise((resolve, reject) => { - const subscription = paginatedResultService.selectPage(1, pageSize).subscribe({ - next: event => { - subscription.unsubscribe(); - resolve(event); - }, - error: (err: unknown) => { - subscription.unsubscribe(); - reject(err); - }, - }); - - // Handle abort signal - if (args.signal) { - args.signal.addEventListener("abort", () => { - subscription.unsubscribe(); - reject(new Error("Operation aborted")); - }); - } - }); - - // Filter results by token limit - const limitedResult: any[] = []; - let currentTokenCount = 0; - - for (const row of resultEvent.table || []) { - const rowTokens = estimateTokenCount(row); - if (currentTokenCount + rowTokens > MAX_OPERATOR_RESULT_TOKEN_LIMIT) { - break; // Stop if adding this row exceeds limit - } - limitedResult.push(row); - currentTokenCount += rowTokens; - } - - const totalRows = paginatedResultService.getCurrentTotalNumTuples(); - const wasLimited = limitedResult.length < (resultEvent.table?.length || 0); - - return { - success: true, - operatorId: args.operatorId, - mode: "pagination", - totalRows: totalRows, - displayedRows: limitedResult.length, - estimatedTokens: currentTokenCount, - truncated: wasLimited, - result: { ...resultEvent, table: limitedResult }, - message: wasLimited - ? `Retrieved ${limitedResult.length} rows (out of ${totalRows} total, limited by token count ~${currentTokenCount} tokens) from paginated table results for operator ${args.operatorId}` - : `Retrieved ${limitedResult.length} rows (out of ${totalRows} total, ~${currentTokenCount} tokens) from paginated table results for operator ${args.operatorId}`, - }; - } catch (error: any) { - return { - success: false, - error: `Failed to fetch paginated results: ${error.message}. This may be due to backend storage issues or results not being ready yet.`, - }; - } - } - - // If pagination mode is not available, try snapshot mode (for visualization results) - const resultService = workflowResultService.getResultService(args.operatorId); - if (resultService) { - const snapshot = resultService.getCurrentResultSnapshot(); - if (!snapshot || snapshot.length === 0) { - return { - success: false, - error: `Result snapshot is empty for operator ${args.operatorId}. Results might not be ready yet.`, - }; - } - - // Filter by token limit - const limitedResult: any[] = []; - let currentTokenCount = 0; - - for (const row of snapshot) { - const rowTokens = estimateTokenCount(row); - if (currentTokenCount + rowTokens > MAX_OPERATOR_RESULT_TOKEN_LIMIT) { - break; // Stop if adding this row exceeds limit - } - limitedResult.push(row); - currentTokenCount += rowTokens; - } - - const wasLimited = limitedResult.length < snapshot.length; - - return { - success: true, - operatorId: args.operatorId, - mode: "snapshot", - totalRows: snapshot.length, - displayedRows: limitedResult.length, - estimatedTokens: currentTokenCount, - truncated: wasLimited, - result: limitedResult, - message: wasLimited - ? `Retrieved ${limitedResult.length} rows (out of ${snapshot.length} total, limited by token count ~${currentTokenCount} tokens) from snapshot results for operator ${args.operatorId}` - : `Retrieved ${limitedResult.length} rows (out of ${snapshot.length} total, ~${currentTokenCount} tokens) from snapshot results for operator ${args.operatorId}`, - }; - } - - // No results available at all - return { - success: false, - error: `No results available for operator ${args.operatorId}. The operator may not have been executed yet, or it may not produce viewable results.`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create getOperatorResultInfo tool for getting operator result information - */ -export function createGetOperatorResultInfoTool( - workflowResultService: WorkflowResultService, - workflowActionService: WorkflowActionService -) { - return tool({ - name: "getOperatorResultInfo", - description: "Get information about an operator's results, including total count and pagination details", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to get result info for"), - }), - execute: async (args: { operatorId: string }) => { - try { - // Clear previous highlights at start of tool execution - - // Highlight operator being inspected - - const paginatedResultService = workflowResultService.getPaginatedResultService(args.operatorId); - if (!paginatedResultService) { - return { - success: false, - error: `No paginated results available for operator ${args.operatorId}`, - }; - } - const totalTuples = paginatedResultService.getCurrentTotalNumTuples(); - const currentPage = paginatedResultService.getCurrentPageIndex(); - const schema = paginatedResultService.getSchema(); - - return { - success: true, - operatorId: args.operatorId, - totalTuples: totalTuples, - currentPage: currentPage, - schema: schema, - message: `Operator ${args.operatorId} has ${totalTuples} result tuples`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create getValidationInfoOfCurrentWorkflow tool for getting workflow validation information - */ -export function createGetValidationInfoOfCurrentWorkflowTool( - validationWorkflowService: ValidationWorkflowService, - workflowActionService: WorkflowActionService -) { - return tool({ - name: "getValidationInfoOfCurrentWorkflow", - description: - "Get all current validation errors in the workflow. This shows which operators have validation issues and what the errors are. Also returns lists of valid and invalid operator IDs. Use this to check if operators are properly configured before execution.", - inputSchema: z.object({}), - execute: async () => { - try { - const validationOutput = validationWorkflowService.getCurrentWorkflowValidationError(); - const errorCount = Object.keys(validationOutput.errors).length; - - const validGraph = validationWorkflowService.getValidTexeraGraph(); - const validOperators = validGraph.getAllOperators(); - const allOperators = workflowActionService.getTexeraGraph().getAllOperators(); - - const validOperatorIds = validOperators.map(op => op.operatorID); - const invalidCount = allOperators.length - validOperators.length; - - return { - success: true, - errors: validationOutput.errors, - errorCount: errorCount, - validOperatorIds: validOperatorIds, - validCount: validOperators.length, - totalCount: allOperators.length, - invalidCount: invalidCount, - message: - errorCount === 0 - ? "No validation errors in the workflow" - : `Found ${errorCount} operator(s) with validation errors. ${validOperators.length} valid operator(s) out of ${allOperators.length} total`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create validateOperator tool for validating a specific operator - */ -export function createValidateOperatorTool(validationWorkflowService: ValidationWorkflowService) { - return tool({ - name: "validateOperator", - description: - "Validate a specific operator to check if it's properly configured. Returns validation status and any error messages if invalid.", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to validate"), - }), - execute: async (args: { operatorId: string }) => { - try { - const validation = validationWorkflowService.validateOperator(args.operatorId); - - if (validation.isValid) { - return { - success: true, - isValid: true, - message: `Operator ${args.operatorId} is valid`, - }; - } else { - return { - success: true, - isValid: false, - errors: validation.messages, - message: `Operator ${args.operatorId} has validation errors`, - }; - } - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -/** - * Create getComputingUnitStatus tool for checking computing unit connection status - */ -export function createGetComputingUnitStatusTool(computingUnitStatusService: any) { - return tool({ - name: "getComputingUnitStatus", - description: - "Check the status of the computing unit connection. This is important before workflow execution - if the unit is disconnected, workflows cannot be executed. Use this when execution fails or to verify readiness for execution.", - inputSchema: z.object({}), - execute: async () => { - try { - const selectedUnit = computingUnitStatusService.getSelectedComputingUnitValue(); - - if (!selectedUnit) { - return { - success: true, - status: "No Computing Unit", - isConnected: false, - message: - "No computing unit is selected. Workflow execution is not available. Please remind the user to connect to a computing unit.", - }; - } - - const unitStatus = selectedUnit.status; - const isConnected = unitStatus === "Running"; - - return { - success: true, - status: unitStatus, - isConnected: isConnected, - computingUnit: { - cuid: selectedUnit.computingUnit.cuid, - name: selectedUnit.computingUnit.name, - }, - message: isConnected - ? `Computing unit "${selectedUnit.computingUnit.name}" is running and ready for workflow execution` - : unitStatus === "Pending" - ? `Computing unit "${selectedUnit.computingUnit.name}" is pending/starting. Workflow execution may not be available yet.` - : `Computing unit is in state: ${unitStatus}. Workflow execution may not be available.`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} From b4f1d822724e34472ca7dfdf344b9a66feb9efac Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 19:16:23 -0800 Subject: [PATCH 117/158] copilot: refactor tools --- .../service/copilot/texera-copilot.ts | 42 ++--- .../service/copilot/workflow-tools.ts | 171 +++++------------- 2 files changed, 67 insertions(+), 146 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 88f44cfaf36..424f96ab7e0 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -21,16 +21,14 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, Observable, from } from "rxjs"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { - createGetOperatorTool, + toolWithTimeout, + createGetOperatorInCurrentWorkflowTool, createGetOperatorPropertiesSchemaTool, createGetOperatorPortsInfoTool, createGetOperatorMetadataTool, - createGetOperatorInputSchemaTool, - createGetOperatorOutputSchemaTool, - toolWithTimeout, createListAllOperatorTypesTool, - createListLinksTool, - createListOperatorIdsTool, + createListLinksInCurrentWorkflowTool, + createListOperatorsInCurrentWorkflowTool, } from "./workflow-tools"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { createOpenAI } from "@ai-sdk/openai"; @@ -222,34 +220,28 @@ export class TexeraCopilot { * Create workflow manipulation tools with timeout protection. */ private createWorkflowTools(): Record { - const listOperatorIdsTool = toolWithTimeout(createListOperatorIdsTool(this.workflowActionService)); - const listLinksTool = toolWithTimeout(createListLinksTool(this.workflowActionService)); - const listAllOperatorTypesTool = toolWithTimeout(createListAllOperatorTypesTool(this.workflowUtilService)); - const getOperatorTool = toolWithTimeout(createGetOperatorTool(this.workflowActionService)); - const getOperatorPropertiesSchemaTool = toolWithTimeout( - createGetOperatorPropertiesSchemaTool(this.workflowActionService, this.operatorMetadataService) + const listOperatorsInCurrentWorkflowTool = toolWithTimeout( + createListOperatorsInCurrentWorkflowTool(this.workflowActionService) ); - const getOperatorPortsInfoTool = toolWithTimeout( - createGetOperatorPortsInfoTool(this.workflowActionService, this.operatorMetadataService) - ); - const getOperatorMetadataTool = toolWithTimeout( - createGetOperatorMetadataTool(this.workflowActionService, this.operatorMetadataService) + const listLinksTool = toolWithTimeout(createListLinksInCurrentWorkflowTool(this.workflowActionService)); + const listAllOperatorTypesTool = toolWithTimeout(createListAllOperatorTypesTool(this.workflowUtilService)); + const getOperatorTool = toolWithTimeout( + createGetOperatorInCurrentWorkflowTool(this.workflowActionService, this.workflowCompilingService) ); - const getOperatorInputSchemaTool = toolWithTimeout(createGetOperatorInputSchemaTool(this.workflowCompilingService)); - const getOperatorOutputSchemaTool = toolWithTimeout( - createGetOperatorOutputSchemaTool(this.workflowCompilingService) + const getOperatorPropertiesSchemaTool = toolWithTimeout( + createGetOperatorPropertiesSchemaTool(this.operatorMetadataService) ); + const getOperatorPortsInfoTool = toolWithTimeout(createGetOperatorPortsInfoTool(this.operatorMetadataService)); + const getOperatorMetadataTool = toolWithTimeout(createGetOperatorMetadataTool(this.operatorMetadataService)); return { listAllOperatorTypes: listAllOperatorTypesTool, - listOperatorIds: listOperatorIdsTool, - listLinks: listLinksTool, - getOperator: getOperatorTool, + listOperatorsInCurrentWorkflow: listOperatorsInCurrentWorkflowTool, + listLinksInCurrentWorkflow: listLinksTool, + getOperatorInCurrentWorkflow: getOperatorTool, getOperatorPropertiesSchema: getOperatorPropertiesSchemaTool, getOperatorPortsInfo: getOperatorPortsInfoTool, getOperatorMetadata: getOperatorMetadataTool, - getOperatorInputSchema: getOperatorInputSchemaTool, - getOperatorOutputSchema: getOperatorOutputSchemaTool, }; } diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index f9829fde233..c6ea7308101 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -57,20 +57,24 @@ export function toolWithTimeout(toolConfig: any): any { }; } -export function createListOperatorIdsTool(workflowActionService: WorkflowActionService) { +export function createListOperatorsInCurrentWorkflowTool(workflowActionService: WorkflowActionService) { return tool({ - name: "listOperatorIds", - description: "Get all operator IDs in the current workflow", + name: "listOperatorsInCurrentWorkflow", + description: "Get all operator IDs, types and custom names in the current workflow", inputSchema: z.object({}), execute: async () => { try { const operators = workflowActionService.getTexeraGraph().getAllOperators(); - const operatorIds = operators.map(op => op.operatorID); + const operatorList = operators.map(op => ({ + operatorId: op.operatorID, + operatorType: op.operatorType, + customDisplayName: op.customDisplayName, + })); return { success: true, - operatorIds: operatorIds, - count: operatorIds.length, + operators: operatorList, + count: operatorList.length, }; } catch (error: any) { return { success: false, error: error.message }; @@ -79,9 +83,9 @@ export function createListOperatorIdsTool(workflowActionService: WorkflowActionS }); } -export function createListLinksTool(workflowActionService: WorkflowActionService) { +export function createListLinksInCurrentWorkflowTool(workflowActionService: WorkflowActionService) { return tool({ - name: "listLinks", + name: "listLinksInCurrentWorkflow", description: "Get all links in the current workflow", inputSchema: z.object({}), execute: async () => { @@ -119,10 +123,14 @@ export function createListAllOperatorTypesTool(workflowUtilService: WorkflowUtil }); } -export function createGetOperatorTool(workflowActionService: WorkflowActionService) { +export function createGetOperatorInCurrentWorkflowTool( + workflowActionService: WorkflowActionService, + workflowCompilingService: WorkflowCompilingService +) { return tool({ - name: "getOperator", - description: "Get detailed information about a specific operator in the workflow", + name: "getOperatorInCurrentWorkflow", + description: + "Get detailed information about a specific operator in the current workflow, including its input and output schemas", inputSchema: z.object({ operatorId: z.string().describe("ID of the operator to retrieve"), }), @@ -130,9 +138,19 @@ export function createGetOperatorTool(workflowActionService: WorkflowActionServi try { const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); + // Get input schema (empty map if not available) + const inputSchemaMap = workflowCompilingService.getOperatorInputSchemaMap(args.operatorId); + const inputSchema = inputSchemaMap || {}; + + // Get output schema (empty map if not available) + const outputSchemaMap = workflowCompilingService.getOperatorOutputSchemaMap(args.operatorId); + const outputSchema = outputSchemaMap || {}; + return { success: true, operator: operator, + inputSchema: inputSchema, + outputSchema: outputSchema, message: `Retrieved operator ${args.operatorId}`, }; } catch (error: any) { @@ -145,25 +163,16 @@ export function createGetOperatorTool(workflowActionService: WorkflowActionServi }); } -export function createGetOperatorPropertiesSchemaTool( - workflowActionService: WorkflowActionService, - operatorMetadataService: OperatorMetadataService -) { +export function createGetOperatorPropertiesSchemaTool(operatorMetadataService: OperatorMetadataService) { return tool({ name: "getOperatorPropertiesSchema", - description: - "Get only the properties schema for an operator. Use this before setting operator properties.", + description: "Get only the properties schema for an operator type. Use this before setting operator properties.", inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to get properties schema for"), + operatorType: z.string().describe("Type of the operator to get properties schema for"), }), - execute: async (args: { operatorId: string }) => { + execute: async (args: { operatorType: string }) => { try { - const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); - if (!operator) { - return { success: false, error: `Operator ${args.operatorId} not found` }; - } - - const schema = operatorMetadataService.getOperatorSchema(operator.operatorType); + const schema = operatorMetadataService.getOperatorSchema(args.operatorType); const propertiesSchema = { properties: schema.jsonSchema.properties, required: schema.jsonSchema.required, @@ -173,8 +182,8 @@ export function createGetOperatorPropertiesSchemaTool( return { success: true, propertiesSchema: propertiesSchema, - operatorType: operator.operatorType, - message: `Retrieved properties schema for operator ${args.operatorId} (type: ${operator.operatorType})`, + operatorType: args.operatorType, + message: `Retrieved properties schema for operator type ${args.operatorType}`, }; } catch (error: any) { return { success: false, error: error.message }; @@ -183,25 +192,17 @@ export function createGetOperatorPropertiesSchemaTool( }); } -export function createGetOperatorPortsInfoTool( - workflowActionService: WorkflowActionService, - operatorMetadataService: OperatorMetadataService -) { +export function createGetOperatorPortsInfoTool(operatorMetadataService: OperatorMetadataService) { return tool({ name: "getOperatorPortsInfo", description: - "Get input and output port information for an operator. This is more token-efficient than getOperatorSchema and returns only port details (display names, multi-input support, etc.).", + "Get input and output port information for an operator type. This is more token-efficient than getOperatorSchema and returns only port details (display names, multi-input support, etc.).", inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to get port information for"), + operatorType: z.string().describe("Type of the operator to get port information for"), }), - execute: async (args: { operatorId: string }) => { + execute: async (args: { operatorType: string }) => { try { - const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); - if (!operator) { - return { success: false, error: `Operator ${args.operatorId} not found` }; - } - - const schema = operatorMetadataService.getOperatorSchema(operator.operatorType); + const schema = operatorMetadataService.getOperatorSchema(args.operatorType); const portsInfo = { inputPorts: schema.additionalMetadata.inputPorts, outputPorts: schema.additionalMetadata.outputPorts, @@ -212,8 +213,8 @@ export function createGetOperatorPortsInfoTool( return { success: true, portsInfo: portsInfo, - operatorType: operator.operatorType, - message: `Retrieved port information for operator ${args.operatorId} (type: ${operator.operatorType})`, + operatorType: args.operatorType, + message: `Retrieved port information for operator type ${args.operatorType}`, }; } catch (error: any) { return { success: false, error: error.message }; @@ -222,32 +223,25 @@ export function createGetOperatorPortsInfoTool( }); } -export function createGetOperatorMetadataTool( - workflowActionService: WorkflowActionService, - operatorMetadataService: OperatorMetadataService -) { +export function createGetOperatorMetadataTool(operatorMetadataService: OperatorMetadataService) { return tool({ name: "getOperatorMetadata", description: - "Get semantic metadata for an operator, including user-friendly name, description, operator group, and capabilities. This is very useful to understand the semantics and purpose of each operator - what it does, how it works, and what kind of data transformation it performs.", + "Get semantic metadata for an operator type, including user-friendly name, description, operator group, and capabilities. This is very useful to understand the semantics and purpose of each operator type - what it does, how it works, and what kind of data transformation it performs.", inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to get metadata for"), + operatorType: z.string().describe("Type of the operator to get metadata for"), }), - execute: async (args: { operatorId: string; signal?: AbortSignal }) => { + execute: async (args: { operatorType: string; signal?: AbortSignal }) => { try { - const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); - if (!operator) { - return { success: false, error: `Operator ${args.operatorId} not found` }; - } - const schema = operatorMetadataService.getOperatorSchema(operator.operatorType); + const schema = operatorMetadataService.getOperatorSchema(args.operatorType); const metadata = schema.additionalMetadata; return { success: true, metadata: metadata, - operatorType: operator.operatorType, + operatorType: args.operatorType, operatorVersion: schema.operatorVersion, - message: `Retrieved metadata for operator ${args.operatorId} (type: ${operator.operatorType})`, + message: `Retrieved metadata for operator type ${args.operatorType}`, }; } catch (error: any) { return { success: false, error: error.message }; @@ -255,68 +249,3 @@ export function createGetOperatorMetadataTool( }, }); } - -export function createGetOperatorInputSchemaTool(workflowCompilingService: WorkflowCompilingService) { - return tool({ - name: "getOperatorInputSchema", - description: - "Get the input schema for an operator, which shows what columns/attributes are available from upstream operators. This is determined by workflow compilation and schema propagation.", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to get input schema for"), - }), - execute: async (args: { operatorId: string }) => { - try { - const inputSchemaMap = workflowCompilingService.getOperatorInputSchemaMap(args.operatorId); - - if (!inputSchemaMap) { - return { - success: true, - inputSchema: null, - message: `Operator ${args.operatorId} has no input schema (may be a source operator or not connected)`, - }; - } - - return { - success: true, - inputSchema: inputSchemaMap, - message: `Retrieved input schema for operator ${args.operatorId}`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -export function createGetOperatorOutputSchemaTool(workflowCompilingService: WorkflowCompilingService) { - return tool({ - name: "getOperatorOutputSchema", - description: - "Get the output schema for an operator, which shows what columns/attributes this operator produces. This is determined by workflow compilation and shows the schema that will be available to downstream operators.", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to get output schema for"), - }), - execute: async (args: { operatorId: string }) => { - try { - const outputSchemaMap = workflowCompilingService.getOperatorOutputSchemaMap(args.operatorId); - - if (!outputSchemaMap) { - return { - success: true, - outputSchema: null, - message: `Operator ${args.operatorId} has no output schema (workflow may not be compiled yet or operator has errors)`, - }; - } - - return { - success: true, - outputSchema: outputSchemaMap, - message: `Retrieved output schema for operator ${args.operatorId}`, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - From f037c486cdb927460fcee30a65c68812d27abe82 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 19:18:23 -0800 Subject: [PATCH 118/158] copilot: refactor tools --- frontend/src/app/workspace/service/copilot/texera-copilot.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 424f96ab7e0..c298ec78aec 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -137,7 +137,6 @@ export class TexeraCopilot { throw new Error("Copilot not initialized"); } - // Guard against sending messages when not available if (this.state !== CopilotState.AVAILABLE) { throw new Error(`Cannot send message: agent is ${this.state}`); } From 744afd69ce5c18306020e0448c724906d807e0ba Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 19:51:14 -0800 Subject: [PATCH 119/158] change the async await to rxjs --- .../agent-chat/agent-chat.component.ts | 21 +- .../agent-panel/agent-panel.component.ts | 8 +- .../agent-registration.component.ts | 26 +- .../copilot/texera-copilot-manager.service.ts | 284 +++++++++++------- .../service/copilot/texera-copilot.ts | 193 ++++++------ 5 files changed, 315 insertions(+), 217 deletions(-) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts index a187c09b13e..3fa5e1bcf8f 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -95,10 +95,11 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { } public showSystemInfo(): void { - const systemInfo = this.copilotManagerService.getSystemInfo(this.agentInfo.id); - this.systemPrompt = systemInfo.systemPrompt; - this.availableTools = systemInfo.tools; - this.isSystemInfoModalVisible = true; + this.copilotManagerService.getSystemInfo(this.agentInfo.id).subscribe(systemInfo => { + this.systemPrompt = systemInfo.systemPrompt; + this.availableTools = systemInfo.tools; + this.isSystemInfoModalVisible = true; + }); } public closeSystemInfoModal(): void { @@ -207,26 +208,26 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { } public stopGeneration(): void { - this.copilotManagerService.stopGeneration(this.agentInfo.id); + this.copilotManagerService.stopGeneration(this.agentInfo.id).subscribe(); } public clearMessages(): void { - this.copilotManagerService.clearMessages(this.agentInfo.id); + this.copilotManagerService.clearMessages(this.agentInfo.id).subscribe(); } public isGenerating(): boolean { - return this.copilotManagerService.getAgentState(this.agentInfo.id) === CopilotState.GENERATING; + return this.agentState === CopilotState.GENERATING; } public isStopping(): boolean { - return this.copilotManagerService.getAgentState(this.agentInfo.id) === CopilotState.STOPPING; + return this.agentState === CopilotState.STOPPING; } public isAvailable(): boolean { - return this.copilotManagerService.getAgentState(this.agentInfo.id) === CopilotState.AVAILABLE; + return this.agentState === CopilotState.AVAILABLE; } public isConnected(): boolean { - return this.copilotManagerService.isAgentConnected(this.agentInfo.id); + return this.agentState !== CopilotState.UNAVAILABLE; } } diff --git a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts index 2a15e1e6e36..82cbc8245fc 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts @@ -75,11 +75,15 @@ export class AgentPanelComponent implements OnInit, OnDestroy { // Subscribe to agent changes this.copilotManagerService.agentChange$.pipe(untilDestroyed(this)).subscribe(() => { - this.agents = this.copilotManagerService.getAllAgents(); + this.copilotManagerService.getAllAgents().subscribe(agents => { + this.agents = agents; + }); }); // Load initial agents - this.agents = this.copilotManagerService.getAllAgents(); + this.copilotManagerService.getAllAgents().subscribe(agents => { + this.agents = agents; + }); } @HostListener("window:beforeunload") diff --git a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts index 71872d406b3..baa7343d244 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts @@ -89,20 +89,18 @@ export class AgentRegistrationComponent implements OnInit, OnDestroy { this.isCreating = true; - try { - const agentInfo = await this.copilotManagerService.createAgent( - this.selectedModelType, - this.customAgentName || undefined - ); - - this.agentCreated.emit(agentInfo.id); - this.selectedModelType = null; - this.customAgentName = ""; - } catch (error) { - this.notificationService.error(`Failed to create agent: ${error}`); - } finally { - this.isCreating = false; - } + this.copilotManagerService.createAgent(this.selectedModelType, this.customAgentName || undefined).subscribe({ + next: agentInfo => { + this.agentCreated.emit(agentInfo.id); + this.selectedModelType = null; + this.customAgentName = ""; + this.isCreating = false; + }, + error: (error: unknown) => { + this.notificationService.error(`Failed to create agent: ${error}`); + this.isCreating = false; + }, + }); } public canCreate(): boolean { diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index 2377a030acc..15e5c56fa50 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -20,18 +20,7 @@ import { Injectable, Injector } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { TexeraCopilot, AgentUIMessage, CopilotState } from "./texera-copilot"; -import { Observable, Subject, catchError, map, of, shareReplay, tap } from "rxjs"; -import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; -import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; -import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; -import { DynamicSchemaService } from "../dynamic-schema/dynamic-schema.service"; -import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; -import { WorkflowResultService } from "../workflow-result/workflow-result.service"; -import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; -import { ValidationWorkflowService } from "../validation/validation-workflow.service"; -import { NotificationService } from "../../../common/service/notification/notification.service"; -import { ComputingUnitStatusService } from "../computing-unit-status/computing-unit-status.service"; -import { WorkflowConsoleService } from "../workflow-console/workflow-console.service"; +import { Observable, Subject, catchError, map, of, shareReplay, tap, defer, throwError, switchMap } from "rxjs"; import { AppSettings } from "../../../common/app-setting"; /** @@ -92,50 +81,79 @@ export class TexeraCopilotManagerService { /** * Create a new agent with the specified model type. + * Returns an Observable that emits the created AgentInfo. */ - public async createAgent(modelType: string, customName?: string): Promise { - const agentId = `agent-${++this.agentCounter}`; - const agentName = customName || `Agent ${this.agentCounter}`; + public createAgent(modelType: string, customName?: string): Observable { + return defer(() => { + const agentId = `agent-${++this.agentCounter}`; + const agentName = customName || `Agent ${this.agentCounter}`; - try { const agentInstance = this.createCopilotInstance(modelType); agentInstance.setAgentInfo(agentId, agentName); - await agentInstance.initialize(); - - const agentInfo: AgentInfo = { - id: agentId, - name: agentName, - modelType, - instance: agentInstance, - createdAt: new Date(), - }; - - this.agents.set(agentId, agentInfo); - this.agentChangeSubject.next(); - - return agentInfo; - } catch (error) { - throw error; - } + + return agentInstance.initialize().pipe( + map(() => { + const agentInfo: AgentInfo = { + id: agentId, + name: agentName, + modelType, + instance: agentInstance, + createdAt: new Date(), + }; + + this.agents.set(agentId, agentInfo); + this.agentChangeSubject.next(); + + return agentInfo; + }), + catchError((error: unknown) => { + return throwError(() => error); + }) + ); + }); } - public getAgent(agentId: string): AgentInfo | undefined { - return this.agents.get(agentId); + /** + * Get an agent by ID. + * Returns an Observable that emits the AgentInfo or throws if not found. + */ + public getAgent(agentId: string): Observable { + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return throwError(() => new Error(`Agent with ID ${agentId} not found`)); + } + return of(agent); + }); } - public getAllAgents(): AgentInfo[] { - return Array.from(this.agents.values()); + /** + * Get all agents. + * Returns an Observable that emits the array of all AgentInfo. + */ + public getAllAgents(): Observable { + return of(Array.from(this.agents.values())); } - public deleteAgent(agentId: string): boolean { - const agent = this.agents.get(agentId); - if (agent) { - agent.instance.disconnect(); - this.agents.delete(agentId); - this.agentChangeSubject.next(); - return true; - } - return false; + /** + * Delete an agent by ID. + * Returns an Observable that emits true if deleted, false if not found. + */ + public deleteAgent(agentId: string): Observable { + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return of(false); + } + + return agent.instance.disconnect().pipe( + map(() => { + this.agents.delete(agentId); + this.agentChangeSubject.next(); + return true; + }) + ); + }); } /** @@ -176,86 +194,146 @@ export class TexeraCopilotManagerService { .join(" "); } - public getAgentCount(): number { - return this.agents.size; + /** + * Get the count of active agents. + * Returns an Observable that emits the count. + */ + public getAgentCount(): Observable { + return of(this.agents.size); } + /** + * Send a message to an agent. + * Returns an Observable that completes when the message is processed. + */ public sendMessage(agentId: string, message: string): Observable { - const agent = this.agents.get(agentId); - if (!agent) { - throw new Error(`Agent with ID ${agentId} not found`); - } - return agent.instance.sendMessage(message); + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return throwError(() => new Error(`Agent with ID ${agentId} not found`)); + } + return agent.instance.sendMessage(message); + }); } + /** + * Get the agent responses observable stream. + * Returns an Observable that emits arrays of AgentUIMessage. + */ public getAgentResponsesObservable(agentId: string): Observable { - const agent = this.agents.get(agentId); - if (!agent) { - throw new Error(`Agent with ID ${agentId} not found`); - } - return agent.instance.agentResponses$; + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return throwError(() => new Error(`Agent with ID ${agentId} not found`)); + } + return agent.instance.agentResponses$; + }); } - public getAgentResponses(agentId: string): AgentUIMessage[] { - const agent = this.agents.get(agentId); - if (!agent) { - throw new Error(`Agent with ID ${agentId} not found`); - } - return agent.instance.getAgentResponses(); + /** + * Get the current agent responses. + * Returns an Observable that emits the current array of AgentUIMessage. + */ + public getAgentResponses(agentId: string): Observable { + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return throwError(() => new Error(`Agent with ID ${agentId} not found`)); + } + return of(agent.instance.getAgentResponses()); + }); } - public clearMessages(agentId: string): void { - const agent = this.agents.get(agentId); - if (!agent) { - throw new Error(`Agent with ID ${agentId} not found`); - } - agent.instance.clearMessages(); + /** + * Clear all messages for an agent. + * Returns an Observable that completes when done. + */ + public clearMessages(agentId: string): Observable { + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return throwError(() => new Error(`Agent with ID ${agentId} not found`)); + } + agent.instance.clearMessages(); + return of(undefined); + }); } - public stopGeneration(agentId: string): void { - const agent = this.agents.get(agentId); - if (!agent) { - throw new Error(`Agent with ID ${agentId} not found`); - } - agent.instance.stopGeneration(); + /** + * Stop generation for an agent. + * Returns an Observable that completes when done. + */ + public stopGeneration(agentId: string): Observable { + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return throwError(() => new Error(`Agent with ID ${agentId} not found`)); + } + agent.instance.stopGeneration(); + return of(undefined); + }); } - public getAgentState(agentId: string) { - const agent = this.agents.get(agentId); - if (!agent) { - throw new Error(`Agent with ID ${agentId} not found`); - } - return agent.instance.getState(); + /** + * Get the current state of an agent. + * Returns an Observable that emits the CopilotState. + */ + public getAgentState(agentId: string): Observable { + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return throwError(() => new Error(`Agent with ID ${agentId} not found`)); + } + return of(agent.instance.getState()); + }); } + /** + * Get the state observable stream for an agent. + * Returns an Observable that emits CopilotState changes. + */ public getAgentStateObservable(agentId: string): Observable { - const agent = this.agents.get(agentId); - if (!agent) { - throw new Error(`Agent with ID ${agentId} not found`); - } - return agent.instance.state$; + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return throwError(() => new Error(`Agent with ID ${agentId} not found`)); + } + return agent.instance.state$; + }); } - public isAgentConnected(agentId: string): boolean { - const agent = this.agents.get(agentId); - if (!agent) { - return false; - } - return agent.instance.isConnected(); + /** + * Check if an agent is connected. + * Returns an Observable that emits a boolean. + */ + public isAgentConnected(agentId: string): Observable { + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return of(false); + } + return of(agent.instance.isConnected()); + }); } - public getSystemInfo(agentId: string): { + /** + * Get system information for an agent. + * Returns an Observable that emits the system prompt and tools. + */ + public getSystemInfo(agentId: string): Observable<{ systemPrompt: string; tools: Array<{ name: string; description: string; inputSchema: any }>; - } { - const agent = this.agents.get(agentId); - if (!agent) { - throw new Error(`Agent with ID ${agentId} not found`); - } - return { - systemPrompt: agent.instance.getSystemPrompt(), - tools: agent.instance.getToolsInfo(), - }; + }> { + return defer(() => { + const agent = this.agents.get(agentId); + if (!agent) { + return throwError(() => new Error(`Agent with ID ${agentId} not found`)); + } + return of({ + systemPrompt: agent.instance.getSystemPrompt(), + tools: agent.instance.getToolsInfo(), + }); + }); } /** diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index c298ec78aec..c81b492cce0 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -18,7 +18,8 @@ */ import { Injectable } from "@angular/core"; -import { BehaviorSubject, Observable, from } from "rxjs"; +import { BehaviorSubject, Observable, from, of, throwError, defer } from "rxjs"; +import { map, catchError, tap, switchMap, finalize } from "rxjs/operators"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { toolWithTimeout, @@ -115,88 +116,96 @@ export class TexeraCopilot { /** * Initialize the copilot with the AI model. + * Returns an Observable that completes when initialization is done. */ - public async initialize(): Promise { - try { - this.model = createOpenAI({ - baseURL: new URL(`${AppSettings.getApiEndpoint()}`, document.baseURI).toString(), - apiKey: "dummy", - }).chat(this.modelType); - - this.setState(CopilotState.AVAILABLE); - } catch (error: unknown) { - this.setState(CopilotState.UNAVAILABLE); - throw error; - } + public initialize(): Observable { + return defer(() => { + try { + this.model = createOpenAI({ + baseURL: new URL(`${AppSettings.getApiEndpoint()}`, document.baseURI).toString(), + apiKey: "dummy", + }).chat(this.modelType); + + this.setState(CopilotState.AVAILABLE); + return of(undefined); + } catch (error: unknown) { + this.setState(CopilotState.UNAVAILABLE); + return throwError(() => error); + } + }); } public sendMessage(message: string): Observable { - return from( - (async () => { - if (!this.model) { - throw new Error("Copilot not initialized"); - } - - if (this.state !== CopilotState.AVAILABLE) { - throw new Error(`Cannot send message: agent is ${this.state}`); - } - - this.setState(CopilotState.GENERATING); - - const userMessage: UserModelMessage = { role: "user", content: message }; - this.messages.push(userMessage); - const userUIMessage: AgentUIMessage = { - role: "user", - content: message, - isBegin: true, - isEnd: true, - }; - this.agentResponses.push(userUIMessage); - this.agentResponsesSubject.next([...this.agentResponses]); - - try { - const tools = this.createWorkflowTools(); - let isFirstStep = true; - - const { response } = await generateText({ - model: this.model, - messages: this.messages, - tools, - system: COPILOT_SYSTEM_PROMPT, - stopWhen: ({ steps }) => { - if (this.state === CopilotState.STOPPING) { - this.notificationService.info(`Agent ${this.agentName} has stopped generation`); - return true; - } - return stepCountIs(50)({ steps }); - }, - onStepFinish: ({ text, toolCalls, toolResults, usage }) => { - if (this.state === CopilotState.STOPPING) { - return; - } - - const stepResponse: AgentUIMessage = { - role: "agent", - content: text || "", - isBegin: isFirstStep, - isEnd: false, - toolCalls: toolCalls, - toolResults: toolResults, - usage: usage as any, - }; - this.agentResponses.push(stepResponse); - this.agentResponsesSubject.next([...this.agentResponses]); - - isFirstStep = false; - }, - }); + return defer(() => { + // Validation + if (!this.model) { + return throwError(() => new Error("Copilot not initialized")); + } + + if (this.state !== CopilotState.AVAILABLE) { + return throwError(() => new Error(`Cannot send message: agent is ${this.state}`)); + } + + // Set state to generating + this.setState(CopilotState.GENERATING); + + // Add user message + const userMessage: UserModelMessage = { role: "user", content: message }; + this.messages.push(userMessage); + const userUIMessage: AgentUIMessage = { + role: "user", + content: message, + isBegin: true, + isEnd: true, + }; + this.agentResponses.push(userUIMessage); + this.agentResponsesSubject.next([...this.agentResponses]); + + const tools = this.createWorkflowTools(); + let isFirstStep = true; + + // Generate text using AI + return from( + generateText({ + model: this.model, + messages: this.messages, + tools, + system: COPILOT_SYSTEM_PROMPT, + stopWhen: ({ steps }) => { + if (this.state === CopilotState.STOPPING) { + this.notificationService.info(`Agent ${this.agentName} has stopped generation`); + return true; + } + return stepCountIs(50)({ steps }); + }, + onStepFinish: ({ text, toolCalls, toolResults, usage }) => { + if (this.state === CopilotState.STOPPING) { + return; + } + + const stepResponse: AgentUIMessage = { + role: "agent", + content: text || "", + isBegin: isFirstStep, + isEnd: false, + toolCalls: toolCalls, + toolResults: toolResults, + usage: usage as any, + }; + this.agentResponses.push(stepResponse); + this.agentResponsesSubject.next([...this.agentResponses]); + + isFirstStep = false; + }, + }) + ).pipe( + tap(({ response }) => { this.messages.push(...response.messages); this.agentResponsesSubject.next([...this.agentResponses]); - - this.setState(CopilotState.AVAILABLE); - } catch (err: any) { - this.setState(CopilotState.AVAILABLE); - const errorText = `Error: ${err?.message ?? String(err)}`; + }), + map(() => undefined), + catchError((err: unknown) => { + const errorText = `Error: ${err instanceof Error ? err.message : String(err)}`; const assistantError: AssistantModelMessage = { role: "assistant", content: errorText }; this.messages.push(assistantError); @@ -209,10 +218,14 @@ export class TexeraCopilot { this.agentResponses.push(errorResponse); this.agentResponsesSubject.next([...this.agentResponses]); - throw err; - } - })() - ); + return throwError(() => err); + }), + finalize(() => { + // Always set state back to available when done + this.setState(CopilotState.AVAILABLE); + }) + ); + }); } /** @@ -265,14 +278,18 @@ export class TexeraCopilot { return this.state; } - public async disconnect(): Promise { - if (this.state === CopilotState.GENERATING) { - this.stopGeneration(); - } + public disconnect(): Observable { + return defer(() => { + if (this.state === CopilotState.GENERATING) { + this.stopGeneration(); + } + + this.clearMessages(); + this.setState(CopilotState.UNAVAILABLE); + this.notificationService.info(`Agent ${this.agentName} is removed successfully`); - this.clearMessages(); - this.setState(CopilotState.UNAVAILABLE); - this.notificationService.info(`Agent ${this.agentName} is removed successfully`); + return of(undefined); + }); } public isConnected(): boolean { From 6d5d67410b2309f4cb9ba9fee185fca28eb878b6 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 21:43:34 -0800 Subject: [PATCH 120/158] improve texera-copilot logic cleaning --- .../service/copilot/texera-copilot.ts | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index c81b492cce0..0289fa68915 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -114,6 +114,36 @@ export class TexeraCopilot { this.stateSubject.next(newState); } + /** + * Add an agent UI message and emit to subscribers. + */ + private emitAgentUIMessage( + role: "user" | "agent", + content: string, + isBegin: boolean, + isEnd: boolean, + toolCalls?: any[], + toolResults?: any[], + usage?: { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + cachedInputTokens?: number; + } + ): void { + const message: AgentUIMessage = { + role, + content, + isBegin, + isEnd, + toolCalls, + toolResults, + usage, + }; + this.agentResponses.push(message); + this.agentResponsesSubject.next([...this.agentResponses]); + } + /** * Initialize the copilot with the AI model. * Returns an Observable that completes when initialization is done. @@ -137,7 +167,6 @@ export class TexeraCopilot { public sendMessage(message: string): Observable { return defer(() => { - // Validation if (!this.model) { return throwError(() => new Error("Copilot not initialized")); } @@ -146,25 +175,18 @@ export class TexeraCopilot { return throwError(() => new Error(`Cannot send message: agent is ${this.state}`)); } - // Set state to generating this.setState(CopilotState.GENERATING); - // Add user message + // Add user message to UI + this.emitAgentUIMessage("user", message, true, true); + + // Add user message to the message history const userMessage: UserModelMessage = { role: "user", content: message }; this.messages.push(userMessage); - const userUIMessage: AgentUIMessage = { - role: "user", - content: message, - isBegin: true, - isEnd: true, - }; - this.agentResponses.push(userUIMessage); - this.agentResponsesSubject.next([...this.agentResponses]); const tools = this.createWorkflowTools(); let isFirstStep = true; - // Generate text using AI return from( generateText({ model: this.model, @@ -183,17 +205,7 @@ export class TexeraCopilot { return; } - const stepResponse: AgentUIMessage = { - role: "agent", - content: text || "", - isBegin: isFirstStep, - isEnd: false, - toolCalls: toolCalls, - toolResults: toolResults, - usage: usage as any, - }; - this.agentResponses.push(stepResponse); - this.agentResponsesSubject.next([...this.agentResponses]); + this.emitAgentUIMessage("agent", text || "", isFirstStep, false, toolCalls, toolResults, usage as any); isFirstStep = false; }, @@ -209,19 +221,11 @@ export class TexeraCopilot { const assistantError: AssistantModelMessage = { role: "assistant", content: errorText }; this.messages.push(assistantError); - const errorResponse: AgentUIMessage = { - role: "agent", - content: errorText, - isBegin: false, - isEnd: true, - }; - this.agentResponses.push(errorResponse); - this.agentResponsesSubject.next([...this.agentResponses]); + this.emitAgentUIMessage("agent", errorText, false, true); return throwError(() => err); }), finalize(() => { - // Always set state back to available when done this.setState(CopilotState.AVAILABLE); }) ); From 49c0eb32ad3e97588a57dc71a5e436de89347ffb Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 7 Nov 2025 23:47:09 -0800 Subject: [PATCH 121/158] add initial test --- .../agent-chat/agent-chat.component.spec.ts | 37 ++- .../texera-copilot-manager.service.spec.ts | 259 ++++++++++++++++ .../copilot/texera-copilot-manager.service.ts | 98 +------ .../service/copilot/texera-copilot.spec.ts | 183 ++++++++++++ .../service/copilot/texera-copilot.ts | 72 +---- .../service/copilot/workflow-tools.spec.ts | 276 ++++++++++++++++++ .../service/copilot/workflow-tools.ts | 119 +++----- 7 files changed, 804 insertions(+), 240 deletions(-) create mode 100644 frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts create mode 100644 frontend/src/app/workspace/service/copilot/texera-copilot.spec.ts create mode 100644 frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.spec.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.spec.ts index 63146f25fb0..a972d71c73f 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.spec.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.spec.ts @@ -18,22 +18,45 @@ */ import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { CopilotChatComponent } from "./copilot-chat.component"; +import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { AgentChatComponent } from "./agent-chat.component"; +import { TexeraCopilotManagerService } from "../../../service/copilot/texera-copilot-manager.service"; +import { NotificationService } from "../../../../common/service/notification/notification.service"; +import { commonTestProviders } from "../../../../common/testing/test-utils"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; -describe("CopilotChatComponent", () => { - let component: CopilotChatComponent; - let fixture: ComponentFixture; +describe("AgentChatComponent", () => { + let component: AgentChatComponent; + let fixture: ComponentFixture; + let mockCopilotManagerService: jasmine.SpyObj; + let mockNotificationService: jasmine.SpyObj; beforeEach(async () => { + mockCopilotManagerService = jasmine.createSpyObj("TexeraCopilotManagerService", [ + "getAgentResponsesObservable", + "getAgentStateObservable", + "sendMessage", + "stopGeneration", + "clearMessages", + "getSystemInfo", + ]); + mockNotificationService = jasmine.createSpyObj("NotificationService", ["info", "error", "success"]); + await TestBed.configureTestingModule({ - declarations: [CopilotChatComponent], + declarations: [AgentChatComponent], + imports: [HttpClientTestingModule], + providers: [ + { provide: TexeraCopilotManagerService, useValue: mockCopilotManagerService }, + { provide: NotificationService, useValue: mockNotificationService }, + ...commonTestProviders, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(CopilotChatComponent); + fixture = TestBed.createComponent(AgentChatComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it("should create", () => { diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts new file mode 100644 index 00000000000..360bc13c0c7 --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts @@ -0,0 +1,259 @@ +/** + * 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 { TestBed } from "@angular/core/testing"; +import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; +import { TexeraCopilotManagerService } from "./texera-copilot-manager.service"; +import { CopilotState } from "./texera-copilot"; +import { commonTestProviders } from "../../../common/testing/test-utils"; +import { AppSettings } from "../../../common/app-setting"; + +describe("TexeraCopilotManagerService", () => { + let service: TexeraCopilotManagerService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [TexeraCopilotManagerService, ...commonTestProviders], + }); + + service = TestBed.inject(TexeraCopilotManagerService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + describe("fetchModelTypes", () => { + it("should fetch and format model types", done => { + const mockResponse = { + data: [ + { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, + { id: "claude-3", object: "model", created: 1234567891, owned_by: "anthropic" }, + ], + object: "list", + }; + + service.fetchModelTypes().subscribe(models => { + expect(models.length).toBe(2); + expect(models[0].id).toBe("gpt-4"); + expect(models[0].name).toBe("Gpt 4"); + expect(models[1].id).toBe("claude-3"); + expect(models[1].name).toBe("Claude 3"); + done(); + }); + + const req = httpMock.expectOne(`${AppSettings.getApiEndpoint()}/models`); + expect(req.request.method).toBe("GET"); + req.flush(mockResponse); + }); + + it("should handle fetch error gracefully", done => { + service.fetchModelTypes().subscribe(models => { + expect(models).toEqual([]); + done(); + }); + + const req = httpMock.expectOne(`${AppSettings.getApiEndpoint()}/models`); + req.error(new ProgressEvent("error")); + }); + + it("should cache model types with shareReplay", done => { + const mockResponse = { + data: [{ id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }], + object: "list", + }; + + service.fetchModelTypes().subscribe(() => { + service.fetchModelTypes().subscribe(models => { + expect(models.length).toBe(1); + done(); + }); + }); + + const req = httpMock.expectOne(`${AppSettings.getApiEndpoint()}/models`); + req.flush(mockResponse); + }); + }); + + describe("getAllAgents", () => { + it("should return empty array initially", done => { + service.getAllAgents().subscribe(agents => { + expect(agents).toEqual([]); + done(); + }); + }); + }); + + describe("getAgentCount", () => { + it("should return 0 initially", done => { + service.getAgentCount().subscribe(count => { + expect(count).toBe(0); + done(); + }); + }); + }); + + describe("getAgent", () => { + it("should throw error when agent not found", done => { + service.getAgent("non-existent").subscribe({ + next: () => fail("Should have thrown error"), + error: (error: Error) => { + expect(error.message).toContain("not found"); + done(); + }, + }); + }); + }); + + describe("isAgentConnected", () => { + it("should return false for non-existent agent", done => { + service.isAgentConnected("non-existent").subscribe(connected => { + expect(connected).toBe(false); + done(); + }); + }); + }); + + describe("agent lifecycle management", () => { + it("should emit agent change event on agent creation", done => { + let eventEmitted = false; + + service.agentChange$.subscribe(() => { + eventEmitted = true; + }); + + setTimeout(() => { + expect(eventEmitted).toBe(false); + done(); + }, 100); + }); + }); + + describe("sendMessage", () => { + it("should throw error for non-existent agent", done => { + service.sendMessage("non-existent", "test message").subscribe({ + next: () => fail("Should have thrown error"), + error: (error: Error) => { + expect(error.message).toContain("not found"); + done(); + }, + }); + }); + }); + + describe("getAgentResponses", () => { + it("should throw error for non-existent agent", done => { + service.getAgentResponses("non-existent").subscribe({ + next: () => fail("Should have thrown error"), + error: (error: Error) => { + expect(error.message).toContain("not found"); + done(); + }, + }); + }); + }); + + describe("getAgentResponsesObservable", () => { + it("should throw error for non-existent agent", done => { + service.getAgentResponsesObservable("non-existent").subscribe({ + next: () => fail("Should have thrown error"), + error: (error: Error) => { + expect(error.message).toContain("not found"); + done(); + }, + }); + }); + }); + + describe("clearMessages", () => { + it("should throw error for non-existent agent", done => { + service.clearMessages("non-existent").subscribe({ + next: () => fail("Should have thrown error"), + error: (error: Error) => { + expect(error.message).toContain("not found"); + done(); + }, + }); + }); + }); + + describe("stopGeneration", () => { + it("should throw error for non-existent agent", done => { + service.stopGeneration("non-existent").subscribe({ + next: () => fail("Should have thrown error"), + error: (error: Error) => { + expect(error.message).toContain("not found"); + done(); + }, + }); + }); + }); + + describe("getAgentState", () => { + it("should throw error for non-existent agent", done => { + service.getAgentState("non-existent").subscribe({ + next: () => fail("Should have thrown error"), + error: (error: Error) => { + expect(error.message).toContain("not found"); + done(); + }, + }); + }); + }); + + describe("getAgentStateObservable", () => { + it("should throw error for non-existent agent", done => { + service.getAgentStateObservable("non-existent").subscribe({ + next: () => fail("Should have thrown error"), + error: (error: Error) => { + expect(error.message).toContain("not found"); + done(); + }, + }); + }); + }); + + describe("getSystemInfo", () => { + it("should throw error for non-existent agent", done => { + service.getSystemInfo("non-existent").subscribe({ + next: () => fail("Should have thrown error"), + error: (error: Error) => { + expect(error.message).toContain("not found"); + done(); + }, + }); + }); + }); + + describe("deleteAgent", () => { + it("should return false for non-existent agent", done => { + service.deleteAgent("non-existent").subscribe(deleted => { + expect(deleted).toBe(false); + done(); + }); + }); + }); +}); diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index 15e5c56fa50..ff13df72dfd 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -23,9 +23,6 @@ import { TexeraCopilot, AgentUIMessage, CopilotState } from "./texera-copilot"; import { Observable, Subject, catchError, map, of, shareReplay, tap, defer, throwError, switchMap } from "rxjs"; import { AppSettings } from "../../../common/app-setting"; -/** - * Agent information for tracking created agents. - */ export interface AgentInfo { id: string; name: string; @@ -34,9 +31,6 @@ export interface AgentInfo { createdAt: Date; } -/** - * Available model types for agent creation. - */ export interface ModelType { id: string; name: string; @@ -44,9 +38,6 @@ export interface ModelType { icon: string; } -/** - * LiteLLM Model API response. - */ interface LiteLLMModel { id: string; object: string; @@ -58,11 +49,6 @@ interface LiteLLMModelsResponse { data: LiteLLMModel[]; object: string; } - -/** - * Service to manage multiple copilot agents. - * Supports multi-agent workflows and agent lifecycle management. - */ @Injectable({ providedIn: "root", }) @@ -79,17 +65,13 @@ export class TexeraCopilotManagerService { private http: HttpClient ) {} - /** - * Create a new agent with the specified model type. - * Returns an Observable that emits the created AgentInfo. - */ public createAgent(modelType: string, customName?: string): Observable { return defer(() => { const agentId = `agent-${++this.agentCounter}`; const agentName = customName || `Agent ${this.agentCounter}`; const agentInstance = this.createCopilotInstance(modelType); - agentInstance.setAgentInfo(agentId, agentName); + agentInstance.setAgentInfo(agentName); return agentInstance.initialize().pipe( map(() => { @@ -113,10 +95,6 @@ export class TexeraCopilotManagerService { }); } - /** - * Get an agent by ID. - * Returns an Observable that emits the AgentInfo or throws if not found. - */ public getAgent(agentId: string): Observable { return defer(() => { const agent = this.agents.get(agentId); @@ -127,18 +105,9 @@ export class TexeraCopilotManagerService { }); } - /** - * Get all agents. - * Returns an Observable that emits the array of all AgentInfo. - */ public getAllAgents(): Observable { return of(Array.from(this.agents.values())); } - - /** - * Delete an agent by ID. - * Returns an Observable that emits true if deleted, false if not found. - */ public deleteAgent(agentId: string): Observable { return defer(() => { const agent = this.agents.get(agentId); @@ -156,11 +125,6 @@ export class TexeraCopilotManagerService { }); } - /** - * Fetch available models from the API. - * Returns an Observable that emits the list of available models. - * Uses shareReplay to cache the result and avoid multiple API calls. - */ public fetchModelTypes(): Observable { if (!this.modelTypes$) { this.modelTypes$ = this.http.get(`${AppSettings.getApiEndpoint()}/models`).pipe( @@ -174,19 +138,14 @@ export class TexeraCopilotManagerService { ), catchError((error: unknown) => { console.error("Failed to fetch models from API:", error); - // Return empty array on error return of([]); }), - shareReplay(1) // Cache the result + shareReplay(1) ); } return this.modelTypes$; } - /** - * Format model ID into a human-readable name. - * Example: "claude-3.7" -> "Claude 3.7" - */ private formatModelName(modelId: string): string { return modelId .split("-") @@ -194,18 +153,9 @@ export class TexeraCopilotManagerService { .join(" "); } - /** - * Get the count of active agents. - * Returns an Observable that emits the count. - */ public getAgentCount(): Observable { return of(this.agents.size); } - - /** - * Send a message to an agent. - * Returns an Observable that completes when the message is processed. - */ public sendMessage(agentId: string, message: string): Observable { return defer(() => { const agent = this.agents.get(agentId); @@ -216,10 +166,6 @@ export class TexeraCopilotManagerService { }); } - /** - * Get the agent responses observable stream. - * Returns an Observable that emits arrays of AgentUIMessage. - */ public getAgentResponsesObservable(agentId: string): Observable { return defer(() => { const agent = this.agents.get(agentId); @@ -230,10 +176,6 @@ export class TexeraCopilotManagerService { }); } - /** - * Get the current agent responses. - * Returns an Observable that emits the current array of AgentUIMessage. - */ public getAgentResponses(agentId: string): Observable { return defer(() => { const agent = this.agents.get(agentId); @@ -244,10 +186,6 @@ export class TexeraCopilotManagerService { }); } - /** - * Clear all messages for an agent. - * Returns an Observable that completes when done. - */ public clearMessages(agentId: string): Observable { return defer(() => { const agent = this.agents.get(agentId); @@ -259,10 +197,6 @@ export class TexeraCopilotManagerService { }); } - /** - * Stop generation for an agent. - * Returns an Observable that completes when done. - */ public stopGeneration(agentId: string): Observable { return defer(() => { const agent = this.agents.get(agentId); @@ -274,10 +208,6 @@ export class TexeraCopilotManagerService { }); } - /** - * Get the current state of an agent. - * Returns an Observable that emits the CopilotState. - */ public getAgentState(agentId: string): Observable { return defer(() => { const agent = this.agents.get(agentId); @@ -288,10 +218,6 @@ export class TexeraCopilotManagerService { }); } - /** - * Get the state observable stream for an agent. - * Returns an Observable that emits CopilotState changes. - */ public getAgentStateObservable(agentId: string): Observable { return defer(() => { const agent = this.agents.get(agentId); @@ -302,10 +228,6 @@ export class TexeraCopilotManagerService { }); } - /** - * Check if an agent is connected. - * Returns an Observable that emits a boolean. - */ public isAgentConnected(agentId: string): Observable { return defer(() => { const agent = this.agents.get(agentId); @@ -316,14 +238,9 @@ export class TexeraCopilotManagerService { }); } - /** - * Get system information for an agent. - * Returns an Observable that emits the system prompt and tools. - */ - public getSystemInfo(agentId: string): Observable<{ - systemPrompt: string; - tools: Array<{ name: string; description: string; inputSchema: any }>; - }> { + public getSystemInfo( + agentId: string + ): Observable<{ systemPrompt: string; tools: Array<{ name: string; description: string; inputSchema: any }> }> { return defer(() => { const agent = this.agents.get(agentId); if (!agent) { @@ -335,11 +252,6 @@ export class TexeraCopilotManagerService { }); }); } - - /** - * Create a copilot instance using Angular's dependency injection. - * Each agent receives a unique instance via a child injector. - */ private createCopilotInstance(modelType: string): TexeraCopilot { const childInjector = Injector.create({ providers: [ diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.spec.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.spec.ts new file mode 100644 index 00000000000..c2c8fc9aa77 --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.spec.ts @@ -0,0 +1,183 @@ +/** + * 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 { TestBed } from "@angular/core/testing"; +import { TexeraCopilot, CopilotState } from "./texera-copilot"; +import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; +import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; +import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; +import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; +import { NotificationService } from "../../../common/service/notification/notification.service"; +import { commonTestProviders } from "../../../common/testing/test-utils"; + +describe("TexeraCopilot", () => { + let service: TexeraCopilot; + let mockWorkflowActionService: jasmine.SpyObj; + let mockWorkflowUtilService: jasmine.SpyObj; + let mockOperatorMetadataService: jasmine.SpyObj; + let mockWorkflowCompilingService: jasmine.SpyObj; + let mockNotificationService: jasmine.SpyObj; + + beforeEach(() => { + mockWorkflowActionService = jasmine.createSpyObj("WorkflowActionService", ["getTexeraGraph"]); + mockWorkflowUtilService = jasmine.createSpyObj("WorkflowUtilService", ["getOperatorTypeList"]); + mockOperatorMetadataService = jasmine.createSpyObj("OperatorMetadataService", ["getOperatorSchema"]); + mockWorkflowCompilingService = jasmine.createSpyObj("WorkflowCompilingService", [ + "getOperatorInputSchemaMap", + "getOperatorOutputSchemaMap", + ]); + mockNotificationService = jasmine.createSpyObj("NotificationService", ["info", "error"]); + + TestBed.configureTestingModule({ + providers: [ + TexeraCopilot, + { provide: WorkflowActionService, useValue: mockWorkflowActionService }, + { provide: WorkflowUtilService, useValue: mockWorkflowUtilService }, + { provide: OperatorMetadataService, useValue: mockOperatorMetadataService }, + { provide: WorkflowCompilingService, useValue: mockWorkflowCompilingService }, + { provide: NotificationService, useValue: mockNotificationService }, + ...commonTestProviders, + ], + }); + + service = TestBed.inject(TexeraCopilot); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + it("should set agent info correctly", () => { + service.setAgentInfo("Test Agent"); + expect(service).toBeTruthy(); + }); + + it("should set model type correctly", () => { + service.setModelType("gpt-4"); + expect(service).toBeTruthy(); + }); + + it("should have initial state as UNAVAILABLE", () => { + expect(service.getState()).toBe(CopilotState.UNAVAILABLE); + }); + + it("should update state correctly", done => { + service.state$.subscribe(state => { + if (state === CopilotState.UNAVAILABLE) { + expect(state).toBe(CopilotState.UNAVAILABLE); + done(); + } + }); + }); + + it("should clear messages correctly", () => { + service.clearMessages(); + expect(service.getAgentResponses().length).toBe(0); + }); + + it("should stop generation when in GENERATING state", () => { + service.stopGeneration(); + expect(service).toBeTruthy(); + }); + + it("should return system prompt", () => { + const prompt = service.getSystemPrompt(); + expect(prompt).toBeTruthy(); + expect(typeof prompt).toBe("string"); + }); + + it("should return tools info", () => { + const tools = service.getToolsInfo(); + expect(tools).toBeTruthy(); + expect(Array.isArray(tools)).toBe(true); + expect(tools.length).toBeGreaterThan(0); + tools.forEach(tool => { + expect(tool.name).toBeTruthy(); + expect(tool.description).toBeTruthy(); + }); + }); + + it("should check if connected", () => { + expect(service.isConnected()).toBe(false); + }); + + it("should emit agent responses correctly", done => { + service.agentResponses$.subscribe(responses => { + if (responses.length > 0) { + expect(responses[0].role).toBe("user"); + expect(responses[0].content).toBe("test message"); + done(); + } + }); + + (service as any).emitAgentUIMessage("user", "test message", true, true); + }); + + it("should return empty agent responses initially", () => { + const responses = service.getAgentResponses(); + expect(responses).toEqual([]); + }); + + describe("disconnect", () => { + it("should disconnect and clear state", done => { + service.disconnect().subscribe(() => { + expect(service.getState()).toBe(CopilotState.UNAVAILABLE); + expect(service.getAgentResponses().length).toBe(0); + done(); + }); + }); + + it("should show notification on disconnect", done => { + service.setAgentInfo("Test Agent"); + service.disconnect().subscribe(() => { + expect(mockNotificationService.info).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe("state management", () => { + it("should transition from UNAVAILABLE to GENERATING to AVAILABLE", done => { + const states: CopilotState[] = []; + + service.state$.subscribe(state => { + states.push(state); + if (states.length === 1) { + expect(states[0]).toBe(CopilotState.UNAVAILABLE); + done(); + } + }); + }); + }); + + describe("workflow tools", () => { + it("should create workflow tools correctly", () => { + const tools = (service as any).createWorkflowTools(); + expect(tools).toBeTruthy(); + expect(typeof tools).toBe("object"); + expect(tools.listAllOperatorTypes).toBeTruthy(); + expect(tools.listOperatorsInCurrentWorkflow).toBeTruthy(); + expect(tools.listLinksInCurrentWorkflow).toBeTruthy(); + expect(tools.getOperatorInCurrentWorkflow).toBeTruthy(); + expect(tools.getOperatorPropertiesSchema).toBeTruthy(); + expect(tools.getOperatorPortsInfo).toBeTruthy(); + expect(tools.getOperatorMetadata).toBeTruthy(); + }); + }); +}); diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 0289fa68915..642b497335b 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -33,16 +33,13 @@ import { } from "./workflow-tools"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { createOpenAI } from "@ai-sdk/openai"; -import { AssistantModelMessage, generateText, type ModelMessage, stepCountIs, UIMessage, UserModelMessage } from "ai"; +import { generateText, type ModelMessage, stepCountIs } from "ai"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { AppSettings } from "../../../common/app-setting"; import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; import { COPILOT_SYSTEM_PROMPT } from "./copilot-prompts"; import { NotificationService } from "../../../common/service/notification/notification.service"; -/** - * Copilot state enum. - */ export enum CopilotState { UNAVAILABLE = "Unavailable", AVAILABLE = "Available", @@ -50,9 +47,6 @@ export enum CopilotState { STOPPING = "Stopping", } -/** - * Agent response for UI display. - */ export interface AgentUIMessage { role: "user" | "agent"; content: string; @@ -67,23 +61,16 @@ export interface AgentUIMessage { cachedInputTokens?: number; }; } - -/** - * Texera Copilot - An AI assistant for workflow manipulation. - * Uses Vercel AI SDK for chat completion. - * Note: Not a singleton - each agent has its own instance. - */ @Injectable() export class TexeraCopilot { private model: any; - private modelType: string; - private agentId: string = ""; - private agentName: string = ""; + private modelType = ""; + private agentName = ""; private messages: ModelMessage[] = []; private agentResponses: AgentUIMessage[] = []; private agentResponsesSubject = new BehaviorSubject([]); public agentResponses$ = this.agentResponsesSubject.asObservable(); - private state: CopilotState = CopilotState.UNAVAILABLE; + private state = CopilotState.UNAVAILABLE; private stateSubject = new BehaviorSubject(CopilotState.UNAVAILABLE); public state$ = this.stateSubject.asObservable(); @@ -93,12 +80,9 @@ export class TexeraCopilot { private operatorMetadataService: OperatorMetadataService, private workflowCompilingService: WorkflowCompilingService, private notificationService: NotificationService - ) { - this.modelType = ""; - } + ) {} - public setAgentInfo(agentId: string, agentName: string): void { - this.agentId = agentId; + public setAgentInfo(agentName: string): void { this.agentName = agentName; } @@ -106,17 +90,10 @@ export class TexeraCopilot { this.modelType = modelType; } - /** - * Update the state and emit to the observable. - */ private setState(newState: CopilotState): void { this.state = newState; this.stateSubject.next(newState); } - - /** - * Add an agent UI message and emit to subscribers. - */ private emitAgentUIMessage( role: "user" | "agent", content: string, @@ -124,30 +101,11 @@ export class TexeraCopilot { isEnd: boolean, toolCalls?: any[], toolResults?: any[], - usage?: { - inputTokens?: number; - outputTokens?: number; - totalTokens?: number; - cachedInputTokens?: number; - } + usage?: AgentUIMessage["usage"] ): void { - const message: AgentUIMessage = { - role, - content, - isBegin, - isEnd, - toolCalls, - toolResults, - usage, - }; - this.agentResponses.push(message); + this.agentResponses.push({ role, content, isBegin, isEnd, toolCalls, toolResults, usage }); this.agentResponsesSubject.next([...this.agentResponses]); } - - /** - * Initialize the copilot with the AI model. - * Returns an Observable that completes when initialization is done. - */ public initialize(): Observable { return defer(() => { try { @@ -177,12 +135,8 @@ export class TexeraCopilot { this.setState(CopilotState.GENERATING); - // Add user message to UI this.emitAgentUIMessage("user", message, true, true); - - // Add user message to the message history - const userMessage: UserModelMessage = { role: "user", content: message }; - this.messages.push(userMessage); + this.messages.push({ role: "user", content: message }); const tools = this.createWorkflowTools(); let isFirstStep = true; @@ -218,11 +172,8 @@ export class TexeraCopilot { map(() => undefined), catchError((err: unknown) => { const errorText = `Error: ${err instanceof Error ? err.message : String(err)}`; - const assistantError: AssistantModelMessage = { role: "assistant", content: errorText }; - this.messages.push(assistantError); - + this.messages.push({ role: "assistant", content: errorText }); this.emitAgentUIMessage("agent", errorText, false, true); - return throwError(() => err); }), finalize(() => { @@ -232,9 +183,6 @@ export class TexeraCopilot { }); } - /** - * Create workflow manipulation tools with timeout protection. - */ private createWorkflowTools(): Record { const listOperatorsInCurrentWorkflowTool = toolWithTimeout( createListOperatorsInCurrentWorkflowTool(this.workflowActionService) diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts new file mode 100644 index 00000000000..cb3ba37b1f4 --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts @@ -0,0 +1,276 @@ +/** + * 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 { TestBed } from "@angular/core/testing"; +import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; +import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; +import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; +import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; +import { + createListOperatorsInCurrentWorkflowTool, + createListLinksInCurrentWorkflowTool, + createListAllOperatorTypesTool, + createGetOperatorInCurrentWorkflowTool, + createGetOperatorPropertiesSchemaTool, + createGetOperatorPortsInfoTool, + createGetOperatorMetadataTool, + toolWithTimeout, +} from "./workflow-tools"; + +describe("Workflow Tools", () => { + let mockWorkflowActionService: jasmine.SpyObj; + let mockWorkflowUtilService: jasmine.SpyObj; + let mockOperatorMetadataService: jasmine.SpyObj; + let mockWorkflowCompilingService: jasmine.SpyObj; + + beforeEach(() => { + mockWorkflowActionService = jasmine.createSpyObj("WorkflowActionService", ["getTexeraGraph"]); + mockWorkflowUtilService = jasmine.createSpyObj("WorkflowUtilService", ["getOperatorTypeList"]); + mockOperatorMetadataService = jasmine.createSpyObj("OperatorMetadataService", ["getOperatorSchema"]); + mockWorkflowCompilingService = jasmine.createSpyObj("WorkflowCompilingService", [ + "getOperatorInputSchemaMap", + "getOperatorOutputSchemaMap", + ]); + }); + + describe("listOperatorsInCurrentWorkflowTool", () => { + it("should return list of operators with IDs, types, and custom names", async () => { + const mockOperators = [ + { operatorID: "op1", operatorType: "ScanSource", customDisplayName: "Scan 1" }, + { operatorID: "op2", operatorType: "Filter", customDisplayName: "Filter 1" }, + ]; + + const mockGraph = { + getAllOperators: () => mockOperators, + }; + mockWorkflowActionService.getTexeraGraph.and.returnValue(mockGraph as any); + + const tool = createListOperatorsInCurrentWorkflowTool(mockWorkflowActionService); + const result = await (tool as any).execute({}, {}); + + expect((result as any).success).toBe(true); + expect((result as any).count).toBe(2); + expect((result as any).operators).toEqual([ + { operatorId: "op1", operatorType: "ScanSource", customDisplayName: "Scan 1" }, + { operatorId: "op2", operatorType: "Filter", customDisplayName: "Filter 1" }, + ]); + }); + + it("should handle errors gracefully", async () => { + mockWorkflowActionService.getTexeraGraph.and.throwError("Graph error"); + + const tool = createListOperatorsInCurrentWorkflowTool(mockWorkflowActionService); + const result = await (tool as any).execute({}, {}); + + expect((result as any).success).toBe(false); + expect((result as any).error).toContain("Graph error"); + }); + }); + + describe("listLinksInCurrentWorkflowTool", () => { + it("should return list of links", async () => { + const mockLinks = [ + { linkID: "link1", source: { operatorID: "op1" }, target: { operatorID: "op2" } }, + { linkID: "link2", source: { operatorID: "op2" }, target: { operatorID: "op3" } }, + ]; + + const mockGraph = { + getAllLinks: () => mockLinks, + }; + mockWorkflowActionService.getTexeraGraph.and.returnValue(mockGraph as any); + + const tool = createListLinksInCurrentWorkflowTool(mockWorkflowActionService); + const result = await (tool as any).execute({}, {}); + + expect((result as any).success).toBe(true); + expect((result as any).count).toBe(2); + expect((result as any).links).toEqual(mockLinks); + }); + }); + + describe("listAllOperatorTypesTool", () => { + it("should return list of all operator types", async () => { + const mockTypes = ["ScanSource", "Filter", "Join", "Aggregate"]; + mockWorkflowUtilService.getOperatorTypeList.and.returnValue(mockTypes); + + const tool = createListAllOperatorTypesTool(mockWorkflowUtilService); + const result = await (tool as any).execute({}, {}); + + expect((result as any).success).toBe(true); + expect((result as any).count).toBe(4); + expect((result as any).operatorTypes).toEqual(mockTypes); + }); + }); + + describe("getOperatorInCurrentWorkflowTool", () => { + it("should return operator details with input and output schemas", async () => { + const mockOperator = { operatorID: "op1", operatorType: "ScanSource" }; + const mockInputSchema = { + port1: [{ attributeName: "field1", attributeType: "string" }], + }; + const mockOutputSchema = { + port1: [{ attributeName: "field1", attributeType: "string" }], + }; + + const mockGraph = { + getOperator: (id: string) => mockOperator, + }; + mockWorkflowActionService.getTexeraGraph.and.returnValue(mockGraph as any); + mockWorkflowCompilingService.getOperatorInputSchemaMap.and.returnValue(mockInputSchema as any); + mockWorkflowCompilingService.getOperatorOutputSchemaMap.and.returnValue(mockOutputSchema as any); + + const tool = createGetOperatorInCurrentWorkflowTool(mockWorkflowActionService, mockWorkflowCompilingService); + const result = await (tool as any).execute({ operatorId: "op1" }, {}); + + expect((result as any).success).toBe(true); + expect((result as any).operator).toEqual(mockOperator); + expect((result as any).inputSchema).toEqual(mockInputSchema); + expect((result as any).outputSchema).toEqual(mockOutputSchema); + }); + + it("should return empty schemas when not available", async () => { + const mockOperator = { operatorID: "op1", operatorType: "ScanSource" }; + + const mockGraph = { + getOperator: (id: string) => mockOperator, + }; + mockWorkflowActionService.getTexeraGraph.and.returnValue(mockGraph as any); + mockWorkflowCompilingService.getOperatorInputSchemaMap.and.returnValue(undefined); + mockWorkflowCompilingService.getOperatorOutputSchemaMap.and.returnValue(undefined); + + const tool = createGetOperatorInCurrentWorkflowTool(mockWorkflowActionService, mockWorkflowCompilingService); + const result = await (tool as any).execute({ operatorId: "op1" }, {}); + + expect((result as any).success).toBe(true); + expect((result as any).inputSchema).toEqual({}); + expect((result as any).outputSchema).toEqual({}); + }); + }); + + describe("getOperatorPropertiesSchemaTool", () => { + it("should return properties schema for operator type", async () => { + const mockSchema = { + jsonSchema: { + properties: { prop1: { type: "string" } }, + required: ["prop1"], + definitions: {}, + }, + }; + mockOperatorMetadataService.getOperatorSchema.and.returnValue(mockSchema as any); + + const tool = createGetOperatorPropertiesSchemaTool(mockOperatorMetadataService); + const result = await (tool as any).execute({ operatorType: "ScanSource" }, {}); + + expect((result as any).success).toBe(true); + expect((result as any).operatorType).toBe("ScanSource"); + expect((result as any).propertiesSchema).toEqual({ + properties: { prop1: { type: "string" } }, + required: ["prop1"], + definitions: {}, + }); + }); + }); + + describe("getOperatorPortsInfoTool", () => { + it("should return port information for operator type", async () => { + const mockSchema = { + additionalMetadata: { + inputPorts: [{ portID: "input-0" }], + outputPorts: [{ portID: "output-0" }], + dynamicInputPorts: false, + dynamicOutputPorts: false, + }, + }; + mockOperatorMetadataService.getOperatorSchema.and.returnValue(mockSchema as any); + + const tool = createGetOperatorPortsInfoTool(mockOperatorMetadataService); + const result = await (tool as any).execute({ operatorType: "Filter" }, {}); + + expect((result as any).success).toBe(true); + expect((result as any).portsInfo).toEqual(mockSchema.additionalMetadata); + }); + }); + + describe("getOperatorMetadataTool", () => { + it("should return metadata for operator type", async () => { + const mockSchema = { + additionalMetadata: { + userFriendlyName: "Scan Source", + operatorDescription: "Reads data from a source", + operatorGroupName: "Source", + }, + operatorVersion: "1.0", + }; + mockOperatorMetadataService.getOperatorSchema.and.returnValue(mockSchema as any); + + const tool = createGetOperatorMetadataTool(mockOperatorMetadataService); + const result = await (tool as any).execute({ operatorType: "ScanSource" }, {}); + + expect((result as any).success).toBe(true); + expect((result as any).metadata).toEqual(mockSchema.additionalMetadata); + expect((result as any).operatorVersion).toBe("1.0"); + }); + }); + + describe("toolWithTimeout", () => { + it("should execute tool successfully within timeout", async () => { + const mockTool = { + execute: jasmine.createSpy("execute").and.returnValue(Promise.resolve({ success: true, data: "result" })), + }; + + const wrappedTool = toolWithTimeout(mockTool); + const result = await (wrappedTool as any).execute({ param: "value" }); + + expect((result as any).success).toBe(true); + expect((result as any).data).toBe("result"); + expect(mockTool.execute).toHaveBeenCalled(); + }); + + it("should handle timeout correctly", async () => { + const mockTool = { + execute: jasmine.createSpy("execute").and.returnValue( + new Promise(resolve => { + setTimeout(() => resolve({ success: true }), 150000); + }) + ), + }; + + const wrappedTool = toolWithTimeout(mockTool); + const result = await (wrappedTool as any).execute({}); + + expect((result as any).success).toBe(false); + expect((result as any).error).toContain("timeout"); + }); + + it("should propagate non-timeout errors", async () => { + const mockTool = { + execute: jasmine.createSpy("execute").and.returnValue(Promise.reject(new Error("Custom error"))), + }; + + const wrappedTool = toolWithTimeout(mockTool); + + try { + await (wrappedTool as any).execute({}); + fail("Should have thrown an error"); + } catch (error: any) { + expect(error.message).toBe("Custom error"); + } + }); + }); +}); diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts index c6ea7308101..6532c822e2a 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.ts @@ -24,32 +24,26 @@ import { OperatorMetadataService } from "../operator-metadata/operator-metadata. import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; -const TOOL_TIMEOUT_MS = 120000; +const TIMEOUT_MS = 120000; export function toolWithTimeout(toolConfig: any): any { const originalExecute = toolConfig.execute; - return { ...toolConfig, execute: async (args: any) => { - const abortController = new AbortController(); - - const timeoutPromise = new Promise((_, reject) => { + const controller = new AbortController(); + const timeout = new Promise((_, reject) => { setTimeout(() => { - abortController.abort(); + controller.abort(); reject(new Error("timeout")); - }, TOOL_TIMEOUT_MS); + }, TIMEOUT_MS); }); try { - const argsWithSignal = { ...args, signal: abortController.signal }; - return await Promise.race([originalExecute(argsWithSignal), timeoutPromise]); + return await Promise.race([originalExecute({ ...args, signal: controller.signal }), timeout]); } catch (error: any) { if (error.message === "timeout") { - return { - success: false, - error: "Tool execution timeout - operation took longer than 2 minutes. Please try again later.", - }; + return { success: false, error: "Tool execution timeout - exceeded 2 minutes" }; } throw error; } @@ -65,16 +59,14 @@ export function createListOperatorsInCurrentWorkflowTool(workflowActionService: execute: async () => { try { const operators = workflowActionService.getTexeraGraph().getAllOperators(); - const operatorList = operators.map(op => ({ - operatorId: op.operatorID, - operatorType: op.operatorType, - customDisplayName: op.customDisplayName, - })); - return { success: true, - operators: operatorList, - count: operatorList.length, + operators: operators.map(op => ({ + operatorId: op.operatorID, + operatorType: op.operatorType, + customDisplayName: op.customDisplayName, + })), + count: operators.length, }; } catch (error: any) { return { success: false, error: error.message }; @@ -91,11 +83,7 @@ export function createListLinksInCurrentWorkflowTool(workflowActionService: Work execute: async () => { try { const links = workflowActionService.getTexeraGraph().getAllLinks(); - return { - success: true, - links: links, - count: links.length, - }; + return { success: true, links, count: links.length }; } catch (error: any) { return { success: false, error: error.message }; } @@ -111,11 +99,7 @@ export function createListAllOperatorTypesTool(workflowUtilService: WorkflowUtil execute: async () => { try { const operatorTypes = workflowUtilService.getOperatorTypeList(); - return { - success: true, - operatorTypes: operatorTypes, - count: operatorTypes.length, - }; + return { success: true, operatorTypes, count: operatorTypes.length }; } catch (error: any) { return { success: false, error: error.message }; } @@ -130,34 +114,24 @@ export function createGetOperatorInCurrentWorkflowTool( return tool({ name: "getOperatorInCurrentWorkflow", description: - "Get detailed information about a specific operator in the current workflow, including its input and output schemas", + "Get detailed information about a specific operator in the current workflow, including input/output schemas", inputSchema: z.object({ operatorId: z.string().describe("ID of the operator to retrieve"), }), execute: async (args: { operatorId: string }) => { try { const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); - - // Get input schema (empty map if not available) - const inputSchemaMap = workflowCompilingService.getOperatorInputSchemaMap(args.operatorId); - const inputSchema = inputSchemaMap || {}; - - // Get output schema (empty map if not available) - const outputSchemaMap = workflowCompilingService.getOperatorOutputSchemaMap(args.operatorId); - const outputSchema = outputSchemaMap || {}; + const inputSchema = workflowCompilingService.getOperatorInputSchemaMap(args.operatorId) || {}; + const outputSchema = workflowCompilingService.getOperatorOutputSchemaMap(args.operatorId) || {}; return { success: true, - operator: operator, - inputSchema: inputSchema, - outputSchema: outputSchema, - message: `Retrieved operator ${args.operatorId}`, + operator, + inputSchema, + outputSchema, }; } catch (error: any) { - return { - success: false, - error: error.message || `Operator ${args.operatorId} not found`, - }; + return { success: false, error: error.message || `Operator ${args.operatorId} not found` }; } }, }); @@ -166,24 +140,21 @@ export function createGetOperatorInCurrentWorkflowTool( export function createGetOperatorPropertiesSchemaTool(operatorMetadataService: OperatorMetadataService) { return tool({ name: "getOperatorPropertiesSchema", - description: "Get only the properties schema for an operator type. Use this before setting operator properties.", + description: "Get properties schema for an operator type. Use before setting operator properties", inputSchema: z.object({ - operatorType: z.string().describe("Type of the operator to get properties schema for"), + operatorType: z.string().describe("Operator type"), }), execute: async (args: { operatorType: string }) => { try { const schema = operatorMetadataService.getOperatorSchema(args.operatorType); - const propertiesSchema = { - properties: schema.jsonSchema.properties, - required: schema.jsonSchema.required, - definitions: schema.jsonSchema.definitions, - }; - return { success: true, - propertiesSchema: propertiesSchema, + propertiesSchema: { + properties: schema.jsonSchema.properties, + required: schema.jsonSchema.required, + definitions: schema.jsonSchema.definitions, + }, operatorType: args.operatorType, - message: `Retrieved properties schema for operator type ${args.operatorType}`, }; } catch (error: any) { return { success: false, error: error.message }; @@ -195,26 +166,22 @@ export function createGetOperatorPropertiesSchemaTool(operatorMetadataService: O export function createGetOperatorPortsInfoTool(operatorMetadataService: OperatorMetadataService) { return tool({ name: "getOperatorPortsInfo", - description: - "Get input and output port information for an operator type. This is more token-efficient than getOperatorSchema and returns only port details (display names, multi-input support, etc.).", + description: "Get input/output port information for an operator type", inputSchema: z.object({ - operatorType: z.string().describe("Type of the operator to get port information for"), + operatorType: z.string().describe("Operator type"), }), execute: async (args: { operatorType: string }) => { try { const schema = operatorMetadataService.getOperatorSchema(args.operatorType); - const portsInfo = { - inputPorts: schema.additionalMetadata.inputPorts, - outputPorts: schema.additionalMetadata.outputPorts, - dynamicInputPorts: schema.additionalMetadata.dynamicInputPorts, - dynamicOutputPorts: schema.additionalMetadata.dynamicOutputPorts, - }; - return { success: true, - portsInfo: portsInfo, + portsInfo: { + inputPorts: schema.additionalMetadata.inputPorts, + outputPorts: schema.additionalMetadata.outputPorts, + dynamicInputPorts: schema.additionalMetadata.dynamicInputPorts, + dynamicOutputPorts: schema.additionalMetadata.dynamicOutputPorts, + }, operatorType: args.operatorType, - message: `Retrieved port information for operator type ${args.operatorType}`, }; } catch (error: any) { return { success: false, error: error.message }; @@ -226,22 +193,18 @@ export function createGetOperatorPortsInfoTool(operatorMetadataService: Operator export function createGetOperatorMetadataTool(operatorMetadataService: OperatorMetadataService) { return tool({ name: "getOperatorMetadata", - description: - "Get semantic metadata for an operator type, including user-friendly name, description, operator group, and capabilities. This is very useful to understand the semantics and purpose of each operator type - what it does, how it works, and what kind of data transformation it performs.", + description: "Get semantic metadata for an operator type (name, description, group, capabilities)", inputSchema: z.object({ - operatorType: z.string().describe("Type of the operator to get metadata for"), + operatorType: z.string().describe("Operator type"), }), - execute: async (args: { operatorType: string; signal?: AbortSignal }) => { + execute: async (args: { operatorType: string }) => { try { const schema = operatorMetadataService.getOperatorSchema(args.operatorType); - - const metadata = schema.additionalMetadata; return { success: true, - metadata: metadata, + metadata: schema.additionalMetadata, operatorType: args.operatorType, operatorVersion: schema.operatorVersion, - message: `Retrieved metadata for operator type ${args.operatorType}`, }; } catch (error: any) { return { success: false, error: error.message }; From c86b300f2d18609ca2554f6479e3c43a1f6e1e94 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 00:00:23 -0800 Subject: [PATCH 122/158] fix format and test --- .../agent-chat/agent-chat.component.ts | 17 +++++---- .../agent-panel/agent-panel.component.ts | 18 ++++++---- .../agent-registration.component.ts | 27 +++++++------- .../texera-copilot-manager.service.spec.ts | 36 +++++++++---------- 4 files changed, 55 insertions(+), 43 deletions(-) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts index 3fa5e1bcf8f..9a350c07526 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -95,11 +95,14 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { } public showSystemInfo(): void { - this.copilotManagerService.getSystemInfo(this.agentInfo.id).subscribe(systemInfo => { - this.systemPrompt = systemInfo.systemPrompt; - this.availableTools = systemInfo.tools; - this.isSystemInfoModalVisible = true; - }); + this.copilotManagerService + .getSystemInfo(this.agentInfo.id) + .pipe(untilDestroyed(this)) + .subscribe(systemInfo => { + this.systemPrompt = systemInfo.systemPrompt; + this.availableTools = systemInfo.tools; + this.isSystemInfoModalVisible = true; + }); } public closeSystemInfoModal(): void { @@ -208,11 +211,11 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { } public stopGeneration(): void { - this.copilotManagerService.stopGeneration(this.agentInfo.id).subscribe(); + this.copilotManagerService.stopGeneration(this.agentInfo.id).pipe(untilDestroyed(this)).subscribe(); } public clearMessages(): void { - this.copilotManagerService.clearMessages(this.agentInfo.id).subscribe(); + this.copilotManagerService.clearMessages(this.agentInfo.id).pipe(untilDestroyed(this)).subscribe(); } public isGenerating(): boolean { diff --git a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts index 82cbc8245fc..53a356a5c4c 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts @@ -75,15 +75,21 @@ export class AgentPanelComponent implements OnInit, OnDestroy { // Subscribe to agent changes this.copilotManagerService.agentChange$.pipe(untilDestroyed(this)).subscribe(() => { - this.copilotManagerService.getAllAgents().subscribe(agents => { - this.agents = agents; - }); + this.copilotManagerService + .getAllAgents() + .pipe(untilDestroyed(this)) + .subscribe(agents => { + this.agents = agents; + }); }); // Load initial agents - this.copilotManagerService.getAllAgents().subscribe(agents => { - this.agents = agents; - }); + this.copilotManagerService + .getAllAgents() + .pipe(untilDestroyed(this)) + .subscribe(agents => { + this.agents = agents; + }); } @HostListener("window:beforeunload") diff --git a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts index baa7343d244..24262b12664 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts @@ -89,18 +89,21 @@ export class AgentRegistrationComponent implements OnInit, OnDestroy { this.isCreating = true; - this.copilotManagerService.createAgent(this.selectedModelType, this.customAgentName || undefined).subscribe({ - next: agentInfo => { - this.agentCreated.emit(agentInfo.id); - this.selectedModelType = null; - this.customAgentName = ""; - this.isCreating = false; - }, - error: (error: unknown) => { - this.notificationService.error(`Failed to create agent: ${error}`); - this.isCreating = false; - }, - }); + this.copilotManagerService + .createAgent(this.selectedModelType, this.customAgentName || undefined) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: agentInfo => { + this.agentCreated.emit(agentInfo.id); + this.selectedModelType = null; + this.customAgentName = ""; + this.isCreating = false; + }, + error: (error: unknown) => { + this.notificationService.error(`Failed to create agent: ${error}`); + this.isCreating = false; + }, + }); } public canCreate(): boolean { diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts index 360bc13c0c7..a08209ba2e1 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts @@ -120,8 +120,8 @@ describe("TexeraCopilotManagerService", () => { it("should throw error when agent not found", done => { service.getAgent("non-existent").subscribe({ next: () => fail("Should have thrown error"), - error: (error: Error) => { - expect(error.message).toContain("not found"); + error: (error: unknown) => { + expect((error as Error).message).toContain("not found"); done(); }, }); @@ -156,8 +156,8 @@ describe("TexeraCopilotManagerService", () => { it("should throw error for non-existent agent", done => { service.sendMessage("non-existent", "test message").subscribe({ next: () => fail("Should have thrown error"), - error: (error: Error) => { - expect(error.message).toContain("not found"); + error: (error: unknown) => { + expect((error as Error).message).toContain("not found"); done(); }, }); @@ -168,8 +168,8 @@ describe("TexeraCopilotManagerService", () => { it("should throw error for non-existent agent", done => { service.getAgentResponses("non-existent").subscribe({ next: () => fail("Should have thrown error"), - error: (error: Error) => { - expect(error.message).toContain("not found"); + error: (error: unknown) => { + expect((error as Error).message).toContain("not found"); done(); }, }); @@ -180,8 +180,8 @@ describe("TexeraCopilotManagerService", () => { it("should throw error for non-existent agent", done => { service.getAgentResponsesObservable("non-existent").subscribe({ next: () => fail("Should have thrown error"), - error: (error: Error) => { - expect(error.message).toContain("not found"); + error: (error: unknown) => { + expect((error as Error).message).toContain("not found"); done(); }, }); @@ -192,8 +192,8 @@ describe("TexeraCopilotManagerService", () => { it("should throw error for non-existent agent", done => { service.clearMessages("non-existent").subscribe({ next: () => fail("Should have thrown error"), - error: (error: Error) => { - expect(error.message).toContain("not found"); + error: (error: unknown) => { + expect((error as Error).message).toContain("not found"); done(); }, }); @@ -204,8 +204,8 @@ describe("TexeraCopilotManagerService", () => { it("should throw error for non-existent agent", done => { service.stopGeneration("non-existent").subscribe({ next: () => fail("Should have thrown error"), - error: (error: Error) => { - expect(error.message).toContain("not found"); + error: (error: unknown) => { + expect((error as Error).message).toContain("not found"); done(); }, }); @@ -216,8 +216,8 @@ describe("TexeraCopilotManagerService", () => { it("should throw error for non-existent agent", done => { service.getAgentState("non-existent").subscribe({ next: () => fail("Should have thrown error"), - error: (error: Error) => { - expect(error.message).toContain("not found"); + error: (error: unknown) => { + expect((error as Error).message).toContain("not found"); done(); }, }); @@ -228,8 +228,8 @@ describe("TexeraCopilotManagerService", () => { it("should throw error for non-existent agent", done => { service.getAgentStateObservable("non-existent").subscribe({ next: () => fail("Should have thrown error"), - error: (error: Error) => { - expect(error.message).toContain("not found"); + error: (error: unknown) => { + expect((error as Error).message).toContain("not found"); done(); }, }); @@ -240,8 +240,8 @@ describe("TexeraCopilotManagerService", () => { it("should throw error for non-existent agent", done => { service.getSystemInfo("non-existent").subscribe({ next: () => fail("Should have thrown error"), - error: (error: Error) => { - expect(error.message).toContain("not found"); + error: (error: unknown) => { + expect((error as Error).message).toContain("not found"); done(); }, }); From 9385ad74769d58adc5e0a8a8d849d7938e8eddf8 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 00:05:56 -0800 Subject: [PATCH 123/158] revert helm chart changes --- bin/k8s/Chart.yaml | 8 +- bin/k8s/templates/external-names.yaml | 29 +----- bin/k8s/templates/litellm-config.yaml | 38 -------- bin/k8s/templates/litellm-deployment.yaml | 92 ------------------- bin/k8s/templates/litellm-secret.yaml | 33 ------- bin/k8s/templates/litellm-service.yaml | 33 ------- .../postgresql-litellm-persistence.yaml | 78 ---------------- bin/k8s/values.yaml | 72 +-------------- 8 files changed, 8 insertions(+), 375 deletions(-) delete mode 100644 bin/k8s/templates/litellm-config.yaml delete mode 100644 bin/k8s/templates/litellm-deployment.yaml delete mode 100644 bin/k8s/templates/litellm-secret.yaml delete mode 100644 bin/k8s/templates/litellm-service.yaml delete mode 100644 bin/k8s/templates/postgresql-litellm-persistence.yaml diff --git a/bin/k8s/Chart.yaml b/bin/k8s/Chart.yaml index b212d229e9c..0c0556e88b2 100644 --- a/bin/k8s/Chart.yaml +++ b/bin/k8s/Chart.yaml @@ -51,12 +51,6 @@ dependencies: version: 16.5.6 repository: https://charts.bitnami.com/bitnami - - name: postgresql - version: 16.5.6 - repository: https://charts.bitnami.com/bitnami - alias: postgresql-litellm - condition: litellm.persistence.enabled - - name: minio version: 15.0.7 repository: https://charts.bitnami.com/bitnami @@ -68,4 +62,4 @@ dependencies: - name: metrics-server version: 3.12.2 repository: https://kubernetes-sigs.github.io/metrics-server/ - condition: metrics-server.enabled + condition: metrics-server.enabled \ No newline at end of file diff --git a/bin/k8s/templates/external-names.yaml b/bin/k8s/templates/external-names.yaml index 72af59eded1..7fc334c5fc8 100644 --- a/bin/k8s/templates/external-names.yaml +++ b/bin/k8s/templates/external-names.yaml @@ -37,13 +37,13 @@ spec: {{- $namespace := .Release.Namespace }} {{- $workflowComputingUnitPoolNamespace := .Values.workflowComputingUnitPool.namespace }} -{{/* +{{/* Create ExternalName services in the workflow namespace to allow compute units to access services in the main namespace using the same service names. */}} {{/* File service ExternalName */}} -{{- include "external-name-service" (dict +{{- include "external-name-service" (dict "name" (printf "%s-svc" .Values.fileService.name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.fileService.name $namespace) @@ -51,7 +51,7 @@ to access services in the main namespace using the same service names. --- {{/* Config service ExternalName */}} -{{- include "external-name-service" (dict +{{- include "external-name-service" (dict "name" (printf "%s-svc" .Values.configService.name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.configService.name $namespace) @@ -66,31 +66,10 @@ to access services in the main namespace using the same service names. ) | nindent 0 }} --- -{{/* PostgreSQL LiteLLM ExternalName */}} -{{- if .Values.litellm.persistence.enabled }} -{{- include "external-name-service" (dict - "name" (printf "%s-postgresql-litellm" .Release.Name) - "namespace" $workflowComputingUnitPoolNamespace - "externalName" (printf "%s-postgresql-litellm.%s.svc.cluster.local" .Release.Name $namespace) -) | nindent 0 }} - ---- -{{- end }} -{{/* LiteLLM service ExternalName */}} -{{- if .Values.litellm.enabled }} -{{- include "external-name-service" (dict - "name" (printf "%s-svc" .Values.litellm.name) - "namespace" $workflowComputingUnitPoolNamespace - "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.litellm.name $namespace) -) | nindent 0 }} - ---- -{{- end }} {{/* Webserver ExternalName */}} -{{- include "external-name-service" (dict +{{- include "external-name-service" (dict "name" (printf "%s-svc" .Values.webserver.name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.webserver.name $namespace) ) | nindent 0 }} - diff --git a/bin/k8s/templates/litellm-config.yaml b/bin/k8s/templates/litellm-config.yaml deleted file mode 100644 index 50dd33072ac..00000000000 --- a/bin/k8s/templates/litellm-config.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - -{{- if .Values.litellm.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: litellm-config - namespace: {{ .Release.Namespace }} -data: - config.yaml: | - model_list: - - model_name: claude-haiku-4.5 - litellm_params: - model: claude-haiku-4-5-20251001 - api_key: "os.environ/ANTHROPIC_API_KEY" - - general_settings: - {{- if .Values.litellm.persistence.enabled }} - master_key: "os.environ/LITELLM_MASTER_KEY" - {{- end }} - # Disable spend tracking and key management for simpler setup - disable_spend_logs: {{ not .Values.litellm.persistence.enabled }} -{{- end }} diff --git a/bin/k8s/templates/litellm-deployment.yaml b/bin/k8s/templates/litellm-deployment.yaml deleted file mode 100644 index 6fcd9afd794..00000000000 --- a/bin/k8s/templates/litellm-deployment.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# 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. - -{{- if .Values.litellm.enabled }} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Values.litellm.name }} - namespace: {{ .Release.Namespace }} -spec: - replicas: {{ .Values.litellm.replicaCount }} - selector: - matchLabels: - app: {{ .Values.litellm.name }} - template: - metadata: - labels: - app: {{ .Values.litellm.name }} - spec: - containers: - - name: litellm - image: {{ .Values.litellm.image.repository }}:{{ .Values.litellm.image.tag }} - imagePullPolicy: {{ .Values.litellm.image.pullPolicy }} - ports: - - containerPort: {{ .Values.litellm.service.port }} - name: http - env: - {{- if .Values.litellm.persistence.enabled }} - - name: DATABASE_URL - valueFrom: - secretKeyRef: - name: litellm-secret - key: DATABASE_URL - - name: LITELLM_MASTER_KEY - valueFrom: - secretKeyRef: - name: litellm-secret - key: LITELLM_MASTER_KEY - {{- end }} - - name: ANTHROPIC_API_KEY - valueFrom: - secretKeyRef: - name: litellm-secret - key: ANTHROPIC_API_KEY - - name: OPENAI_API_KEY - valueFrom: - secretKeyRef: - name: litellm-secret - key: OPENAI_API_KEY - command: - - litellm - - --config - - /etc/litellm/config.yaml - - --port - - "{{ .Values.litellm.service.port }}" - volumeMounts: - - name: config - mountPath: /etc/litellm - readOnly: true - resources: - {{- toYaml .Values.litellm.resources | nindent 12 }} - livenessProbe: - httpGet: - path: /health/liveliness - port: {{ .Values.litellm.service.port }} - initialDelaySeconds: 30 - periodSeconds: 10 - readinessProbe: - httpGet: - path: /health/readiness - port: {{ .Values.litellm.service.port }} - initialDelaySeconds: 10 - periodSeconds: 5 - volumes: - - name: config - configMap: - name: litellm-config -{{- end }} diff --git a/bin/k8s/templates/litellm-secret.yaml b/bin/k8s/templates/litellm-secret.yaml deleted file mode 100644 index 1cb5d2a68b5..00000000000 --- a/bin/k8s/templates/litellm-secret.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# 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. - -{{- if .Values.litellm.enabled }} -apiVersion: v1 -kind: Secret -metadata: - name: litellm-secret - namespace: {{ .Release.Namespace }} -type: Opaque -data: - ANTHROPIC_API_KEY: {{ .Values.litellm.apiKeys.anthropic | b64enc | quote }} - OPENAI_API_KEY: {{ .Values.litellm.apiKeys.openai | b64enc | quote }} - {{- if .Values.litellm.persistence.enabled }} - {{- $postgresqlLitellm := index .Values "postgresql-litellm" }} - DATABASE_URL: {{ printf "postgresql://%s:%s@%s-postgresql-litellm:5432/%s" $postgresqlLitellm.auth.username $postgresqlLitellm.auth.password .Release.Name $postgresqlLitellm.auth.database | b64enc | quote }} - LITELLM_MASTER_KEY: {{ .Values.litellm.masterKey | b64enc | quote }} - {{- end }} -{{- end }} diff --git a/bin/k8s/templates/litellm-service.yaml b/bin/k8s/templates/litellm-service.yaml deleted file mode 100644 index 982ffabbec9..00000000000 --- a/bin/k8s/templates/litellm-service.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# 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. - -{{- if .Values.litellm.enabled }} -apiVersion: v1 -kind: Service -metadata: - name: {{ .Values.litellm.name }}-svc - namespace: {{ .Release.Namespace }} -spec: - type: {{ .Values.litellm.service.type }} - selector: - app: {{ .Values.litellm.name }} - ports: - - protocol: TCP - port: {{ .Values.litellm.service.port }} - targetPort: {{ .Values.litellm.service.port }} - name: http -{{- end }} diff --git a/bin/k8s/templates/postgresql-litellm-persistence.yaml b/bin/k8s/templates/postgresql-litellm-persistence.yaml deleted file mode 100644 index 027ae758b9e..00000000000 --- a/bin/k8s/templates/postgresql-litellm-persistence.yaml +++ /dev/null @@ -1,78 +0,0 @@ -# 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. - -{{/* Define storage path configuration, please change it to your own path and make sure the path exists with the right permission*/}} -{{/* This path only works for local-path storage class */}} -{{- $hostBasePath := .Values.persistence.postgresqlHostLocalPath }} - -{{- if .Values.litellm.persistence.enabled }} -{{- $name := "postgresql-litellm" }} -{{- $persistence := (index .Values "postgresql-litellm").primary.persistence }} -{{- $volumeName := printf "%s-data-pv" $name }} -{{- $claimName := printf "%s-data-pvc" $name }} -{{- $storageClass := $persistence.storageClass | default "local-path" }} -{{- $size := $persistence.size | default "5Gi" }} -{{- $hostPath := printf "%s/%s/%s" $hostBasePath $.Release.Name $name }} - -{{/* Only create PV for local-path storage class */}} -{{- if and (eq $storageClass "local-path") (ne $hostBasePath "") }} -apiVersion: v1 -kind: PersistentVolume -metadata: - name: {{ $volumeName }} - {{- if not $.Values.persistence.removeAfterUninstall }} - annotations: - "helm.sh/resource-policy": keep - {{- end }} - labels: - type: local - app: {{ $.Release.Name }} - component: {{ $name }} -spec: - storageClassName: {{ $storageClass }} - capacity: - storage: {{ $size }} - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain - hostPath: - path: {{ $hostPath }} ---- -{{- end }} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ $claimName }} - namespace: {{ $.Release.Namespace }} - {{- if not $.Values.persistence.removeAfterUninstall }} - annotations: - "helm.sh/resource-policy": keep - {{- end }} - labels: - app: {{ $.Release.Name }} - component: {{ $name }} -spec: - storageClassName: {{ $storageClass }} - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ $size }} - {{- if and (eq $storageClass "local-path") (ne $hostBasePath "") }} - volumeName: {{ $volumeName }} - {{- end }} -{{- end }} diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml index d54f9e08239..c19be410abc 100644 --- a/bin/k8s/values.yaml +++ b/bin/k8s/values.yaml @@ -21,8 +21,8 @@ global: # Persistence Configuration # This controls how Persistent Volumes (PVs) and Persistent Volume Claims (PVCs) are managed -# -# removeAfterUninstall: +# +# removeAfterUninstall: # - true: PVCs will be deleted when helm uninstalls the chart # - false: PVCs will remain after uninstall to preserve the data persistence: @@ -293,66 +293,6 @@ pythonLanguageServer: cpu: "100m" memory: "100Mi" -# LiteLLM Proxy configuration -litellm: - enabled: true - name: litellm - replicaCount: 1 - image: - repository: ghcr.io/berriai/litellm-database - tag: main-stable - pullPolicy: IfNotPresent - service: - type: ClusterIP - port: 4000 - resources: - limits: - cpu: "2" - memory: "2Gi" - requests: - cpu: "500m" - memory: "1Gi" - # Database persistence configuration - persistence: - enabled: true - # Master key for LiteLLM admin API (must start with "sk-") - masterKey: "sk-texera-litellm-masterkey" # Change this in production - apiKeys: - # Set your Anthropic API key here - # IMPORTANT: In production, use external secrets management (e.g., sealed-secrets, external-secrets) - anthropic: "" # Replace with your actual API key or use external secret - # Set your OpenAI API key here - openai: "" # Replace with your actual API key or use external secret - -# PostgreSQL database for LiteLLM persistence -postgresql-litellm: - image: - repository: texera/postgres17-pgroonga - tag: latest - debug: true - auth: - postgresPassword: litellm_root_password # Change this in production - username: litellm - password: litellm_password # Should match litellm.persistence.database.password - database: litellm - primary: - livenessProbe: - initialDelaySeconds: 30 - readinessProbe: - initialDelaySeconds: 30 - resources: - requests: - cpu: "500m" - memory: "512Mi" - limits: - cpu: "1" - memory: "1Gi" - persistence: - enabled: true - size: 5Gi - storageClass: local-path - existingClaim: "postgresql-litellm-data-pvc" - # Metrics Server configuration metrics-server: enabled: true # set to false if metrics-server is already installed @@ -409,12 +349,6 @@ ingressPaths: pathType: ImplementationSpecific serviceName: envoy-svc servicePort: 10000 - - path: /api/models - serviceName: texera-access-control-service-svc - servicePort: 9096 - - path: /api/chat - serviceName: texera-access-control-service-svc - servicePort: 9096 - path: /api serviceName: webserver-svc servicePort: 8080 @@ -426,4 +360,4 @@ ingressPaths: servicePort: 3000 - path: / serviceName: webserver-svc - servicePort: 8080 + servicePort: 8080 \ No newline at end of file From 222e6b740b2b51ac996eea70ffbebbfffb26b8b7 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 00:31:59 -0800 Subject: [PATCH 124/158] revert helm chart changes --- bin/k8s/templates/access-control-service-deployment.yaml | 9 --------- bin/k8s/templates/external-names.yaml | 1 + bin/k8s/values.yaml | 2 +- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/bin/k8s/templates/access-control-service-deployment.yaml b/bin/k8s/templates/access-control-service-deployment.yaml index fc6784c5189..a332c65bd39 100644 --- a/bin/k8s/templates/access-control-service-deployment.yaml +++ b/bin/k8s/templates/access-control-service-deployment.yaml @@ -46,15 +46,6 @@ spec: secretKeyRef: name: {{ .Release.Name }}-postgresql key: postgres-password -{{- if .Values.litellm.enabled }} - - name: LITELLM_MASTER_KEY - valueFrom: - secretKeyRef: - name: litellm-secret - key: LITELLM_MASTER_KEY - - name: LITELLM_BASE_URL - value: "http://{{ .Values.litellm.name }}-svc:{{ .Values.litellm.service.port }}" -{{- end }} livenessProbe: httpGet: path: /api/healthcheck diff --git a/bin/k8s/templates/external-names.yaml b/bin/k8s/templates/external-names.yaml index 7fc334c5fc8..148a9e8ff28 100644 --- a/bin/k8s/templates/external-names.yaml +++ b/bin/k8s/templates/external-names.yaml @@ -73,3 +73,4 @@ to access services in the main namespace using the same service names. "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.webserver.name $namespace) ) | nindent 0 }} + diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml index c19be410abc..279a0607f86 100644 --- a/bin/k8s/values.yaml +++ b/bin/k8s/values.yaml @@ -360,4 +360,4 @@ ingressPaths: servicePort: 3000 - path: / serviceName: webserver-svc - servicePort: 8080 \ No newline at end of file + servicePort: 8080 From fa74615409fa35f294dd744e9ff348ad1154db3f Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 00:34:08 -0800 Subject: [PATCH 125/158] revert helm chart changes --- bin/k8s/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/k8s/Chart.yaml b/bin/k8s/Chart.yaml index 0c0556e88b2..ee57a2b8427 100644 --- a/bin/k8s/Chart.yaml +++ b/bin/k8s/Chart.yaml @@ -62,4 +62,4 @@ dependencies: - name: metrics-server version: 3.12.2 repository: https://kubernetes-sigs.github.io/metrics-server/ - condition: metrics-server.enabled \ No newline at end of file + condition: metrics-server.enabled From 2368468f581ef3ac775a0d3537d3b37d334924a9 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 00:39:58 -0800 Subject: [PATCH 126/158] revert helm chart changes --- bin/k8s/templates/external-names.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/k8s/templates/external-names.yaml b/bin/k8s/templates/external-names.yaml index 148a9e8ff28..507df4b73a3 100644 --- a/bin/k8s/templates/external-names.yaml +++ b/bin/k8s/templates/external-names.yaml @@ -43,7 +43,7 @@ to access services in the main namespace using the same service names. */}} {{/* File service ExternalName */}} -{{- include "external-name-service" (dict +{{- include "external-name-service" (dict "name" (printf "%s-svc" .Values.fileService.name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.fileService.name $namespace) @@ -51,7 +51,7 @@ to access services in the main namespace using the same service names. --- {{/* Config service ExternalName */}} -{{- include "external-name-service" (dict +{{- include "external-name-service" (dict "name" (printf "%s-svc" .Values.configService.name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.configService.name $namespace) @@ -59,7 +59,7 @@ to access services in the main namespace using the same service names. --- {{/* PostgreSQL ExternalName */}} -{{- include "external-name-service" (dict +{{- include "external-name-service" (dict "name" (printf "%s-postgresql" .Release.Name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-postgresql.%s.svc.cluster.local" .Release.Name $namespace) @@ -67,7 +67,7 @@ to access services in the main namespace using the same service names. --- {{/* Webserver ExternalName */}} -{{- include "external-name-service" (dict +{{- include "external-name-service" (dict "name" (printf "%s-svc" .Values.webserver.name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.webserver.name $namespace) From ac8a5d3f74b9adac6d547c912d1b379060344675 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 00:58:43 -0800 Subject: [PATCH 127/158] revert helm chart changes --- bin/k8s/templates/external-names.yaml | 2 +- bin/k8s/values.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/k8s/templates/external-names.yaml b/bin/k8s/templates/external-names.yaml index 507df4b73a3..259c5fa6956 100644 --- a/bin/k8s/templates/external-names.yaml +++ b/bin/k8s/templates/external-names.yaml @@ -37,7 +37,7 @@ spec: {{- $namespace := .Release.Namespace }} {{- $workflowComputingUnitPoolNamespace := .Values.workflowComputingUnitPool.namespace }} -{{/* +{{/* Create ExternalName services in the workflow namespace to allow compute units to access services in the main namespace using the same service names. */}} diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml index 279a0607f86..95810913eb4 100644 --- a/bin/k8s/values.yaml +++ b/bin/k8s/values.yaml @@ -21,8 +21,8 @@ global: # Persistence Configuration # This controls how Persistent Volumes (PVs) and Persistent Volume Claims (PVCs) are managed -# -# removeAfterUninstall: +# +# removeAfterUninstall: # - true: PVCs will be deleted when helm uninstalls the chart # - false: PVCs will remain after uninstall to preserve the data persistence: From 29a3930ff5154eb8f1a6f33e9fe4b877d2bec4f7 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 11:11:15 -0800 Subject: [PATCH 128/158] remove deep chat --- frontend/package.json | 1 - frontend/src/app/app.module.ts | 1 - frontend/yarn.lock | 460 +-------------------------------- 3 files changed, 14 insertions(+), 448 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index f54d57fbe89..416d44a6545 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -53,7 +53,6 @@ "content-disposition": "0.5.4", "d3": "6.4.0", "dagre": "0.8.5", - "deep-chat": "^2.2.2", "deep-map": "2.0.0", "edit-distance": "1.0.4", "es6-weak-map": "2.0.3", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 0ebacddc144..73feecfdba3 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -181,7 +181,6 @@ import { AdminSettingsComponent } from "./dashboard/component/admin/settings/adm import { FormlyRepeatDndComponent } from "./common/formly/repeat-dnd/repeat-dnd.component"; import { NzInputNumberModule } from "ng-zorro-antd/input-number"; import { NzCheckboxModule } from "ng-zorro-antd/checkbox"; -import "deep-chat"; registerLocaleData(en); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index b5d262363d2..6e8927b2c98 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3578,33 +3578,6 @@ __metadata: languageName: node linkType: hard -"@microsoft/fetch-event-source@npm:^2.0.1": - version: 2.0.1 - resolution: "@microsoft/fetch-event-source@npm:2.0.1" - checksum: 10c0/38c69e9b9990e6cee715c7bbfa2752f943b42575acadb36facf19bb831f1520c469f854277439154258e0e1dc8650cc85038230d1f451e3f6b62e8faeaa1126c - languageName: node - linkType: hard - -"@modelcontextprotocol/sdk@npm:^1.20.1": - version: 1.20.1 - resolution: "@modelcontextprotocol/sdk@npm:1.20.1" - dependencies: - ajv: "npm:^6.12.6" - content-type: "npm:^1.0.5" - cors: "npm:^2.8.5" - cross-spawn: "npm:^7.0.5" - eventsource: "npm:^3.0.2" - eventsource-parser: "npm:^3.0.0" - express: "npm:^5.0.1" - express-rate-limit: "npm:^7.5.0" - pkce-challenge: "npm:^5.0.0" - raw-body: "npm:^3.0.0" - zod: "npm:^3.23.8" - zod-to-json-schema: "npm:^3.24.1" - checksum: 10c0/23e1ec9bc0f34dabb172d16175b4083d9890ed573252b9072c8bd5d22a8620afb3b4535e73f1c8ba84e3de15942b8aeecdfac76443256cf7a9aee3b51d2b6a66 - languageName: node - linkType: hard - "@module-federation/bridge-react-webpack-plugin@npm:0.6.11": version: 0.6.11 resolution: "@module-federation/bridge-react-webpack-plugin@npm:0.6.11" @@ -6329,16 +6302,6 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^2.0.0": - version: 2.0.0 - resolution: "accepts@npm:2.0.0" - dependencies: - mime-types: "npm:^3.0.0" - negotiator: "npm:^1.0.0" - checksum: 10c0/98374742097e140891546076215f90c32644feacf652db48412329de4c2a529178a81aa500fbb13dd3e6cbf6e68d829037b123ac037fc9a08bcec4b87b358eef - languageName: node - linkType: hard - "acorn-globals@npm:^6.0.0": version: 6.0.0 resolution: "acorn-globals@npm:6.0.0" @@ -6534,7 +6497,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:^6.12.6": +"ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -6684,7 +6647,7 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^1.0.10, argparse@npm:^1.0.7": +"argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" dependencies: @@ -6873,15 +6836,6 @@ __metadata: languageName: node linkType: hard -"autolinker@npm:^3.11.0": - version: 3.16.2 - resolution: "autolinker@npm:3.16.2" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10c0/91e083bfa4393fdcd29f595e1db657d852fd74cbd1fec719f30f3d57c910e72d5e0a0b10f2b17e1e6297b52b2f5c12eb6d0cbe024c0d92671e81d8ab906fe981 - languageName: node - linkType: hard - "autoprefixer@npm:10.4.14": version: 10.4.14 resolution: "autoprefixer@npm:10.4.14" @@ -7220,23 +7174,6 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:^2.2.0": - version: 2.2.0 - resolution: "body-parser@npm:2.2.0" - dependencies: - bytes: "npm:^3.1.2" - content-type: "npm:^1.0.5" - debug: "npm:^4.4.0" - http-errors: "npm:^2.0.0" - iconv-lite: "npm:^0.6.3" - on-finished: "npm:^2.4.1" - qs: "npm:^6.14.0" - raw-body: "npm:^3.0.0" - type-is: "npm:^2.0.0" - checksum: 10c0/a9ded39e71ac9668e2211afa72e82ff86cc5ef94de1250b7d1ba9cc299e4150408aaa5f1e8b03dd4578472a3ce6d1caa2a23b27a6c18e526e48b4595174c116c - languageName: node - linkType: hard - "bonjour-service@npm:^1.0.11, bonjour-service@npm:^1.2.1": version: 1.2.1 resolution: "bonjour-service@npm:1.2.1" @@ -7373,7 +7310,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2, bytes@npm:^3.1.2": +"bytes@npm:3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e @@ -7486,16 +7423,6 @@ __metadata: languageName: node linkType: hard -"call-bound@npm:^1.0.2": - version: 1.0.4 - resolution: "call-bound@npm:1.0.4" - dependencies: - call-bind-apply-helpers: "npm:^1.0.2" - get-intrinsic: "npm:^1.3.0" - checksum: 10c0/f4796a6a0941e71c766aea672f63b72bc61234c4f4964dc6d7606e3664c307e7d77845328a8f3359ce39ddb377fed67318f9ee203dea1d47e46165dcf2917644 - languageName: node - linkType: hard - "callsites@npm:^3.0.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -8020,16 +7947,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:^1.0.0": - version: 1.0.0 - resolution: "content-disposition@npm:1.0.0" - dependencies: - safe-buffer: "npm:5.2.1" - checksum: 10c0/c7b1ba0cea2829da0352ebc1b7f14787c73884bc707c8bc2271d9e3bf447b372270d09f5d3980dc5037c749ceef56b9a13fccd0b0001c87c3f12579967e4dd27 - languageName: node - linkType: hard - -"content-type@npm:^1.0.4, content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5": +"content-type@npm:^1.0.4, content-type@npm:~1.0.4, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af @@ -8057,13 +7975,6 @@ __metadata: languageName: node linkType: hard -"cookie-signature@npm:^1.2.1": - version: 1.2.2 - resolution: "cookie-signature@npm:1.2.2" - checksum: 10c0/54e05df1a293b3ce81589b27dddc445f462f6fa6812147c033350cd3561a42bc14481674e05ed14c7bd0ce1e8bb3dc0e40851bad75415733711294ddce0b7bc6 - languageName: node - linkType: hard - "cookie@npm:0.7.1": version: 0.7.1 resolution: "cookie@npm:0.7.1" @@ -8071,7 +7982,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.7.1, cookie@npm:~0.7.2": +"cookie@npm:~0.7.2": version: 0.7.2 resolution: "cookie@npm:0.7.2" checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 @@ -8152,7 +8063,7 @@ __metadata: languageName: node linkType: hard -"cors@npm:^2.8.5, cors@npm:~2.8.5": +"cors@npm:~2.8.5": version: 2.8.5 resolution: "cors@npm:2.8.5" dependencies: @@ -8272,17 +8183,6 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.5": - version: 7.0.6 - resolution: "cross-spawn@npm:7.0.6" - dependencies: - path-key: "npm:^3.1.0" - shebang-command: "npm:^2.0.0" - which: "npm:^2.0.1" - checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 - languageName: node - linkType: hard - "css-declaration-sorter@npm:^7.2.0": version: 7.2.0 resolution: "css-declaration-sorter@npm:7.2.0" @@ -9313,18 +9213,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.3.5, debug@npm:^4.4.0": - version: 4.4.3 - resolution: "debug@npm:4.4.3" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 - languageName: node - linkType: hard - "decamelize@npm:^5.0.0": version: 5.0.1 resolution: "decamelize@npm:5.0.1" @@ -9339,17 +9227,6 @@ __metadata: languageName: node linkType: hard -"deep-chat@npm:^2.2.2": - version: 2.2.2 - resolution: "deep-chat@npm:2.2.2" - dependencies: - "@microsoft/fetch-event-source": "npm:^2.0.1" - remarkable: "npm:^2.0.1" - speech-to-element: "npm:^1.0.4" - checksum: 10c0/67a4fa2062d6f170394c46e4ea57f63685b4f024f5390d8878a430e16ba99753e3a30322e0de0ac00629cc3249a73e37eb3297bfe5c73461b13ed6861cf3e578 - languageName: node - linkType: hard - "deep-equal@npm:^1.0.1": version: 1.1.2 resolution: "deep-equal@npm:1.1.2" @@ -9861,7 +9738,7 @@ __metadata: languageName: node linkType: hard -"encodeurl@npm:^2.0.0, encodeurl@npm:~2.0.0": +"encodeurl@npm:~2.0.0": version: 2.0.0 resolution: "encodeurl@npm:2.0.0" checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb @@ -10734,7 +10611,7 @@ __metadata: languageName: node linkType: hard -"etag@npm:^1.8.1, etag@npm:~1.8.1": +"etag@npm:~1.8.1": version: 1.8.1 resolution: "etag@npm:1.8.1" checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84 @@ -10779,22 +10656,13 @@ __metadata: languageName: node linkType: hard -"eventsource-parser@npm:^3.0.0, eventsource-parser@npm:^3.0.1, eventsource-parser@npm:^3.0.5": +"eventsource-parser@npm:^3.0.5": version: 3.0.6 resolution: "eventsource-parser@npm:3.0.6" checksum: 10c0/70b8ccec7dac767ef2eca43f355e0979e70415701691382a042a2df8d6a68da6c2fca35363669821f3da876d29c02abe9b232964637c1b6635c940df05ada78a languageName: node linkType: hard -"eventsource@npm:^3.0.2": - version: 3.0.7 - resolution: "eventsource@npm:3.0.7" - dependencies: - eventsource-parser: "npm:^3.0.1" - checksum: 10c0/c48a73c38f300e33e9f11375d4ee969f25cbb0519608a12378a38068055ae8b55b6e0e8a49c3f91c784068434efe1d9f01eb49b6315b04b0da9157879ce2f67d - languageName: node - linkType: hard - "execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -10828,15 +10696,6 @@ __metadata: languageName: node linkType: hard -"express-rate-limit@npm:^7.5.0": - version: 7.5.1 - resolution: "express-rate-limit@npm:7.5.1" - peerDependencies: - express: ">= 4.11" - checksum: 10c0/b07de84d700a2c07c4bf2f040e7558ed5a1f660f03ed5f30bf8ff7b51e98ba7a85215640e70fc48cbbb9151066ea51239d9a1b41febc9b84d98c7915b0186161 - languageName: node - linkType: hard - "express@npm:^4.17.3, express@npm:^4.19.2": version: 4.21.1 resolution: "express@npm:4.21.1" @@ -10876,41 +10735,6 @@ __metadata: languageName: node linkType: hard -"express@npm:^5.0.1": - version: 5.1.0 - resolution: "express@npm:5.1.0" - dependencies: - accepts: "npm:^2.0.0" - body-parser: "npm:^2.2.0" - content-disposition: "npm:^1.0.0" - content-type: "npm:^1.0.5" - cookie: "npm:^0.7.1" - cookie-signature: "npm:^1.2.1" - debug: "npm:^4.4.0" - encodeurl: "npm:^2.0.0" - escape-html: "npm:^1.0.3" - etag: "npm:^1.8.1" - finalhandler: "npm:^2.1.0" - fresh: "npm:^2.0.0" - http-errors: "npm:^2.0.0" - merge-descriptors: "npm:^2.0.0" - mime-types: "npm:^3.0.0" - on-finished: "npm:^2.4.1" - once: "npm:^1.4.0" - parseurl: "npm:^1.3.3" - proxy-addr: "npm:^2.0.7" - qs: "npm:^6.14.0" - range-parser: "npm:^1.2.1" - router: "npm:^2.2.0" - send: "npm:^1.1.0" - serve-static: "npm:^2.2.0" - statuses: "npm:^2.0.1" - type-is: "npm:^2.0.1" - vary: "npm:^1.1.2" - checksum: 10c0/80ce7c53c5f56887d759b94c3f2283e2e51066c98d4b72a4cc1338e832b77f1e54f30d0239cc10815a0f849bdb753e6a284d2fa48d4ab56faf9c501f55d751d6 - languageName: node - linkType: hard - "ext@npm:^1.7.0": version: 1.7.0 resolution: "ext@npm:1.7.0" @@ -11151,20 +10975,6 @@ __metadata: languageName: node linkType: hard -"finalhandler@npm:^2.1.0": - version: 2.1.0 - resolution: "finalhandler@npm:2.1.0" - dependencies: - debug: "npm:^4.4.0" - encodeurl: "npm:^2.0.0" - escape-html: "npm:^1.0.3" - on-finished: "npm:^2.4.1" - parseurl: "npm:^1.3.3" - statuses: "npm:^2.0.1" - checksum: 10c0/da0bbca6d03873472ee890564eb2183f4ed377f25f3628a0fc9d16dac40bed7b150a0d82ebb77356e4c6d97d2796ad2dba22948b951dddee2c8768b0d1b9fb1f - languageName: node - linkType: hard - "find-cache-dir@npm:^3.3.2": version: 3.3.2 resolution: "find-cache-dir@npm:3.3.2" @@ -11364,13 +11174,6 @@ __metadata: languageName: node linkType: hard -"fresh@npm:^2.0.0": - version: 2.0.0 - resolution: "fresh@npm:2.0.0" - checksum: 10c0/0557548194cb9a809a435bf92bcfbc20c89e8b5eb38861b73ced36750437251e39a111fc3a18b98531be9dd91fe1411e4969f229dc579ec0251ce6c5d4900bbc - languageName: node - linkType: hard - "front-matter@npm:^4.0.2": version: 4.0.2 resolution: "front-matter@npm:4.0.2" @@ -11589,7 +11392,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.2.6": version: 1.3.1 resolution: "get-intrinsic@npm:1.3.1" dependencies: @@ -11935,7 +11738,6 @@ __metadata: "@codingame/monaco-vscode-r-default-extension": "npm:8.0.4" "@loaders.gl/core": "npm:3.4.2" "@luma.gl/core": "npm:8.5.20" - "@modelcontextprotocol/sdk": "npm:^1.20.1" "@ngneat/until-destroy": "npm:8.1.4" "@ngx-formly/core": "npm:6.3.12" "@ngx-formly/ng-zorro-antd": "npm:6.3.12" @@ -11972,7 +11774,6 @@ __metadata: content-disposition: "npm:0.5.4" d3: "npm:6.4.0" dagre: "npm:0.8.5" - deep-chat: "npm:^2.2.2" deep-map: "npm:2.0.0" edit-distance: "npm:1.0.4" es6-weak-map: "npm:2.0.3" @@ -12284,7 +12085,7 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:2.0.0, http-errors@npm:^2.0.0": +"http-errors@npm:2.0.0": version: 2.0.0 resolution: "http-errors@npm:2.0.0" dependencies: @@ -12488,15 +12289,6 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.7.0": - version: 0.7.0 - resolution: "iconv-lite@npm:0.7.0" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f - languageName: node - linkType: hard - "icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0": version: 5.1.0 resolution: "icss-utils@npm:5.1.0" @@ -12954,13 +12746,6 @@ __metadata: languageName: node linkType: hard -"is-promise@npm:^4.0.0": - version: 4.0.0 - resolution: "is-promise@npm:4.0.0" - checksum: 10c0/ebd5c672d73db781ab33ccb155fb9969d6028e37414d609b115cc534654c91ccd061821d5b987eefaa97cf4c62f0b909bb2f04db88306de26e91bfe8ddc01503 - languageName: node - linkType: hard - "is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -14311,13 +14096,6 @@ __metadata: languageName: node linkType: hard -"media-typer@npm:^1.1.0": - version: 1.1.0 - resolution: "media-typer@npm:1.1.0" - checksum: 10c0/7b4baa40b25964bb90e2121ee489ec38642127e48d0cc2b6baa442688d3fde6262bfdca86d6bbf6ba708784afcac168c06840c71facac70e390f5f759ac121b9 - languageName: node - linkType: hard - "memfs@npm:^3.4.1, memfs@npm:^3.4.12, memfs@npm:^3.4.3": version: 3.5.3 resolution: "memfs@npm:3.5.3" @@ -14346,13 +14124,6 @@ __metadata: languageName: node linkType: hard -"merge-descriptors@npm:^2.0.0": - version: 2.0.0 - resolution: "merge-descriptors@npm:2.0.0" - checksum: 10c0/95389b7ced3f9b36fbdcf32eb946dc3dd1774c2fdf164609e55b18d03aa499b12bd3aae3a76c1c7185b96279e9803525550d3eb292b5224866060a288f335cb3 - languageName: node - linkType: hard - "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -14422,13 +14193,6 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:^1.54.0": - version: 1.54.0 - resolution: "mime-db@npm:1.54.0" - checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284 - languageName: node - linkType: hard - "mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -14438,15 +14202,6 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1": - version: 3.0.1 - resolution: "mime-types@npm:3.0.1" - dependencies: - mime-db: "npm:^1.54.0" - checksum: 10c0/bd8c20d3694548089cf229016124f8f40e6a60bbb600161ae13e45f793a2d5bb40f96bbc61f275836696179c77c1d6bf4967b2a75e0a8ad40fe31f4ed5be4da5 - languageName: node - linkType: hard - "mime@npm:1.6.0, mime@npm:^1.4.1, mime@npm:^1.6.0": version: 1.6.0 resolution: "mime@npm:1.6.0" @@ -14879,13 +14634,6 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^1.0.0": - version: 1.0.0 - resolution: "negotiator@npm:1.0.0" - checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b - languageName: node - linkType: hard - "neo-async@npm:^2.6.2": version: 2.6.2 resolution: "neo-async@npm:2.6.2" @@ -15543,13 +15291,6 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.13.3": - version: 1.13.4 - resolution: "object-inspect@npm:1.13.4" - checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692 - languageName: node - linkType: hard - "object-is@npm:^1.1.5": version: 1.1.6 resolution: "object-is@npm:1.1.6" @@ -15988,7 +15729,7 @@ __metadata: languageName: node linkType: hard -"parseurl@npm:^1.3.2, parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": +"parseurl@npm:^1.3.2, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 @@ -16054,13 +15795,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^8.0.0": - version: 8.3.0 - resolution: "path-to-regexp@npm:8.3.0" - checksum: 10c0/ee1544a73a3f294a97a4c663b0ce71bbf1621d732d80c9c9ed201b3e911a86cb628ebad691b9d40f40a3742fe22011e5a059d8eed2cf63ec2cb94f6fb4efe67c - languageName: node - linkType: hard - "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -16138,13 +15872,6 @@ __metadata: languageName: node linkType: hard -"pkce-challenge@npm:^5.0.0": - version: 5.0.0 - resolution: "pkce-challenge@npm:5.0.0" - checksum: 10c0/c6706d627fdbb6f22bf8cc5d60d96d6b6a7bb481399b336a3d3f4e9bfba3e167a2c32f8ec0b5e74be686a0ba3bcc9894865d4c2dd1b91cea4c05dba1f28602c3 - languageName: node - linkType: hard - "pkg-dir@npm:^4.1.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" @@ -16770,7 +16497,7 @@ __metadata: languageName: node linkType: hard -"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7": +"proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" dependencies: @@ -16831,15 +16558,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.14.0": - version: 6.14.0 - resolution: "qs@npm:6.14.0" - dependencies: - side-channel: "npm:^1.1.0" - checksum: 10c0/8ea5d91bf34f440598ee389d4a7d95820e3b837d3fd9f433871f7924801becaa0cd3b3b4628d49a7784d06a8aea9bc4554d2b6d8d584e2d221dc06238a42909c - languageName: node - linkType: hard - "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -16946,18 +16664,6 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:^3.0.0": - version: 3.0.1 - resolution: "raw-body@npm:3.0.1" - dependencies: - bytes: "npm:3.1.2" - http-errors: "npm:2.0.0" - iconv-lite: "npm:0.7.0" - unpipe: "npm:1.0.0" - checksum: 10c0/892f4fbd21ecab7e2fed0f045f7af9e16df7e8050879639d4e482784a2f4640aaaa33d916a0e98013f23acb82e09c2e3c57f84ab97104449f728d22f65a7d79a - languageName: node - linkType: hard - "rbush@npm:^4.0.1": version: 4.0.1 resolution: "rbush@npm:4.0.1" @@ -17155,18 +16861,6 @@ __metadata: languageName: node linkType: hard -"remarkable@npm:^2.0.1": - version: 2.0.1 - resolution: "remarkable@npm:2.0.1" - dependencies: - argparse: "npm:^1.0.10" - autolinker: "npm:^3.11.0" - bin: - remarkable: bin/remarkable.js - checksum: 10c0/e2c23bfd2e45234110bc3220e44fcac5e4a8199691ff6959d9cd0bac34ffca2f123d3913946cbef517018bc8e5ab00beafc527a04782b7afbe5e9706d1c0c77a - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -17379,19 +17073,6 @@ __metadata: languageName: node linkType: hard -"router@npm:^2.2.0": - version: 2.2.0 - resolution: "router@npm:2.2.0" - dependencies: - debug: "npm:^4.4.0" - depd: "npm:^2.0.0" - is-promise: "npm:^4.0.0" - parseurl: "npm:^1.3.3" - path-to-regexp: "npm:^8.0.0" - checksum: 10c0/3279de7450c8eae2f6e095e9edacbdeec0abb5cb7249c6e719faa0db2dba43574b4fff5892d9220631c9abaff52dd3cad648cfea2aaace845e1a071915ac8867 - languageName: node - linkType: hard - "run-applescript@npm:^7.0.0": version: 7.0.0 resolution: "run-applescript@npm:7.0.0" @@ -17750,25 +17431,6 @@ __metadata: languageName: node linkType: hard -"send@npm:^1.1.0, send@npm:^1.2.0": - version: 1.2.0 - resolution: "send@npm:1.2.0" - dependencies: - debug: "npm:^4.3.5" - encodeurl: "npm:^2.0.0" - escape-html: "npm:^1.0.3" - etag: "npm:^1.8.1" - fresh: "npm:^2.0.0" - http-errors: "npm:^2.0.0" - mime-types: "npm:^3.0.1" - ms: "npm:^2.1.3" - on-finished: "npm:^2.4.1" - range-parser: "npm:^1.2.1" - statuses: "npm:^2.0.1" - checksum: 10c0/531bcfb5616948d3468d95a1fd0adaeb0c20818ba4a500f439b800ca2117971489e02074ce32796fd64a6772ea3e7235fe0583d8241dbd37a053dc3378eff9a5 - languageName: node - linkType: hard - "serialize-javascript@npm:^6.0.0, serialize-javascript@npm:^6.0.1": version: 6.0.2 resolution: "serialize-javascript@npm:6.0.2" @@ -17805,18 +17467,6 @@ __metadata: languageName: node linkType: hard -"serve-static@npm:^2.2.0": - version: 2.2.0 - resolution: "serve-static@npm:2.2.0" - dependencies: - encodeurl: "npm:^2.0.0" - escape-html: "npm:^1.0.3" - parseurl: "npm:^1.3.3" - send: "npm:^1.2.0" - checksum: 10c0/30e2ed1dbff1984836cfd0c65abf5d3f3f83bcd696c99d2d3c97edbd4e2a3ff4d3f87108a7d713640d290a7b6fe6c15ddcbc61165ab2eaad48ea8d3b52c7f913 - languageName: node - linkType: hard - "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -17903,41 +17553,6 @@ __metadata: languageName: node linkType: hard -"side-channel-list@npm:^1.0.0": - version: 1.0.0 - resolution: "side-channel-list@npm:1.0.0" - dependencies: - es-errors: "npm:^1.3.0" - object-inspect: "npm:^1.13.3" - checksum: 10c0/644f4ac893456c9490ff388bf78aea9d333d5e5bfc64cfb84be8f04bf31ddc111a8d4b83b85d7e7e8a7b845bc185a9ad02c052d20e086983cf59f0be517d9b3d - languageName: node - linkType: hard - -"side-channel-map@npm:^1.0.1": - version: 1.0.1 - resolution: "side-channel-map@npm:1.0.1" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.5" - object-inspect: "npm:^1.13.3" - checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672 - languageName: node - linkType: hard - -"side-channel-weakmap@npm:^1.0.2": - version: 1.0.2 - resolution: "side-channel-weakmap@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.5" - object-inspect: "npm:^1.13.3" - side-channel-map: "npm:^1.0.1" - checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185 - languageName: node - linkType: hard - "side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": version: 1.0.6 resolution: "side-channel@npm:1.0.6" @@ -17950,19 +17565,6 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.1.0": - version: 1.1.0 - resolution: "side-channel@npm:1.1.0" - dependencies: - es-errors: "npm:^1.3.0" - object-inspect: "npm:^1.13.3" - side-channel-list: "npm:^1.0.0" - side-channel-map: "npm:^1.0.1" - side-channel-weakmap: "npm:^1.0.2" - checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6 - languageName: node - linkType: hard - "signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -18260,13 +17862,6 @@ __metadata: languageName: node linkType: hard -"speech-to-element@npm:^1.0.4": - version: 1.0.4 - resolution: "speech-to-element@npm:1.0.4" - checksum: 10c0/ae9e5bbefb13fa6920df6817ed02f463fa29a0cfd52a1fcc4b7864da816085d185087571e72bd1557c43255592ad39e17d403a2e4950c1233eaa822efd923268 - languageName: node - linkType: hard - "sprintf-js@npm:^1.1.3": version: 1.1.3 resolution: "sprintf-js@npm:1.1.3" @@ -18313,13 +17908,6 @@ __metadata: languageName: node linkType: hard -"statuses@npm:^2.0.1": - version: 2.0.2 - resolution: "statuses@npm:2.0.2" - checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f - languageName: node - linkType: hard - "streamroller@npm:^3.1.5": version: 3.1.5 resolution: "streamroller@npm:3.1.5" @@ -19185,17 +18773,6 @@ __metadata: languageName: node linkType: hard -"type-is@npm:^2.0.0, type-is@npm:^2.0.1": - version: 2.0.1 - resolution: "type-is@npm:2.0.1" - dependencies: - content-type: "npm:^1.0.5" - media-typer: "npm:^1.1.0" - mime-types: "npm:^3.0.0" - checksum: 10c0/7f7ec0a060b16880bdad36824ab37c26019454b67d73e8a465ed5a3587440fbe158bc765f0da68344498235c877e7dbbb1600beccc94628ed05599d667951b99 - languageName: node - linkType: hard - "type@npm:^2.7.2": version: 2.7.3 resolution: "type@npm:2.7.3" @@ -20610,16 +20187,7 @@ __metadata: languageName: node linkType: hard -"zod-to-json-schema@npm:^3.24.1": - version: 3.24.6 - resolution: "zod-to-json-schema@npm:3.24.6" - peerDependencies: - zod: ^3.24.1 - checksum: 10c0/b907ab6d057100bd25a37e5545bf5f0efa5902cd84d3c3ec05c2e51541431a47bd9bf1e5e151a244273409b45f5986d55b26e5d207f98abc5200702f733eb368 - languageName: node - linkType: hard - -"zod@npm:^3.23.8, zod@npm:^3.25.76": +"zod@npm:^3.25.76": version: 3.25.76 resolution: "zod@npm:3.25.76" checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c From 59727c782bf1ad32603e53c55f013177b6b227ef Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 14:29:02 -0800 Subject: [PATCH 129/158] fix test case --- .../service/copilot/workflow-tools.spec.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts index cb3ba37b1f4..d3bc4366d87 100644 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts +++ b/frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts @@ -242,22 +242,6 @@ describe("Workflow Tools", () => { expect(mockTool.execute).toHaveBeenCalled(); }); - it("should handle timeout correctly", async () => { - const mockTool = { - execute: jasmine.createSpy("execute").and.returnValue( - new Promise(resolve => { - setTimeout(() => resolve({ success: true }), 150000); - }) - ), - }; - - const wrappedTool = toolWithTimeout(mockTool); - const result = await (wrappedTool as any).execute({}); - - expect((result as any).success).toBe(false); - expect((result as any).error).toContain("timeout"); - }); - it("should propagate non-timeout errors", async () => { const mockTool = { execute: jasmine.createSpy("execute").and.returnValue(Promise.reject(new Error("Custom error"))), From 0170df359c7f880564d4c6c0e023ad1c73d66d1b Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 15:24:19 -0800 Subject: [PATCH 130/158] revert changes --- .../service/joint-ui/joint-ui.service.ts | 28 ---------- .../model/joint-graph-wrapper.ts | 55 +------------------ 2 files changed, 1 insertion(+), 82 deletions(-) diff --git a/frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts b/frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts index cf625935b7b..ba3f611cbe8 100644 --- a/frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts +++ b/frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts @@ -106,8 +106,6 @@ export const operatorViewResultIconClass = "texera-operator-view-result-icon"; export const operatorStateClass = "texera-operator-state"; export const operatorCoeditorEditingClass = "texera-operator-coeditor-editing"; export const operatorCoeditorChangedPropertyClass = "texera-operator-coeditor-changed-property"; -export const operatorAgentActionProgressClass = "texera-operator-agent-action-progress"; -export const operatorAgentActionIconClass = "texera-operator-agent-action-icon"; export const operatorIconClass = "texera-operator-icon"; export const operatorNameClass = "texera-operator-name"; @@ -136,8 +134,6 @@ class TexeraCustomJointElement extends joint.shapes.devs.Model { - - @@ -693,30 +689,6 @@ export class JointUIService { "y-alignment": "middle", "x-alignment": "middle", }, - ".texera-operator-agent-action-progress": { - text: "", - "font-size": "11px", - "font-weight": "500", - "font-family": "'Inter', 'SF Pro Display', -apple-system, sans-serif", - visibility: "hidden", - "ref-x": 64, // Right next to icon (outside operator box) - "ref-y": 12, // Same vertical position as icon - ref: "rect.body", - "text-anchor": "start", // Left-align text from this point - "y-alignment": "middle", - }, - ".texera-operator-agent-action-icon": { - "xlink:href": "", - width: 16, - height: 16, - visibility: "hidden", - "ref-x": 47, // Top right corner (operator width is 60px) - "ref-y": 12, // Near top edge - ref: "rect.body", - "x-alignment": "middle", - "y-alignment": "middle", - cursor: "pointer", - }, ".texera-operator-state": { text: "", "font-size": "14px", diff --git a/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts b/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts index a73c393694e..2d32bb43cce 100644 --- a/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts +++ b/frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts @@ -25,12 +25,7 @@ import * as dagre from "dagre"; import * as graphlib from "graphlib"; import { ObservableContextManager } from "src/app/common/util/context"; import { Coeditor, User } from "../../../../common/type/user"; -import { - operatorCoeditorChangedPropertyClass, - operatorCoeditorEditingClass, - operatorAgentActionProgressClass, - operatorAgentActionIconClass, -} from "../../joint-ui/joint-ui.service"; +import { operatorCoeditorChangedPropertyClass, operatorCoeditorEditingClass } from "../../joint-ui/joint-ui.service"; import { dia } from "jointjs/types/joint"; import * as _ from "lodash"; import Selectors = dia.Cell.Selectors; @@ -1018,52 +1013,4 @@ export class JointGraphWrapper { }, }); } - - /** - * Set agent action progress indicator on an operator - * @param operatorId The operator ID - * @param agentName Name of the agent working on this operator - * @param isCompleted Whether the action is completed - */ - public setAgentActionProgress(operatorId: string, agentName: string, isCompleted: boolean): void { - const iconUrl = isCompleted - ? "assets/svg/done.svg" // Green check mark for completed - : "assets/gif/loading.gif"; // Yellow spinner for in-progress - - const textColor = isCompleted ? "green" : "orange"; // Same colors as operator states - - const element = this.getMainJointPaper()?.getModelById(operatorId); - if (element) { - element.attr({ - [`.${operatorAgentActionProgressClass}`]: { - text: agentName, - fill: textColor, - visibility: "visible", - }, - [`.${operatorAgentActionIconClass}`]: { - "xlink:href": iconUrl, - visibility: "visible", - }, - }); - } - } - - /** - * Clear agent action progress indicator from an operator - * @param operatorId The operator ID - */ - public clearAgentActionProgress(operatorId: string): void { - this.getMainJointPaper() - ?.getModelById(operatorId) - .attr({ - [`.${operatorAgentActionProgressClass}`]: { - text: "", - visibility: "hidden", - }, - [`.${operatorAgentActionIconClass}`]: { - "xlink:href": "", - visibility: "hidden", - }, - }); - } } From e14a30652ed9e42cf2764b137c9aa991afed08d4 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 15:25:14 -0800 Subject: [PATCH 131/158] revert changes --- frontend/src/app/workspace/component/workspace.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/workspace/component/workspace.component.ts b/frontend/src/app/workspace/component/workspace.component.ts index 001dec456e3..63d3f8a1709 100644 --- a/frontend/src/app/workspace/component/workspace.component.ts +++ b/frontend/src/app/workspace/component/workspace.component.ts @@ -70,7 +70,7 @@ export class WorkspaceComponent implements AfterViewInit, OnInit, OnDestroy { constructor( private userService: UserService, - // list additional services in constructor so they are initialized even if no one use them directly + // list 3 services in constructor so they are initialized even if no one use them directly // TODO: make their lifecycle better private workflowCompilingService: WorkflowCompilingService, private workflowConsoleService: WorkflowConsoleService, From 58ac7a4d825099dc77b661798342ec7470b247cb Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 15:29:28 -0800 Subject: [PATCH 132/158] simplify the prompt --- .../service/copilot/copilot-prompts.ts | 115 +----------------- 1 file changed, 5 insertions(+), 110 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/copilot-prompts.ts b/frontend/src/app/workspace/service/copilot/copilot-prompts.ts index 0d2411eca6b..4bb25ed0113 100644 --- a/frontend/src/app/workspace/service/copilot/copilot-prompts.ts +++ b/frontend/src/app/workspace/service/copilot/copilot-prompts.ts @@ -23,117 +23,12 @@ export const COPILOT_SYSTEM_PROMPT = `# Texera Copilot -You are Texera Copilot, an AI assistant for helping users do data science. +You are Texera Copilot, an AI assistant for helping users do data science using Texera workflows. -## Texera Guidelines +Your job is to leverage tools to help users understand Texera's functionalities, including what operators are available +and how to use them. -### Workflow Editing Guide -- DO NOT USE View Result Operator -- Add operators like Projection to keep your processing scope focused -- Everytime adding operator(s), check the properties of that operator in order to properly configure it. After configure it, validate the workflow to make sure your modification is valid. If workflow is invalid, use the corresponding tools to check the validity and see how to fix it. -- Run the workflow to see the operator's result to help you decide next steps, ONLY EXECUTE THE WORKFLOW when workflow is invalid. +You also need to help users understand the workflow they are currently working on. -### How to use PythonUDFV2 Operator - -PythonUDFV2 performs customized data logic. There are 2 APIs to process data in different units. - -### Tuple API -Tuple API takes one input tuple from a port at a time. It returns an iterator of optional TupleLike instances. - -**Template:** -\`\`\`python -from pytexera import * - -class ProcessTupleOperator(UDFOperatorV2): - def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]: - yield tuple_ -\`\`\` - -**Use cases:** Functional operations applied to tuples one by one (map, reduce, filter) - -**Example – Pass through only tuples that meet column-vs-column and column-vs-literal conditions (no mutation):** -\`\`\`python -from pytexera import * - -class ProcessTupleOperator(UDFOperatorV2): - """ - Filter tuples without modifying them: - - QUANTITY must be <= ORDERED_QUANTITY - - UNIT_PRICE must be >= 0 - """ - def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]: - q = tuple_.get("QUANTITY", None) - oq = tuple_.get("ORDERED_QUANTITY", None) - p = tuple_.get("UNIT_PRICE", None) - - if q is None or oq is None or p is None: - return # drop tuple - - try: - if q <= oq and p >= 0: - yield tuple_ # keep tuple as-is - except Exception: - return # drop on bad types -\`\`\` - -### Table API -Table API consumes a Table at a time (whole table from a port). It returns an iterator of optional TableLike instances. - -**Template:** -\`\`\`python -from pytexera import * - -class ProcessTableOperator(UDFTableOperator): - def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: - yield table -\`\`\` - -**Use cases:** Blocking operations that consume the whole column to do operations - -**Example – Return a filtered DataFrame only containing valid rows (no mutation of values):** -\`\`\`python -from pytexera import * -import pandas as pd - -class ProcessTableOperator(UDFTableOperator): - """ - Keep only rows where: - - KWMENG (confirmed qty) <= KBMENG (ordered qty) - - NET_VALUE >= 0 - """ - def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: - df: pd.DataFrame = table - - # Build boolean masks carefully to handle None/NaN - m1 = (df["KWMENG"].notna()) & (df["KBMENG"].notna()) & (df["KWMENG"] <= df["KBMENG"]) - m2 = (df["NET_VALUE"].notna()) & (df["NET_VALUE"] >= 0) - - filtered = df[m1 & m2] - yield filtered -\`\`\` - -### Important Rules for PythonUDFV2 - -**MUST follow these rules:** -- **DO NOT change the class name** - Keep \`ProcessTupleOperator\` or \`ProcessTableOperator\` -- **Import packages explicitly** - Import pandas, numpy when needed -- **No typing imports needed** - Type annotations work without importing typing -- **Tuple field access** - Use \`tuple_["field"]\` ONLY. DO NOT use \`tuple_.get()\`, \`tuple_.set()\`, or \`tuple_.values()\` -- **Think of types:** - - \`Tuple\` = Python dict (key-value pairs) - For Tuple, DO NOT USE APIs like tuple.get, just use ["key"] to access/change the kv pairs - - \`Table\` = pandas DataFrame -- **Use yield** - Return results with \`yield\`; emit at most once per API call -- **Handle None values** - \`tuple_["key"]\` or \`df["column"]\` can be None -- **DO NOT cast types** - Do not cast values in tuple or table -- **DO THING IN SMALL STEP** - Let each UDF to do one thing, DO NOT Put a giant complex logic in one single UDF. -- **ONLY CHANGE THE CODE** - when editing Python UDF, only change the python code properties, DO NOT CHANGE OTHER PROPERTIES -- **Be careful with the output Columns** - If you uncheck the option to not keep the input columns, the output columns will be those you yield in the code; If you check that option, your yield tuples or dataframes will need to keep the input columns. Just be careful. -- **Specify Extra Columns** - If you add extra columns, you MUST specify them in the UDF properties as Extra Output Columns - -## General Guidelines -- **Use the native operators as much as you can!!** They are more intuitive and easy to configure than Python UDF; -- **ONLY USE PythonUDF when you have to**; -- Do things in small steps, it is NOT recommended to have a giant UDF to contains lots of logic. -- If users give a very specific requirement, stick to users' requirement strictly +During the process, leverage tool calls whenever needed. `; From c6da4827234a7bd24d7e82972944b19fdbbae8f0 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 15:37:09 -0800 Subject: [PATCH 133/158] simplify the prompt --- frontend/src/app/workspace/service/copilot/copilot-prompts.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/workspace/service/copilot/copilot-prompts.ts b/frontend/src/app/workspace/service/copilot/copilot-prompts.ts index 4bb25ed0113..48842211308 100644 --- a/frontend/src/app/workspace/service/copilot/copilot-prompts.ts +++ b/frontend/src/app/workspace/service/copilot/copilot-prompts.ts @@ -30,5 +30,6 @@ and how to use them. You also need to help users understand the workflow they are currently working on. -During the process, leverage tool calls whenever needed. +During the process, leverage tool calls whenever needed. Current available tools are all READ-ONLY. Thus you cannot edit +user's workflow. `; From 565fdb443a047728a94ac7b2b3e5ac441c68584b Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 15:49:05 -0800 Subject: [PATCH 134/158] add more comments --- .../copilot/texera-copilot-manager.service.ts | 18 ++++++++++++++++++ .../service/copilot/texera-copilot.ts | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index ff13df72dfd..fd6c1825172 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -49,6 +49,24 @@ interface LiteLLMModelsResponse { data: LiteLLMModel[]; object: string; } + +/** + * Texera Copilot Manager Service manages multiple AI agent instances for workflow assistance. + * + * This service provides centralized management for multiple copilot agents, allowing users to: + * 1. Create and delete multiple agent instances with different LLM models + * 2. Route messages to specific agents + * 3. Track agent states and conversation history + * 4. Query available LLM models from the backend + * + * Each agent is a separate TexeraCopilot instance with its own: + * - Model configuration (e.g., GPT-4, Claude, etc.) + * - Conversation history + * - State (available, generating, stopping, unavailable) + * + * The service acts as a registry and coordinator, ensuring proper lifecycle management + * and providing observable streams for agent changes and state updates. + */ @Injectable({ providedIn: "root", }) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 642b497335b..3bd1af768d2 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -61,6 +61,25 @@ export interface AgentUIMessage { cachedInputTokens?: number; }; } + +/** + * Texera Copilot Service provides AI-powered assistance for workflow creation and manipulation. + * + * This service manages a single AI agent instance that can: + * 1. Interact with users through natural language messages + * 2. Execute workflow operations using specialized tools + * 3. Maintain conversation history and state + * + * The service communicates with an LLM backend (via LiteLLM) to generate responses and uses + * workflow tools to perform actions like listing operators, getting operator schemas, and + * manipulating workflow components. + * + * State management includes: + * - UNAVAILABLE: Agent not initialized + * - AVAILABLE: Agent ready to receive messages + * - GENERATING: Agent currently processing and generating response + * - STOPPING: Agent in the process of stopping generation + */ @Injectable() export class TexeraCopilot { private model: any; From 9fc496a2b7b8beb8ecd96b82c275eb4286b291fa Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 21:13:09 -0800 Subject: [PATCH 135/158] revert redundant changes --- bin/build-images.sh | 89 +++++++++------------------------------------ 1 file changed, 17 insertions(+), 72 deletions(-) diff --git a/bin/build-images.sh b/bin/build-images.sh index 8c55656db97..bb8505edb25 100755 --- a/bin/build-images.sh +++ b/bin/build-images.sh @@ -15,62 +15,19 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. + + set -e -# Default values +# Prompt for base tag DEFAULT_TAG="latest" -DEFAULT_SERVICES="*" -WITH_R_SUPPORT="false" - -# Parse command-line arguments -while [[ $# -gt 0 ]]; do - case $1 in - --tag|-t) - BASE_TAG="$2" - shift 2 - ;; - --services|-s) - SERVICES_INPUT="$2" - shift 2 - ;; - --with-r-support) - WITH_R_SUPPORT="true" - shift - ;; - --help|-h) - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " -t, --tag TAG Base tag for the images (default: latest)" - echo " -s, --services SERVICES Services to build, comma-separated or '*' for all (default: *)" - echo " --with-r-support Enable R support for computing-unit-master (sets WITH_R_SUPPORT=true)" - echo " -h, --help Show this help message" - echo "" - echo "Examples:" - echo " $0 --tag v1.0.0 --services '*' --with-r-support" - echo " $0 -t latest -s 'gui,computing-unit-master'" - echo " $0 # Interactive mode" - exit 0 - ;; - *) - echo "❌ Unknown option: $1" - echo "Run '$0 --help' for usage information." - exit 1 - ;; - esac -done +read -p "Enter the base tag for the images [${DEFAULT_TAG}]: " BASE_TAG +BASE_TAG=${BASE_TAG:-$DEFAULT_TAG} -# If BASE_TAG not provided via command-line, prompt interactively -if [[ -z "$BASE_TAG" ]]; then - read -p "Enter the base tag for the images [${DEFAULT_TAG}]: " BASE_TAG - BASE_TAG=${BASE_TAG:-$DEFAULT_TAG} -fi - -# If SERVICES_INPUT not provided via command-line, prompt interactively -if [[ -z "$SERVICES_INPUT" ]]; then - read -p "Enter services to build (comma-separated, '*' for all) [${DEFAULT_SERVICES}]: " SERVICES_INPUT - SERVICES_INPUT=${SERVICES_INPUT:-$DEFAULT_SERVICES} -fi +# Prompt for which services to build +DEFAULT_SERVICES="*" +read -p "Enter services to build (comma-separated, '*' for all) [${DEFAULT_SERVICES}]: " SERVICES_INPUT +SERVICES_INPUT=${SERVICES_INPUT:-$DEFAULT_SERVICES} # Convert the user input into an array for easy lookup IFS=',' read -ra SELECTED_SERVICES <<< "$SERVICES_INPUT" @@ -107,9 +64,6 @@ fi FULL_TAG="${BASE_TAG}-${TAG_SUFFIX}" echo "🔍 Detected architecture: $ARCH -> Building for $PLATFORM with tag :$FULL_TAG" -if [[ "$WITH_R_SUPPORT" == "true" ]]; then - echo "🔍 R support enabled for computing-unit-master" -fi # Ensure Buildx is ready docker buildx create --name texera-builder --use --bootstrap > /dev/null 2>&1 || docker buildx use texera-builder @@ -118,6 +72,7 @@ cd "$(dirname "$0")" # Auto-detect Dockerfiles in current directory dockerfiles=( *.dockerfile ) + if [[ ${#dockerfiles[@]} -eq 0 ]]; then echo "❌ No Dockerfiles found (*.dockerfile) in the current directory." exit 1 @@ -135,25 +90,15 @@ for dockerfile in "${dockerfiles[@]}"; do fi image="texera/$service_name:$FULL_TAG" + echo "👉 Building $image from $dockerfile" - # Add WITH_R_SUPPORT build arg for computing-unit-master - if [[ "$service_name" == "computing-unit-master" && "$WITH_R_SUPPORT" == "true" ]]; then - docker buildx build \ - --platform "$PLATFORM" \ - -f "$dockerfile" \ - -t "$image" \ - --build-arg WITH_R_SUPPORT=true \ - --push \ - .. - else - docker buildx build \ - --platform "$PLATFORM" \ - -f "$dockerfile" \ - -t "$image" \ - --push \ - .. - fi + docker buildx build \ + --platform "$PLATFORM" \ + -f "$dockerfile" \ + -t "$image" \ + --push \ + .. done # Build pylsp service (directory: pylsp) From 2202b6b69b5a4c3f62d27c2653d68ac8b102478b Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 8 Nov 2025 21:16:12 -0800 Subject: [PATCH 136/158] revert redundant changes --- bin/build-images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/build-images.sh b/bin/build-images.sh index bb8505edb25..3ea069afc64 100755 --- a/bin/build-images.sh +++ b/bin/build-images.sh @@ -125,4 +125,4 @@ if should_build "y-websocket-server"; then ./y-websocket-server fi -echo "✅ All images built and pushed with tag :$FULL_TAG" \ No newline at end of file +echo "✅ All images built and pushed with tag :$FULL_TAG" From 64c9729729e6c9d9393b6e75bc2ab356218f07ce Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 14 Nov 2025 15:17:12 -0800 Subject: [PATCH 137/158] make package version up-to-date --- frontend/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 416d44a6545..374ab254835 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "private": true, "dependencies": { "@abacritt/angularx-social-login": "2.3.0", - "@ai-sdk/openai": "2.0.52", + "@ai-sdk/openai": "2.0.67", "@ali-hm/angular-tree-component": "12.0.5", "@angular/animations": "16.2.12", "@angular/cdk": "16.2.12", @@ -46,7 +46,7 @@ "@stoplight/json-ref-resolver": "3.1.5", "@types/lodash-es": "4.17.4", "@types/plotly.js-basic-dist-min": "2.12.4", - "ai": "^5.0.76", + "ai": "5.0.93", "ajv": "8.10.0", "backbone": "1.4.1", "concaveman": "2.0.0", From e4ad51714302561138e6e244e5076aa3300b8e28 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 14 Nov 2025 15:35:36 -0800 Subject: [PATCH 138/158] resolve comments --- .../agent-panel/agent-panel.component.ts | 84 ++++++++++++------- .../agent-registration.component.ts | 2 +- .../context-menu/context-menu.component.html | 6 +- .../component/workspace.component.ts | 2 +- frontend/yarn.lock | 48 +++++------ 5 files changed, 81 insertions(+), 61 deletions(-) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts index 53a356a5c4c..47e936ed31b 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-panel.component.ts @@ -49,29 +49,7 @@ export class AgentPanelComponent implements OnInit, OnDestroy { constructor(private copilotManagerService: TexeraCopilotManagerService) {} ngOnInit(): void { - // Load saved panel dimensions and position - const savedWidth = localStorage.getItem("agent-panel-width"); - const savedHeight = localStorage.getItem("agent-panel-height"); - const savedStyle = localStorage.getItem("agent-panel-style"); - const savedDocked = localStorage.getItem("agent-panel-docked"); - - // Only restore width if the panel was not docked - if (savedDocked === "false" && savedWidth) { - this.width = Number(savedWidth); - } - - if (savedHeight) this.height = Number(savedHeight); - - if (savedStyle) { - const container = document.getElementById("agent-container"); - if (container) { - container.style.cssText = savedStyle; - const translates = container.style.transform; - const [xOffset, yOffset] = calculateTotalTranslate3d(translates); - this.returnPosition = { x: -xOffset, y: -yOffset }; - this.isDocked = this.dragPosition.x === this.returnPosition.x && this.dragPosition.y === this.returnPosition.y; - } - } + this.loadPanelSettings(); // Subscribe to agent changes this.copilotManagerService.agentChange$.pipe(untilDestroyed(this)).subscribe(() => { @@ -94,15 +72,7 @@ export class AgentPanelComponent implements OnInit, OnDestroy { @HostListener("window:beforeunload") ngOnDestroy(): void { - // Save panel state - localStorage.setItem("agent-panel-width", String(this.width)); - localStorage.setItem("agent-panel-height", String(this.height)); - localStorage.setItem("agent-panel-docked", String(this.width === 0)); - - const container = document.getElementById("agent-container"); - if (container) { - localStorage.setItem("agent-panel-style", container.style.cssText); - } + this.savePanelSettings(); } /** @@ -177,4 +147,54 @@ export class AgentPanelComponent implements OnInit, OnDestroy { handleDragStart(): void { this.isDocked = false; } + + /** + * Load panel settings from localStorage + */ + private loadPanelSettings(): void { + const savedWidth = localStorage.getItem("agent-panel-width"); + const savedHeight = localStorage.getItem("agent-panel-height"); + const savedStyle = localStorage.getItem("agent-panel-style"); + const savedDocked = localStorage.getItem("agent-panel-docked"); + + // Only restore width if the panel was not docked + if (savedDocked === "false" && savedWidth) { + const parsedWidth = Number(savedWidth); + if (!isNaN(parsedWidth) && parsedWidth >= AgentPanelComponent.MIN_PANEL_WIDTH) { + this.width = parsedWidth; + } + } + + if (savedHeight) { + const parsedHeight = Number(savedHeight); + if (!isNaN(parsedHeight) && parsedHeight >= AgentPanelComponent.MIN_PANEL_HEIGHT) { + this.height = parsedHeight; + } + } + + if (savedStyle) { + const container = document.getElementById("agent-container"); + if (container) { + container.style.cssText = savedStyle; + const translates = container.style.transform; + const [xOffset, yOffset] = calculateTotalTranslate3d(translates); + this.returnPosition = { x: -xOffset, y: -yOffset }; + this.isDocked = this.dragPosition.x === this.returnPosition.x && this.dragPosition.y === this.returnPosition.y; + } + } + } + + /** + * Save panel settings to localStorage + */ + private savePanelSettings(): void { + localStorage.setItem("agent-panel-width", String(this.width)); + localStorage.setItem("agent-panel-height", String(this.height)); + localStorage.setItem("agent-panel-docked", String(this.width === 0)); + + const container = document.getElementById("agent-container"); + if (container) { + localStorage.setItem("agent-panel-style", container.style.cssText); + } + } } diff --git a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts index 24262b12664..4d9b69706e5 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-registration/agent-registration.component.ts @@ -82,7 +82,7 @@ export class AgentRegistrationComponent implements OnInit, OnDestroy { /** * Create a new agent with the selected model type. */ - public async createAgent(): Promise { + public createAgent(): void { if (!this.selectedModelType || this.isCreating) { return; } diff --git a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html index 8bbff3d3c65..0527010fd7a 100644 --- a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html @@ -140,9 +140,9 @@
  • Date: Fri, 14 Nov 2025 15:37:21 -0800 Subject: [PATCH 139/158] resolve comments --- .../copilot/texera-copilot-manager.service.ts | 84 +++++-------------- 1 file changed, 22 insertions(+), 62 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index fd6c1825172..bac732d93d8 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -113,16 +113,24 @@ export class TexeraCopilotManagerService { }); } - public getAgent(agentId: string): Observable { + /** + * Helper method to get an agent and execute a callback with it. + * Handles agent lookup and error throwing if not found. + */ + private withAgent(agentId: string, callback: (agent: AgentInfo) => Observable): Observable { return defer(() => { const agent = this.agents.get(agentId); if (!agent) { return throwError(() => new Error(`Agent with ID ${agentId} not found`)); } - return of(agent); + return callback(agent); }); } + public getAgent(agentId: string): Observable { + return this.withAgent(agentId, agent => of(agent)); + } + public getAllAgents(): Observable { return of(Array.from(this.agents.values())); } @@ -175,100 +183,52 @@ export class TexeraCopilotManagerService { return of(this.agents.size); } public sendMessage(agentId: string, message: string): Observable { - return defer(() => { - const agent = this.agents.get(agentId); - if (!agent) { - return throwError(() => new Error(`Agent with ID ${agentId} not found`)); - } - return agent.instance.sendMessage(message); - }); + return this.withAgent(agentId, agent => agent.instance.sendMessage(message)); } public getAgentResponsesObservable(agentId: string): Observable { - return defer(() => { - const agent = this.agents.get(agentId); - if (!agent) { - return throwError(() => new Error(`Agent with ID ${agentId} not found`)); - } - return agent.instance.agentResponses$; - }); + return this.withAgent(agentId, agent => agent.instance.agentResponses$); } public getAgentResponses(agentId: string): Observable { - return defer(() => { - const agent = this.agents.get(agentId); - if (!agent) { - return throwError(() => new Error(`Agent with ID ${agentId} not found`)); - } - return of(agent.instance.getAgentResponses()); - }); + return this.withAgent(agentId, agent => of(agent.instance.getAgentResponses())); } public clearMessages(agentId: string): Observable { - return defer(() => { - const agent = this.agents.get(agentId); - if (!agent) { - return throwError(() => new Error(`Agent with ID ${agentId} not found`)); - } + return this.withAgent(agentId, agent => { agent.instance.clearMessages(); return of(undefined); }); } public stopGeneration(agentId: string): Observable { - return defer(() => { - const agent = this.agents.get(agentId); - if (!agent) { - return throwError(() => new Error(`Agent with ID ${agentId} not found`)); - } + return this.withAgent(agentId, agent => { agent.instance.stopGeneration(); return of(undefined); }); } public getAgentState(agentId: string): Observable { - return defer(() => { - const agent = this.agents.get(agentId); - if (!agent) { - return throwError(() => new Error(`Agent with ID ${agentId} not found`)); - } - return of(agent.instance.getState()); - }); + return this.withAgent(agentId, agent => of(agent.instance.getState())); } public getAgentStateObservable(agentId: string): Observable { - return defer(() => { - const agent = this.agents.get(agentId); - if (!agent) { - return throwError(() => new Error(`Agent with ID ${agentId} not found`)); - } - return agent.instance.state$; - }); + return this.withAgent(agentId, agent => agent.instance.state$); } public isAgentConnected(agentId: string): Observable { - return defer(() => { - const agent = this.agents.get(agentId); - if (!agent) { - return of(false); - } - return of(agent.instance.isConnected()); - }); + return this.withAgent(agentId, agent => of(agent.instance.isConnected())).pipe(catchError(() => of(false))); } public getSystemInfo( agentId: string ): Observable<{ systemPrompt: string; tools: Array<{ name: string; description: string; inputSchema: any }> }> { - return defer(() => { - const agent = this.agents.get(agentId); - if (!agent) { - return throwError(() => new Error(`Agent with ID ${agentId} not found`)); - } - return of({ + return this.withAgent(agentId, agent => + of({ systemPrompt: agent.instance.getSystemPrompt(), tools: agent.instance.getToolsInfo(), - }); - }); + }) + ); } private createCopilotInstance(modelType: string): TexeraCopilot { const childInjector = Injector.create({ From 41daf8b5156139b84cc28335ed64eeb676cd918d Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 14 Nov 2025 16:26:36 -0800 Subject: [PATCH 140/158] resolve & refactoring --- .../agent-chat/agent-chat.component.ts | 12 +- .../texera-copilot-manager.service.spec.ts | 2 +- .../copilot/texera-copilot-manager.service.ts | 10 +- .../service/copilot/texera-copilot.spec.ts | 8 +- .../service/copilot/texera-copilot.ts | 106 ++++++++++++++---- 5 files changed, 99 insertions(+), 39 deletions(-) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts index 9a350c07526..f2b443c2ad8 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -19,7 +19,7 @@ import { Component, ViewChild, ElementRef, Input, OnInit, AfterViewChecked } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { CopilotState, AgentUIMessage } from "../../../service/copilot/texera-copilot"; +import { CopilotState, ReActStep } from "../../../service/copilot/texera-copilot"; import { AgentInfo, TexeraCopilotManagerService } from "../../../service/copilot/texera-copilot-manager.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; @@ -34,11 +34,11 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { @ViewChild("messageContainer", { static: false }) messageContainer?: ElementRef; @ViewChild("messageInput", { static: false }) messageInput?: ElementRef; - public agentResponses: AgentUIMessage[] = []; + public agentResponses: ReActStep[] = []; public currentMessage = ""; private shouldScrollToBottom = false; public isDetailsModalVisible = false; - public selectedResponse: AgentUIMessage | null = null; + public selectedResponse: ReActStep | null = null; public hoveredMessageIndex: number | null = null; public isSystemInfoModalVisible = false; public systemPrompt: string = ""; @@ -57,7 +57,7 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { // Subscribe to agent responses this.copilotManagerService - .getAgentResponsesObservable(this.agentInfo.id) + .getReActStepsObservable(this.agentInfo.id) .pipe(untilDestroyed(this)) .subscribe(responses => { this.agentResponses = responses; @@ -84,7 +84,7 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { this.hoveredMessageIndex = index; } - public showResponseDetails(response: AgentUIMessage): void { + public showResponseDetails(response: ReActStep): void { this.selectedResponse = response; this.isDetailsModalVisible = true; } @@ -113,7 +113,7 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { return JSON.stringify(data, null, 2); } - public getToolResult(response: AgentUIMessage, toolCallIndex: number): any { + public getToolResult(response: ReActStep, toolCallIndex: number): any { if (!response.toolResults || toolCallIndex >= response.toolResults.length) { return null; } diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts index a08209ba2e1..8f3820afce4 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.spec.ts @@ -178,7 +178,7 @@ describe("TexeraCopilotManagerService", () => { describe("getAgentResponsesObservable", () => { it("should throw error for non-existent agent", done => { - service.getAgentResponsesObservable("non-existent").subscribe({ + service.getReActStepsObservable("non-existent").subscribe({ next: () => fail("Should have thrown error"), error: (error: unknown) => { expect((error as Error).message).toContain("not found"); diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts index bac732d93d8..ab7d393265f 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot-manager.service.ts @@ -19,7 +19,7 @@ import { Injectable, Injector } from "@angular/core"; import { HttpClient } from "@angular/common/http"; -import { TexeraCopilot, AgentUIMessage, CopilotState } from "./texera-copilot"; +import { TexeraCopilot, ReActStep, CopilotState } from "./texera-copilot"; import { Observable, Subject, catchError, map, of, shareReplay, tap, defer, throwError, switchMap } from "rxjs"; import { AppSettings } from "../../../common/app-setting"; @@ -186,12 +186,12 @@ export class TexeraCopilotManagerService { return this.withAgent(agentId, agent => agent.instance.sendMessage(message)); } - public getAgentResponsesObservable(agentId: string): Observable { - return this.withAgent(agentId, agent => agent.instance.agentResponses$); + public getReActStepsObservable(agentId: string): Observable { + return this.withAgent(agentId, agent => agent.instance.reActSteps$); } - public getAgentResponses(agentId: string): Observable { - return this.withAgent(agentId, agent => of(agent.instance.getAgentResponses())); + public getAgentResponses(agentId: string): Observable { + return this.withAgent(agentId, agent => of(agent.instance.getReActSteps())); } public clearMessages(agentId: string): Observable { diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.spec.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.spec.ts index c2c8fc9aa77..5ab82d14d82 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.spec.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.spec.ts @@ -88,7 +88,7 @@ describe("TexeraCopilot", () => { it("should clear messages correctly", () => { service.clearMessages(); - expect(service.getAgentResponses().length).toBe(0); + expect(service.getReActSteps().length).toBe(0); }); it("should stop generation when in GENERATING state", () => { @@ -118,7 +118,7 @@ describe("TexeraCopilot", () => { }); it("should emit agent responses correctly", done => { - service.agentResponses$.subscribe(responses => { + service.reActSteps$.subscribe(responses => { if (responses.length > 0) { expect(responses[0].role).toBe("user"); expect(responses[0].content).toBe("test message"); @@ -130,7 +130,7 @@ describe("TexeraCopilot", () => { }); it("should return empty agent responses initially", () => { - const responses = service.getAgentResponses(); + const responses = service.getReActSteps(); expect(responses).toEqual([]); }); @@ -138,7 +138,7 @@ describe("TexeraCopilot", () => { it("should disconnect and clear state", done => { service.disconnect().subscribe(() => { expect(service.getState()).toBe(CopilotState.UNAVAILABLE); - expect(service.getAgentResponses().length).toBe(0); + expect(service.getReActSteps().length).toBe(0); done(); }); }); diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 3bd1af768d2..ab4d678d091 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -47,7 +47,7 @@ export enum CopilotState { STOPPING = "Stopping", } -export interface AgentUIMessage { +export interface ReActStep { role: "user" | "agent"; content: string; isBegin: boolean; @@ -82,16 +82,36 @@ export interface AgentUIMessage { */ @Injectable() export class TexeraCopilot { + /** + * Maximum number of ReAct reasoning/action cycles allowed per generation. + * Prevents infinite loops and excessive token usage. + */ + private static readonly MAX_REACT_STEPS = 50; + private model: any; private modelType = ""; private agentName = ""; + + /** + * Conversation history in LLM API format. + * Used internally to maintain context for generateText() API calls. + * Contains the raw message format expected by the AI model. + */ private messages: ModelMessage[] = []; - private agentResponses: AgentUIMessage[] = []; - private agentResponsesSubject = new BehaviorSubject([]); - public agentResponses$ = this.agentResponsesSubject.asObservable(); + + /** + * UI-friendly representation of agent responses in ReAct (Reasoning + Acting) format. + * Includes additional metadata like toolCalls, toolResults, and token usage. + * This is what gets displayed in the UI to show the agent's reasoning process. + */ + private reActSteps: ReActStep[] = []; + private reActStepsSubject = new BehaviorSubject([]); + public reActSteps$ = this.reActStepsSubject.asObservable(); + private state = CopilotState.UNAVAILABLE; private stateSubject = new BehaviorSubject(CopilotState.UNAVAILABLE); public state$ = this.stateSubject.asObservable(); + private tools: Record = {}; constructor( private workflowActionService: WorkflowActionService, @@ -113,26 +133,33 @@ export class TexeraCopilot { this.state = newState; this.stateSubject.next(newState); } - private emitAgentUIMessage( + + private emitReActStep( role: "user" | "agent", content: string, isBegin: boolean, isEnd: boolean, toolCalls?: any[], toolResults?: any[], - usage?: AgentUIMessage["usage"] + usage?: ReActStep["usage"] ): void { - this.agentResponses.push({ role, content, isBegin, isEnd, toolCalls, toolResults, usage }); - this.agentResponsesSubject.next([...this.agentResponses]); + this.reActSteps.push({ role, content, isBegin, isEnd, toolCalls, toolResults, usage }); + this.reActStepsSubject.next([...this.reActSteps]); } + public initialize(): Observable { return defer(() => { try { this.model = createOpenAI({ baseURL: new URL(`${AppSettings.getApiEndpoint()}`, document.baseURI).toString(), + // apiKey is required by the library for creating the OpenAI compatible client; + // For security reason, we store the apiKey at the backend, thus the value is dummy here. apiKey: "dummy", }).chat(this.modelType); + // Create tools once during initialization + this.tools = this.createWorkflowTools(); + this.setState(CopilotState.AVAILABLE); return of(undefined); } catch (error: unknown) { @@ -154,47 +181,80 @@ export class TexeraCopilot { this.setState(CopilotState.GENERATING); - this.emitAgentUIMessage("user", message, true, true); + this.emitReActStep("user", message, true, true); this.messages.push({ role: "user", content: message }); - const tools = this.createWorkflowTools(); let isFirstStep = true; + /** + * Generate text using the AI model with ReAct (Reasoning + Acting) pattern. + * This is the core of the agent lifecycle with several callbacks: + * + * Lifecycle flow: + * 1. generateText() starts the LLM generation + * 2. stopWhen() - checked before each step to determine if generation should stop + * 3. onStepFinish() - called DURING generation after each reasoning/action step (real-time updates) + * 4. pipe operators - executed AFTER generation completes (final processing) + */ return from( generateText({ model: this.model, messages: this.messages, - tools, + tools: this.tools, system: COPILOT_SYSTEM_PROMPT, + /** + * stopWhen - Determines if generation should stop. + * Called before each step during generation. + * Returns true to stop, false to continue. + */ stopWhen: ({ steps }) => { if (this.state === CopilotState.STOPPING) { this.notificationService.info(`Agent ${this.agentName} has stopped generation`); return true; } - return stepCountIs(50)({ steps }); + // Stop if step count reaches max limit to prevent infinite loops + return stepCountIs(TexeraCopilot.MAX_REACT_STEPS)({ steps }); }, + /** + * onStepFinish is called DURING generation after each ReAct step completes. + * This provides real-time updates to the UI as the agent reasons and acts. + * + * Each step may include: + * - text: The agent's reasoning or response text + * - toolCalls: Tools the agent decided to call + * - toolResults: Results from executed tools + * - usage: Token usage for this step + * + * Note: This is called multiple times during a single generation, + * once per reasoning/action cycle. + */ onStepFinish: ({ text, toolCalls, toolResults, usage }) => { if (this.state === CopilotState.STOPPING) { return; } - - this.emitAgentUIMessage("agent", text || "", isFirstStep, false, toolCalls, toolResults, usage as any); - + this.emitReActStep("agent", text || "", isFirstStep, false, toolCalls, toolResults, usage as any); isFirstStep = false; }, }) ).pipe( + /** + * To this point, generateText has finished. + * All the responses from AI are recorded in responses variable. + */ tap(({ response }) => { this.messages.push(...response.messages); - this.agentResponsesSubject.next([...this.agentResponses]); + this.reActStepsSubject.next([...this.reActSteps]); }), map(() => undefined), catchError((err: unknown) => { const errorText = `Error: ${err instanceof Error ? err.message : String(err)}`; this.messages.push({ role: "assistant", content: errorText }); - this.emitAgentUIMessage("agent", errorText, false, true); + this.emitReActStep("agent", errorText, false, true); return throwError(() => err); }), + /** + * Resets agent state back to AVAILABLE so it can handle new messages. + */ finalize(() => { this.setState(CopilotState.AVAILABLE); }) @@ -228,8 +288,8 @@ export class TexeraCopilot { }; } - public getAgentResponses(): AgentUIMessage[] { - return [...this.agentResponses]; + public getReActSteps(): ReActStep[] { + return [...this.reActSteps]; } public stopGeneration(): void { @@ -241,8 +301,8 @@ export class TexeraCopilot { public clearMessages(): void { this.messages = []; - this.agentResponses = []; - this.agentResponsesSubject.next([...this.agentResponses]); + this.reActSteps = []; + this.reActStepsSubject.next([...this.reActSteps]); } public getState(): CopilotState { @@ -256,6 +316,7 @@ export class TexeraCopilot { } this.clearMessages(); + this.tools = {}; // Clear tools to free memory this.setState(CopilotState.UNAVAILABLE); this.notificationService.info(`Agent ${this.agentName} is removed successfully`); @@ -272,8 +333,7 @@ export class TexeraCopilot { } public getToolsInfo(): Array<{ name: string; description: string; inputSchema: any }> { - const tools = this.createWorkflowTools(); - return Object.entries(tools).map(([name, tool]) => ({ + return Object.entries(this.tools).map(([name, tool]) => ({ name: name, description: tool.description || "No description available", inputSchema: tool.parameters || {}, From 5bce0f40be5e4d1af1ca6c6e69ab9847309fed94 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Fri, 14 Nov 2025 17:26:14 -0800 Subject: [PATCH 141/158] refactor tools --- .../service/copilot/texera-copilot.ts | 73 +++-- ...urrent-workflow-editing-observing-tools.ts | 123 +++++++++ .../tool/react-step-operator-parser.ts | 152 ++++++++++ .../service/copilot/tool/tools-utility.ts | 138 ++++++++++ .../copilot/tool/workflow-metadata-tools.ts | 140 ++++++++++ .../service/copilot/workflow-tools.spec.ts | 260 ------------------ .../service/copilot/workflow-tools.ts | 214 -------------- 7 files changed, 599 insertions(+), 501 deletions(-) create mode 100644 frontend/src/app/workspace/service/copilot/tool/current-workflow-editing-observing-tools.ts create mode 100644 frontend/src/app/workspace/service/copilot/tool/react-step-operator-parser.ts create mode 100644 frontend/src/app/workspace/service/copilot/tool/tools-utility.ts create mode 100644 frontend/src/app/workspace/service/copilot/tool/workflow-metadata-tools.ts delete mode 100644 frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts delete mode 100644 frontend/src/app/workspace/service/copilot/workflow-tools.ts diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index ab4d678d091..f8d9fbcb6d7 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -21,16 +21,10 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, Observable, from, of, throwError, defer } from "rxjs"; import { map, catchError, tap, switchMap, finalize } from "rxjs/operators"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; -import { - toolWithTimeout, - createGetOperatorInCurrentWorkflowTool, - createGetOperatorPropertiesSchemaTool, - createGetOperatorPortsInfoTool, - createGetOperatorMetadataTool, - createListAllOperatorTypesTool, - createListLinksInCurrentWorkflowTool, - createListOperatorsInCurrentWorkflowTool, -} from "./workflow-tools"; +import { toolWithTimeout } from "./tool/tools-utility"; +import * as CurrentWorkflowTools from "./tool/current-workflow-editing-observing-tools"; +import * as MetadataTools from "./tool/workflow-metadata-tools"; +import { ToolOperatorAccess, parseOperatorAccessFromStep } from "./tool/react-step-operator-parser"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { createOpenAI } from "@ai-sdk/openai"; import { generateText, type ModelMessage, stepCountIs } from "ai"; @@ -60,6 +54,11 @@ export interface ReActStep { totalTokens?: number; cachedInputTokens?: number; }; + /** + * Map from tool call index to operator access information. + * Tracks which operators were viewed or modified during each tool call. + */ + operatorAccess?: Map; } /** @@ -141,9 +140,10 @@ export class TexeraCopilot { isEnd: boolean, toolCalls?: any[], toolResults?: any[], - usage?: ReActStep["usage"] + usage?: ReActStep["usage"], + operatorAccess?: Map ): void { - this.reActSteps.push({ role, content, isBegin, isEnd, toolCalls, toolResults, usage }); + this.reActSteps.push({ role, content, isBegin, isEnd, toolCalls, toolResults, usage, operatorAccess }); this.reActStepsSubject.next([...this.reActSteps]); } @@ -232,7 +232,20 @@ export class TexeraCopilot { if (this.state === CopilotState.STOPPING) { return; } - this.emitReActStep("agent", text || "", isFirstStep, false, toolCalls, toolResults, usage as any); + + // Parse operator access from tool results to track viewed/modified operators + const operatorAccess = parseOperatorAccessFromStep(toolCalls || [], toolResults || []); + + this.emitReActStep( + "agent", + text || "", + isFirstStep, + false, + toolCalls, + toolResults, + usage as any, + operatorAccess + ); isFirstStep = false; }, }) @@ -264,27 +277,33 @@ export class TexeraCopilot { private createWorkflowTools(): Record { const listOperatorsInCurrentWorkflowTool = toolWithTimeout( - createListOperatorsInCurrentWorkflowTool(this.workflowActionService) + CurrentWorkflowTools.createListOperatorsInCurrentWorkflowTool(this.workflowActionService) + ); + const listLinksTool = toolWithTimeout(CurrentWorkflowTools.createListCurrentLinksTool(this.workflowActionService)); + const listAllOperatorTypesTool = toolWithTimeout( + MetadataTools.createListAllOperatorTypesTool(this.workflowUtilService) ); - const listLinksTool = toolWithTimeout(createListLinksInCurrentWorkflowTool(this.workflowActionService)); - const listAllOperatorTypesTool = toolWithTimeout(createListAllOperatorTypesTool(this.workflowUtilService)); const getOperatorTool = toolWithTimeout( - createGetOperatorInCurrentWorkflowTool(this.workflowActionService, this.workflowCompilingService) + CurrentWorkflowTools.createGetCurrentOperatorTool(this.workflowActionService, this.workflowCompilingService) ); const getOperatorPropertiesSchemaTool = toolWithTimeout( - createGetOperatorPropertiesSchemaTool(this.operatorMetadataService) + MetadataTools.createGetOperatorPropertiesSchemaTool(this.operatorMetadataService) + ); + const getOperatorPortsInfoTool = toolWithTimeout( + MetadataTools.createGetOperatorPortsInfoTool(this.operatorMetadataService) + ); + const getOperatorMetadataTool = toolWithTimeout( + MetadataTools.createGetOperatorMetadataTool(this.operatorMetadataService) ); - const getOperatorPortsInfoTool = toolWithTimeout(createGetOperatorPortsInfoTool(this.operatorMetadataService)); - const getOperatorMetadataTool = toolWithTimeout(createGetOperatorMetadataTool(this.operatorMetadataService)); return { - listAllOperatorTypes: listAllOperatorTypesTool, - listOperatorsInCurrentWorkflow: listOperatorsInCurrentWorkflowTool, - listLinksInCurrentWorkflow: listLinksTool, - getOperatorInCurrentWorkflow: getOperatorTool, - getOperatorPropertiesSchema: getOperatorPropertiesSchemaTool, - getOperatorPortsInfo: getOperatorPortsInfoTool, - getOperatorMetadata: getOperatorMetadataTool, + [MetadataTools.TOOL_NAME_LIST_ALL_OPERATOR_TYPES]: listAllOperatorTypesTool, + [CurrentWorkflowTools.TOOL_NAME_LIST_OPERATORS_IN_CURRENT_WORKFLOW]: listOperatorsInCurrentWorkflowTool, + [CurrentWorkflowTools.TOOL_NAME_LIST_CURRENT_LINKS]: listLinksTool, + [CurrentWorkflowTools.TOOL_NAME_GET_CURRENT_OPERATOR]: getOperatorTool, + [MetadataTools.TOOL_NAME_GET_OPERATOR_PROPERTIES_SCHEMA]: getOperatorPropertiesSchemaTool, + [MetadataTools.TOOL_NAME_GET_OPERATOR_PORTS_INFO]: getOperatorPortsInfoTool, + [MetadataTools.TOOL_NAME_GET_OPERATOR_METADATA]: getOperatorMetadataTool, }; } diff --git a/frontend/src/app/workspace/service/copilot/tool/current-workflow-editing-observing-tools.ts b/frontend/src/app/workspace/service/copilot/tool/current-workflow-editing-observing-tools.ts new file mode 100644 index 00000000000..c979fefc6e4 --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/tool/current-workflow-editing-observing-tools.ts @@ -0,0 +1,123 @@ +/** + * 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 { z } from "zod"; +import { tool } from "ai"; +import { WorkflowActionService } from "../../workflow-graph/model/workflow-action.service"; +import { OperatorMetadataService } from "../../operator-metadata/operator-metadata.service"; +import { OperatorLink } from "../../../types/workflow-common.interface"; +import { WorkflowUtilService } from "../../workflow-graph/util/workflow-util.service"; +import { WorkflowCompilingService } from "../../compile-workflow/workflow-compiling.service"; +import { ValidationWorkflowService } from "../../validation/validation-workflow.service"; +import { createSuccessResult, createErrorResult } from "./tools-utility"; + +// Tool name constants +export const TOOL_NAME_LIST_OPERATORS_IN_CURRENT_WORKFLOW = "listOperatorsInCurrentWorkflow"; +export const TOOL_NAME_LIST_CURRENT_LINKS = "listCurrentLinks"; +export const TOOL_NAME_GET_CURRENT_OPERATOR = "getCurrentOperator"; + +/** + * Create listLinksInCurrentWorkflow tool for getting all links in the workflow + */ +export function createListCurrentLinksTool(workflowActionService: WorkflowActionService) { + return tool({ + name: TOOL_NAME_LIST_CURRENT_LINKS, + description: "Get all links in the current workflow", + inputSchema: z.object({}), + execute: async () => { + try { + const links = workflowActionService.getTexeraGraph().getAllLinks(); + return createSuccessResult( + { + links: links, + count: links.length, + }, + [], + [] + ); + } catch (error: any) { + return createErrorResult(error.message); + } + }, + }); +} + +export function createListOperatorsInCurrentWorkflowTool(workflowActionService: WorkflowActionService) { + return tool({ + name: TOOL_NAME_LIST_OPERATORS_IN_CURRENT_WORKFLOW, + description: "Get all operator IDs, types and custom names in the current workflow", + inputSchema: z.object({}), + execute: async () => { + try { + const operators = workflowActionService.getTexeraGraph().getAllOperators(); + return { + success: true, + operators: operators.map(op => ({ + operatorId: op.operatorID, + operatorType: op.operatorType, + customDisplayName: op.customDisplayName, + })), + count: operators.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} + +export function createGetCurrentOperatorTool( + workflowActionService: WorkflowActionService, + workflowCompilingService: WorkflowCompilingService +) { + return tool({ + name: TOOL_NAME_GET_CURRENT_OPERATOR, + description: + "Get detailed information about a specific operator in the current workflow, including its input and output schemas", + inputSchema: z.object({ + operatorId: z.string().describe("ID of the operator to retrieve"), + }), + execute: async (args: { operatorId: string }) => { + try { + const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); + + // Get input schema (empty map if not available) + const inputSchemaMap = workflowCompilingService.getOperatorInputSchemaMap(args.operatorId); + const inputSchema = inputSchemaMap || {}; + + // Get output schema (empty map if not available) + const outputSchemaMap = workflowCompilingService.getOperatorOutputSchemaMap(args.operatorId); + const outputSchema = outputSchemaMap || {}; + + return createSuccessResult( + { + operator: operator, + inputSchema: inputSchema, + outputSchema: outputSchema, + message: `Retrieved operator ${args.operatorId}`, + }, + [args.operatorId], + [] + ); + } catch (error: any) { + return createErrorResult(error.message || `Operator ${args.operatorId} not found`); + } + }, + }); +} diff --git a/frontend/src/app/workspace/service/copilot/tool/react-step-operator-parser.ts b/frontend/src/app/workspace/service/copilot/tool/react-step-operator-parser.ts new file mode 100644 index 00000000000..2477a21579d --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/tool/react-step-operator-parser.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. + */ + +/** + * Central parser module to extract operator access information from tool results. + * Tools should populate viewedOperatorIds and modifiedOperatorIds in their results. + */ + +/** + * Operator access information indicating which operators were viewed or modified. + * Tools should populate these fields in their results to indicate operator interaction. + */ +export interface ToolOperatorAccess { + viewedOperatorIds: string[]; + modifiedOperatorIds: string[]; +} + +/** + * Parse operator access from a tool call's result. + * Tools should populate viewedOperatorIds and modifiedOperatorIds in their results. + * + * @param toolCall - The tool call object containing toolName and args + * @param toolResult - The tool result object containing the output/result with operator IDs + * @returns ToolOperatorAccess object with viewedOperatorIds and modifiedOperatorIds + */ +export function parseOperatorAccessFromToolCall(toolCall: any, toolResult?: any): ToolOperatorAccess { + const access: ToolOperatorAccess = { viewedOperatorIds: [], modifiedOperatorIds: [] }; + + if (!toolResult || !toolResult.output) { + return access; + } + + try { + const output = toolResult.output; + + // Extract viewedOperatorIds from tool result + if (Array.isArray(output.viewedOperatorIds)) { + access.viewedOperatorIds = output.viewedOperatorIds.filter((id: any) => id && typeof id === "string"); + } + + // Extract modifiedOperatorIds from tool result + if (Array.isArray(output.modifiedOperatorIds)) { + access.modifiedOperatorIds = output.modifiedOperatorIds.filter((id: any) => id && typeof id === "string"); + } + + // Remove duplicates + access.viewedOperatorIds = [...new Set(access.viewedOperatorIds)]; + access.modifiedOperatorIds = [...new Set(access.modifiedOperatorIds)]; + } catch (error) { + console.error("Error parsing operator access from tool result:", error); + } + + return access; +} + +/** + * Parse operator access for all tool calls in a step. + * + * @param toolCalls - Array of tool call objects + * @param toolResults - Array of corresponding tool result objects + * @returns Map from tool call index to ToolOperatorAccess + */ +export function parseOperatorAccessFromStep(toolCalls: any[], toolResults?: any[]): Map { + const accessMap = new Map(); + + if (!toolCalls || toolCalls.length === 0) { + return accessMap; + } + + for (let i = 0; i < toolCalls.length; i++) { + const toolCall = toolCalls[i]; + const toolResult = toolResults && toolResults[i] ? toolResults[i] : undefined; + const access = parseOperatorAccessFromToolCall(toolCall, toolResult); + + // Only add to map if there are any viewed or modified operations + if (access.viewedOperatorIds.length > 0 || access.modifiedOperatorIds.length > 0) { + accessMap.set(i, access); + } + } + + return accessMap; +} + +/** + * Extract all viewed operator IDs from a ReActStep. + * + * @param step - The ReActStep to extract from + * @returns Array of unique operator IDs that were viewed + */ +export function getAllViewedOperatorIds(step: { operatorAccess?: Map }): string[] { + if (!step.operatorAccess) { + return []; + } + + const allViewedIds: string[] = []; + for (const access of step.operatorAccess.values()) { + allViewedIds.push(...access.viewedOperatorIds); + } + + // Return unique operator IDs + return [...new Set(allViewedIds)]; +} + +/** + * Extract all modified operator IDs from a ReActStep. + * + * @param step - The ReActStep to extract from + * @returns Array of unique operator IDs that were modified + */ +export function getAllModifiedOperatorIds(step: { operatorAccess?: Map }): string[] { + if (!step.operatorAccess) { + return []; + } + + const allModifiedIds: string[] = []; + for (const access of step.operatorAccess.values()) { + allModifiedIds.push(...access.modifiedOperatorIds); + } + + // Return unique operator IDs + return [...new Set(allModifiedIds)]; +} + +/** + * Extract all operator IDs (both viewed and modified) from a ReActStep. + * + * @param step - The ReActStep to extract from + * @returns Array of unique operator IDs involved in this step + */ +export function getAllOperatorIds(step: { operatorAccess?: Map }): string[] { + const viewedIds = getAllViewedOperatorIds(step); + const modifiedIds = getAllModifiedOperatorIds(step); + + // Combine and return unique IDs + return [...new Set([...viewedIds, ...modifiedIds])]; +} diff --git a/frontend/src/app/workspace/service/copilot/tool/tools-utility.ts b/frontend/src/app/workspace/service/copilot/tool/tools-utility.ts new file mode 100644 index 00000000000..f098bd1d91c --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/tool/tools-utility.ts @@ -0,0 +1,138 @@ +/** + * 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. + */ + +// Tool execution timeout in milliseconds (2 minutes) +export const TOOL_TIMEOUT_MS = 120000; + +// Maximum token limit for operator result data to prevent overwhelming LLM context +// Estimated as characters / 4 (common approximation for token counting) +export const MAX_OPERATOR_RESULT_TOKEN_LIMIT = 1000; + +/** + * Base interface for all tool execution results. + * Ensures consistent structure across all tools with required tracking fields. + */ +export interface BaseToolResult { + /** + * Indicates whether the tool execution was successful. + */ + success: boolean; + + /** + * Error message if the tool execution failed. + */ + error?: string; + + /** + * List of operator IDs that were viewed/read during tool execution. + * Empty array if no operators were viewed. + */ + viewedOperatorIds: string[]; + + /** + * List of operator IDs that were modified/written during tool execution. + * Empty array if no operators were modified. + */ + modifiedOperatorIds: string[]; +} + +/** + * Creates a successful tool result with default values for required fields. + * Tools can extend this with additional custom fields. + * + * @param data - Custom data fields for the tool result + * @param viewedOperatorIds - Operator IDs that were viewed (default: []) + * @param modifiedOperatorIds - Operator IDs that were modified (default: []) + * @returns BaseToolResult with success=true and provided data + */ +export function createSuccessResult>( + data: T, + viewedOperatorIds: string[] = [], + modifiedOperatorIds: string[] = [] +): BaseToolResult & T { + return { + success: true, + viewedOperatorIds, + modifiedOperatorIds, + ...data, + }; +} + +/** + * Creates a failed tool result with an error message. + * + * @param error - Error message describing the failure + * @returns BaseToolResult with success=false and error message + */ +export function createErrorResult(error: string): BaseToolResult { + return { + success: false, + error, + viewedOperatorIds: [], + modifiedOperatorIds: [], + }; +} + +/** + * Estimates the number of tokens in a JSON-serializable object + * Uses a common approximation: tokens ≈ characters / 4 + */ +export function estimateTokenCount(data: any): number { + try { + const jsonString = JSON.stringify(data); + return Math.ceil(jsonString.length / 4); + } catch (error) { + // Fallback if JSON.stringify fails + return 0; + } +} + +/** + * Wraps a tool definition to add timeout protection to its execute function + * Uses AbortController to properly cancel operations on timeout + */ +export function toolWithTimeout(toolConfig: any): any { + const originalExecute = toolConfig.execute; + + return { + ...toolConfig, + execute: async (args: any) => { + const abortController = new AbortController(); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + abortController.abort(); + reject(new Error("timeout")); + }, TOOL_TIMEOUT_MS); + }); + + try { + const argsWithSignal = { ...args, signal: abortController.signal }; + return await Promise.race([originalExecute(argsWithSignal), timeoutPromise]); + } catch (error: any) { + if (error.message === "timeout") { + return createErrorResult( + "Tool execution timeout - operation took longer than 2 minutes. Please try again later." + ); + } + throw error; + } + }, + }; +} diff --git a/frontend/src/app/workspace/service/copilot/tool/workflow-metadata-tools.ts b/frontend/src/app/workspace/service/copilot/tool/workflow-metadata-tools.ts new file mode 100644 index 00000000000..eca6cb76afc --- /dev/null +++ b/frontend/src/app/workspace/service/copilot/tool/workflow-metadata-tools.ts @@ -0,0 +1,140 @@ +/** + * 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 { z } from "zod"; +import { tool } from "ai"; +import { OperatorMetadataService } from "../../operator-metadata/operator-metadata.service"; +import { WorkflowUtilService } from "../../workflow-graph/util/workflow-util.service"; + +// Tool name constants +export const TOOL_NAME_LIST_ALL_OPERATOR_TYPES = "listAllOperatorTypes"; +export const TOOL_NAME_GET_OPERATOR_PROPERTIES_SCHEMA = "getOperatorPropertiesSchema"; +export const TOOL_NAME_GET_OPERATOR_PORTS_INFO = "getOperatorPortsInfo"; +export const TOOL_NAME_GET_OPERATOR_METADATA = "getOperatorMetadata"; + +export function createListAllOperatorTypesTool(workflowUtilService: WorkflowUtilService) { + return tool({ + name: TOOL_NAME_LIST_ALL_OPERATOR_TYPES, + description: "Get all available operator types in the system", + inputSchema: z.object({}), + execute: async () => { + try { + const operatorTypes = workflowUtilService.getOperatorTypeList(); + return { + success: true, + operatorTypes: operatorTypes, + count: operatorTypes.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} + +/** + * Create getOperatorPropertiesSchema tool for getting just the properties schema + * More token-efficient than getOperatorSchema for property-focused queries + */ +export function createGetOperatorPropertiesSchemaTool(operatorMetadataService: OperatorMetadataService) { + return tool({ + name: TOOL_NAME_GET_OPERATOR_PROPERTIES_SCHEMA, + description: "Get only the properties schema for an operator type. Use this before setting operator properties.", + inputSchema: z.object({ + operatorType: z.string().describe("Type of the operator to get properties schema for"), + }), + execute: async (args: { operatorType: string }) => { + try { + const schema = operatorMetadataService.getOperatorSchema(args.operatorType); + const propertiesSchema = { + properties: schema.jsonSchema.properties, + required: schema.jsonSchema.required, + definitions: schema.jsonSchema.definitions, + }; + + return { + success: true, + propertiesSchema: propertiesSchema, + operatorType: args.operatorType, + message: `Retrieved properties schema for operator type ${args.operatorType}`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} + +export function createGetOperatorPortsInfoTool(operatorMetadataService: OperatorMetadataService) { + return tool({ + name: TOOL_NAME_GET_OPERATOR_PORTS_INFO, + description: + "Get input and output port information for an operator type. This is more token-efficient than getOperatorSchema and returns only port details (display names, multi-input support, etc.).", + inputSchema: z.object({ + operatorType: z.string().describe("Type of the operator to get port information for"), + }), + execute: async (args: { operatorType: string }) => { + try { + const schema = operatorMetadataService.getOperatorSchema(args.operatorType); + const portsInfo = { + inputPorts: schema.additionalMetadata.inputPorts, + outputPorts: schema.additionalMetadata.outputPorts, + dynamicInputPorts: schema.additionalMetadata.dynamicInputPorts, + dynamicOutputPorts: schema.additionalMetadata.dynamicOutputPorts, + }; + + return { + success: true, + portsInfo: portsInfo, + operatorType: args.operatorType, + message: `Retrieved port information for operator type ${args.operatorType}`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} + +export function createGetOperatorMetadataTool(operatorMetadataService: OperatorMetadataService) { + return tool({ + name: TOOL_NAME_GET_OPERATOR_METADATA, + description: + "Get semantic metadata for an operator type, including user-friendly name, description, operator group, and capabilities. This is very useful to understand the semantics and purpose of each operator type - what it does, how it works, and what kind of data transformation it performs.", + inputSchema: z.object({ + operatorType: z.string().describe("Type of the operator to get metadata for"), + }), + execute: async (args: { operatorType: string; signal?: AbortSignal }) => { + try { + const schema = operatorMetadataService.getOperatorSchema(args.operatorType); + + const metadata = schema.additionalMetadata; + return { + success: true, + metadata: metadata, + operatorType: args.operatorType, + operatorVersion: schema.operatorVersion, + message: `Retrieved metadata for operator type ${args.operatorType}`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }); +} diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts deleted file mode 100644 index d3bc4366d87..00000000000 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.spec.ts +++ /dev/null @@ -1,260 +0,0 @@ -/** - * 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 { TestBed } from "@angular/core/testing"; -import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; -import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; -import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; -import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; -import { - createListOperatorsInCurrentWorkflowTool, - createListLinksInCurrentWorkflowTool, - createListAllOperatorTypesTool, - createGetOperatorInCurrentWorkflowTool, - createGetOperatorPropertiesSchemaTool, - createGetOperatorPortsInfoTool, - createGetOperatorMetadataTool, - toolWithTimeout, -} from "./workflow-tools"; - -describe("Workflow Tools", () => { - let mockWorkflowActionService: jasmine.SpyObj; - let mockWorkflowUtilService: jasmine.SpyObj; - let mockOperatorMetadataService: jasmine.SpyObj; - let mockWorkflowCompilingService: jasmine.SpyObj; - - beforeEach(() => { - mockWorkflowActionService = jasmine.createSpyObj("WorkflowActionService", ["getTexeraGraph"]); - mockWorkflowUtilService = jasmine.createSpyObj("WorkflowUtilService", ["getOperatorTypeList"]); - mockOperatorMetadataService = jasmine.createSpyObj("OperatorMetadataService", ["getOperatorSchema"]); - mockWorkflowCompilingService = jasmine.createSpyObj("WorkflowCompilingService", [ - "getOperatorInputSchemaMap", - "getOperatorOutputSchemaMap", - ]); - }); - - describe("listOperatorsInCurrentWorkflowTool", () => { - it("should return list of operators with IDs, types, and custom names", async () => { - const mockOperators = [ - { operatorID: "op1", operatorType: "ScanSource", customDisplayName: "Scan 1" }, - { operatorID: "op2", operatorType: "Filter", customDisplayName: "Filter 1" }, - ]; - - const mockGraph = { - getAllOperators: () => mockOperators, - }; - mockWorkflowActionService.getTexeraGraph.and.returnValue(mockGraph as any); - - const tool = createListOperatorsInCurrentWorkflowTool(mockWorkflowActionService); - const result = await (tool as any).execute({}, {}); - - expect((result as any).success).toBe(true); - expect((result as any).count).toBe(2); - expect((result as any).operators).toEqual([ - { operatorId: "op1", operatorType: "ScanSource", customDisplayName: "Scan 1" }, - { operatorId: "op2", operatorType: "Filter", customDisplayName: "Filter 1" }, - ]); - }); - - it("should handle errors gracefully", async () => { - mockWorkflowActionService.getTexeraGraph.and.throwError("Graph error"); - - const tool = createListOperatorsInCurrentWorkflowTool(mockWorkflowActionService); - const result = await (tool as any).execute({}, {}); - - expect((result as any).success).toBe(false); - expect((result as any).error).toContain("Graph error"); - }); - }); - - describe("listLinksInCurrentWorkflowTool", () => { - it("should return list of links", async () => { - const mockLinks = [ - { linkID: "link1", source: { operatorID: "op1" }, target: { operatorID: "op2" } }, - { linkID: "link2", source: { operatorID: "op2" }, target: { operatorID: "op3" } }, - ]; - - const mockGraph = { - getAllLinks: () => mockLinks, - }; - mockWorkflowActionService.getTexeraGraph.and.returnValue(mockGraph as any); - - const tool = createListLinksInCurrentWorkflowTool(mockWorkflowActionService); - const result = await (tool as any).execute({}, {}); - - expect((result as any).success).toBe(true); - expect((result as any).count).toBe(2); - expect((result as any).links).toEqual(mockLinks); - }); - }); - - describe("listAllOperatorTypesTool", () => { - it("should return list of all operator types", async () => { - const mockTypes = ["ScanSource", "Filter", "Join", "Aggregate"]; - mockWorkflowUtilService.getOperatorTypeList.and.returnValue(mockTypes); - - const tool = createListAllOperatorTypesTool(mockWorkflowUtilService); - const result = await (tool as any).execute({}, {}); - - expect((result as any).success).toBe(true); - expect((result as any).count).toBe(4); - expect((result as any).operatorTypes).toEqual(mockTypes); - }); - }); - - describe("getOperatorInCurrentWorkflowTool", () => { - it("should return operator details with input and output schemas", async () => { - const mockOperator = { operatorID: "op1", operatorType: "ScanSource" }; - const mockInputSchema = { - port1: [{ attributeName: "field1", attributeType: "string" }], - }; - const mockOutputSchema = { - port1: [{ attributeName: "field1", attributeType: "string" }], - }; - - const mockGraph = { - getOperator: (id: string) => mockOperator, - }; - mockWorkflowActionService.getTexeraGraph.and.returnValue(mockGraph as any); - mockWorkflowCompilingService.getOperatorInputSchemaMap.and.returnValue(mockInputSchema as any); - mockWorkflowCompilingService.getOperatorOutputSchemaMap.and.returnValue(mockOutputSchema as any); - - const tool = createGetOperatorInCurrentWorkflowTool(mockWorkflowActionService, mockWorkflowCompilingService); - const result = await (tool as any).execute({ operatorId: "op1" }, {}); - - expect((result as any).success).toBe(true); - expect((result as any).operator).toEqual(mockOperator); - expect((result as any).inputSchema).toEqual(mockInputSchema); - expect((result as any).outputSchema).toEqual(mockOutputSchema); - }); - - it("should return empty schemas when not available", async () => { - const mockOperator = { operatorID: "op1", operatorType: "ScanSource" }; - - const mockGraph = { - getOperator: (id: string) => mockOperator, - }; - mockWorkflowActionService.getTexeraGraph.and.returnValue(mockGraph as any); - mockWorkflowCompilingService.getOperatorInputSchemaMap.and.returnValue(undefined); - mockWorkflowCompilingService.getOperatorOutputSchemaMap.and.returnValue(undefined); - - const tool = createGetOperatorInCurrentWorkflowTool(mockWorkflowActionService, mockWorkflowCompilingService); - const result = await (tool as any).execute({ operatorId: "op1" }, {}); - - expect((result as any).success).toBe(true); - expect((result as any).inputSchema).toEqual({}); - expect((result as any).outputSchema).toEqual({}); - }); - }); - - describe("getOperatorPropertiesSchemaTool", () => { - it("should return properties schema for operator type", async () => { - const mockSchema = { - jsonSchema: { - properties: { prop1: { type: "string" } }, - required: ["prop1"], - definitions: {}, - }, - }; - mockOperatorMetadataService.getOperatorSchema.and.returnValue(mockSchema as any); - - const tool = createGetOperatorPropertiesSchemaTool(mockOperatorMetadataService); - const result = await (tool as any).execute({ operatorType: "ScanSource" }, {}); - - expect((result as any).success).toBe(true); - expect((result as any).operatorType).toBe("ScanSource"); - expect((result as any).propertiesSchema).toEqual({ - properties: { prop1: { type: "string" } }, - required: ["prop1"], - definitions: {}, - }); - }); - }); - - describe("getOperatorPortsInfoTool", () => { - it("should return port information for operator type", async () => { - const mockSchema = { - additionalMetadata: { - inputPorts: [{ portID: "input-0" }], - outputPorts: [{ portID: "output-0" }], - dynamicInputPorts: false, - dynamicOutputPorts: false, - }, - }; - mockOperatorMetadataService.getOperatorSchema.and.returnValue(mockSchema as any); - - const tool = createGetOperatorPortsInfoTool(mockOperatorMetadataService); - const result = await (tool as any).execute({ operatorType: "Filter" }, {}); - - expect((result as any).success).toBe(true); - expect((result as any).portsInfo).toEqual(mockSchema.additionalMetadata); - }); - }); - - describe("getOperatorMetadataTool", () => { - it("should return metadata for operator type", async () => { - const mockSchema = { - additionalMetadata: { - userFriendlyName: "Scan Source", - operatorDescription: "Reads data from a source", - operatorGroupName: "Source", - }, - operatorVersion: "1.0", - }; - mockOperatorMetadataService.getOperatorSchema.and.returnValue(mockSchema as any); - - const tool = createGetOperatorMetadataTool(mockOperatorMetadataService); - const result = await (tool as any).execute({ operatorType: "ScanSource" }, {}); - - expect((result as any).success).toBe(true); - expect((result as any).metadata).toEqual(mockSchema.additionalMetadata); - expect((result as any).operatorVersion).toBe("1.0"); - }); - }); - - describe("toolWithTimeout", () => { - it("should execute tool successfully within timeout", async () => { - const mockTool = { - execute: jasmine.createSpy("execute").and.returnValue(Promise.resolve({ success: true, data: "result" })), - }; - - const wrappedTool = toolWithTimeout(mockTool); - const result = await (wrappedTool as any).execute({ param: "value" }); - - expect((result as any).success).toBe(true); - expect((result as any).data).toBe("result"); - expect(mockTool.execute).toHaveBeenCalled(); - }); - - it("should propagate non-timeout errors", async () => { - const mockTool = { - execute: jasmine.createSpy("execute").and.returnValue(Promise.reject(new Error("Custom error"))), - }; - - const wrappedTool = toolWithTimeout(mockTool); - - try { - await (wrappedTool as any).execute({}); - fail("Should have thrown an error"); - } catch (error: any) { - expect(error.message).toBe("Custom error"); - } - }); - }); -}); diff --git a/frontend/src/app/workspace/service/copilot/workflow-tools.ts b/frontend/src/app/workspace/service/copilot/workflow-tools.ts deleted file mode 100644 index 6532c822e2a..00000000000 --- a/frontend/src/app/workspace/service/copilot/workflow-tools.ts +++ /dev/null @@ -1,214 +0,0 @@ -/** - * 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 { z } from "zod"; -import { tool } from "ai"; -import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; -import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; -import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; -import { WorkflowCompilingService } from "../compile-workflow/workflow-compiling.service"; - -const TIMEOUT_MS = 120000; - -export function toolWithTimeout(toolConfig: any): any { - const originalExecute = toolConfig.execute; - return { - ...toolConfig, - execute: async (args: any) => { - const controller = new AbortController(); - const timeout = new Promise((_, reject) => { - setTimeout(() => { - controller.abort(); - reject(new Error("timeout")); - }, TIMEOUT_MS); - }); - - try { - return await Promise.race([originalExecute({ ...args, signal: controller.signal }), timeout]); - } catch (error: any) { - if (error.message === "timeout") { - return { success: false, error: "Tool execution timeout - exceeded 2 minutes" }; - } - throw error; - } - }, - }; -} - -export function createListOperatorsInCurrentWorkflowTool(workflowActionService: WorkflowActionService) { - return tool({ - name: "listOperatorsInCurrentWorkflow", - description: "Get all operator IDs, types and custom names in the current workflow", - inputSchema: z.object({}), - execute: async () => { - try { - const operators = workflowActionService.getTexeraGraph().getAllOperators(); - return { - success: true, - operators: operators.map(op => ({ - operatorId: op.operatorID, - operatorType: op.operatorType, - customDisplayName: op.customDisplayName, - })), - count: operators.length, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -export function createListLinksInCurrentWorkflowTool(workflowActionService: WorkflowActionService) { - return tool({ - name: "listLinksInCurrentWorkflow", - description: "Get all links in the current workflow", - inputSchema: z.object({}), - execute: async () => { - try { - const links = workflowActionService.getTexeraGraph().getAllLinks(); - return { success: true, links, count: links.length }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -export function createListAllOperatorTypesTool(workflowUtilService: WorkflowUtilService) { - return tool({ - name: "listAllOperatorTypes", - description: "Get all available operator types in the system", - inputSchema: z.object({}), - execute: async () => { - try { - const operatorTypes = workflowUtilService.getOperatorTypeList(); - return { success: true, operatorTypes, count: operatorTypes.length }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -export function createGetOperatorInCurrentWorkflowTool( - workflowActionService: WorkflowActionService, - workflowCompilingService: WorkflowCompilingService -) { - return tool({ - name: "getOperatorInCurrentWorkflow", - description: - "Get detailed information about a specific operator in the current workflow, including input/output schemas", - inputSchema: z.object({ - operatorId: z.string().describe("ID of the operator to retrieve"), - }), - execute: async (args: { operatorId: string }) => { - try { - const operator = workflowActionService.getTexeraGraph().getOperator(args.operatorId); - const inputSchema = workflowCompilingService.getOperatorInputSchemaMap(args.operatorId) || {}; - const outputSchema = workflowCompilingService.getOperatorOutputSchemaMap(args.operatorId) || {}; - - return { - success: true, - operator, - inputSchema, - outputSchema, - }; - } catch (error: any) { - return { success: false, error: error.message || `Operator ${args.operatorId} not found` }; - } - }, - }); -} - -export function createGetOperatorPropertiesSchemaTool(operatorMetadataService: OperatorMetadataService) { - return tool({ - name: "getOperatorPropertiesSchema", - description: "Get properties schema for an operator type. Use before setting operator properties", - inputSchema: z.object({ - operatorType: z.string().describe("Operator type"), - }), - execute: async (args: { operatorType: string }) => { - try { - const schema = operatorMetadataService.getOperatorSchema(args.operatorType); - return { - success: true, - propertiesSchema: { - properties: schema.jsonSchema.properties, - required: schema.jsonSchema.required, - definitions: schema.jsonSchema.definitions, - }, - operatorType: args.operatorType, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -export function createGetOperatorPortsInfoTool(operatorMetadataService: OperatorMetadataService) { - return tool({ - name: "getOperatorPortsInfo", - description: "Get input/output port information for an operator type", - inputSchema: z.object({ - operatorType: z.string().describe("Operator type"), - }), - execute: async (args: { operatorType: string }) => { - try { - const schema = operatorMetadataService.getOperatorSchema(args.operatorType); - return { - success: true, - portsInfo: { - inputPorts: schema.additionalMetadata.inputPorts, - outputPorts: schema.additionalMetadata.outputPorts, - dynamicInputPorts: schema.additionalMetadata.dynamicInputPorts, - dynamicOutputPorts: schema.additionalMetadata.dynamicOutputPorts, - }, - operatorType: args.operatorType, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} - -export function createGetOperatorMetadataTool(operatorMetadataService: OperatorMetadataService) { - return tool({ - name: "getOperatorMetadata", - description: "Get semantic metadata for an operator type (name, description, group, capabilities)", - inputSchema: z.object({ - operatorType: z.string().describe("Operator type"), - }), - execute: async (args: { operatorType: string }) => { - try { - const schema = operatorMetadataService.getOperatorSchema(args.operatorType); - return { - success: true, - metadata: schema.additionalMetadata, - operatorType: args.operatorType, - operatorVersion: schema.operatorVersion, - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }, - }); -} From 4741e716f0b00c4fd76f2ed805e25c5fd5f029bb Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 15 Nov 2025 00:48:43 -0800 Subject: [PATCH 142/158] refactor react step with mapping --- .../agent-chat/agent-chat.component.html | 24 +++++----- .../agent-chat/agent-chat.component.ts | 30 +++++++++---- .../copilot/texera-copilot-manager.service.ts | 4 +- .../service/copilot/texera-copilot.ts | 45 ++++++++++++++----- 4 files changed, 69 insertions(+), 34 deletions(-) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html index 8a52b28f578..718cf30f568 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html @@ -79,19 +79,19 @@ #messageContainer class="messages-container">
    + [class.user-message]="entry.value.role === 'user'" + [class.ai-message]="entry.value.role === 'agent'">
    + *ngIf="entry.value.role === 'user' || entry.value.isBegin"> - {{ response.role === 'user' ? 'You' : agentInfo.name }} + {{ entry.value.role === 'user' ? 'You' : agentInfo.name }}
    @@ -101,23 +101,23 @@ (mouseleave)="setHoveredMessage(null)" style="position: relative">
    - +
    - Execute {{ response.toolCalls.length }} tool{{ response.toolCalls.length > 1 ? 's' : '' }} + Execute {{ entry.value.toolCalls.length }} tool{{ entry.value.toolCalls.length > 1 ? 's' : '' }}
    +
    +
    + Operator Access: +
    +
    +
    + + + VIEWED: + + + {{ opId }} + +
    +
    + + + MODIFIED: + + + {{ opId }} + +
    +
    +
    diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts index e7d363d50d8..66160416124 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -121,6 +121,27 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { return toolResult.output || toolResult.result || toolResult; } + public getReActStepOperatorAccess( + response: ReActStep, + toolCallIndex: number + ): { viewedOperatorIds: string[]; modifiedOperatorIds: string[] } | null { + if (!response.toolResults || toolCallIndex >= response.toolResults.length) { + return null; + } + const toolResult = response.toolResults[toolCallIndex]; + const result = toolResult.output || toolResult.result || toolResult; + + // Check if the result has operator access information + if (result && (result.viewedOperatorIds || result.modifiedOperatorIds)) { + return { + viewedOperatorIds: result.viewedOperatorIds || [], + modifiedOperatorIds: result.modifiedOperatorIds || [], + }; + } + + return null; + } + public getTotalInputTokens(): number { // Iterate in reverse to find the most recent usage (already sorted by timestamp) for (let i = this.responses.length - 1; i >= 0; i--) { diff --git a/frontend/src/app/workspace/service/copilot/tool/current-workflow-editing-observing-tools.ts b/frontend/src/app/workspace/service/copilot/tool/current-workflow-editing-observing-tools.ts index c979fefc6e4..c67a7b1707c 100644 --- a/frontend/src/app/workspace/service/copilot/tool/current-workflow-editing-observing-tools.ts +++ b/frontend/src/app/workspace/service/copilot/tool/current-workflow-editing-observing-tools.ts @@ -66,17 +66,21 @@ export function createListOperatorsInCurrentWorkflowTool(workflowActionService: execute: async () => { try { const operators = workflowActionService.getTexeraGraph().getAllOperators(); - return { - success: true, - operators: operators.map(op => ({ - operatorId: op.operatorID, - operatorType: op.operatorType, - customDisplayName: op.customDisplayName, - })), - count: operators.length, - }; + const operatorIds = operators.map(op => op.operatorID); + return createSuccessResult( + { + operators: operators.map(op => ({ + operatorId: op.operatorID, + operatorType: op.operatorType, + customDisplayName: op.customDisplayName, + })), + count: operators.length, + }, + operatorIds, + [] + ); } catch (error: any) { - return { success: false, error: error.message }; + return createErrorResult(error.message); } }, }); From 95a0a8f1d40ea208d1bee1b6c70375cef3b4f867 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Sat, 15 Nov 2025 22:37:33 -0800 Subject: [PATCH 145/158] improve the prompt --- .../app/workspace/service/copilot/texera-copilot.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/workspace/service/copilot/texera-copilot.ts b/frontend/src/app/workspace/service/copilot/texera-copilot.ts index 80ffca11f1d..cb7cc91e111 100644 --- a/frontend/src/app/workspace/service/copilot/texera-copilot.ts +++ b/frontend/src/app/workspace/service/copilot/texera-copilot.ts @@ -41,6 +41,10 @@ export enum CopilotState { STOPPING = "Stopping", } +/** + * Represents a single step in the ReAct (Reasoning and Acting) conversation flow. + * Each step can be either a user message or an agent response with potential tool calls. + */ export interface ReActStep { messageId: string; stepId: number; @@ -58,8 +62,7 @@ export interface ReActStep { cachedInputTokens?: number; }; /** - * Map from tool call index to operator access information. - * Tracks which operators were viewed or modified during each tool call. + * Map from tool call index to operator access information, which tracks operators were viewed or modified during the tool call. */ operatorAccess?: Map; } @@ -102,8 +105,7 @@ export class TexeraCopilot { private messages: ModelMessage[] = []; /** - * UI-friendly representation of agent responses in ReAct (Reasoning + Acting) format. - * Includes additional metadata like toolCalls, toolResults, and token usage. + * Representing a step in ReAct (Reasoning + Acting). * This is what gets displayed in the UI to show the agent's reasoning process. * Each step contains messageId (randomly generated UUID) and stepId (incremental from 0). */ From 0f0bd0789d8e7cfd638a4fe3a1606e6ba7290940 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 18:57:10 +0000 Subject: [PATCH 146/158] chore: clean up code per PR review feedback - Remove unused isStopping() method from agent-chat.component.ts - Revert accidental formatting changes in context-menu.component.html --- .../agent-panel/agent-chat/agent-chat.component.ts | 4 ---- .../context-menu/context-menu/context-menu.component.html | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts index 66160416124..eef18246ea1 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.ts @@ -243,10 +243,6 @@ export class AgentChatComponent implements OnInit, AfterViewChecked { return this.agentState === CopilotState.GENERATING; } - public isStopping(): boolean { - return this.agentState === CopilotState.STOPPING; - } - public isAvailable(): boolean { return this.agentState === CopilotState.AVAILABLE; } diff --git a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html index 0527010fd7a..8bbff3d3c65 100644 --- a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html @@ -140,9 +140,9 @@
  • Date: Fri, 21 Nov 2025 18:57:21 +0000 Subject: [PATCH 147/158] fix: revert user-select change that breaks drag-and-drop Reverts the global user-select style from 'text' back to 'none' to restore proper drag-and-drop functionality for operators in the operator menu. The 'user-select: text' change was making operators selectable, which interfered with the drag-and-drop interaction. --- frontend/src/styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index b3d49e9d330..7ed7614d177 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -20,7 +20,7 @@ @import "@ali-hm/angular-tree-component/css/angular-tree-component.css"; * { - user-select: text; + user-select: none; } .ant-image-preview-img { From b772748868d4770b2d7e45a3b54bf05f692fe2e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 18:57:35 +0000 Subject: [PATCH 148/158] feat: add configuration flag for copilot feature Adds a new 'copilotEnabled' configuration flag that allows administrators to enable/disable the AI copilot feature. The feature is disabled by default and can be controlled via the GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED environment variable. Changes: - Add copilot-enabled flag to gui.conf (default: false) - Add parsing logic in GuiConfig.scala - Add flag to ConfigResource API response - Add copilotEnabled to frontend GuiConfig type - Add default value in mock config service - Conditionally render agent-panel based on flag in workspace This addresses the PR feedback requesting a configuration flag that admins can use to turn the feature on/off dynamically. --- common/config/src/main/resources/gui.conf | 4 ++++ .../src/main/scala/org/apache/texera/config/GuiConfig.scala | 2 ++ .../org/apache/texera/service/resource/ConfigResource.scala | 1 + frontend/src/app/common/service/gui-config.service.mock.ts | 1 + frontend/src/app/common/type/gui-config.ts | 1 + frontend/src/app/workspace/component/workspace.component.html | 2 +- 6 files changed, 10 insertions(+), 1 deletion(-) diff --git a/common/config/src/main/resources/gui.conf b/common/config/src/main/resources/gui.conf index 601c63e92e8..f73cba82c3e 100644 --- a/common/config/src/main/resources/gui.conf +++ b/common/config/src/main/resources/gui.conf @@ -104,5 +104,9 @@ gui { # amount of time to be elapsed in minutes before user is detected as inactive active-time-in-minutes = 15 active-time-in-minutes = ${?GUI_WORKFLOW_WORKSPACE_ACTIVE_TIME_IN_MINUTES} + + # whether AI copilot feature is enabled + copilot-enabled = false + copilot-enabled = ${?GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED} } } \ No newline at end of file diff --git a/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala b/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala index 170ff5e90e5..5e16529bb6f 100644 --- a/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala +++ b/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala @@ -67,4 +67,6 @@ object GuiConfig { conf.getBoolean("gui.workflow-workspace.workflow-email-notification-enabled") val guiWorkflowWorkspaceActiveTimeInMinutes: Int = conf.getInt("gui.workflow-workspace.active-time-in-minutes") + val guiWorkflowWorkspaceCopilotEnabled: Boolean = + conf.getBoolean("gui.workflow-workspace.copilot-enabled") } diff --git a/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala b/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala index 8a74ea8d356..aa907ec3030 100644 --- a/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala +++ b/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala @@ -55,6 +55,7 @@ class ConfigResource { "password" -> GuiConfig.guiLoginDefaultLocalUserPassword ), "activeTimeInMinutes" -> GuiConfig.guiWorkflowWorkspaceActiveTimeInMinutes, + "copilotEnabled" -> GuiConfig.guiWorkflowWorkspaceCopilotEnabled, // flags from the auth.conf if needed "expirationTimeInMinutes" -> AuthConfig.jwtExpirationMinutes ) diff --git a/frontend/src/app/common/service/gui-config.service.mock.ts b/frontend/src/app/common/service/gui-config.service.mock.ts index 392f8447eec..610169a7862 100644 --- a/frontend/src/app/common/service/gui-config.service.mock.ts +++ b/frontend/src/app/common/service/gui-config.service.mock.ts @@ -48,6 +48,7 @@ export class MockGuiConfigService { defaultLocalUser: { username: "", password: "" }, expirationTimeInMinutes: 2880, activeTimeInMinutes: 15, + copilotEnabled: false, }; get env(): GuiConfig { diff --git a/frontend/src/app/common/type/gui-config.ts b/frontend/src/app/common/type/gui-config.ts index c634ebb6fe0..d9b4ad279ad 100644 --- a/frontend/src/app/common/type/gui-config.ts +++ b/frontend/src/app/common/type/gui-config.ts @@ -39,6 +39,7 @@ export interface GuiConfig { defaultLocalUser?: { username?: string; password?: string }; expirationTimeInMinutes: number; activeTimeInMinutes: number; + copilotEnabled: boolean; } export interface SidebarTabs { diff --git a/frontend/src/app/workspace/component/workspace.component.html b/frontend/src/app/workspace/component/workspace.component.html index 20d6f18a3a8..a28d0ba3660 100644 --- a/frontend/src/app/workspace/component/workspace.component.html +++ b/frontend/src/app/workspace/component/workspace.component.html @@ -33,6 +33,6 @@ - + From 600fd6e1866551403c44ddf8fe15329b9db97b80 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 25 Nov 2025 17:47:51 -0800 Subject: [PATCH 149/158] block agent traffic at the backend --- .../service/resource/AccessControlResource.scala | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala b/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala index ccea926f48d..1f2e2bc11f9 100644 --- a/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala +++ b/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala @@ -26,7 +26,7 @@ import jakarta.ws.rs.{Consumes, GET, POST, Path, Produces} import org.apache.texera.auth.JwtParser.parseToken import org.apache.texera.auth.SessionUser import org.apache.texera.auth.util.{ComputingUnitAccess, HeaderField} -import org.apache.texera.config.LLMConfig +import org.apache.texera.config.{GuiConfig, LLMConfig} import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum import java.net.URLDecoder @@ -222,6 +222,13 @@ class LiteLLMProxyResource extends LazyLogging { @Context headers: HttpHeaders, body: String ): Response = { + if (!GuiConfig.guiWorkflowWorkspaceCopilotEnabled) { + return Response + .status(Response.Status.FORBIDDEN) + .entity("""{"error": "Copilot feature is disabled"}""") + .build() + } + // uriInfo.getPath returns "chat/completions" for /api/chat/completions // We want to forward as "/chat/completions" to LiteLLM val fullPath = uriInfo.getPath @@ -281,6 +288,13 @@ class LiteLLMModelsResource extends LazyLogging { @GET def getModels: Response = { + if (!GuiConfig.guiWorkflowWorkspaceCopilotEnabled) { + return Response + .status(Response.Status.FORBIDDEN) + .entity("""{"error": "Copilot feature is disabled"}""") + .build() + } + val targetUrl = s"$litellmBaseUrl/models" logger.info(s"Fetching models from LiteLLM: $targetUrl") From f8719d0749025e9f9efb4f2e2438102f21f6eaae Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 25 Nov 2025 17:48:19 -0800 Subject: [PATCH 150/158] fix agent toggle --- frontend/src/app/workspace/component/workspace.component.html | 2 +- frontend/src/app/workspace/component/workspace.component.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/workspace/component/workspace.component.html b/frontend/src/app/workspace/component/workspace.component.html index a28d0ba3660..abe3d2a786f 100644 --- a/frontend/src/app/workspace/component/workspace.component.html +++ b/frontend/src/app/workspace/component/workspace.component.html @@ -33,6 +33,6 @@ - + diff --git a/frontend/src/app/workspace/component/workspace.component.ts b/frontend/src/app/workspace/component/workspace.component.ts index 8958f08df10..14e1a937a0c 100644 --- a/frontend/src/app/workspace/component/workspace.component.ts +++ b/frontend/src/app/workspace/component/workspace.component.ts @@ -283,4 +283,8 @@ export class WorkspaceComponent implements AfterViewInit, OnInit, OnDestroy { public triggerCenter(): void { this.workflowActionService.getTexeraGraph().triggerCenterEvent(); } + + public get copilotEnabled(): boolean { + return this.config.env.copilotEnabled; + } } From db005a13abef640e1fccc548b8c22488a04a6ccf Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 25 Nov 2025 17:48:32 -0800 Subject: [PATCH 151/158] improve scss for the agent chat --- .../agent-chat/agent-chat.component.scss | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss index 08368c93f57..62c090f539d 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss @@ -132,18 +132,12 @@ border-radius: 6px; line-height: 1.5; word-wrap: break-word; - white-space: pre-wrap; // Preserves whitespace and line breaks - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 14px; + user-select: text; - // Style for markdown content ::ng-deep markdown { display: block; - // Reset whitespace handling for markdown - white-space: normal; - - // Style markdown elements p { margin: 0 0 8px 0; @@ -156,7 +150,6 @@ background: rgba(0, 0, 0, 0.06); padding: 2px 6px; border-radius: 3px; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13px; } @@ -204,14 +197,6 @@ } } - strong { - font-weight: 600; - } - - em { - font-style: italic; - } - a { color: #1890ff; text-decoration: none; @@ -223,7 +208,6 @@ } } -// Override styles for user messages with markdown .user-message .message-content ::ng-deep markdown { code { background: rgba(255, 255, 255, 0.2); From 5c8a87708a85bd97c6376595450c993f8c9c9296 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 25 Nov 2025 17:53:58 -0800 Subject: [PATCH 152/158] rollback the context menu change --- .../context-menu/context-menu/context-menu.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html index 8bbff3d3c65..7a6fcba6f25 100644 --- a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html +++ b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html @@ -139,7 +139,7 @@
  • Date: Tue, 25 Nov 2025 18:04:16 -0800 Subject: [PATCH 153/158] improve the CSS style --- .../agent-chat/agent-chat.component.scss | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss index 62c090f539d..5c784194456 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.scss @@ -83,17 +83,6 @@ .message-content { background: #1890ff; color: white; - - ::ng-deep { - code { - background: rgba(255, 255, 255, 0.2); - color: white; - } - - strong { - color: white; - } - } } } @@ -183,20 +172,6 @@ color: rgba(0, 0, 0, 0.65); } - h1, - h2, - h3, - h4, - h5, - h6 { - margin: 12px 0 8px 0; - font-weight: 600; - - &:first-child { - margin-top: 0; - } - } - a { color: #1890ff; text-decoration: none; @@ -211,10 +186,15 @@ .user-message .message-content ::ng-deep markdown { code { background: rgba(255, 255, 255, 0.2); + color: white; } pre { background: rgba(255, 255, 255, 0.15); + + code { + color: white; + } } blockquote { From 7967ccf1b00a9a7c71f678aa609daebae1d5c701 Mon Sep 17 00:00:00 2001 From: Jiadong Bai Date: Tue, 25 Nov 2025 18:15:10 -0800 Subject: [PATCH 154/158] improve the icon display --- .../agent-chat/agent-chat.component.html | 10 +++--- .../agent-chat/agent-chat.component.ts | 32 +++++++++++++++--- frontend/src/assets/gif/loading.gif | Bin 7610 -> 0 bytes frontend/src/assets/svg/done.svg | 21 ------------ 4 files changed, 34 insertions(+), 29 deletions(-) delete mode 100644 frontend/src/assets/gif/loading.gif delete mode 100644 frontend/src/assets/svg/done.svg diff --git a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html index e613a21e48a..a723f5789e1 100644 --- a/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html +++ b/frontend/src/app/workspace/component/agent-panel/agent-chat/agent-chat.component.html @@ -24,12 +24,14 @@
    - + [style.color]="getStateIconColor()" + style="font-size: 16px; margin-right: 6px; vertical-align: middle"> Model: {{ agentInfo.modelType }}