From caaa45d54cfe940d71130402093bf54ff8d1e4c0 Mon Sep 17 00:00:00 2001 From: Patrick Chou <1326306+pChou19@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:16:52 -0400 Subject: [PATCH 1/9] Search concept using grpc --- .mvn/jvm.config | 1 + application/pom.xml | 82 +++++ .../src/main/java/dev/ikm/komet/app/App.java | 93 +++++- .../main/java/dev/ikm/komet/app/AppPages.java | 35 +- application/src/main/java/module-info.java | 1 + .../context/AddToContextMenuSimple.java | 3 +- .../search/SearchPanelController.java | 66 ++++ .../framework/search/SearchResultCell.java | 47 +++ grpc-provider/pom.xml | 267 +++++++++++++++ .../dev/ikm/komet/grpc/GrpcSearchClient.java | 104 ++++++ grpc-provider/src/main/java/module-info.java | 44 +++ .../src/main/proto/tinkar_search.proto | 305 ++++++++++++++++++ .../controls/KLConceptNavigatorControl.java | 19 +- .../kview/fxutils/SlideOutTrayHelper.java | 12 + .../view/search/NextGenSearchController.java | 19 ++ pom.xml | 18 ++ 16 files changed, 1096 insertions(+), 20 deletions(-) create mode 100644 grpc-provider/pom.xml create mode 100644 grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchClient.java create mode 100644 grpc-provider/src/main/java/module-info.java create mode 100644 grpc-provider/src/main/proto/tinkar_search.proto diff --git a/.mvn/jvm.config b/.mvn/jvm.config index cb06d51cbb..c102b09c21 100644 --- a/.mvn/jvm.config +++ b/.mvn/jvm.config @@ -1 +1,2 @@ -Ddevelocity.scan.disabled=true +--enable-preview diff --git a/application/pom.xml b/application/pom.xml index 9a4b848e95..d090034a1d 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -189,6 +189,45 @@ dev.ikm.tinkar events + + ${project.groupId} + grpc-provider + + + + io.grpc + grpc-stub + 1.78.0 + runtime + + com.google.protobufprotobuf-java + com.google.code.findbugsjsr305 + javax.annotationjavax.annotation-api + + + + io.grpc + grpc-protobuf + 1.78.0 + runtime + + com.google.protobufprotobuf-java + com.google.code.findbugsjsr305 + javax.annotationjavax.annotation-api + + + + io.perfmark + perfmark-api + 0.27.0 + runtime + @@ -213,6 +252,15 @@ true + + + + + + + + + @@ -226,6 +274,28 @@ org.openjfx javafx-maven-plugin + + + dev.ikm.komet.application/dev.ikm.komet.app.App + + + + + + + + + + + + + + + + @@ -241,6 +311,8 @@ true kometJlink + + @@ -249,6 +321,16 @@ + + + + + + diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index a8fc37959f..d1a3730366 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -22,6 +22,9 @@ import static dev.ikm.komet.app.AppState.SHUTDOWN; import static dev.ikm.komet.app.AppState.STARTING; import static dev.ikm.komet.app.LoginFeatureFlag.ENABLED_WEB_ONLY; +import dev.ikm.komet.framework.search.SearchPanelController; +import dev.ikm.komet.grpc.GrpcSearchClient; +import dev.ikm.tinkar.service.proto.SearchSortOption; import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; import static dev.ikm.komet.app.util.CssFile.KVIEW_CSS; import static dev.ikm.komet.app.util.CssUtils.addStylesheets; @@ -179,9 +182,12 @@ private static void addShutdownHook() { LOG.info("Starting shutdown hook"); try { - // Save and stop primitive data services gracefully - PrimitiveData.save(); - PrimitiveData.stop(); + if (!GrpcSearchClient.isAvailable()) { + PrimitiveData.save(); + PrimitiveData.stop(); + } else { + GrpcSearchClient.get().close(); + } } catch (Exception e) { LOG.error("Error during shutdown hook execution", e); } @@ -318,11 +324,21 @@ public void start(Stage stage) { /** * Handles the login feature based on the provided {@link LoginFeatureFlag} and platform. + *

+ * When the system property {@code komet.grpc.port} is set, the application starts in + * gRPC mode: datasource selection and author login are skipped, and concept + * searches are routed to the running tinkar-core service instead of a local provider. + * Use {@code komet.grpc.host} to override the hostname (default: {@code localhost}). * * @param loginFeatureFlag the current state of the login feature * @param stage the current application stage */ public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) { + String grpcPortProp = System.getProperty("komet.grpc.port"); + if (grpcPortProp != null && !grpcPortProp.isBlank()) { + startGrpcMode(stage, grpcPortProp); + return; + } switch (loginFeatureFlag) { case ENABLED_WEB_ONLY -> { if (IS_BROWSER) { @@ -343,6 +359,68 @@ public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) { } } + /** + * Initialises the gRPC client and moves the application directly to {@link AppState#RUNNING}, + * bypassing datasource selection and author login. + * + * @param stage the primary stage + * @param grpcPortProp value of the {@code komet.grpc.port} system property + */ + private void startGrpcMode(Stage stage, String grpcPortProp) { + String host = System.getProperty("komet.grpc.host", "localhost"); + int port; + try { + port = Integer.parseInt(grpcPortProp.strip()); + } catch (NumberFormatException e) { + LOG.error("Invalid komet.grpc.port value '{}', falling back to datasource selection", grpcPortProp); + startSelectDataSource(stage); + return; + } + + GrpcSearchClient.initialize(host, port); + + // Start an ephemeral (in-memory) data store so that framework components + // that call PrimitiveData.get() (e.g. WindowSettings, Coordinates) work + // without a local dataset. Actual concept search is routed through gRPC. + // getControllerOptions() triggers ServiceLifecycleManager.discoverServices(). + try { + var controllers = PrimitiveData.getControllerOptions(); + var ephemeralOpt = controllers.stream() + .filter(c -> c.controllerName().toLowerCase().contains("ephemeral")) + .findFirst(); + if (ephemeralOpt.isPresent()) { + PrimitiveData.selectControllerByName(ephemeralOpt.get().controllerName()); + PrimitiveData.start(); + LOG.info("Ephemeral PrimitiveData started for gRPC mode (controller: {})", + ephemeralOpt.get().controllerName()); + } else { + LOG.warn("No ephemeral data provider found; available: {}", + controllers.stream().map(c -> c.controllerName()).toList()); + } + } catch (Exception e) { + LOG.error("Failed to start ephemeral data provider", e); + } + + SearchPanelController.setGrpcSearchProvider((query, maxResults) -> { + var response = GrpcSearchClient.get().conceptSearchWithSort( + query, maxResults, SearchSortOption.TOP_COMPONENT); + return response.getGroupedResultsList().stream() + .map(g -> new SearchPanelController.GrpcGroupedResult( + g.getFullyQualifiedName(), + g.getActive(), + g.getTopScore(), + g.getMatchingSemanticsList().stream() + .map(m -> new SearchPanelController.GrpcMatchingResult( + m.getHighlightedText(), m.getScore())) + .toList())) + .toList(); + }); + + LOG.info("gRPC mode active → {}:{}", host, port); + state.addListener(this::appStateChangeListener); + state.set(RUNNING); + } + /** * Initiates the login process by setting the application state to {@link AppState#LOGIN} * and launching the login page. @@ -507,8 +585,13 @@ public void quit() { saveJournalWindowsToPreferences(); LOG.info(">>> Saved journal windows to preferences"); - PrimitiveData.stop(); - LOG.info(">>> PrimitiveData stopped"); + if (GrpcSearchClient.isAvailable()) { + GrpcSearchClient.get().close(); + LOG.info(">>> gRPC client closed"); + } else { + PrimitiveData.stop(); + LOG.info(">>> PrimitiveData stopped"); + } Preferences.stop(); LOG.info(">>> Preferences stopped"); diff --git a/application/src/main/java/dev/ikm/komet/app/AppPages.java b/application/src/main/java/dev/ikm/komet/app/AppPages.java index ec349e04c3..7c60ac336a 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppPages.java +++ b/application/src/main/java/dev/ikm/komet/app/AppPages.java @@ -226,8 +226,13 @@ public void launchLandingPage(Stage stage, ConceptFacade loggedInUser) { String username = windowSettings.getView().calculator().getPreferredDescriptionTextWithFallbackOrNid(loggedInUser.nid()); app.landingPageController = landingPageLoader.getController(); - // Set the logged-in user as author on the controller's single edit coordinate - app.landingPageController.editCoordinate().authorForChangesProperty().setValue(loggedInUser); + // Set the logged-in user as author on the controller's single edit coordinate. + // In gRPC mode the ephemeral store has no entities, so nid resolution may fail; suppress. + try { + app.landingPageController.editCoordinate().authorForChangesProperty().setValue(loggedInUser); + } catch (Exception e) { + LOG.warn("Could not set author concept (expected in gRPC mode with empty data store): {}", e.getMessage()); + } app.landingPageController.getWelcomeTitleLabel().setText("Welcome " + username); app.landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name()); app.landingPageController.getGithubStatusHyperlink().setOnAction(_ -> app.appGithub.connectToGithub()); @@ -330,13 +335,25 @@ void launchJournalViewPage(PrefX journalWindowSettings, ConceptFacade loggedInUs KometNodeFactory navigatorNodeFactory = new GraphNavigatorNodeFactory(); KometNodeFactory searchNodeFactory = new SearchNodeFactory(); - journalController.launchKometFactoryNodes( - journalWindowSettings.getValue(JOURNAL_TITLE), - navigatorNodeFactory, - searchNodeFactory); - // load additional panels - journalController.loadNextGenReasonerPanel(); - journalController.loadNextGenSearchPanel(); + try { + journalController.launchKometFactoryNodes( + journalWindowSettings.getValue(JOURNAL_TITLE), + navigatorNodeFactory, + searchNodeFactory); + } catch (Exception e) { + LOG.error("Failed to launch navigator/search factory nodes (non-fatal in gRPC mode)", e); + } + // load additional panels — run independently so a nav failure doesn't block search + try { + journalController.loadNextGenReasonerPanel(); + } catch (Exception e) { + LOG.error("Failed to load NextGen Reasoner panel", e); + } + try { + journalController.loadNextGenSearchPanel(); + } catch (Exception e) { + LOG.error("Failed to load NextGen Search panel", e); + } }); // disable the delete menu option for a Journal Card. journalWindowSettings.setValue(CAN_DELETE, false); diff --git a/application/src/main/java/module-info.java b/application/src/main/java/module-info.java index abf0c7f9a4..36f7e90820 100644 --- a/application/src/main/java/module-info.java +++ b/application/src/main/java/module-info.java @@ -75,6 +75,7 @@ requires jdk.management; requires dev.ikm.tinkar.reasoner.service; requires org.eclipse.jgit; + requires dev.ikm.komet.grpc.provider; // Logging related modules requires org.apache.logging.log4j.core; diff --git a/framework/src/main/java/dev/ikm/komet/framework/context/AddToContextMenuSimple.java b/framework/src/main/java/dev/ikm/komet/framework/context/AddToContextMenuSimple.java index 13065bd5fc..284090e537 100644 --- a/framework/src/main/java/dev/ikm/komet/framework/context/AddToContextMenuSimple.java +++ b/framework/src/main/java/dev/ikm/komet/framework/context/AddToContextMenuSimple.java @@ -38,7 +38,6 @@ import dev.ikm.tinkar.common.id.PublicIdStringKey; import dev.ikm.tinkar.common.service.PrimitiveData; import dev.ikm.tinkar.entity.*; -import dev.ikm.tinkar.entity.EntityStringUtil; import dev.ikm.tinkar.terms.EntityFacade; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -118,7 +117,7 @@ public void addToContextMenu(Control control, ContextMenu contextMenu, ViewPrope if (entityFacade != null) { final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent content = new ClipboardContent(); - content.putString(EntityStringUtil.recursiveEntityToString(entityFacade)); + content.putString(PrimitiveData.text(entityFacade.nid())); clipboard.setContent(content); } }); diff --git a/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java b/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java index cbbfdda361..7a8b1abbdf 100644 --- a/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java +++ b/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java @@ -64,6 +64,51 @@ public class SearchPanelController implements ListChangeListener> { private static final Logger LOG = LoggerFactory.getLogger(SearchPanelController.class); + + /** + * Plug-in point for gRPC-backed search. When set (from the application layer), + * text searches are delegated to the remote service instead of the local + * {@code ViewCalculator}. The provider receives the query and max-results count + * and returns a flat list of {@link GrpcGroupedResult} each carrying its child + * {@link GrpcMatchingResult} list. + */ + @FunctionalInterface + public interface GrpcSearchProvider { + List search(String query, int maxResults); + } + + private static volatile GrpcSearchProvider grpcSearchProvider; + + /** Called once at startup by {@code App} when running in gRPC mode. */ + public static void setGrpcSearchProvider(GrpcSearchProvider provider) { + grpcSearchProvider = provider; + } + + public static GrpcSearchProvider getGrpcSearchProvider() { + return grpcSearchProvider; + } + + /** + * A top-level (grouped) search result returned by the gRPC service. + * + * @param fullyQualifiedName FQN of the matching concept + * @param active whether the concept is currently active + * @param topScore highest relevance score among child matches + * @param matchingResults child semantic matches + */ + public record GrpcGroupedResult( + String fullyQualifiedName, + boolean active, + float topScore, + List matchingResults) {} + + /** + * A single semantic match within a {@link GrpcGroupedResult}. + * + * @param highlightedText matched text with {@code } markup + * @param score relevance score + */ + public record GrpcMatchingResult(String highlightedText, float score) {} protected ReadOnlyObjectProperty> activityStreamKeyProperty = new SimpleObjectProperty<>(); @FXML private ResourceBundle resources; @@ -134,6 +179,27 @@ void doSearch(ActionEvent event) { UuidUtil.getUUID(queryText).ifPresent(uuid -> { addComponentFromNid(PrimitiveData.nid(PublicIds.of(uuid))); }); + } else if (grpcSearchProvider != null) { + String queryText2 = queryString.getText().strip(); + TinkExecutor.threadPool().execute(() -> { + try { + List groups = grpcSearchProvider.search(queryText2, 1000); + LOG.info("Finished gRPC search. Groups: {}", groups.size()); + TreeItem tempRoot = new TreeItem<>("Temp root"); + for (GrpcGroupedResult group : groups) { + TreeItem groupItem = new TreeItem<>(group); + for (GrpcMatchingResult match : group.matchingResults()) { + groupItem.getChildren().add(new TreeItem<>(match)); + } + groupItem.setExpanded(true); + tempRoot.getChildren().add(groupItem); + } + Platform.runLater(() -> resultsRoot.getChildren().setAll(tempRoot.getChildren())); + } catch (Throwable e) { + AlertStreams.getRoot().dispatch(AlertObject.makeError( + e.getClass().getSimpleName() + " during gRPC search", queryText2, e)); + } + }); } else { TinkExecutor.threadPool().execute(() -> { try { diff --git a/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java b/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java index c3f7367a6d..29e421d3ae 100644 --- a/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java +++ b/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java @@ -128,6 +128,11 @@ public void updateItem(Object item, boolean empty) { pseudoClassStateChanged(INACTIVE_PSEUDO_CLASS, !semanticEntityVersion.active()); }); + } else if (item instanceof SearchPanelController.GrpcGroupedResult grpcGroup) { + setTextFlow(grpcGroup.fullyQualifiedName(), SEARCH_TOP_COMPONENT); + pseudoClassStateChanged(INACTIVE_PSEUDO_CLASS, !grpcGroup.active()); + } else if (item instanceof SearchPanelController.GrpcMatchingResult grpcMatch) { + renderHighlightedText(grpcMatch.highlightedText()); } else if (item instanceof String itemString) { setTextFlow(itemString); } else if (item instanceof SearchPanelController.NidTextRecord nidTextRecord) { @@ -171,4 +176,46 @@ void setTextFlow(String text) { HBox hBox = new HBox(textFlow); setGraphic(hBox); } + + /** + * Renders a gRPC highlighted-text string that uses {@code } markup, + * reusing the same logic already present in the {@link LatestVersionSearchResult} branch. + */ + void renderHighlightedText(String matchedText) { + if (matchedText == null || matchedText.isBlank()) { + setTextFlow(""); + return; + } + TextFlow textFlow = newTextFlow(); + String startToken = ""; + String endToken = ""; + int startIdx = matchedText.indexOf(startToken); + while (startIdx != -1) { + if (startIdx != 0) { + Text t = new Text(matchedText.substring(0, startIdx)); + t.getStyleClass().add(SEARCH_NOT_MATCHED.toString()); + textFlow.getChildren().add(t); + } + int endIdx = matchedText.indexOf(endToken); + if (endIdx == -1) { + Text t = new Text(matchedText.substring(startIdx + startToken.length())); + t.getStyleClass().add(SEARCH_MATCH.toString()); + textFlow.getChildren().add(t); + matchedText = ""; + startIdx = -1; + } else { + Text t = new Text(matchedText.substring(startIdx + startToken.length(), endIdx)); + t.getStyleClass().add(SEARCH_MATCH.toString()); + textFlow.getChildren().add(t); + matchedText = matchedText.substring(endIdx + endToken.length()); + startIdx = matchedText.indexOf(startToken); + } + } + if (!matchedText.isBlank()) { + Text t = new Text(matchedText); + t.getStyleClass().add(SEARCH_NOT_MATCHED.toString()); + textFlow.getChildren().add(t); + } + setGraphic(new HBox(textFlow)); + } } diff --git a/grpc-provider/pom.xml b/grpc-provider/pom.xml new file mode 100644 index 0000000000..6f65c4c59a --- /dev/null +++ b/grpc-provider/pom.xml @@ -0,0 +1,267 @@ + + + 4.1.0 + + grpc-provider + Komet gRPC Provider + + + 1.78.0 + + 4.30.2 + + + + + + io.grpc + grpc-netty-shaded + ${grpc.version} + runtime + + com.google.protobufprotobuf-java + com.google.code.findbugsjsr305 + javax.annotationjavax.annotation-api + + + + + + io.grpc + grpc-api + ${grpc.version} + provided + + com.google.protobufprotobuf-java + com.google.code.findbugsjsr305 + javax.annotationjavax.annotation-api + + + + io.grpc + grpc-stub + ${grpc.version} + provided + + com.google.protobufprotobuf-java + com.google.code.findbugsjsr305 + javax.annotationjavax.annotation-api + + + + io.grpc + grpc-protobuf + ${grpc.version} + provided + + com.google.protobufprotobuf-java + com.google.code.findbugsjsr305 + javax.annotationjavax.annotation-api + + + + + + dev.ikm.jpms + protobuf-java + + + + dev.ikm.tinkar + tinkar-schema + + + + dev.ikm.jpms + jsr305 + + + + com.google.guava + guava + 33.1.0-jre + provided + + com.google.code.findbugsjsr305 + com.google.errorproneerror_prone_annotations + com.google.j2objcj2objc-annotations + com.google.guavalistenablefuture + com.google.code.findbugsjsr305 + org.checkerframeworkchecker-qual + + + + + com.google.errorprone + error_prone_annotations + 2.26.1 + provided + + + org.slf4j + slf4j-api + + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + --add-reads + dev.ikm.komet.grpc.provider=ALL-UNNAMED + + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + ${project.basedir}/../../tinkar-schema + + + + + + compile + compile-custom + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + + false + false + + + + io.grpc:grpc-api + io.grpc:grpc-stub + io.grpc:grpc-protobuf + io.grpc:grpc-protobuf-lite + com.google.guava:guava + com.google.guava:failureaccess + + + + + + io.grpc:grpc-api + META-INF/versions/*/module-info.class + + + io.grpc:grpc-stub + META-INF/versions/*/module-info.class + + + io.grpc:grpc-protobuf + META-INF/versions/*/module-info.class + + + io.grpc:grpc-protobuf-lite + META-INF/versions/*/module-info.class + + + com.google.guava:guava + META-INF/versions/*/module-info.class + + + + + + + + + + module-info.class + ${project.build.outputDirectory}/module-info.class + + + + + + + + + diff --git a/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchClient.java b/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchClient.java new file mode 100644 index 0000000000..ea4b7bdeba --- /dev/null +++ b/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchClient.java @@ -0,0 +1,104 @@ +/* + * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) + * + * Licensed 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 dev.ikm.komet.grpc; + +import dev.ikm.tinkar.service.proto.SearchSortOption; +import dev.ikm.tinkar.service.proto.TinkarConceptSearchWithSortRequest; +import dev.ikm.tinkar.service.proto.TinkarConceptSearchWithSortResponse; +import dev.ikm.tinkar.service.proto.TinkarSearchServiceGrpc; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * Manages a gRPC channel to a running tinkar-core service and exposes + * concept-search operations. Configured via system properties: + *
    + *
  • {@code komet.grpc.host} – hostname (default: {@code localhost})
  • + *
  • {@code komet.grpc.port} – port number (default: {@code 9090})
  • + *
+ * Call {@link #initialize(String, int)} once at startup, then access via {@link #get()}. + */ +public class GrpcSearchClient implements AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(GrpcSearchClient.class); + + private static volatile GrpcSearchClient instance; + + private final ManagedChannel channel; + private final TinkarSearchServiceGrpc.TinkarSearchServiceBlockingStub stub; + + private GrpcSearchClient(String host, int port) { + this.channel = ManagedChannelBuilder.forAddress(host, port) + .usePlaintext() + .build(); + this.stub = TinkarSearchServiceGrpc.newBlockingStub(channel); + LOG.info("gRPC client initialised → {}:{}", host, port); + } + + /** + * Creates and registers the singleton client. + * + * @param host gRPC server hostname + * @param port gRPC server port + */ + public static void initialize(String host, int port) { + instance = new GrpcSearchClient(host, port); + } + + /** Returns {@code true} when the client has been initialised. */ + public static boolean isAvailable() { + return instance != null; + } + + /** Returns the singleton client, or {@code null} if not yet initialised. */ + public static GrpcSearchClient get() { + return instance; + } + + /** + * Calls {@code TinkarSearchService.ConceptSearchWithSort} on the remote service. + * + * @param query free-text search string + * @param maxResults maximum number of results to return + * @param sortBy sort order for results + * @return the response from the server + */ + public TinkarConceptSearchWithSortResponse conceptSearchWithSort( + String query, int maxResults, SearchSortOption sortBy) { + + TinkarConceptSearchWithSortRequest request = TinkarConceptSearchWithSortRequest.newBuilder() + .setQuery(query) + .setMaxResults(maxResults) + .setSortBy(sortBy) + .build(); + return stub.conceptSearchWithSort(request); + } + + @Override + public void close() { + try { + channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); + LOG.info("gRPC channel shut down"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + channel.shutdownNow(); + } + } +} diff --git a/grpc-provider/src/main/java/module-info.java b/grpc-provider/src/main/java/module-info.java new file mode 100644 index 0000000000..ae0c462d01 --- /dev/null +++ b/grpc-provider/src/main/java/module-info.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) + * + * Licensed 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. + */ + +/** + * gRPC client provider for Komet. Provides {@link dev.ikm.komet.grpc.GrpcSearchClient} + * which connects to a running tinkar-core service and delegates concept search over gRPC. + * + *

The gRPC runtime libraries (grpc-api, grpc-stub, grpc-protobuf, guava) are shaded + * into this jar by maven-shade-plugin so that jlink sees a single named module rather + * than a mix of automatic modules. + * + *

grpc-netty-shaded (the transport) remains on the classpath as a runtime-only + * automatic module and is discovered at runtime via ServiceLoader. + */ +module dev.ikm.komet.grpc.provider { + + exports dev.ikm.komet.grpc; + // Generated proto/gRPC stub classes (used from App.java to build requests) + exports dev.ikm.tinkar.service.proto; + + // Protobuf runtime — JPMS-wrapped; provides com.google.protobuf.* packages + requires dev.ikm.jpms.protobuf; + + // Generated schema message classes from Tinkar.proto + requires dev.ikm.tinkar.schema; + + // javax.annotation — JPMS-wrapped jsr305 (module: dev.ikm.jpms.javax.annotation) + requires dev.ikm.jpms.javax.annotation; + + requires org.slf4j; +} diff --git a/grpc-provider/src/main/proto/tinkar_search.proto b/grpc-provider/src/main/proto/tinkar_search.proto new file mode 100644 index 0000000000..f3308748c1 --- /dev/null +++ b/grpc-provider/src/main/proto/tinkar_search.proto @@ -0,0 +1,305 @@ +// DEPRECATED: This service is maintained for backward compatibility only. +// New clients should use: +// - ike_graph_rag.proto (Tier 1: IkeGraphRAG service) +// - ike_knowledge_graph.proto (Tier 2: IkeKnowledgeGraph service) + +syntax = "proto3"; + +package dev.ikm.tinkar.service; + +import "Tinkar.proto"; + +option java_multiple_files = true; +option java_package = "dev.ikm.tinkar.service.proto"; +option java_outer_classname = "TinkarSearchProto"; + +// Search query request +message TinkarSearchQueryRequest { + string query = 1; +} + +// Concept search request +message TinkarConceptSearchRequest { + string query = 1; + /* + * Maximum number of results to return. If not specified or <= 0, uses server default (100). + */ + int32 max_results = 2; +} + +// Concept ID request for single ID lookups +message TinkarConceptIdRequest { + /* + * The public ID of the concept (supports multiple UUIDs). + */ + dev.ikm.tinkar.schema.PublicId public_id = 1; +} + +// Tinkar search result representing a concept with its descriptions and STAMP info +message TinkarSearchResult { + /* + * The public ID of the concept (supports multiple UUIDs). + */ + dev.ikm.tinkar.schema.PublicId public_id = 1; + + /* + * The descriptions associated with this concept. + */ + TinkarConceptDescriptions descriptions = 2; + + /* + * The STAMP version info for this concept (status, author, module, path, time). + */ + dev.ikm.tinkar.schema.StampVersion stamp = 3; +} + +// Descriptions associated with a concept +message TinkarConceptDescriptions { + /* + * The fully qualified name of the concept. + */ + string fully_qualified_name = 1; + /* + * The regular/preferred description text of the concept. + */ + string regular_name = 2; + /* + * Additional definition or description text. + */ + string definition = 3; +} + +// Search query response +message TinkarSearchQueryResponse { + string query = 1; + int64 total_count = 2; + repeated TinkarSearchResult results = 3; + bool success = 4; + string error_message = 5; +} + +// Rebuild index response +message TinkarRebuildIndexResponse { + string message = 1; + bool success = 2; +} + +// Empty request for rebuild index +message TinkarRebuildIndexRequest { +} + +// Sort options for search results +enum SearchSortOption { + TOP_COMPONENT = 0; // Group by top component, sorted by relevance score (highest first) + TOP_COMPONENT_ALPHA = 1; // Group by top component, sorted alphabetically + SEMANTIC = 2; // Individual semantic matches, sorted by relevance score (highest first) + SEMANTIC_ALPHA = 3; // Individual semantic matches, sorted alphabetically +} + +// Concept search with sort request +message TinkarConceptSearchWithSortRequest { + string query = 1; + /* + * Maximum number of results to return. If not specified or <= 0, uses server default (100). + */ + int32 max_results = 2; + /* + * Sort option for results. If not specified, uses TOP_COMPONENT. + */ + SearchSortOption sort_by = 3; +} + +// Individual semantic search result with score +message TinkarSemanticSearchResult { + /* + * Public ID (list of UUIDs) of the concept. + */ + repeated string public_id = 1; + /* + * Fully qualified name of the concept. + */ + string fully_qualified_name = 2; + /* + * Regular/preferred description. + */ + string regular_name = 3; + /* + * The matched text with highlighting (HTML bold tags). + */ + string highlighted_text = 4; + /* + * The relevance score of this match. + */ + float score = 5; + /* + * Whether the concept is active. + */ + bool active = 6; +} + +// A matching semantic within a grouped result +message TinkarMatchingSemantic { + /* + * The matched text with highlighting (HTML bold tags). + */ + string highlighted_text = 1; + /* + * The plain text without highlighting. + */ + string plain_text = 2; + /* + * The relevance score of this match. + */ + float score = 3; +} + +// Grouped search result by top-level component +message TinkarGroupedSearchResult { + /* + * Public ID (list of UUIDs) of the top-level concept. + */ + repeated string public_id = 1; + /* + * Fully qualified name of the top-level concept. + */ + string fully_qualified_name = 2; + /* + * Whether the concept is active. + */ + bool active = 3; + /* + * The highest relevance score among matching semantics. + */ + float top_score = 4; + /* + * List of matching semantics for this concept. + */ + repeated TinkarMatchingSemantic matching_semantics = 5; +} + +// Response for concept search with sort +message TinkarConceptSearchWithSortResponse { + /* + * The original search query. + */ + string query = 1; + /* + * Total number of matching results. + */ + int64 total_count = 2; + /* + * The sort option used. + */ + SearchSortOption sort_by = 3; + /* + * List of search results (flat structure for SEMANTIC modes). + */ + repeated TinkarSemanticSearchResult results = 4; + /* + * List of grouped results (for TOP_COMPONENT modes). + */ + repeated TinkarGroupedSearchResult grouped_results = 5; + /* + * Whether the search was successful. + */ + bool success = 6; + /* + * Error message if search failed. + */ + string error_message = 7; +} + +// Human-readable STAMP information for display purposes +message TinkarStampInfo { + /* + * Status (e.g., Active, Inactive). + */ + string status = 1; + /* + * Author name. + */ + string author = 2; + /* + * Module name. + */ + string module = 3; + /* + * Path name. + */ + string path = 4; + /* + * Timestamp in epoch milliseconds. + */ + int64 time = 5; + /* + * Human-readable timestamp. + */ + string formatted_time = 6; +} + +// Information about a single semantic attached to a concept +message TinkarConceptSemanticInfo { + /* + * Semantic public ID. + */ + dev.ikm.tinkar.schema.PublicId semantic_public_id = 1; + /* + * Pattern public ID. + */ + dev.ikm.tinkar.schema.PublicId pattern_public_id = 2; + /* + * Pattern name (human-readable, e.g., Comment Pattern, Description Pattern). + */ + string pattern_name = 3; + /* + * Field values of the semantic (using Tinkar Field type). + */ + repeated dev.ikm.tinkar.schema.Field fields = 4; + /* + * Human-readable STAMP information. + */ + TinkarStampInfo stamp = 5; +} + +// Response for concept semantics +message TinkarConceptSemanticsResponse { + /* + * The concept public ID. + */ + dev.ikm.tinkar.schema.PublicId concept_public_id = 1; + /* + * The concept description (human-readable). + */ + string concept_description = 2; + /* + * Total number of semantics found. + */ + int32 total_count = 3; + /* + * List of semantics attached to this concept. + */ + repeated TinkarConceptSemanticInfo semantics = 4; + /* + * Whether the query was successful. + */ + bool success = 5; + /* + * Error message if query failed. + */ + string error_message = 6; +} + +// gRPC service definition +service TinkarSearchService { + rpc Search(TinkarSearchQueryRequest) returns (TinkarSearchQueryResponse); + rpc ConceptSearch(TinkarConceptSearchRequest) returns (TinkarSearchQueryResponse); + rpc ConceptSearchWithSort(TinkarConceptSearchWithSortRequest) returns (TinkarConceptSearchWithSortResponse); + rpc GetEntity(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); + rpc GetChildConcepts(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); + rpc GetDescendantConcepts(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); + rpc GetLIDRRecordConceptsFromTestKit(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); + rpc GetResultConformanceConceptsFromLIDRRecord(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); + rpc GetAllowedResultConceptsFromResultConformance(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); + rpc RebuildSearchIndex(TinkarRebuildIndexRequest) returns (TinkarRebuildIndexResponse); + rpc GetConceptSemantics(TinkarConceptIdRequest) returns (TinkarConceptSemanticsResponse); +} diff --git a/kview/src/main/java/dev/ikm/komet/kview/controls/KLConceptNavigatorControl.java b/kview/src/main/java/dev/ikm/komet/kview/controls/KLConceptNavigatorControl.java index 16c874d781..ca886d86ce 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/controls/KLConceptNavigatorControl.java +++ b/kview/src/main/java/dev/ikm/komet/kview/controls/KLConceptNavigatorControl.java @@ -250,7 +250,11 @@ public final void setActivation(double value) { @Override protected void invalidated() { if (get() != null) { - ConceptNavigatorTreeItem first = getConceptNavigatorRoot().getFirst(); + List roots = getConceptNavigatorRoot(); + if (roots.isEmpty()) { + return; + } + ConceptNavigatorTreeItem first = roots.getFirst(); setRoot(first); // debug // new Thread(() -> ConceptNavigatorUtils.getConceptNavigatorDepth(first.getValue().nid(), get())).start(); @@ -363,11 +367,11 @@ public String getUserAgentStylesheet() { */ private List getConceptNavigatorRoot() { return Arrays.stream(getNavigator().getRootNids()) - .mapToObj(rootNid -> { - ConceptNavigatorTreeItem treeItem = getConceptNavigatorTreeItem(rootNid, -1); + .mapToObj(rootNid -> getConceptNavigatorTreeItem(rootNid, -1)) + .filter(treeItem -> treeItem != null) + .peek(treeItem -> { fetchChildren(treeItem); treeItem.setExpanded(true); - return treeItem; }) .toList(); } @@ -382,6 +386,7 @@ private List getConceptNavigatorRoot() { private List getChildren(int nid) { return getNavigator().getChildEdges(nid).stream() .map(edge -> getConceptNavigatorTreeItem(edge.destinationNid(), nid)) + .filter(item -> item != null) .toList(); } @@ -395,6 +400,9 @@ private List getChildren(int nid) { */ private ConceptNavigatorTreeItem getConceptNavigatorTreeItem(int nid, int parentNid) { ConceptNavigatorTreeItem conceptNavigatorTreeItem = createSingleConceptNavigatorTreeItem(nid, parentNid); + if (conceptNavigatorTreeItem == null) { + return null; + } conceptNavigatorTreeItem.expandedProperty().subscribe((_, expanded) -> { if (expanded && conceptNavigatorTreeItem.getChildren().isEmpty()) { // when a new branch is expanded, prune the collapsed branches of the treeView, @@ -473,6 +481,9 @@ private Future fetchChildrenTask(ConceptNavigatorTreeItem conceptNaviga */ private ConceptNavigatorTreeItem createSingleConceptNavigatorTreeItem(int nid, int parentNid) { ConceptFacade facade = Entity.getFast(nid); + if (facade == null) { + return null; + } ConceptNavigatorTreeItem conceptNavigatorTreeItem = new ConceptNavigatorTreeItem(getNavigator(), facade, parentNid); conceptNavigatorTreeItem.setDefined(ConceptNavigatorUtils.isDefined(getNavigator().getViewCalculator(), facade)); conceptNavigatorTreeItem.setMultiParent(ConceptNavigatorUtils.getParentNids(getNavigator(), nid).length > 1); diff --git a/kview/src/main/java/dev/ikm/komet/kview/fxutils/SlideOutTrayHelper.java b/kview/src/main/java/dev/ikm/komet/kview/fxutils/SlideOutTrayHelper.java index b2fe1c0cc4..9bc946ca26 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/fxutils/SlideOutTrayHelper.java +++ b/kview/src/main/java/dev/ikm/komet/kview/fxutils/SlideOutTrayHelper.java @@ -93,6 +93,9 @@ static void slideOut(Pane trayPane) { * If false, the panel will appear immediately in its final position. */ static void slideOut(Pane trayPane, boolean animated) { + if (trayPane.getChildren().isEmpty()) { + return; + } final Node panel = trayPane.getChildren().getFirst(); final double width = panel.getBoundsInLocal().getWidth(); @@ -146,6 +149,9 @@ static void slideIn(Pane trayPane) { * If false, the panel will disappear immediately. */ static void slideIn(Pane trayPane, boolean animated) { + if (trayPane.getChildren().isEmpty()) { + return; + } final Node panel = trayPane.getChildren().getFirst(); final double width = panel.getBoundsInLocal().getWidth(); @@ -199,6 +205,9 @@ static void slideOut(Pane trayPane, Pane owningPanel) { * If false, both panels will adjust immediately. */ static void slideOut(Pane trayPane, Pane owningPanel, boolean animated) { + if (trayPane.getChildren().isEmpty()) { + return; + } final Node panel = trayPane.getChildren().getFirst(); final double width = panel.getBoundsInLocal().getWidth(); @@ -254,6 +263,9 @@ static void slideIn(Pane trayPane, Pane owningPanel) { * If false, both panels will adjust immediately. */ static void slideIn(Pane trayPane, Pane owningPanel, boolean animated) { + if (trayPane.getChildren().isEmpty()) { + return; + } final Node panel = trayPane.getChildren().getFirst(); final double width = panel.getBoundsInLocal().getWidth(); diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java index 869894dddb..e8f1a9d276 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java @@ -284,6 +284,25 @@ private void doSearch(ActionEvent actionEvent) { UuidUtil.getUUID(queryText).ifPresent(uuid -> { addComponentFromNid(PrimitiveData.nid(PublicIds.of(uuid))); }); + } else if (SearchPanelController.getGrpcSearchProvider() != null) { + final String grpcQuery = queryText; + SearchPanelController.GrpcSearchProvider grpcProvider = SearchPanelController.getGrpcSearchProvider(); + List grpcResults = grpcProvider.search(grpcQuery, MAX_RESULT_SIZE); + LOG.info("{} gRPC search results returned for query: {}", grpcResults.size(), grpcQuery); + searchResultsListView.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(Object item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else if (item instanceof SearchPanelController.GrpcGroupedResult grpcGroup) { + setText(grpcGroup.fullyQualifiedName()); + } else { + setText(item.toString()); + } + } + }); + searchResultsListView.getItems().setAll(grpcResults); } else { List results = getViewProperties().calculator().search(queryText, MAX_RESULT_SIZE).toList(); LOG.info("{} search results returned for query: {}", results.size(), queryText); diff --git a/pom.xml b/pom.xml index e363a129bc..655ed79326 100644 --- a/pom.xml +++ b/pom.xml @@ -59,11 +59,15 @@ kview sync knowledge-layout-editor + grpc-provider application UTF-8 1.7.3 + + + localhost 1.21.1-r11 0.0.8 1.1.2 @@ -91,6 +95,11 @@ pom import + + dev.ikm.komet + grpc-provider + ${project.version} + @@ -142,6 +151,15 @@ + + + tinkar-nexus-plugins + https://nexus.tinkar.org/repository/ike-public/ + true + true + + + From 2251356565e0bf385079c8fa76d6f2653c631a63 Mon Sep 17 00:00:00 2001 From: Patrick Chou <1326306+pChou19@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:39:38 -0400 Subject: [PATCH 2/9] GrpcSearchService that implements SearchService --- .../src/main/java/dev/ikm/komet/app/App.java | 20 +- .../search/SearchPanelController.java | 65 ------ .../framework/search/SearchResultCell.java | 5 - grpc-provider/pom.xml | 11 + .../dev/ikm/komet/grpc/GrpcSearchService.java | 218 ++++++++++++++++++ grpc-provider/src/main/java/module-info.java | 5 + .../src/main/proto/tinkar_search.proto | 12 + kview/pom.xml | 4 + .../view/search/NextGenSearchController.java | 41 ++-- .../search/SearchCellGrpcSemanticResult.java | 120 ++++++++++ .../search/SearchCellGrpcTopComponent.java | 179 ++++++++++++++ kview/src/main/java/module-info.java | 1 + 12 files changed, 575 insertions(+), 106 deletions(-) create mode 100644 grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchService.java create mode 100644 kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcSemanticResult.java create mode 100644 kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcTopComponent.java diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index d1a3730366..a16dcbd5de 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -22,9 +22,8 @@ import static dev.ikm.komet.app.AppState.SHUTDOWN; import static dev.ikm.komet.app.AppState.STARTING; import static dev.ikm.komet.app.LoginFeatureFlag.ENABLED_WEB_ONLY; -import dev.ikm.komet.framework.search.SearchPanelController; import dev.ikm.komet.grpc.GrpcSearchClient; -import dev.ikm.tinkar.service.proto.SearchSortOption; +import dev.ikm.komet.grpc.GrpcSearchService; import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; import static dev.ikm.komet.app.util.CssFile.KVIEW_CSS; import static dev.ikm.komet.app.util.CssUtils.addStylesheets; @@ -377,7 +376,7 @@ private void startGrpcMode(Stage stage, String grpcPortProp) { return; } - GrpcSearchClient.initialize(host, port); + GrpcSearchService.initialize(host, port); // Start an ephemeral (in-memory) data store so that framework components // that call PrimitiveData.get() (e.g. WindowSettings, Coordinates) work @@ -401,21 +400,6 @@ private void startGrpcMode(Stage stage, String grpcPortProp) { LOG.error("Failed to start ephemeral data provider", e); } - SearchPanelController.setGrpcSearchProvider((query, maxResults) -> { - var response = GrpcSearchClient.get().conceptSearchWithSort( - query, maxResults, SearchSortOption.TOP_COMPONENT); - return response.getGroupedResultsList().stream() - .map(g -> new SearchPanelController.GrpcGroupedResult( - g.getFullyQualifiedName(), - g.getActive(), - g.getTopScore(), - g.getMatchingSemanticsList().stream() - .map(m -> new SearchPanelController.GrpcMatchingResult( - m.getHighlightedText(), m.getScore())) - .toList())) - .toList(); - }); - LOG.info("gRPC mode active → {}:{}", host, port); state.addListener(this::appStateChangeListener); state.set(RUNNING); diff --git a/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java b/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java index 7a8b1abbdf..2b5a907a5d 100644 --- a/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java +++ b/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java @@ -65,50 +65,6 @@ public class SearchPanelController implements ListChangeListener> { private static final Logger LOG = LoggerFactory.getLogger(SearchPanelController.class); - /** - * Plug-in point for gRPC-backed search. When set (from the application layer), - * text searches are delegated to the remote service instead of the local - * {@code ViewCalculator}. The provider receives the query and max-results count - * and returns a flat list of {@link GrpcGroupedResult} each carrying its child - * {@link GrpcMatchingResult} list. - */ - @FunctionalInterface - public interface GrpcSearchProvider { - List search(String query, int maxResults); - } - - private static volatile GrpcSearchProvider grpcSearchProvider; - - /** Called once at startup by {@code App} when running in gRPC mode. */ - public static void setGrpcSearchProvider(GrpcSearchProvider provider) { - grpcSearchProvider = provider; - } - - public static GrpcSearchProvider getGrpcSearchProvider() { - return grpcSearchProvider; - } - - /** - * A top-level (grouped) search result returned by the gRPC service. - * - * @param fullyQualifiedName FQN of the matching concept - * @param active whether the concept is currently active - * @param topScore highest relevance score among child matches - * @param matchingResults child semantic matches - */ - public record GrpcGroupedResult( - String fullyQualifiedName, - boolean active, - float topScore, - List matchingResults) {} - - /** - * A single semantic match within a {@link GrpcGroupedResult}. - * - * @param highlightedText matched text with {@code } markup - * @param score relevance score - */ - public record GrpcMatchingResult(String highlightedText, float score) {} protected ReadOnlyObjectProperty> activityStreamKeyProperty = new SimpleObjectProperty<>(); @FXML private ResourceBundle resources; @@ -179,27 +135,6 @@ void doSearch(ActionEvent event) { UuidUtil.getUUID(queryText).ifPresent(uuid -> { addComponentFromNid(PrimitiveData.nid(PublicIds.of(uuid))); }); - } else if (grpcSearchProvider != null) { - String queryText2 = queryString.getText().strip(); - TinkExecutor.threadPool().execute(() -> { - try { - List groups = grpcSearchProvider.search(queryText2, 1000); - LOG.info("Finished gRPC search. Groups: {}", groups.size()); - TreeItem tempRoot = new TreeItem<>("Temp root"); - for (GrpcGroupedResult group : groups) { - TreeItem groupItem = new TreeItem<>(group); - for (GrpcMatchingResult match : group.matchingResults()) { - groupItem.getChildren().add(new TreeItem<>(match)); - } - groupItem.setExpanded(true); - tempRoot.getChildren().add(groupItem); - } - Platform.runLater(() -> resultsRoot.getChildren().setAll(tempRoot.getChildren())); - } catch (Throwable e) { - AlertStreams.getRoot().dispatch(AlertObject.makeError( - e.getClass().getSimpleName() + " during gRPC search", queryText2, e)); - } - }); } else { TinkExecutor.threadPool().execute(() -> { try { diff --git a/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java b/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java index 29e421d3ae..a8bdb0b2ee 100644 --- a/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java +++ b/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java @@ -128,11 +128,6 @@ public void updateItem(Object item, boolean empty) { pseudoClassStateChanged(INACTIVE_PSEUDO_CLASS, !semanticEntityVersion.active()); }); - } else if (item instanceof SearchPanelController.GrpcGroupedResult grpcGroup) { - setTextFlow(grpcGroup.fullyQualifiedName(), SEARCH_TOP_COMPONENT); - pseudoClassStateChanged(INACTIVE_PSEUDO_CLASS, !grpcGroup.active()); - } else if (item instanceof SearchPanelController.GrpcMatchingResult grpcMatch) { - renderHighlightedText(grpcMatch.highlightedText()); } else if (item instanceof String itemString) { setTextFlow(itemString); } else if (item instanceof SearchPanelController.NidTextRecord nidTextRecord) { diff --git a/grpc-provider/pom.xml b/grpc-provider/pom.xml index 6f65c4c59a..5af06e91f0 100644 --- a/grpc-provider/pom.xml +++ b/grpc-provider/pom.xml @@ -126,6 +126,17 @@ org.slf4j slf4j-api + + + dev.ikm.tinkar + search-provider + provided + + + dev.ikm.tinkar + common + provided + diff --git a/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchService.java b/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchService.java new file mode 100644 index 0000000000..0294580a34 --- /dev/null +++ b/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchService.java @@ -0,0 +1,218 @@ +/* + * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) + * + * Licensed 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 dev.ikm.komet.grpc; + +import dev.ikm.tinkar.common.service.PrimitiveDataSearchResult; +import dev.ikm.tinkar.provider.search.SearchService; +import dev.ikm.tinkar.service.proto.SearchSortOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Implementation of {@link SearchService} that delegates search calls to a remote + * tinkar-core gRPC service via {@link GrpcSearchClient}. + * + *

This is the plug-in point for gRPC-backed search in Komet. When Komet is started + * with {@code -Dkomet.grpc.port}, this service is activated via {@link #initialize}. + * Downstream code (e.g., {@code NextGenSearchController}) checks {@link #isActive()} and + * calls {@link #searchGrouped} or {@link #searchFlat} instead of the local Lucene path. + * + *

The {@link #search} method satisfies the {@link SearchService} contract but returns + * an empty array — all meaningful results come through the typed methods that carry + * grouped/semantic structure back from the service. + */ +public class GrpcSearchService implements SearchService { + + private static final Logger LOG = LoggerFactory.getLogger(GrpcSearchService.class); + + private static volatile GrpcSearchService INSTANCE; + + /** + * Sort options that mirror the UI sort buttons and map 1-to-1 to the + * proto {@link SearchSortOption} enum. + */ + public enum SortOption { + TOP_COMPONENT, + TOP_COMPONENT_ALPHA, + SEMANTIC, + SEMANTIC_ALPHA + } + + /** + * A top-level (grouped) search result — one per matching concept. + * + * @param publicId stable UUIDs identifying the concept + * @param fullyQualifiedName FQN of the concept + * @param active whether the concept is currently active + * @param topScore highest relevance score among child semantics + * @param matchingSemantics child semantic matches + */ + public record GroupedResult( + List publicId, + String fullyQualifiedName, + boolean active, + float topScore, + List matchingSemantics) {} + + /** + * A flat semantic search result (SEMANTIC sort modes) — one per matched semantic. + * + * @param publicId stable UUIDs identifying the concept + * @param fullyQualifiedName FQN of the concept + * @param highlightedText matched text with {@code } markup + * @param active whether the concept is currently active + * @param score relevance score + */ + public record SemanticResult( + List publicId, + String fullyQualifiedName, + String highlightedText, + boolean active, + float score) {} + + /** + * A single semantic match within a {@link GroupedResult}. + * + * @param highlightedText matched text with {@code } markup + * @param plainText plain text without HTML markup + * @param score relevance score + */ + public record MatchingSemantic(String highlightedText, String plainText, float score) {} + + private GrpcSearchService() {} + + /** + * Activates gRPC search mode by initializing the underlying {@link GrpcSearchClient}. + * Must be called once at startup before any search calls. + */ + public static void initialize(String host, int port) { + GrpcSearchClient.initialize(host, port); + INSTANCE = new GrpcSearchService(); + LOG.info("GrpcSearchService initialized → {}:{}", host, port); + } + + /** + * Returns {@code true} when gRPC mode has been initialized and is ready. + */ + public static boolean isActive() { + return INSTANCE != null && GrpcSearchClient.isAvailable(); + } + + /** + * Returns the active singleton, or throws if not initialized. + */ + public static GrpcSearchService get() { + if (INSTANCE == null) { + throw new IllegalStateException("GrpcSearchService not initialized — pass -Dkomet.grpc.port to activate"); + } + return INSTANCE; + } + + /** + * Performs a search returning grouped results (TOP_COMPONENT modes). + * The sort option controls both ordering and grouping behaviour server-side. + */ + public List searchGrouped(String query, int maxResults, SortOption sortOption) { + SearchSortOption protoSort = toProtoSort(sortOption); + var response = GrpcSearchClient.get().conceptSearchWithSort(query, maxResults, protoSort); + return response.getGroupedResultsList().stream() + .map(g -> new GroupedResult( + g.getPublicIdList(), + g.getFullyQualifiedName(), + g.getActive(), + g.getTopScore(), + g.getMatchingSemanticsList().stream() + .map(m -> new MatchingSemantic( + m.getHighlightedText(), m.getPlainText(), m.getScore())) + .toList())) + .toList(); + } + + /** + * Performs a search returning flat semantic results (SEMANTIC modes). + */ + public List searchFlat(String query, int maxResults, SortOption sortOption) { + SearchSortOption protoSort = toProtoSort(sortOption); + var response = GrpcSearchClient.get().conceptSearchWithSort(query, maxResults, protoSort); + return response.getResultsList().stream() + .map(r -> new SemanticResult( + r.getPublicIdList(), + r.getFullyQualifiedName(), + r.getHighlightedText(), + r.getActive(), + r.getScore())) + .toList(); + } + + // --- SearchService contract --- + + /** + * Not meaningful in gRPC mode — indexing is handled server-side. + */ + @Override + public void index(Object object) { + LOG.debug("GrpcSearchService.index() called — no-op in gRPC mode"); + } + + /** + * Not meaningful in gRPC mode — index commits are handled server-side. + */ + @Override + public void commit() throws IOException { + LOG.debug("GrpcSearchService.commit() called — no-op in gRPC mode"); + } + + /** + * Satisfies the {@link SearchService} contract. Returns raw Lucene-style results by + * delegating to {@link #searchFlat} and converting to {@link PrimitiveDataSearchResult}. + * NIDs are 0 since the local entity store is ephemeral; callers that need rich display + * should use {@link #searchGrouped} or {@link #searchFlat} directly. + */ + @Override + public PrimitiveDataSearchResult[] search(String query, int maxResultSize) { + List flat = searchFlat(query, maxResultSize, SortOption.SEMANTIC); + return flat.stream() + .map(r -> new PrimitiveDataSearchResult(0, 0, 0, 0, r.score(), r.highlightedText())) + .toArray(PrimitiveDataSearchResult[]::new); + } + + /** + * Not meaningful in gRPC mode — index recreation is handled server-side. + */ + @Override + public CompletableFuture recreateIndex() { + LOG.debug("GrpcSearchService.recreateIndex() called — no-op in gRPC mode"); + return CompletableFuture.completedFuture(null); + } + + @Override + public String name() { + return "GrpcSearchService"; + } + + private static SearchSortOption toProtoSort(SortOption sortOption) { + return switch (sortOption) { + case TOP_COMPONENT -> SearchSortOption.TOP_COMPONENT; + case TOP_COMPONENT_ALPHA -> SearchSortOption.TOP_COMPONENT_ALPHA; + case SEMANTIC -> SearchSortOption.SEMANTIC; + case SEMANTIC_ALPHA -> SearchSortOption.SEMANTIC_ALPHA; + }; + } +} diff --git a/grpc-provider/src/main/java/module-info.java b/grpc-provider/src/main/java/module-info.java index ae0c462d01..10f76a3892 100644 --- a/grpc-provider/src/main/java/module-info.java +++ b/grpc-provider/src/main/java/module-info.java @@ -40,5 +40,10 @@ // javax.annotation — JPMS-wrapped jsr305 (module: dev.ikm.jpms.javax.annotation) requires dev.ikm.jpms.javax.annotation; + // SearchService contract from tinkar-core search-provider + requires dev.ikm.tinkar.provider.search; + // PrimitiveDataSearchResult lives in tinkar-core common + requires dev.ikm.tinkar.common; + requires org.slf4j; } diff --git a/grpc-provider/src/main/proto/tinkar_search.proto b/grpc-provider/src/main/proto/tinkar_search.proto index f3308748c1..07ea6ca8ff 100644 --- a/grpc-provider/src/main/proto/tinkar_search.proto +++ b/grpc-provider/src/main/proto/tinkar_search.proto @@ -151,6 +151,14 @@ message TinkarMatchingSemantic { * The relevance score of this match. */ float score = 3; + /* + * The index of the matched field within the semantic. + */ + int32 field_index = 4; + /* + * The NID of the matching semantic entity. + */ + int32 semantic_nid = 5; } // Grouped search result by top-level component @@ -175,6 +183,10 @@ message TinkarGroupedSearchResult { * List of matching semantics for this concept. */ repeated TinkarMatchingSemantic matching_semantics = 5; + /* + * The NID of the top-level concept entity. + */ + int32 concept_nid = 6; } // Response for concept search with sort diff --git a/kview/pom.xml b/kview/pom.xml index 5cb86335b2..59cd19bedb 100644 --- a/kview/pom.xml +++ b/kview/pom.xml @@ -24,6 +24,10 @@ dev.ikm.tinkar search-provider + + ${project.groupId} + grpc-provider + dev.ikm.tinkar composer diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java index e8f1a9d276..3b7f8f4dd1 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java @@ -29,6 +29,7 @@ import dev.ikm.komet.framework.dnd.DragImageMaker; import dev.ikm.komet.framework.dnd.KometClipboard; import dev.ikm.komet.framework.search.SearchPanelController; +import dev.ikm.komet.grpc.GrpcSearchService; import dev.ikm.komet.framework.view.ViewProperties; import dev.ikm.komet.kview.controls.AutoCompleteTextField; import dev.ikm.komet.kview.controls.FilterOptionsPopup; @@ -284,25 +285,29 @@ private void doSearch(ActionEvent actionEvent) { UuidUtil.getUUID(queryText).ifPresent(uuid -> { addComponentFromNid(PrimitiveData.nid(PublicIds.of(uuid))); }); - } else if (SearchPanelController.getGrpcSearchProvider() != null) { + } else if (GrpcSearchService.isActive()) { final String grpcQuery = queryText; - SearchPanelController.GrpcSearchProvider grpcProvider = SearchPanelController.getGrpcSearchProvider(); - List grpcResults = grpcProvider.search(grpcQuery, MAX_RESULT_SIZE); - LOG.info("{} gRPC search results returned for query: {}", grpcResults.size(), grpcQuery); - searchResultsListView.setCellFactory(lv -> new ListCell<>() { - @Override - protected void updateItem(Object item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - setText(null); - } else if (item instanceof SearchPanelController.GrpcGroupedResult grpcGroup) { - setText(grpcGroup.fullyQualifiedName()); - } else { - setText(item.toString()); - } - } - }); - searchResultsListView.getItems().setAll(grpcResults); + GrpcSearchService.SortOption sortOption = switch (sortByButton.getText()) { + case BUTTON_TEXT_TOP_COMPONENT_ALPHA -> GrpcSearchService.SortOption.TOP_COMPONENT_ALPHA; + case BUTTON_TEXT_DESCRIPTION_SEMANTIC -> GrpcSearchService.SortOption.SEMANTIC; + case BUTTON_TEXT_DESCRIPTION_SEMANTIC_ALPHA -> GrpcSearchService.SortOption.SEMANTIC_ALPHA; + default -> GrpcSearchService.SortOption.TOP_COMPONENT; + }; + boolean isSemanticMode = sortOption == GrpcSearchService.SortOption.SEMANTIC + || sortOption == GrpcSearchService.SortOption.SEMANTIC_ALPHA; + if (isSemanticMode) { + List results = + GrpcSearchService.get().searchFlat(grpcQuery, MAX_RESULT_SIZE, sortOption); + LOG.info("{} gRPC flat results returned for query: {} sortBy: {}", results.size(), grpcQuery, sortOption); + searchResultsListView.setCellFactory(lv -> new SearchCellGrpcSemanticResult()); + searchResultsListView.getItems().setAll(results); + } else { + List results = + GrpcSearchService.get().searchGrouped(grpcQuery, MAX_RESULT_SIZE, sortOption); + LOG.info("{} gRPC grouped results returned for query: {} sortBy: {}", results.size(), grpcQuery, sortOption); + searchResultsListView.setCellFactory(lv -> new SearchCellGrpcTopComponent()); + searchResultsListView.getItems().setAll(results); + } } else { List results = getViewProperties().calculator().search(queryText, MAX_RESULT_SIZE).toList(); LOG.info("{} search results returned for query: {}", results.size(), queryText); diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcSemanticResult.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcSemanticResult.java new file mode 100644 index 0000000000..e093bf6049 --- /dev/null +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcSemanticResult.java @@ -0,0 +1,120 @@ +/* + * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) + * + * Licensed 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 dev.ikm.komet.kview.mvvm.view.search; + +import dev.ikm.komet.framework.Identicon; +import dev.ikm.komet.grpc.GrpcSearchService; +import dev.ikm.tinkar.common.id.PublicIds; +import javafx.geometry.Insets; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +import java.util.UUID; + +/** + * A {@link ListCell} that renders a {@link GrpcSearchService.SemanticResult} using + * the same visual structure as the local-search {@code SearchCellDescriptionSemantic}, but + * driven entirely by data returned over gRPC (no local entity store required). + * + *

Uses raw {@link ListCell} (not typed) to avoid the JavaFX cell-reuse bug where defunct + * cells from a previous cell factory still receive item updates of the wrong type. + */ +@SuppressWarnings("rawtypes") +public class SearchCellGrpcSemanticResult extends ListCell { + + private final BorderPane root = new BorderPane(); + private final ImageView identiconView = new ImageView(); + private final Label retiredLabel = new Label("Retired"); + private final Text semanticText = new Text(); + private final TextFlow textFlow = new TextFlow(semanticText); + private final HBox retiredHBox = new HBox(); + + public SearchCellGrpcSemanticResult() { + identiconView.setFitHeight(24); + identiconView.setFitWidth(20); + identiconView.setPreserveRatio(true); + + retiredLabel.getStyleClass().add("search-retired-label"); + retiredLabel.setPadding(new Insets(6, 3, 6, 3)); + + textFlow.getStyleClass().add("semantic-text-container"); + textFlow.setPrefWidth(164); + semanticText.getStyleClass().add("search-semantic-entry-text"); + + retiredHBox.getStyleClass().add("search-entry-left-pane"); + retiredHBox.setMaxWidth(Double.MAX_VALUE); + retiredHBox.setPrefWidth(346); + retiredHBox.setPadding(new Insets(3, 3, 3, 3)); + + root.getStyleClass().add("search-entry-hbox"); + root.setPrefWidth(332); + root.setPrefHeight(30); + root.setCenter(retiredHBox); + + setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + } + + @Override + @SuppressWarnings("unchecked") + protected void updateItem(Object item, boolean empty) { + super.updateItem(item, empty); + if (item == null || empty || !(item instanceof GrpcSearchService.SemanticResult)) { + setGraphic(null); + return; + } + GrpcSearchService.SemanticResult result = (GrpcSearchService.SemanticResult) item; + + // Identicon from concept public UUIDs + if (result.publicId() != null && !result.publicId().isEmpty()) { + try { + UUID[] uuids = result.publicId().stream() + .map(UUID::fromString) + .toArray(UUID[]::new); + identiconView.setImage(Identicon.generateIdenticonImage(PublicIds.of(uuids))); + } catch (Exception e) { + identiconView.setImage(null); + } + } + + // Semantic text: strip HTML bold tags for the flat view + String text = result.highlightedText() != null && !result.highlightedText().isEmpty() + ? result.highlightedText().replaceAll("", "").replaceAll("\\s+", " ") + : result.fullyQualifiedName() != null ? result.fullyQualifiedName() : ""; + semanticText.setText(text); + + // Rebuild header row: identicon, optional retired badge, text + retiredHBox.getChildren().clear(); + HBox.setMargin(identiconView, new Insets(4, 0, 4, 6)); + retiredHBox.getChildren().add(identiconView); + if (!result.active()) { + HBox.setMargin(retiredLabel, new Insets(4, 0, 0, 0)); + retiredHBox.getChildren().add(retiredLabel); + } else { + textFlow.getStyleClass().add("search-semantic-active"); + } + HBox.setMargin(textFlow, new Insets(8, 36, 8, 2)); + retiredHBox.getChildren().add(textFlow); + + setGraphic(root); + } +} diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcTopComponent.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcTopComponent.java new file mode 100644 index 0000000000..31a9bc16ef --- /dev/null +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcTopComponent.java @@ -0,0 +1,179 @@ +/* + * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) + * + * Licensed 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 dev.ikm.komet.kview.mvvm.view.search; + +import dev.ikm.komet.framework.Identicon; +import dev.ikm.komet.grpc.GrpcSearchService; +import dev.ikm.tinkar.common.id.PublicIds; +import javafx.geometry.Insets; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.TitledPane; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +import java.util.List; +import java.util.UUID; + +/** + * A {@link ListCell} that renders a {@link GrpcSearchService.GroupedResult} using + * the same visual structure as the local-search {@code SearchCellTopComponent}, but driven + * entirely by data returned over gRPC (no local entity store required). + * + *

Uses raw {@link ListCell} (not typed) to avoid the JavaFX cell-reuse bug where defunct + * cells from a previous cell factory still receive item updates of the wrong type. + */ +@SuppressWarnings("rawtypes") +public class SearchCellGrpcTopComponent extends ListCell { + + private final VBox root = new VBox(); + private final ImageView identiconView = new ImageView(); + private final StackPane identiconContainer = new StackPane(identiconView); + private final Label retiredLabel = new Label("Retired"); + private final Text componentText = new Text(); + private final TextFlow componentTextFlow = new TextFlow(componentText); + private final HBox headerHBox = new HBox(11); + private final ListView descriptionsListView = new ListView<>(); + private final TitledPane titledPane = new TitledPane("DESCRIPTION SEMANTICS", descriptionsListView); + + public SearchCellGrpcTopComponent() { + identiconView.setFitHeight(20); + identiconView.setFitWidth(20); + identiconView.setPreserveRatio(true); + identiconContainer.getStyleClass().add("identicon-container"); + + retiredLabel.getStyleClass().add("search-retired-label"); + retiredLabel.setPadding(new Insets(6, 3, 6, 3)); + + componentTextFlow.getStyleClass().add("search-entry-text-container"); + componentTextFlow.setPrefWidth(253); + componentText.getStyleClass().add("search-entry-text"); + + headerHBox.getStyleClass().add("search-entry-header"); + headerHBox.setMaxWidth(Double.MAX_VALUE); + headerHBox.setPrefWidth(346); + + titledPane.setAnimated(false); + titledPane.setExpanded(false); + titledPane.getStyleClass().add("search-entry-title-pane"); + + descriptionsListView.setMaxHeight(426); + descriptionsListView.setFixedCellSize(40); + descriptionsListView.getStyleClass().add("descriptions-list-view"); + descriptionsListView.setCellFactory(lv -> new GrpcDescriptionCell()); + + BorderPane headerPane = new BorderPane(headerHBox); + root.getStyleClass().add("search-entry-container"); + root.setPrefWidth(332); + root.getChildren().addAll(headerPane, titledPane); + + setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + } + + @Override + @SuppressWarnings("unchecked") + protected void updateItem(Object item, boolean empty) { + super.updateItem(item, empty); + if (item == null || empty || !(item instanceof GrpcSearchService.GroupedResult)) { + setGraphic(null); + return; + } + GrpcSearchService.GroupedResult grpcItem = (GrpcSearchService.GroupedResult) item; + + // Identicon from public UUIDs + if (grpcItem.publicId() != null && !grpcItem.publicId().isEmpty()) { + try { + UUID[] uuids = grpcItem.publicId().stream() + .map(UUID::fromString) + .toArray(UUID[]::new); + identiconView.setImage(Identicon.generateIdenticonImage(PublicIds.of(uuids))); + } catch (Exception e) { + identiconView.setImage(null); + } + } + + // Concept name + componentText.setText(grpcItem.fullyQualifiedName()); + + // Active / Retired header row + headerHBox.getChildren().clear(); + headerHBox.getChildren().add(identiconContainer); + if (!grpcItem.active()) { + headerHBox.getChildren().add(retiredLabel); + } + headerHBox.getChildren().add(componentTextFlow); + + // Description semantics list + List matches = grpcItem.matchingSemantics(); + descriptionsListView.getItems().setAll(matches != null ? matches : List.of()); + int count = matches != null ? matches.size() : 0; + descriptionsListView.setPrefHeight(Math.min(count * 43.0, 426)); + + setGraphic(root); + } + + // Inner cell for each matching semantic entry + private static class GrpcDescriptionCell extends ListCell { + + private final HBox container = new HBox(); + private final TextFlow textFlow = new TextFlow(); + + public GrpcDescriptionCell() { + container.getChildren().add(textFlow); + container.getStyleClass().add("cell-container"); + textFlow.getStyleClass().add("text-container"); + setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + } + + @Override + protected void updateItem(GrpcSearchService.MatchingSemantic item, boolean empty) { + super.updateItem(item, empty); + if (item == null || empty) { + setGraphic(null); + return; + } + String text = item.highlightedText() != null && !item.highlightedText().isEmpty() + ? item.highlightedText() + : item.plainText() != null ? item.plainText() : ""; + updateTextFlow(text); + setGraphic(container); + } + + private void updateTextFlow(String highlighted) { + textFlow.getChildren().clear(); + String[] words = highlighted.split(" "); + for (String word : words) { + Text text = new Text(); + StackPane wordContainer = new StackPane(text); + if (word.contains("") || word.contains("")) { + text.setText(word.replaceAll("", "").replaceAll("\\s+", " ")); + wordContainer.getStyleClass().add("highlight"); + } else { + text.setText(word); + } + wordContainer.getStyleClass().add("word-container"); + textFlow.getChildren().add(wordContainer); + } + } + } +} diff --git a/kview/src/main/java/module-info.java b/kview/src/main/java/module-info.java index 3fde493a5a..1269b715cd 100644 --- a/kview/src/main/java/module-info.java +++ b/kview/src/main/java/module-info.java @@ -21,6 +21,7 @@ requires transitive dev.ikm.komet.framework; requires dev.ikm.komet.search; requires dev.ikm.tinkar.provider.search; + requires dev.ikm.komet.grpc.provider; requires dev.ikm.komet.navigator; requires dev.ikm.komet.classification; requires dev.ikm.komet.progress; From a96f713fed2e88ed2c9a4986e449620face5f099 Mon Sep 17 00:00:00 2001 From: Patrick Chou <1326306+pChou19@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:57:54 -0400 Subject: [PATCH 3/9] Remove gRPC specific components --- .../search/SearchPanelController.java | 6 +- .../view/search/NextGenSearchController.java | 30 ++- .../search/SearchCellDescriptionSemantic.java | 38 ++-- .../search/SearchCellGrpcSemanticResult.java | 120 ------------ .../search/SearchCellGrpcTopComponent.java | 179 ------------------ .../view/search/SearchCellTopComponent.java | 19 +- 6 files changed, 73 insertions(+), 319 deletions(-) delete mode 100644 kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcSemanticResult.java delete mode 100644 kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcTopComponent.java diff --git a/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java b/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java index 2b5a907a5d..ad75f08789 100644 --- a/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java +++ b/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java @@ -57,6 +57,7 @@ import java.util.List; import java.util.OptionalInt; import java.util.ResourceBundle; +import java.util.UUID; import java.util.function.Consumer; import java.util.function.Function; @@ -373,6 +374,9 @@ public String toString() { } } - public record NidTextRecord(int nid, String text, boolean active) { + public record NidTextRecord(int nid, String text, boolean active, List publicIds) { + public NidTextRecord(int nid, String text, boolean active) { + this(nid, text, active, List.of()); + } } } diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java index 3b7f8f4dd1..91450ffe10 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java @@ -50,6 +50,7 @@ import dev.ikm.tinkar.entity.EntityVersion; import dev.ikm.tinkar.entity.PatternEntity; import dev.ikm.tinkar.entity.SemanticEntity; +import dev.ikm.tinkar.entity.SemanticEntityVersion; import dev.ikm.tinkar.entity.StampEntity; import dev.ikm.tinkar.events.EvtBus; import dev.ikm.tinkar.events.EvtBusFactory; @@ -296,17 +297,38 @@ private void doSearch(ActionEvent actionEvent) { boolean isSemanticMode = sortOption == GrpcSearchService.SortOption.SEMANTIC || sortOption == GrpcSearchService.SortOption.SEMANTIC_ALPHA; if (isSemanticMode) { + setCurrentSearchResultType(SearchResultType.DESCRIPTION_SEMANTICS); List results = GrpcSearchService.get().searchFlat(grpcQuery, MAX_RESULT_SIZE, sortOption); LOG.info("{} gRPC flat results returned for query: {} sortBy: {}", results.size(), grpcQuery, sortOption); - searchResultsListView.setCellFactory(lv -> new SearchCellGrpcSemanticResult()); - searchResultsListView.getItems().setAll(results); + List converted = results.stream() + .map(r -> new LatestVersionSearchResult( + new Latest<>(SemanticEntityVersion.class), + 0, + r.score(), + r.highlightedText())) + .toList(); + searchResultsListView.getItems().setAll(converted); } else { + setCurrentSearchResultType(SearchResultType.TOP_COMPONENT); List results = GrpcSearchService.get().searchGrouped(grpcQuery, MAX_RESULT_SIZE, sortOption); LOG.info("{} gRPC grouped results returned for query: {} sortBy: {}", results.size(), grpcQuery, sortOption); - searchResultsListView.setCellFactory(lv -> new SearchCellGrpcTopComponent()); - searchResultsListView.getItems().setAll(results); + List>> entries = + results.stream().map(g -> { + List uuids = g.publicId().stream().map(UUID::fromString).toList(); + SearchPanelController.NidTextRecord key = + new SearchPanelController.NidTextRecord(0, g.fullyQualifiedName(), g.active(), uuids); + List semantics = g.matchingSemantics().stream() + .map(m -> new LatestVersionSearchResult( + new Latest<>(SemanticEntityVersion.class), + 0, + m.score(), + m.highlightedText())) + .toList(); + return Map.entry(key, semantics); + }).toList(); + searchResultsListView.getItems().setAll(entries); } } else { List results = getViewProperties().calculator().search(queryText, MAX_RESULT_SIZE).toList(); diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java index 89bc345652..ec544cf285 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java @@ -72,23 +72,33 @@ protected void updateItem(Object item, boolean empty) { setGraphic(null); } else { if (item instanceof LatestVersionSearchResult latestVersionSearchResult) { - SemanticEntityVersion semantic = latestVersionSearchResult.latestVersion().get(); - - controller.setIdenticon(Identicon.generateIdenticonImage(semantic.publicId())); - controller.setSemanticText(formatHighlightedString(latestVersionSearchResult.highlightedString())); - controller.setWindowView(observableViewNoOverride); - Entity entity = Entity.getConceptForSemantic(semantic.nid()).get(); - controller.setData(entity); - if (semantic.active()) { + if (latestVersionSearchResult.latestVersion().isPresent()) { + SemanticEntityVersion semantic = latestVersionSearchResult.latestVersion().get(); + + controller.setIdenticon(Identicon.generateIdenticonImage(semantic.publicId())); + controller.setSemanticText(formatHighlightedString(latestVersionSearchResult.highlightedString())); + controller.setWindowView(observableViewNoOverride); + Entity entity = Entity.getConceptForSemantic(semantic.nid()).get(); + controller.setData(entity); + if (semantic.active()) { + controller.getRetiredHBox().getChildren().remove(controller.getRetiredLabel()); + controller.increaseTextFlowWidth(); + } + + VBox.setMargin(content, new Insets(2, 0, 2, 0)); + + setUpDraggable(content, entity, getDragAndDropType(entity)); + + setGraphic(content); + } else { + // gRPC mode: no local entity, render text only + controller.setSemanticText(formatHighlightedString(latestVersionSearchResult.highlightedString())); + controller.setWindowView(observableViewNoOverride); controller.getRetiredHBox().getChildren().remove(controller.getRetiredLabel()); controller.increaseTextFlowWidth(); + VBox.setMargin(content, new Insets(2, 0, 2, 0)); + setGraphic(content); } - - VBox.setMargin(content, new Insets(2, 0, 2, 0)); - - setUpDraggable(content, entity, getDragAndDropType(entity)); - - setGraphic(content); } } } diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcSemanticResult.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcSemanticResult.java deleted file mode 100644 index e093bf6049..0000000000 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcSemanticResult.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) - * - * Licensed 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 dev.ikm.komet.kview.mvvm.view.search; - -import dev.ikm.komet.framework.Identicon; -import dev.ikm.komet.grpc.GrpcSearchService; -import dev.ikm.tinkar.common.id.PublicIds; -import javafx.geometry.Insets; -import javafx.scene.control.ContentDisplay; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.image.ImageView; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; - -import java.util.UUID; - -/** - * A {@link ListCell} that renders a {@link GrpcSearchService.SemanticResult} using - * the same visual structure as the local-search {@code SearchCellDescriptionSemantic}, but - * driven entirely by data returned over gRPC (no local entity store required). - * - *

Uses raw {@link ListCell} (not typed) to avoid the JavaFX cell-reuse bug where defunct - * cells from a previous cell factory still receive item updates of the wrong type. - */ -@SuppressWarnings("rawtypes") -public class SearchCellGrpcSemanticResult extends ListCell { - - private final BorderPane root = new BorderPane(); - private final ImageView identiconView = new ImageView(); - private final Label retiredLabel = new Label("Retired"); - private final Text semanticText = new Text(); - private final TextFlow textFlow = new TextFlow(semanticText); - private final HBox retiredHBox = new HBox(); - - public SearchCellGrpcSemanticResult() { - identiconView.setFitHeight(24); - identiconView.setFitWidth(20); - identiconView.setPreserveRatio(true); - - retiredLabel.getStyleClass().add("search-retired-label"); - retiredLabel.setPadding(new Insets(6, 3, 6, 3)); - - textFlow.getStyleClass().add("semantic-text-container"); - textFlow.setPrefWidth(164); - semanticText.getStyleClass().add("search-semantic-entry-text"); - - retiredHBox.getStyleClass().add("search-entry-left-pane"); - retiredHBox.setMaxWidth(Double.MAX_VALUE); - retiredHBox.setPrefWidth(346); - retiredHBox.setPadding(new Insets(3, 3, 3, 3)); - - root.getStyleClass().add("search-entry-hbox"); - root.setPrefWidth(332); - root.setPrefHeight(30); - root.setCenter(retiredHBox); - - setContentDisplay(ContentDisplay.GRAPHIC_ONLY); - } - - @Override - @SuppressWarnings("unchecked") - protected void updateItem(Object item, boolean empty) { - super.updateItem(item, empty); - if (item == null || empty || !(item instanceof GrpcSearchService.SemanticResult)) { - setGraphic(null); - return; - } - GrpcSearchService.SemanticResult result = (GrpcSearchService.SemanticResult) item; - - // Identicon from concept public UUIDs - if (result.publicId() != null && !result.publicId().isEmpty()) { - try { - UUID[] uuids = result.publicId().stream() - .map(UUID::fromString) - .toArray(UUID[]::new); - identiconView.setImage(Identicon.generateIdenticonImage(PublicIds.of(uuids))); - } catch (Exception e) { - identiconView.setImage(null); - } - } - - // Semantic text: strip HTML bold tags for the flat view - String text = result.highlightedText() != null && !result.highlightedText().isEmpty() - ? result.highlightedText().replaceAll("", "").replaceAll("\\s+", " ") - : result.fullyQualifiedName() != null ? result.fullyQualifiedName() : ""; - semanticText.setText(text); - - // Rebuild header row: identicon, optional retired badge, text - retiredHBox.getChildren().clear(); - HBox.setMargin(identiconView, new Insets(4, 0, 4, 6)); - retiredHBox.getChildren().add(identiconView); - if (!result.active()) { - HBox.setMargin(retiredLabel, new Insets(4, 0, 0, 0)); - retiredHBox.getChildren().add(retiredLabel); - } else { - textFlow.getStyleClass().add("search-semantic-active"); - } - HBox.setMargin(textFlow, new Insets(8, 36, 8, 2)); - retiredHBox.getChildren().add(textFlow); - - setGraphic(root); - } -} diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcTopComponent.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcTopComponent.java deleted file mode 100644 index 31a9bc16ef..0000000000 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellGrpcTopComponent.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) - * - * Licensed 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 dev.ikm.komet.kview.mvvm.view.search; - -import dev.ikm.komet.framework.Identicon; -import dev.ikm.komet.grpc.GrpcSearchService; -import dev.ikm.tinkar.common.id.PublicIds; -import javafx.geometry.Insets; -import javafx.scene.control.ContentDisplay; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.control.TitledPane; -import javafx.scene.image.ImageView; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; - -import java.util.List; -import java.util.UUID; - -/** - * A {@link ListCell} that renders a {@link GrpcSearchService.GroupedResult} using - * the same visual structure as the local-search {@code SearchCellTopComponent}, but driven - * entirely by data returned over gRPC (no local entity store required). - * - *

Uses raw {@link ListCell} (not typed) to avoid the JavaFX cell-reuse bug where defunct - * cells from a previous cell factory still receive item updates of the wrong type. - */ -@SuppressWarnings("rawtypes") -public class SearchCellGrpcTopComponent extends ListCell { - - private final VBox root = new VBox(); - private final ImageView identiconView = new ImageView(); - private final StackPane identiconContainer = new StackPane(identiconView); - private final Label retiredLabel = new Label("Retired"); - private final Text componentText = new Text(); - private final TextFlow componentTextFlow = new TextFlow(componentText); - private final HBox headerHBox = new HBox(11); - private final ListView descriptionsListView = new ListView<>(); - private final TitledPane titledPane = new TitledPane("DESCRIPTION SEMANTICS", descriptionsListView); - - public SearchCellGrpcTopComponent() { - identiconView.setFitHeight(20); - identiconView.setFitWidth(20); - identiconView.setPreserveRatio(true); - identiconContainer.getStyleClass().add("identicon-container"); - - retiredLabel.getStyleClass().add("search-retired-label"); - retiredLabel.setPadding(new Insets(6, 3, 6, 3)); - - componentTextFlow.getStyleClass().add("search-entry-text-container"); - componentTextFlow.setPrefWidth(253); - componentText.getStyleClass().add("search-entry-text"); - - headerHBox.getStyleClass().add("search-entry-header"); - headerHBox.setMaxWidth(Double.MAX_VALUE); - headerHBox.setPrefWidth(346); - - titledPane.setAnimated(false); - titledPane.setExpanded(false); - titledPane.getStyleClass().add("search-entry-title-pane"); - - descriptionsListView.setMaxHeight(426); - descriptionsListView.setFixedCellSize(40); - descriptionsListView.getStyleClass().add("descriptions-list-view"); - descriptionsListView.setCellFactory(lv -> new GrpcDescriptionCell()); - - BorderPane headerPane = new BorderPane(headerHBox); - root.getStyleClass().add("search-entry-container"); - root.setPrefWidth(332); - root.getChildren().addAll(headerPane, titledPane); - - setContentDisplay(ContentDisplay.GRAPHIC_ONLY); - } - - @Override - @SuppressWarnings("unchecked") - protected void updateItem(Object item, boolean empty) { - super.updateItem(item, empty); - if (item == null || empty || !(item instanceof GrpcSearchService.GroupedResult)) { - setGraphic(null); - return; - } - GrpcSearchService.GroupedResult grpcItem = (GrpcSearchService.GroupedResult) item; - - // Identicon from public UUIDs - if (grpcItem.publicId() != null && !grpcItem.publicId().isEmpty()) { - try { - UUID[] uuids = grpcItem.publicId().stream() - .map(UUID::fromString) - .toArray(UUID[]::new); - identiconView.setImage(Identicon.generateIdenticonImage(PublicIds.of(uuids))); - } catch (Exception e) { - identiconView.setImage(null); - } - } - - // Concept name - componentText.setText(grpcItem.fullyQualifiedName()); - - // Active / Retired header row - headerHBox.getChildren().clear(); - headerHBox.getChildren().add(identiconContainer); - if (!grpcItem.active()) { - headerHBox.getChildren().add(retiredLabel); - } - headerHBox.getChildren().add(componentTextFlow); - - // Description semantics list - List matches = grpcItem.matchingSemantics(); - descriptionsListView.getItems().setAll(matches != null ? matches : List.of()); - int count = matches != null ? matches.size() : 0; - descriptionsListView.setPrefHeight(Math.min(count * 43.0, 426)); - - setGraphic(root); - } - - // Inner cell for each matching semantic entry - private static class GrpcDescriptionCell extends ListCell { - - private final HBox container = new HBox(); - private final TextFlow textFlow = new TextFlow(); - - public GrpcDescriptionCell() { - container.getChildren().add(textFlow); - container.getStyleClass().add("cell-container"); - textFlow.getStyleClass().add("text-container"); - setContentDisplay(ContentDisplay.GRAPHIC_ONLY); - } - - @Override - protected void updateItem(GrpcSearchService.MatchingSemantic item, boolean empty) { - super.updateItem(item, empty); - if (item == null || empty) { - setGraphic(null); - return; - } - String text = item.highlightedText() != null && !item.highlightedText().isEmpty() - ? item.highlightedText() - : item.plainText() != null ? item.plainText() : ""; - updateTextFlow(text); - setGraphic(container); - } - - private void updateTextFlow(String highlighted) { - textFlow.getChildren().clear(); - String[] words = highlighted.split(" "); - for (String word : words) { - Text text = new Text(); - StackPane wordContainer = new StackPane(text); - if (word.contains("") || word.contains("")) { - text.setText(word.replaceAll("", "").replaceAll("\\s+", " ")); - wordContainer.getStyleClass().add("highlight"); - } else { - text.setText(word); - } - wordContainer.getStyleClass().add("word-container"); - textFlow.getChildren().add(wordContainer); - } - } - } -} diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java index 0c8d469a42..034deea887 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java @@ -4,6 +4,7 @@ import dev.ikm.komet.framework.search.SearchPanelController; import dev.ikm.komet.framework.view.ObservableViewNoOverride; import dev.ikm.komet.framework.view.ViewProperties; +import dev.ikm.tinkar.common.id.PublicIds; import dev.ikm.tinkar.coordinate.stamp.calculator.Latest; import dev.ikm.tinkar.coordinate.stamp.calculator.LatestVersionSearchResult; import dev.ikm.tinkar.entity.Entity; @@ -81,8 +82,9 @@ protected void updateItem(Object item, boolean empty) { } else { if (item instanceof Map.Entry) { Map.Entry> mapEntry = (Map.Entry>) item; + SearchPanelController.NidTextRecord nidTextRecord = mapEntry.getKey(); - int topNid = mapEntry.getKey().nid(); + int topNid = nidTextRecord.nid(); String topText = viewProperties.nodeView().calculator().getDescriptionTextOrNid(topNid); Latest latestTopVersion = viewProperties.nodeView().calculator().latest(topNid); if (latestTopVersion.isPresent()) { @@ -107,6 +109,21 @@ protected void updateItem(Object item, boolean empty) { setUpDraggable(parentPane, entity, CONCEPT); + setGraphic(parentPane); + } else if (!nidTextRecord.publicIds().isEmpty()) { + // gRPC mode: no local entity, render using data carried in NidTextRecord + UUID[] uuids = nidTextRecord.publicIds().toArray(new UUID[0]); + controller.setIdenticon(Identicon.generateIdenticonImage(PublicIds.of(uuids))); + controller.setWindowView(observableViewNoOverride); + controller.setData(null); + controller.setComponentText(nidTextRecord.text()); + controller.getDescriptionListViewItems().setAll(mapEntry.getValue()); + if (nidTextRecord.active()) { + controller.getRetiredHBox().getChildren().remove(controller.getRetiredLabel()); + } else if (!controller.getRetiredHBox().getChildren().contains(controller.getRetiredLabel())) { + controller.getRetiredHBox().getChildren().add(1, controller.getRetiredLabel()); + } + controller.setRetired(!nidTextRecord.active()); setGraphic(parentPane); } else { setGraphic(null); From 88e473732e4103a06f41f7ebd36d262309d54078 Mon Sep 17 00:00:00 2001 From: Patrick Chou <1326306+pChou19@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:56:17 -0400 Subject: [PATCH 4/9] Use GrpcSearchClient from tinkar core --- application/pom.xml | 10 +- .../src/main/java/dev/ikm/komet/app/App.java | 4 +- application/src/main/java/module-info.java | 2 +- grpc-provider/pom.xml | 266 +-------------- .../dev/ikm/komet/grpc/GrpcSearchClient.java | 104 ------ .../dev/ikm/komet/grpc/GrpcSearchService.java | 218 ------------ grpc-provider/src/main/java/module-info.java | 32 +- .../src/main/proto/tinkar_search.proto | 317 ------------------ kview/pom.xml | 4 +- .../view/search/NextGenSearchController.java | 2 +- kview/src/main/java/module-info.java | 2 +- pom.xml | 5 + 12 files changed, 19 insertions(+), 947 deletions(-) delete mode 100644 grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchClient.java delete mode 100644 grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchService.java delete mode 100644 grpc-provider/src/main/proto/tinkar_search.proto diff --git a/application/pom.xml b/application/pom.xml index d090034a1d..ee9a44de80 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -193,13 +193,7 @@ ${project.groupId} grpc-provider - + io.grpc grpc-stub @@ -330,7 +324,7 @@ pulls grpc-api onto the classpath; add-reads grants access. --> - + diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index a16dcbd5de..84478dc42a 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -22,8 +22,8 @@ import static dev.ikm.komet.app.AppState.SHUTDOWN; import static dev.ikm.komet.app.AppState.STARTING; import static dev.ikm.komet.app.LoginFeatureFlag.ENABLED_WEB_ONLY; -import dev.ikm.komet.grpc.GrpcSearchClient; -import dev.ikm.komet.grpc.GrpcSearchService; +import dev.ikm.tinkar.provider.grpc.GrpcSearchClient; +import dev.ikm.tinkar.provider.grpc.GrpcSearchService; import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; import static dev.ikm.komet.app.util.CssFile.KVIEW_CSS; import static dev.ikm.komet.app.util.CssUtils.addStylesheets; diff --git a/application/src/main/java/module-info.java b/application/src/main/java/module-info.java index 36f7e90820..a6d5eb58e3 100644 --- a/application/src/main/java/module-info.java +++ b/application/src/main/java/module-info.java @@ -75,7 +75,7 @@ requires jdk.management; requires dev.ikm.tinkar.reasoner.service; requires org.eclipse.jgit; - requires dev.ikm.komet.grpc.provider; + requires dev.ikm.tinkar.provider.grpc; // Logging related modules requires org.apache.logging.log4j.core; diff --git a/grpc-provider/pom.xml b/grpc-provider/pom.xml index 5af06e91f0..6d2bfd1e99 100644 --- a/grpc-provider/pom.xml +++ b/grpc-provider/pom.xml @@ -6,273 +6,11 @@ grpc-provider Komet gRPC Provider - - 1.78.0 - - 4.30.2 - - - - - io.grpc - grpc-netty-shaded - ${grpc.version} - runtime - - com.google.protobufprotobuf-java - com.google.code.findbugsjsr305 - javax.annotationjavax.annotation-api - - - - - - io.grpc - grpc-api - ${grpc.version} - provided - - com.google.protobufprotobuf-java - com.google.code.findbugsjsr305 - javax.annotationjavax.annotation-api - - - - io.grpc - grpc-stub - ${grpc.version} - provided - - com.google.protobufprotobuf-java - com.google.code.findbugsjsr305 - javax.annotationjavax.annotation-api - - - - io.grpc - grpc-protobuf - ${grpc.version} - provided - - com.google.protobufprotobuf-java - com.google.code.findbugsjsr305 - javax.annotationjavax.annotation-api - - - - - - dev.ikm.jpms - protobuf-java - - + dev.ikm.tinkar - tinkar-schema - - - - dev.ikm.jpms - jsr305 - - - - com.google.guava - guava - 33.1.0-jre - provided - - com.google.code.findbugsjsr305 - com.google.errorproneerror_prone_annotations - com.google.j2objcj2objc-annotations - com.google.guavalistenablefuture - com.google.code.findbugsjsr305 - org.checkerframeworkchecker-qual - - - - - com.google.errorprone - error_prone_annotations - 2.26.1 - provided - - - org.slf4j - slf4j-api - - - - dev.ikm.tinkar - search-provider - provided - - - dev.ikm.tinkar - common - provided + grpc-search-provider - - - - - - kr.motd.maven - os-maven-plugin - 1.7.1 - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - --add-reads - dev.ikm.komet.grpc.provider=ALL-UNNAMED - - - - - - - org.xolstice.maven.plugins - protobuf-maven-plugin - 0.6.1 - - com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} - grpc-java - io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} - - - ${project.basedir}/../../tinkar-schema - - - - - - compile - compile-custom - - - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.0 - - - package - - shade - - - - false - false - - - - io.grpc:grpc-api - io.grpc:grpc-stub - io.grpc:grpc-protobuf - io.grpc:grpc-protobuf-lite - com.google.guava:guava - com.google.guava:failureaccess - - - - - - io.grpc:grpc-api - META-INF/versions/*/module-info.class - - - io.grpc:grpc-stub - META-INF/versions/*/module-info.class - - - io.grpc:grpc-protobuf - META-INF/versions/*/module-info.class - - - io.grpc:grpc-protobuf-lite - META-INF/versions/*/module-info.class - - - com.google.guava:guava - META-INF/versions/*/module-info.class - - - - - - - - - - module-info.class - ${project.build.outputDirectory}/module-info.class - - - - - - - - diff --git a/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchClient.java b/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchClient.java deleted file mode 100644 index ea4b7bdeba..0000000000 --- a/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchClient.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) - * - * Licensed 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 dev.ikm.komet.grpc; - -import dev.ikm.tinkar.service.proto.SearchSortOption; -import dev.ikm.tinkar.service.proto.TinkarConceptSearchWithSortRequest; -import dev.ikm.tinkar.service.proto.TinkarConceptSearchWithSortResponse; -import dev.ikm.tinkar.service.proto.TinkarSearchServiceGrpc; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.TimeUnit; - -/** - * Manages a gRPC channel to a running tinkar-core service and exposes - * concept-search operations. Configured via system properties: - *

    - *
  • {@code komet.grpc.host} – hostname (default: {@code localhost})
  • - *
  • {@code komet.grpc.port} – port number (default: {@code 9090})
  • - *
- * Call {@link #initialize(String, int)} once at startup, then access via {@link #get()}. - */ -public class GrpcSearchClient implements AutoCloseable { - - private static final Logger LOG = LoggerFactory.getLogger(GrpcSearchClient.class); - - private static volatile GrpcSearchClient instance; - - private final ManagedChannel channel; - private final TinkarSearchServiceGrpc.TinkarSearchServiceBlockingStub stub; - - private GrpcSearchClient(String host, int port) { - this.channel = ManagedChannelBuilder.forAddress(host, port) - .usePlaintext() - .build(); - this.stub = TinkarSearchServiceGrpc.newBlockingStub(channel); - LOG.info("gRPC client initialised → {}:{}", host, port); - } - - /** - * Creates and registers the singleton client. - * - * @param host gRPC server hostname - * @param port gRPC server port - */ - public static void initialize(String host, int port) { - instance = new GrpcSearchClient(host, port); - } - - /** Returns {@code true} when the client has been initialised. */ - public static boolean isAvailable() { - return instance != null; - } - - /** Returns the singleton client, or {@code null} if not yet initialised. */ - public static GrpcSearchClient get() { - return instance; - } - - /** - * Calls {@code TinkarSearchService.ConceptSearchWithSort} on the remote service. - * - * @param query free-text search string - * @param maxResults maximum number of results to return - * @param sortBy sort order for results - * @return the response from the server - */ - public TinkarConceptSearchWithSortResponse conceptSearchWithSort( - String query, int maxResults, SearchSortOption sortBy) { - - TinkarConceptSearchWithSortRequest request = TinkarConceptSearchWithSortRequest.newBuilder() - .setQuery(query) - .setMaxResults(maxResults) - .setSortBy(sortBy) - .build(); - return stub.conceptSearchWithSort(request); - } - - @Override - public void close() { - try { - channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); - LOG.info("gRPC channel shut down"); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - channel.shutdownNow(); - } - } -} diff --git a/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchService.java b/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchService.java deleted file mode 100644 index 0294580a34..0000000000 --- a/grpc-provider/src/main/java/dev/ikm/komet/grpc/GrpcSearchService.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) - * - * Licensed 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 dev.ikm.komet.grpc; - -import dev.ikm.tinkar.common.service.PrimitiveDataSearchResult; -import dev.ikm.tinkar.provider.search.SearchService; -import dev.ikm.tinkar.service.proto.SearchSortOption; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -/** - * Implementation of {@link SearchService} that delegates search calls to a remote - * tinkar-core gRPC service via {@link GrpcSearchClient}. - * - *

This is the plug-in point for gRPC-backed search in Komet. When Komet is started - * with {@code -Dkomet.grpc.port}, this service is activated via {@link #initialize}. - * Downstream code (e.g., {@code NextGenSearchController}) checks {@link #isActive()} and - * calls {@link #searchGrouped} or {@link #searchFlat} instead of the local Lucene path. - * - *

The {@link #search} method satisfies the {@link SearchService} contract but returns - * an empty array — all meaningful results come through the typed methods that carry - * grouped/semantic structure back from the service. - */ -public class GrpcSearchService implements SearchService { - - private static final Logger LOG = LoggerFactory.getLogger(GrpcSearchService.class); - - private static volatile GrpcSearchService INSTANCE; - - /** - * Sort options that mirror the UI sort buttons and map 1-to-1 to the - * proto {@link SearchSortOption} enum. - */ - public enum SortOption { - TOP_COMPONENT, - TOP_COMPONENT_ALPHA, - SEMANTIC, - SEMANTIC_ALPHA - } - - /** - * A top-level (grouped) search result — one per matching concept. - * - * @param publicId stable UUIDs identifying the concept - * @param fullyQualifiedName FQN of the concept - * @param active whether the concept is currently active - * @param topScore highest relevance score among child semantics - * @param matchingSemantics child semantic matches - */ - public record GroupedResult( - List publicId, - String fullyQualifiedName, - boolean active, - float topScore, - List matchingSemantics) {} - - /** - * A flat semantic search result (SEMANTIC sort modes) — one per matched semantic. - * - * @param publicId stable UUIDs identifying the concept - * @param fullyQualifiedName FQN of the concept - * @param highlightedText matched text with {@code } markup - * @param active whether the concept is currently active - * @param score relevance score - */ - public record SemanticResult( - List publicId, - String fullyQualifiedName, - String highlightedText, - boolean active, - float score) {} - - /** - * A single semantic match within a {@link GroupedResult}. - * - * @param highlightedText matched text with {@code } markup - * @param plainText plain text without HTML markup - * @param score relevance score - */ - public record MatchingSemantic(String highlightedText, String plainText, float score) {} - - private GrpcSearchService() {} - - /** - * Activates gRPC search mode by initializing the underlying {@link GrpcSearchClient}. - * Must be called once at startup before any search calls. - */ - public static void initialize(String host, int port) { - GrpcSearchClient.initialize(host, port); - INSTANCE = new GrpcSearchService(); - LOG.info("GrpcSearchService initialized → {}:{}", host, port); - } - - /** - * Returns {@code true} when gRPC mode has been initialized and is ready. - */ - public static boolean isActive() { - return INSTANCE != null && GrpcSearchClient.isAvailable(); - } - - /** - * Returns the active singleton, or throws if not initialized. - */ - public static GrpcSearchService get() { - if (INSTANCE == null) { - throw new IllegalStateException("GrpcSearchService not initialized — pass -Dkomet.grpc.port to activate"); - } - return INSTANCE; - } - - /** - * Performs a search returning grouped results (TOP_COMPONENT modes). - * The sort option controls both ordering and grouping behaviour server-side. - */ - public List searchGrouped(String query, int maxResults, SortOption sortOption) { - SearchSortOption protoSort = toProtoSort(sortOption); - var response = GrpcSearchClient.get().conceptSearchWithSort(query, maxResults, protoSort); - return response.getGroupedResultsList().stream() - .map(g -> new GroupedResult( - g.getPublicIdList(), - g.getFullyQualifiedName(), - g.getActive(), - g.getTopScore(), - g.getMatchingSemanticsList().stream() - .map(m -> new MatchingSemantic( - m.getHighlightedText(), m.getPlainText(), m.getScore())) - .toList())) - .toList(); - } - - /** - * Performs a search returning flat semantic results (SEMANTIC modes). - */ - public List searchFlat(String query, int maxResults, SortOption sortOption) { - SearchSortOption protoSort = toProtoSort(sortOption); - var response = GrpcSearchClient.get().conceptSearchWithSort(query, maxResults, protoSort); - return response.getResultsList().stream() - .map(r -> new SemanticResult( - r.getPublicIdList(), - r.getFullyQualifiedName(), - r.getHighlightedText(), - r.getActive(), - r.getScore())) - .toList(); - } - - // --- SearchService contract --- - - /** - * Not meaningful in gRPC mode — indexing is handled server-side. - */ - @Override - public void index(Object object) { - LOG.debug("GrpcSearchService.index() called — no-op in gRPC mode"); - } - - /** - * Not meaningful in gRPC mode — index commits are handled server-side. - */ - @Override - public void commit() throws IOException { - LOG.debug("GrpcSearchService.commit() called — no-op in gRPC mode"); - } - - /** - * Satisfies the {@link SearchService} contract. Returns raw Lucene-style results by - * delegating to {@link #searchFlat} and converting to {@link PrimitiveDataSearchResult}. - * NIDs are 0 since the local entity store is ephemeral; callers that need rich display - * should use {@link #searchGrouped} or {@link #searchFlat} directly. - */ - @Override - public PrimitiveDataSearchResult[] search(String query, int maxResultSize) { - List flat = searchFlat(query, maxResultSize, SortOption.SEMANTIC); - return flat.stream() - .map(r -> new PrimitiveDataSearchResult(0, 0, 0, 0, r.score(), r.highlightedText())) - .toArray(PrimitiveDataSearchResult[]::new); - } - - /** - * Not meaningful in gRPC mode — index recreation is handled server-side. - */ - @Override - public CompletableFuture recreateIndex() { - LOG.debug("GrpcSearchService.recreateIndex() called — no-op in gRPC mode"); - return CompletableFuture.completedFuture(null); - } - - @Override - public String name() { - return "GrpcSearchService"; - } - - private static SearchSortOption toProtoSort(SortOption sortOption) { - return switch (sortOption) { - case TOP_COMPONENT -> SearchSortOption.TOP_COMPONENT; - case TOP_COMPONENT_ALPHA -> SearchSortOption.TOP_COMPONENT_ALPHA; - case SEMANTIC -> SearchSortOption.SEMANTIC; - case SEMANTIC_ALPHA -> SearchSortOption.SEMANTIC_ALPHA; - }; - } -} diff --git a/grpc-provider/src/main/java/module-info.java b/grpc-provider/src/main/java/module-info.java index 10f76a3892..90a036b940 100644 --- a/grpc-provider/src/main/java/module-info.java +++ b/grpc-provider/src/main/java/module-info.java @@ -15,35 +15,9 @@ */ /** - * gRPC client provider for Komet. Provides {@link dev.ikm.komet.grpc.GrpcSearchClient} - * which connects to a running tinkar-core service and delegates concept search over gRPC. - * - *

The gRPC runtime libraries (grpc-api, grpc-stub, grpc-protobuf, guava) are shaded - * into this jar by maven-shade-plugin so that jlink sees a single named module rather - * than a mix of automatic modules. - * - *

grpc-netty-shaded (the transport) remains on the classpath as a runtime-only - * automatic module and is discovered at runtime via ServiceLoader. + * Thin re-export shim. All gRPC search logic lives in + * {@code dev.ikm.tinkar.provider.grpc} (tinkar-core). */ module dev.ikm.komet.grpc.provider { - - exports dev.ikm.komet.grpc; - // Generated proto/gRPC stub classes (used from App.java to build requests) - exports dev.ikm.tinkar.service.proto; - - // Protobuf runtime — JPMS-wrapped; provides com.google.protobuf.* packages - requires dev.ikm.jpms.protobuf; - - // Generated schema message classes from Tinkar.proto - requires dev.ikm.tinkar.schema; - - // javax.annotation — JPMS-wrapped jsr305 (module: dev.ikm.jpms.javax.annotation) - requires dev.ikm.jpms.javax.annotation; - - // SearchService contract from tinkar-core search-provider - requires dev.ikm.tinkar.provider.search; - // PrimitiveDataSearchResult lives in tinkar-core common - requires dev.ikm.tinkar.common; - - requires org.slf4j; + requires transitive dev.ikm.tinkar.provider.grpc; } diff --git a/grpc-provider/src/main/proto/tinkar_search.proto b/grpc-provider/src/main/proto/tinkar_search.proto deleted file mode 100644 index 07ea6ca8ff..0000000000 --- a/grpc-provider/src/main/proto/tinkar_search.proto +++ /dev/null @@ -1,317 +0,0 @@ -// DEPRECATED: This service is maintained for backward compatibility only. -// New clients should use: -// - ike_graph_rag.proto (Tier 1: IkeGraphRAG service) -// - ike_knowledge_graph.proto (Tier 2: IkeKnowledgeGraph service) - -syntax = "proto3"; - -package dev.ikm.tinkar.service; - -import "Tinkar.proto"; - -option java_multiple_files = true; -option java_package = "dev.ikm.tinkar.service.proto"; -option java_outer_classname = "TinkarSearchProto"; - -// Search query request -message TinkarSearchQueryRequest { - string query = 1; -} - -// Concept search request -message TinkarConceptSearchRequest { - string query = 1; - /* - * Maximum number of results to return. If not specified or <= 0, uses server default (100). - */ - int32 max_results = 2; -} - -// Concept ID request for single ID lookups -message TinkarConceptIdRequest { - /* - * The public ID of the concept (supports multiple UUIDs). - */ - dev.ikm.tinkar.schema.PublicId public_id = 1; -} - -// Tinkar search result representing a concept with its descriptions and STAMP info -message TinkarSearchResult { - /* - * The public ID of the concept (supports multiple UUIDs). - */ - dev.ikm.tinkar.schema.PublicId public_id = 1; - - /* - * The descriptions associated with this concept. - */ - TinkarConceptDescriptions descriptions = 2; - - /* - * The STAMP version info for this concept (status, author, module, path, time). - */ - dev.ikm.tinkar.schema.StampVersion stamp = 3; -} - -// Descriptions associated with a concept -message TinkarConceptDescriptions { - /* - * The fully qualified name of the concept. - */ - string fully_qualified_name = 1; - /* - * The regular/preferred description text of the concept. - */ - string regular_name = 2; - /* - * Additional definition or description text. - */ - string definition = 3; -} - -// Search query response -message TinkarSearchQueryResponse { - string query = 1; - int64 total_count = 2; - repeated TinkarSearchResult results = 3; - bool success = 4; - string error_message = 5; -} - -// Rebuild index response -message TinkarRebuildIndexResponse { - string message = 1; - bool success = 2; -} - -// Empty request for rebuild index -message TinkarRebuildIndexRequest { -} - -// Sort options for search results -enum SearchSortOption { - TOP_COMPONENT = 0; // Group by top component, sorted by relevance score (highest first) - TOP_COMPONENT_ALPHA = 1; // Group by top component, sorted alphabetically - SEMANTIC = 2; // Individual semantic matches, sorted by relevance score (highest first) - SEMANTIC_ALPHA = 3; // Individual semantic matches, sorted alphabetically -} - -// Concept search with sort request -message TinkarConceptSearchWithSortRequest { - string query = 1; - /* - * Maximum number of results to return. If not specified or <= 0, uses server default (100). - */ - int32 max_results = 2; - /* - * Sort option for results. If not specified, uses TOP_COMPONENT. - */ - SearchSortOption sort_by = 3; -} - -// Individual semantic search result with score -message TinkarSemanticSearchResult { - /* - * Public ID (list of UUIDs) of the concept. - */ - repeated string public_id = 1; - /* - * Fully qualified name of the concept. - */ - string fully_qualified_name = 2; - /* - * Regular/preferred description. - */ - string regular_name = 3; - /* - * The matched text with highlighting (HTML bold tags). - */ - string highlighted_text = 4; - /* - * The relevance score of this match. - */ - float score = 5; - /* - * Whether the concept is active. - */ - bool active = 6; -} - -// A matching semantic within a grouped result -message TinkarMatchingSemantic { - /* - * The matched text with highlighting (HTML bold tags). - */ - string highlighted_text = 1; - /* - * The plain text without highlighting. - */ - string plain_text = 2; - /* - * The relevance score of this match. - */ - float score = 3; - /* - * The index of the matched field within the semantic. - */ - int32 field_index = 4; - /* - * The NID of the matching semantic entity. - */ - int32 semantic_nid = 5; -} - -// Grouped search result by top-level component -message TinkarGroupedSearchResult { - /* - * Public ID (list of UUIDs) of the top-level concept. - */ - repeated string public_id = 1; - /* - * Fully qualified name of the top-level concept. - */ - string fully_qualified_name = 2; - /* - * Whether the concept is active. - */ - bool active = 3; - /* - * The highest relevance score among matching semantics. - */ - float top_score = 4; - /* - * List of matching semantics for this concept. - */ - repeated TinkarMatchingSemantic matching_semantics = 5; - /* - * The NID of the top-level concept entity. - */ - int32 concept_nid = 6; -} - -// Response for concept search with sort -message TinkarConceptSearchWithSortResponse { - /* - * The original search query. - */ - string query = 1; - /* - * Total number of matching results. - */ - int64 total_count = 2; - /* - * The sort option used. - */ - SearchSortOption sort_by = 3; - /* - * List of search results (flat structure for SEMANTIC modes). - */ - repeated TinkarSemanticSearchResult results = 4; - /* - * List of grouped results (for TOP_COMPONENT modes). - */ - repeated TinkarGroupedSearchResult grouped_results = 5; - /* - * Whether the search was successful. - */ - bool success = 6; - /* - * Error message if search failed. - */ - string error_message = 7; -} - -// Human-readable STAMP information for display purposes -message TinkarStampInfo { - /* - * Status (e.g., Active, Inactive). - */ - string status = 1; - /* - * Author name. - */ - string author = 2; - /* - * Module name. - */ - string module = 3; - /* - * Path name. - */ - string path = 4; - /* - * Timestamp in epoch milliseconds. - */ - int64 time = 5; - /* - * Human-readable timestamp. - */ - string formatted_time = 6; -} - -// Information about a single semantic attached to a concept -message TinkarConceptSemanticInfo { - /* - * Semantic public ID. - */ - dev.ikm.tinkar.schema.PublicId semantic_public_id = 1; - /* - * Pattern public ID. - */ - dev.ikm.tinkar.schema.PublicId pattern_public_id = 2; - /* - * Pattern name (human-readable, e.g., Comment Pattern, Description Pattern). - */ - string pattern_name = 3; - /* - * Field values of the semantic (using Tinkar Field type). - */ - repeated dev.ikm.tinkar.schema.Field fields = 4; - /* - * Human-readable STAMP information. - */ - TinkarStampInfo stamp = 5; -} - -// Response for concept semantics -message TinkarConceptSemanticsResponse { - /* - * The concept public ID. - */ - dev.ikm.tinkar.schema.PublicId concept_public_id = 1; - /* - * The concept description (human-readable). - */ - string concept_description = 2; - /* - * Total number of semantics found. - */ - int32 total_count = 3; - /* - * List of semantics attached to this concept. - */ - repeated TinkarConceptSemanticInfo semantics = 4; - /* - * Whether the query was successful. - */ - bool success = 5; - /* - * Error message if query failed. - */ - string error_message = 6; -} - -// gRPC service definition -service TinkarSearchService { - rpc Search(TinkarSearchQueryRequest) returns (TinkarSearchQueryResponse); - rpc ConceptSearch(TinkarConceptSearchRequest) returns (TinkarSearchQueryResponse); - rpc ConceptSearchWithSort(TinkarConceptSearchWithSortRequest) returns (TinkarConceptSearchWithSortResponse); - rpc GetEntity(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); - rpc GetChildConcepts(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); - rpc GetDescendantConcepts(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); - rpc GetLIDRRecordConceptsFromTestKit(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); - rpc GetResultConformanceConceptsFromLIDRRecord(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); - rpc GetAllowedResultConceptsFromResultConformance(TinkarConceptIdRequest) returns (TinkarSearchQueryResponse); - rpc RebuildSearchIndex(TinkarRebuildIndexRequest) returns (TinkarRebuildIndexResponse); - rpc GetConceptSemantics(TinkarConceptIdRequest) returns (TinkarConceptSemanticsResponse); -} diff --git a/kview/pom.xml b/kview/pom.xml index 59cd19bedb..88005ffe43 100644 --- a/kview/pom.xml +++ b/kview/pom.xml @@ -25,8 +25,8 @@ search-provider - ${project.groupId} - grpc-provider + dev.ikm.tinkar + grpc-search-provider dev.ikm.tinkar diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java index 91450ffe10..51ebe6d9e7 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java @@ -29,7 +29,7 @@ import dev.ikm.komet.framework.dnd.DragImageMaker; import dev.ikm.komet.framework.dnd.KometClipboard; import dev.ikm.komet.framework.search.SearchPanelController; -import dev.ikm.komet.grpc.GrpcSearchService; +import dev.ikm.tinkar.provider.grpc.GrpcSearchService; import dev.ikm.komet.framework.view.ViewProperties; import dev.ikm.komet.kview.controls.AutoCompleteTextField; import dev.ikm.komet.kview.controls.FilterOptionsPopup; diff --git a/kview/src/main/java/module-info.java b/kview/src/main/java/module-info.java index 1269b715cd..2c24c124e7 100644 --- a/kview/src/main/java/module-info.java +++ b/kview/src/main/java/module-info.java @@ -21,7 +21,7 @@ requires transitive dev.ikm.komet.framework; requires dev.ikm.komet.search; requires dev.ikm.tinkar.provider.search; - requires dev.ikm.komet.grpc.provider; + requires dev.ikm.tinkar.provider.grpc; requires dev.ikm.komet.navigator; requires dev.ikm.komet.classification; requires dev.ikm.komet.progress; diff --git a/pom.xml b/pom.xml index 655ed79326..7dfefd89bf 100644 --- a/pom.xml +++ b/pom.xml @@ -100,6 +100,11 @@ grpc-provider ${project.version} + + dev.ikm.tinkar + grpc-search-provider + 1.127.2-SNAPSHOT + From ac42f29f019662449017cf3b2d7197f39f7bd530 Mon Sep 17 00:00:00 2001 From: Patrick Chou <1326306+pChou19@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:36:14 -0400 Subject: [PATCH 5/9] Get concept details from gRPC --- .../ObservableLanguageCoordinateBase.java | 5 ++ .../view/ObservableStampCoordinateBase.java | 5 ++ .../kview/controls/FilterOptionsUtils.java | 12 ++++- .../skin/KLConceptNavigatorTreeViewSkin.java | 17 ++++--- .../EditDescriptionFormController.java | 6 +++ .../EditFullyQualifiedNameController.java | 12 ++++- .../view/search/SearchCellTopComponent.java | 1 + .../SortResultConceptEntryController.java | 47 +++++++++++++++++++ 8 files changed, 95 insertions(+), 10 deletions(-) diff --git a/framework/src/main/java/dev/ikm/komet/framework/view/ObservableLanguageCoordinateBase.java b/framework/src/main/java/dev/ikm/komet/framework/view/ObservableLanguageCoordinateBase.java index e152ecc1a2..c49deba509 100644 --- a/framework/src/main/java/dev/ikm/komet/framework/view/ObservableLanguageCoordinateBase.java +++ b/framework/src/main/java/dev/ikm/komet/framework/view/ObservableLanguageCoordinateBase.java @@ -141,6 +141,11 @@ private void dialectPatternPreferenceListChanged(ListChangeListener.Change observable, ConceptFacade oldLanguageConcept, ConceptFacade newLanguageConcept) { + if (newLanguageConcept == null) { + // Can occur when Entity.getFast() returns null in gRPC/ephemeral-store mode + // (language entity not yet loaded). Retain the existing coordinate value. + return; + } this.setValue(LanguageCoordinateRecord.make(newLanguageConcept.nid(), descriptionPatternPreferenceNidList(), descriptionTypePreferenceNidList(), diff --git a/framework/src/main/java/dev/ikm/komet/framework/view/ObservableStampCoordinateBase.java b/framework/src/main/java/dev/ikm/komet/framework/view/ObservableStampCoordinateBase.java index f917aec04a..11ecb095fb 100644 --- a/framework/src/main/java/dev/ikm/komet/framework/view/ObservableStampCoordinateBase.java +++ b/framework/src/main/java/dev/ikm/komet/framework/view/ObservableStampCoordinateBase.java @@ -118,6 +118,11 @@ private void timeChanged(ObservableValue observable, Number ol private void pathConceptChanged(ObservableValue observablePathConcept, ConceptFacade oldPathConcept, ConceptFacade newPathConcept) { + if (newPathConcept == null) { + // Can occur when Entity.getFast() returns null in gRPC/ephemeral-store mode + // (path entity not yet loaded). Retain the existing coordinate value. + return; + } this.setValue(StampCoordinateRecord.make(allowedStates(), StampPositionRecord.make(timeProperty.longValue(), newPathConcept.nid()), moduleNids(), diff --git a/kview/src/main/java/dev/ikm/komet/kview/controls/FilterOptionsUtils.java b/kview/src/main/java/dev/ikm/komet/kview/controls/FilterOptionsUtils.java index c84a944f3f..0abcb9e4c0 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/controls/FilterOptionsUtils.java +++ b/kview/src/main/java/dev/ikm/komet/kview/controls/FilterOptionsUtils.java @@ -159,7 +159,10 @@ public void subscribeFilterOptionsToView(FilterOptions filterOptions, Observable mainCoordinates.getPath().selectedOptions().clear(); mainCoordinates.getPath().selectedOptions().addAll(path); } - observableViewForFilterProperty.stampCoordinate().pathConceptProperty().set(path); + // Guard against null: Entity.getFast() returns null in gRPC/ephemeral-store mode. + if (path != null) { + observableViewForFilterProperty.stampCoordinate().pathConceptProperty().set(path); + } fromView = false; })); @@ -187,7 +190,12 @@ public void subscribeFilterOptionsToView(FilterOptions filterOptions, Observable } else { observableViewForFilterProperty.languageCoordinates().getFirst().dialectPatternPreferenceListProperty().clear(); } - observableViewForFilterProperty.languageCoordinates().getFirst().languageConceptProperty().set(lang); + // Guard against null: Entity.getFast() returns null in gRPC/ephemeral-store mode + // when the language entity hasn't been loaded yet. Passing null to set() triggers + // a NPE in ObservableLanguageCoordinateBase.languageConceptChanged. + if (lang != null) { + observableViewForFilterProperty.languageCoordinates().getFirst().languageConceptProperty().set(lang); + } fromView = false; })); diff --git a/kview/src/main/java/dev/ikm/komet/kview/controls/skin/KLConceptNavigatorTreeViewSkin.java b/kview/src/main/java/dev/ikm/komet/kview/controls/skin/KLConceptNavigatorTreeViewSkin.java index eb25411f51..4e7780c655 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/controls/skin/KLConceptNavigatorTreeViewSkin.java +++ b/kview/src/main/java/dev/ikm/komet/kview/controls/skin/KLConceptNavigatorTreeViewSkin.java @@ -892,12 +892,17 @@ private class ModifiedEntitySubscriber { protected void invalidated() { EntityFacade entityFacade = get(); if (entityFacade != null) { - InvertedTree newInvertedTree = ConceptNavigatorUtils.buildInvertedTree(entityFacade.nid(), treeView.getNavigator()); - newInvertedTree.compareTo(oldInvertedTree).ifPresent(item -> { - expandConcept(item, false); - setValue(null); - }); - oldInvertedTree = newInvertedTree; + try { + InvertedTree newInvertedTree = ConceptNavigatorUtils.buildInvertedTree(entityFacade.nid(), treeView.getNavigator()); + newInvertedTree.compareTo(oldInvertedTree).ifPresent(item -> { + expandConcept(item, false); + setValue(null); + }); + oldInvertedTree = newInvertedTree; + } catch (Exception e) { + LOG.warn("Could not build inverted tree for nid {} — ancestor entity may be absent (gRPC mode): {}", + entityFacade.nid(), e.getMessage()); + } } else { oldInvertedTree = null; } diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditDescriptionFormController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditDescriptionFormController.java index 84f5e54a27..5ee86f8b68 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditDescriptionFormController.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditDescriptionFormController.java @@ -200,6 +200,12 @@ private void populateDialectComboBoxes() { Entity acceptable = EntityService.get().getEntityFast(TinkarTerm.ACCEPTABLE); Entity preferred = EntityService.get().getEntityFast(TinkarTerm.PREFERRED); + // In gRPC read-only mode the ephemeral entity store may not contain vocabulary meta-concepts; + // skip dialect population rather than throwing NPE. + if (acceptable == null || preferred == null) { + return; + } + // each combo box has a separate list instance setupComboBox(dialectComboBox1, Arrays.asList(Entity.getFast(acceptable.nid()), Entity.getFast(preferred.nid()))); dialectComboBox1.getSelectionModel().select(Entity.getFast(acceptable.nid())); diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditFullyQualifiedNameController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditFullyQualifiedNameController.java index 4b731249fb..fd616ad29d 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditFullyQualifiedNameController.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditFullyQualifiedNameController.java @@ -183,8 +183,16 @@ private void validateForm() { private void populateDialectComboBoxes() { // Get acceptable and preferred concepts - ConceptEntity acceptable = EntityHandle.getConceptOrThrow(TinkarTerm.ACCEPTABLE.nid()); - ConceptEntity preferred = EntityHandle.getConceptOrThrow(TinkarTerm.PREFERRED.nid()); + // In gRPC read-only mode the ephemeral entity store may not contain vocabulary meta-concepts; + // skip dialect population rather than throwing. + ConceptEntity acceptable; + ConceptEntity preferred; + try { + acceptable = EntityHandle.getConceptOrThrow(TinkarTerm.ACCEPTABLE.nid()); + preferred = EntityHandle.getConceptOrThrow(TinkarTerm.PREFERRED.nid()); + } catch (Exception e) { + return; + } // each combo box has a separate list instance setupComboBox(dialectComboBox1, Arrays.asList(acceptable, preferred)); diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java index 034deea887..361017f4c6 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java @@ -116,6 +116,7 @@ protected void updateItem(Object item, boolean empty) { controller.setIdenticon(Identicon.generateIdenticonImage(PublicIds.of(uuids))); controller.setWindowView(observableViewNoOverride); controller.setData(null); + controller.setGrpcPublicIds(nidTextRecord.publicIds()); controller.setComponentText(nidTextRecord.text()); controller.getDescriptionListViewItems().setAll(mapEntry.getValue()); if (nidTextRecord.active()) { diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java index 4e16890491..541b716e21 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java @@ -36,7 +36,11 @@ import dev.ikm.tinkar.entity.PatternEntity; import dev.ikm.tinkar.events.EvtBus; import dev.ikm.tinkar.events.EvtBusFactory; +import dev.ikm.tinkar.provider.grpc.GrpcSearchService; import dev.ikm.tinkar.terms.EntityFacade; +import javafx.application.Platform; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.ActionEvent; @@ -58,10 +62,12 @@ import org.carlfx.cognitive.viewmodel.SimpleViewModel; import org.carlfx.cognitive.viewmodel.ViewModel; +import java.util.List; import java.util.UUID; public class SortResultConceptEntryController extends AbstractBasicController { + private static final Logger LOG = LoggerFactory.getLogger(SortResultConceptEntryController.class); private static final int LIST_VIEW_CELL_SIZE = 40; @FXML @@ -91,6 +97,9 @@ public class SortResultConceptEntryController extends AbstractBasicController { private Entity entity; + /** Public UUIDs carried from a gRPC search result when no local entity is available. */ + private List grpcPublicIds; + private ObservableViewNoOverride windowView; @InjectViewModel @@ -112,6 +121,10 @@ public void initialize() { conceptEntity)); } else if (entity instanceof PatternEntity patternEntity) { eventBus.publish(searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC), new MakePatternWindowEvent(this, MakePatternWindowEvent.OPEN_PATTERN, patternEntity, getViewProperties())); + } else if (grpcPublicIds != null && !grpcPublicIds.isEmpty()) { + // gRPC mode: fetch full entity graph from server, load into ephemeral store, + // then open the concept window as normal. + openGrpcConcept(); } } } @@ -185,6 +198,40 @@ public void setData(Entity entity) { this.entity = entity; } + /** + * Sets the public UUIDs from a gRPC search result. Used when no local entity is + * available (gRPC mode) so that double-click can fetch the full concept from the server. + */ + public void setGrpcPublicIds(List publicIds) { + this.grpcPublicIds = publicIds; + } + + /** + * Background-fetches the concept entity graph via gRPC, loads it into the local + * ephemeral entity store, then fires {@link MakeConceptWindowEvent} on the UI thread. + */ + private void openGrpcConcept() { + List ids = List.copyOf(grpcPublicIds); + UUID journalTopic = searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC); + Thread.ofVirtual().start(() -> { + try { + int nid = GrpcSearchService.get().loadConceptWithSemantics(ids); + Entity loaded = Entity.getFast(nid); + if (loaded instanceof ConceptEntity loadedConcept) { + Platform.runLater(() -> + eventBus.publish(journalTopic, + new MakeConceptWindowEvent(this, + MakeConceptWindowEvent.OPEN_CONCEPT_FROM_CONCEPT, + loadedConcept))); + } else { + LOG.warn("Loaded entity for {} is not a ConceptEntity: {}", ids, loaded); + } + } catch (Exception ex) { + LOG.warn("Failed to load concept details from gRPC for {}: {}", ids, ex.getMessage()); + } + }); + } + public void setWindowView(ObservableViewNoOverride windowView) { this.windowView = windowView; } From 32c34ecad6f22c74fd644322a4fc4aa1ab447ec4 Mon Sep 17 00:00:00 2001 From: Patrick Chou <1326306+pChou19@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:27:11 -0400 Subject: [PATCH 6/9] Catch exception when in gRPC mode --- .../propsheet/KometPropertyEditorFactory.java | 33 ++++++++++++------- .../mvvm/view/concept/ConceptController.java | 11 +++++-- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java b/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java index 853928d95c..41e47ee5dd 100644 --- a/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java +++ b/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java @@ -79,7 +79,10 @@ public PropertyEditor call(PropertySheet.Item item) { propertyEditor = ed.get(); } else { propertyEditor = null; - AlertStreams.getRoot().dispatch(AlertObject.makeWarning("No editor for item " + item.getName(), item.toString())); + // createCustomEditor already dispatched an error alert for unexpected failures. + // Only log here — a second UI dialog for "No editor" would be redundant and + // confusing, especially in gRPC mode where missing entities are expected. + LOG.warn("No editor for item '{}': {}", item.getName(), item); } } else { return null; @@ -184,17 +187,25 @@ public static final Optional> createCustomEditor(final SheetIt } if (editorClass == AxiomView.class) { //TODO add stated/inferred to root property? - DiTree axiomTree = (DiTree) property.getValue(); - PremiseType premiseType = PremiseType.STATED; - if (property.getObservableField().definition(viewProperties.calculator()).meaningNid() == TinkarTerm.EL_PLUS_PLUS_INFERRED_TERMINOLOGICAL_AXIOMS.nid()) { - premiseType = PremiseType.INFERRED; - } - int semanticNid = property.observableField.field().nid(); - ObservableSemantic axiomSemantic = ObservableSemantic.get(semanticNid); - ObservableSemanticVersion axiomSemanticVersion = axiomSemantic.getVersionFast(property.observableField.field().versionStampNid()); + try { + DiTree axiomTree = (DiTree) property.getValue(); + PremiseType premiseType = PremiseType.STATED; + if (property.getObservableField().definition(viewProperties.calculator()).meaningNid() == TinkarTerm.EL_PLUS_PLUS_INFERRED_TERMINOLOGICAL_AXIOMS.nid()) { + premiseType = PremiseType.INFERRED; + } + int semanticNid = property.observableField.field().nid(); + ObservableSemantic axiomSemantic = ObservableSemantic.get(semanticNid); + ObservableSemanticVersion axiomSemanticVersion = axiomSemantic.getVersionFast(property.observableField.field().versionStampNid()); - AxiomView axiomView = AxiomView.create(axiomSemanticVersion, premiseType, viewProperties); - return Optional.of(axiomView); + AxiomView axiomView = AxiomView.create(axiomSemanticVersion, premiseType, viewProperties); + return Optional.of(axiomView); + } catch (IllegalStateException e) { + // In gRPC/ephemeral-store mode the pattern entity backing axiom field + // definitions may not be loaded yet. Return empty without showing an error + // dialog — the axiom view will be silently unavailable. + LOG.warn("Axiom editor not available — pattern entity absent (gRPC mode): {}", e.getMessage()); + return Optional.empty(); + } } } return property.getPropertyEditorClass().map(cls -> { diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/concept/ConceptController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/concept/ConceptController.java index 586ff7d8c2..11961f7352 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/concept/ConceptController.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/concept/ConceptController.java @@ -1444,9 +1444,16 @@ private void makeSheetItem(ViewProperties viewProperties, semanticVersion.ifPresent(semanticEntityVersion -> { Latest statedPatternVersion = conceptViewModel.getViewProperties().calculator().latestPatternEntityVersion(semanticEntityVersion.pattern()); ImmutableList fields = fields(semanticEntityVersion, statedPatternVersion.get(), conceptViewModel.getViewProperties().calculator()); - fields.forEach(field -> + fields.forEach(field -> { + try { // create a row as a label: editor. For Axioms we hide the left labels. - propertySheet.getItems().add(SheetItem.make(field, semanticEntityVersion, conceptViewModel.getViewProperties()))); + propertySheet.getItems().add(SheetItem.make(field, semanticEntityVersion, conceptViewModel.getViewProperties())); + } catch (Exception e) { + // In gRPC/read-only mode, field-definition data-type entities (e.g. concept, string) + // may not be in the ephemeral store yet. Skip rather than crashing with a dialog. + LOG.warn("Could not create axiom sheet item — field data-type entity may be absent (gRPC mode): {}", e.getMessage()); + } + }); }); } From c781fb9133209986b64b895bde6057e9dffd3463 Mon Sep 17 00:00:00 2001 From: Patrick Chou <1326306+pChou19@users.noreply.github.com> Date: Wed, 22 Apr 2026 09:26:52 -0400 Subject: [PATCH 7/9] Catches for axiom view --- .../propsheet/KometPropertyEditorFactory.java | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java b/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java index 41e47ee5dd..672e8d9c8a 100644 --- a/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java +++ b/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java @@ -189,21 +189,37 @@ public static final Optional> createCustomEditor(final SheetIt //TODO add stated/inferred to root property? try { DiTree axiomTree = (DiTree) property.getValue(); + // Determine STATED vs INFERRED via fieldDefinition() which goes directly to + // Entity.getFast() rather than the Observable layer (avoids thread-check + // and absent-entity exceptions). Fall back to STATED on any failure. PremiseType premiseType = PremiseType.STATED; - if (property.getObservableField().definition(viewProperties.calculator()).meaningNid() == TinkarTerm.EL_PLUS_PLUS_INFERRED_TERMINOLOGICAL_AXIOMS.nid()) { - premiseType = PremiseType.INFERRED; + try { + if (property.observableField.field() + .fieldDefinition(viewProperties.calculator()).meaningNid() + == TinkarTerm.EL_PLUS_PLUS_INFERRED_TERMINOLOGICAL_AXIOMS.nid()) { + premiseType = PremiseType.INFERRED; + } + } catch (Exception e) { + LOG.debug("Could not determine axiom premise type from field definition, defaulting to STATED: {}", e.getMessage()); } int semanticNid = property.observableField.field().nid(); ObservableSemantic axiomSemantic = ObservableSemantic.get(semanticNid); + if (axiomSemantic == null) { + LOG.warn("Axiom semantic not available for NID {} — returning no editor (gRPC mode)", semanticNid); + return Optional.empty(); + } ObservableSemanticVersion axiomSemanticVersion = axiomSemantic.getVersionFast(property.observableField.field().versionStampNid()); - + if (axiomSemanticVersion == null) { + LOG.warn("Axiom semantic version not available for stamp NID {} — returning no editor (gRPC mode)", + property.observableField.field().versionStampNid()); + return Optional.empty(); + } AxiomView axiomView = AxiomView.create(axiomSemanticVersion, premiseType, viewProperties); return Optional.of(axiomView); - } catch (IllegalStateException e) { - // In gRPC/ephemeral-store mode the pattern entity backing axiom field - // definitions may not be loaded yet. Return empty without showing an error - // dialog — the axiom view will be silently unavailable. - LOG.warn("Axiom editor not available — pattern entity absent (gRPC mode): {}", e.getMessage()); + } catch (Exception e) { + // Any remaining failure (pattern absent, stamp mismatch, etc.) in + // gRPC/ephemeral-store mode — return empty without showing an error dialog. + LOG.warn("Axiom editor not available (gRPC mode): {} — {}", e.getClass().getSimpleName(), e.getMessage()); return Optional.empty(); } } From 8a44e9d96f11b00073e95a32b0d7a7b52a7593f1 Mon Sep 17 00:00:00 2001 From: Patrick Chou <1326306+pChou19@users.noreply.github.com> Date: Mon, 4 May 2026 10:57:40 -0400 Subject: [PATCH 8/9] Add option to select connecting to gRPC service --- .../src/main/java/dev/ikm/komet/app/App.java | 28 +++++++++++-------- .../komet/app/SelectDataSourceController.java | 10 ++++++- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index 84478dc42a..8d7006464f 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -378,26 +378,25 @@ private void startGrpcMode(Stage stage, String grpcPortProp) { GrpcSearchService.initialize(host, port); - // Start an ephemeral (in-memory) data store so that framework components - // that call PrimitiveData.get() (e.g. WindowSettings, Coordinates) work - // without a local dataset. Actual concept search is routed through gRPC. + // Start GrpcPrimitiveDataService so that all PrimitiveData.get().* call sites + // (entity bytes, NID resolution, iteration) work without a local dataset. // getControllerOptions() triggers ServiceLifecycleManager.discoverServices(). try { var controllers = PrimitiveData.getControllerOptions(); - var ephemeralOpt = controllers.stream() - .filter(c -> c.controllerName().toLowerCase().contains("ephemeral")) + var providerOpt = controllers.stream() + .filter(c -> c.controllerName().toLowerCase().contains("grpc")) .findFirst(); - if (ephemeralOpt.isPresent()) { - PrimitiveData.selectControllerByName(ephemeralOpt.get().controllerName()); + if (providerOpt.isPresent()) { + PrimitiveData.selectControllerByName(providerOpt.get().controllerName()); PrimitiveData.start(); - LOG.info("Ephemeral PrimitiveData started for gRPC mode (controller: {})", - ephemeralOpt.get().controllerName()); + LOG.info("PrimitiveData started for gRPC mode (controller: {})", + providerOpt.get().controllerName()); } else { - LOG.warn("No ephemeral data provider found; available: {}", + LOG.warn("No gRPC data provider found; available: {}", controllers.stream().map(c -> c.controllerName()).toList()); } } catch (Exception e) { - LOG.error("Failed to start ephemeral data provider", e); + LOG.error("Failed to start data provider for gRPC mode", e); } LOG.info("gRPC mode active → {}:{}", host, port); @@ -546,7 +545,12 @@ private void appStateChangeListener(ObservableValue observab TinkExecutor.threadPool().submit(new LoadDataSourceTask(state)); } case SELECT_USER -> { - appPages.launchLoginAuthor(primaryStage); + // gRPC mode has no local user store — skip author login and go straight to RUNNING + if (GrpcSearchService.isActive()) { + Platform.runLater(() -> state.set(RUNNING)); + } else { + appPages.launchLoginAuthor(primaryStage); + } } case RUNNING -> { if (userProperty.get() == null) { diff --git a/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java b/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java index 83d9875455..2f127d18d5 100644 --- a/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java +++ b/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java @@ -123,7 +123,15 @@ void dataSourceChanged(ObservableValue> obser fileListView.getItems().sort(NaturalOrder.getObjectComparator()); fileListView.getSelectionModel().selectFirst(); fileListView.getSelectionModel().selectFirst(); - fileListView.requestFocus(); + boolean hasOptions = !fileListView.getItems().isEmpty(); + fileListView.setVisible(hasOptions); + fileListView.setManaged(hasOptions); + // When there is no file list, move the property sheet up and let it span both rows + GridPane.setRowIndex(propertySheet, hasOptions ? 1 : 0); + GridPane.setRowSpan(propertySheet, hasOptions ? 1 : 2); + if (hasOptions) { + fileListView.requestFocus(); + } propertySheet.getItems().clear(); validationSupport = new ValidationSupport(); From cba95f1f2ec51d88510028aca1f3923d4e21d601 Mon Sep 17 00:00:00 2001 From: Patrick Chou <1326306+pChou19@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:30:40 -0400 Subject: [PATCH 9/9] Move gRPC mode to plugin --- application/pom.xml | 90 +++++++++-------- .../src/main/java/dev/ikm/komet/app/App.java | 98 +++++++------------ application/src/main/java/module-info.java | 9 +- grpc-provider/pom.xml | 16 --- grpc-provider/src/main/java/module-info.java | 23 ----- .../layout/editor/StandardEditorWindows.java | 32 ++++++ .../editor/model/EditorPatternModel.java | 6 +- kview/pom.xml | 4 - .../view/search/NextGenSearchController.java | 37 ++++--- .../search/SearchCellDescriptionSemantic.java | 5 +- .../view/search/SearchCellTopComponent.java | 4 +- .../SortResultConceptEntryController.java | 39 ++++---- kview/src/main/java/module-info.java | 1 - pom.xml | 19 ++-- 14 files changed, 189 insertions(+), 194 deletions(-) delete mode 100644 grpc-provider/pom.xml delete mode 100644 grpc-provider/src/main/java/module-info.java diff --git a/application/pom.xml b/application/pom.xml index c05c8a702c..39f5172bcf 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -114,6 +114,21 @@ dev.ikm.tinkar changeset-writer-provider + + dev.ikm.ike + rocks-kb-engine + + + network.ike.komet + komet-claude-plugin + 1-SNAPSHOT + + + + dev.ikm.jpms + rocksdbjni + ${rocksdbjni.classifier} + @@ -185,38 +200,12 @@ dev.ikm.tinkar events + - ${project.groupId} - grpc-provider - - - - io.grpc - grpc-stub - 1.78.0 - runtime - - com.google.protobufprotobuf-java - com.google.code.findbugsjsr305 - javax.annotationjavax.annotation-api - - - - io.grpc - grpc-protobuf - 1.78.0 - runtime - - com.google.protobufprotobuf-java - com.google.code.findbugsjsr305 - javax.annotationjavax.annotation-api - - - - io.perfmark - perfmark-api - 0.27.0 - runtime + network.ike.komet + komet-grpc-plugin @@ -280,10 +269,6 @@ org.openjfx javafx-maven-plugin - dev.ikm.komet.application/dev.ikm.komet.app.App @@ -297,9 +282,6 @@ - - - @@ -327,16 +309,42 @@ - + + + + + + + + + + + + + + + + + + + @@ -523,6 +531,7 @@ PKG src/main/resources/icons/KOMET.icns installer_resources/resourceDir_unix + osx @@ -537,6 +546,7 @@ RPM src/main/resources/icons/Komet.png installer_resources/resourceDir_unix + linux64 diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index 3eeded324e..d6eee2d587 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -18,12 +18,11 @@ import static dev.ikm.komet.app.AppState.LOADING_DATA_SOURCE; import static dev.ikm.komet.app.AppState.LOGIN; import static dev.ikm.komet.app.AppState.RUNNING; +import static dev.ikm.komet.app.AppState.SELECTED_DATA_SOURCE; import static dev.ikm.komet.app.AppState.SELECT_DATA_SOURCE; import static dev.ikm.komet.app.AppState.SHUTDOWN; import static dev.ikm.komet.app.AppState.STARTING; import static dev.ikm.komet.app.LoginFeatureFlag.ENABLED_WEB_ONLY; -import dev.ikm.tinkar.provider.grpc.GrpcSearchClient; -import dev.ikm.tinkar.provider.grpc.GrpcSearchService; import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; import static dev.ikm.komet.app.util.CssFile.KVIEW_CSS; import static dev.ikm.komet.app.util.CssUtils.addStylesheets; @@ -51,6 +50,7 @@ import dev.ikm.komet.preferences.Preferences; import dev.ikm.tinkar.common.alert.AlertObject; import dev.ikm.tinkar.common.alert.AlertStreams; +import dev.ikm.tinkar.common.service.NoLocalUserStore; import dev.ikm.tinkar.common.service.PrimitiveData; import dev.ikm.tinkar.common.service.TinkExecutor; import dev.ikm.tinkar.events.Evt; @@ -195,12 +195,8 @@ private static void addShutdownHook() { LOG.info("Starting shutdown hook"); try { - if (!GrpcSearchClient.isAvailable()) { - PrimitiveData.save(); - PrimitiveData.stop(); - } else { - GrpcSearchClient.get().close(); - } + PrimitiveData.save(); + PrimitiveData.stop(); } catch (Exception e) { LOG.error("Error during shutdown hook execution", e); } @@ -339,18 +335,20 @@ public void start(Stage stage) { /** * Handles the login feature based on the provided {@link LoginFeatureFlag} and platform. *

- * When the system property {@code komet.grpc.port} is set, the application starts in - * gRPC mode: datasource selection and author login are skipped, and concept - * searches are routed to the running tinkar-core service instead of a local provider. - * Use {@code komet.grpc.host} to override the hostname (default: {@code localhost}). + * When the system property {@code komet.datastore.controller} is set, the application + * auto-selects the named {@link dev.ikm.tinkar.common.service.DataServiceController} + * (matched by exact name, then by case-insensitive substring) instead of showing the + * datasource-selection screen. Author login is still skipped or shown afterward based on + * whether the selected provider implements {@link NoLocalUserStore} — see + * {@link #appStateChangeListener}. * * @param loginFeatureFlag the current state of the login feature * @param stage the current application stage */ public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) { - String grpcPortProp = System.getProperty("komet.grpc.port"); - if (grpcPortProp != null && !grpcPortProp.isBlank()) { - startGrpcMode(stage, grpcPortProp); + String datastoreControllerProp = System.getProperty("komet.datastore.controller"); + if (datastoreControllerProp != null && !datastoreControllerProp.isBlank()) { + startWithNamedDataSource(stage, datastoreControllerProp); return; } switch (loginFeatureFlag) { @@ -374,49 +372,31 @@ public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) { } /** - * Initialises the gRPC client and moves the application directly to {@link AppState#RUNNING}, - * bypassing datasource selection and author login. + * Auto-selects a named data store controller and feeds it through the same + * {@link AppState#SELECTED_DATA_SOURCE} → {@link LoadDataSourceTask} → {@link AppState#SELECT_USER} + * pipeline that manual datasource selection uses, instead of showing the selection screen. * - * @param stage the primary stage - * @param grpcPortProp value of the {@code komet.grpc.port} system property + * @param stage the primary stage + * @param controllerName value of the {@code komet.datastore.controller} system property */ - private void startGrpcMode(Stage stage, String grpcPortProp) { - String host = System.getProperty("komet.grpc.host", "localhost"); - int port; - try { - port = Integer.parseInt(grpcPortProp.strip()); - } catch (NumberFormatException e) { - LOG.error("Invalid komet.grpc.port value '{}', falling back to datasource selection", grpcPortProp); + private void startWithNamedDataSource(Stage stage, String controllerName) { + var controllers = PrimitiveData.getControllerOptions(); + var match = controllers.stream() + .filter(c -> c.controllerName().equalsIgnoreCase(controllerName)) + .findFirst() + .or(() -> controllers.stream() + .filter(c -> c.controllerName().toLowerCase().contains(controllerName.toLowerCase())) + .findFirst()); + if (match.isEmpty()) { + LOG.error("No data store controller matching '{}'; available: {}", controllerName, + controllers.stream().map(c -> c.controllerName()).toList()); startSelectDataSource(stage); return; } - - GrpcSearchService.initialize(host, port); - - // Start GrpcPrimitiveDataService so that all PrimitiveData.get().* call sites - // (entity bytes, NID resolution, iteration) work without a local dataset. - // getControllerOptions() triggers ServiceLifecycleManager.discoverServices(). - try { - var controllers = PrimitiveData.getControllerOptions(); - var providerOpt = controllers.stream() - .filter(c -> c.controllerName().toLowerCase().contains("grpc")) - .findFirst(); - if (providerOpt.isPresent()) { - PrimitiveData.selectControllerByName(providerOpt.get().controllerName()); - PrimitiveData.start(); - LOG.info("PrimitiveData started for gRPC mode (controller: {})", - providerOpt.get().controllerName()); - } else { - LOG.warn("No gRPC data provider found; available: {}", - controllers.stream().map(c -> c.controllerName()).toList()); - } - } catch (Exception e) { - LOG.error("Failed to start data provider for gRPC mode", e); - } - - LOG.info("gRPC mode active → {}:{}", host, port); + PrimitiveData.selectControllerByName(match.get().controllerName()); + LOG.info("Auto-selected data store controller: {}", match.get().controllerName()); state.addListener(this::appStateChangeListener); - state.set(RUNNING); + state.set(SELECTED_DATA_SOURCE); } /** @@ -560,8 +540,9 @@ private void appStateChangeListener(ObservableValue observab TinkExecutor.threadPool().submit(new LoadDataSourceTask(state)); } case SELECT_USER -> { - // gRPC mode has no local user store — skip author login and go straight to RUNNING - if (GrpcSearchService.isActive()) { + // Providers with no local author/STAMP store (e.g. a remote-backed provider) + // skip login and go straight to RUNNING. + if (PrimitiveData.get() instanceof NoLocalUserStore) { Platform.runLater(() -> state.set(RUNNING)); } else { appPages.launchLoginAuthor(primaryStage); @@ -588,13 +569,8 @@ public void quit() { saveJournalWindowsToPreferences(); LOG.info(">>> Saved journal windows to preferences"); - if (GrpcSearchClient.isAvailable()) { - GrpcSearchClient.get().close(); - LOG.info(">>> gRPC client closed"); - } else { - PrimitiveData.stop(); - LOG.info(">>> PrimitiveData stopped"); - } + PrimitiveData.stop(); + LOG.info(">>> PrimitiveData stopped"); Preferences.stop(); LOG.info(">>> Preferences stopped"); diff --git a/application/src/main/java/module-info.java b/application/src/main/java/module-info.java index a6d5eb58e3..6cf78dbf78 100644 --- a/application/src/main/java/module-info.java +++ b/application/src/main/java/module-info.java @@ -38,6 +38,14 @@ requires dev.ikm.tinkar.provider.ephemeral; // End not happy... + // Plugin modules — must be explicit so the JVM includes them in the module graph + // (service binding via transitively-required modules' `uses` is not sufficient). + // komet-grpc-plugin's jar retains the module name dev.ikm.tinkar.provider.grpc + // (its original name from tinkar-core, before the implementation moved into the + // external plugin repo) rather than a komet.* name — no wrapper module needed. + requires komet.claude; + requires dev.ikm.tinkar.provider.grpc; + // JPro related modules requires jpro.webapi; requires one.jpro.platform.auth.core; @@ -75,7 +83,6 @@ requires jdk.management; requires dev.ikm.tinkar.reasoner.service; requires org.eclipse.jgit; - requires dev.ikm.tinkar.provider.grpc; // Logging related modules requires org.apache.logging.log4j.core; diff --git a/grpc-provider/pom.xml b/grpc-provider/pom.xml deleted file mode 100644 index 6d2bfd1e99..0000000000 --- a/grpc-provider/pom.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - 4.1.0 - - grpc-provider - Komet gRPC Provider - - - - - dev.ikm.tinkar - grpc-search-provider - - - diff --git a/grpc-provider/src/main/java/module-info.java b/grpc-provider/src/main/java/module-info.java deleted file mode 100644 index 90a036b940..0000000000 --- a/grpc-provider/src/main/java/module-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) - * - * Licensed 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. - */ - -/** - * Thin re-export shim. All gRPC search logic lives in - * {@code dev.ikm.tinkar.provider.grpc} (tinkar-core). - */ -module dev.ikm.komet.grpc.provider { - requires transitive dev.ikm.tinkar.provider.grpc; -} diff --git a/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/StandardEditorWindows.java b/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/StandardEditorWindows.java index 7d4f1c313e..55aacdd8df 100644 --- a/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/StandardEditorWindows.java +++ b/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/StandardEditorWindows.java @@ -3,8 +3,13 @@ import dev.ikm.komet.layout.editor.model.EditorPatternModel; import dev.ikm.komet.layout.editor.model.EditorWindowModel; import dev.ikm.komet.preferences.KometPreferences; +import dev.ikm.tinkar.common.service.RemoteConceptSearchService; +import dev.ikm.tinkar.common.service.ServiceLifecycleManager; import dev.ikm.tinkar.coordinate.view.calculator.ViewCalculator; +import dev.ikm.tinkar.terms.EntityFacade; import dev.ikm.tinkar.terms.TinkarTerm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; @@ -18,6 +23,8 @@ */ public final class StandardEditorWindows { + private static final Logger LOG = LoggerFactory.getLogger(StandardEditorWindows.class); + /** Title of the standard Concept window. */ public static final String CONCEPT_WINDOW_2 = "Concept (2)"; @@ -48,6 +55,8 @@ private static void saveConceptWindow2(KometPreferences standardWindowsPreferenc EditorWindowModel window = new EditorWindowModel(); window.setTitle(CONCEPT_WINDOW_2); + ensureLocallyResolvable(viewCalculator, TinkarTerm.DESCRIPTION_PATTERN); + EditorPatternModel descriptionPattern = new EditorPatternModel(viewCalculator, TinkarTerm.DESCRIPTION_PATTERN.nid()); descriptionPattern.setRequired(true); @@ -55,4 +64,27 @@ private static void saveConceptWindow2(KometPreferences standardWindowsPreferenc window.save(standardWindowsPreferences); } + + /** + * If {@code concept} has no local description text — e.g. a remote-backed provider whose + * local entity store starts empty and only loads entities on demand — fetches its full + * entity graph from the active {@link RemoteConceptSearchService} so its name resolves + * normally afterward. No-op when the concept already resolves locally, or when no remote + * search service is active (plain local providers always have core TinkarTerm concepts + * loaded from starter data). + */ + private static void ensureLocallyResolvable(ViewCalculator viewCalculator, EntityFacade concept) { + if (viewCalculator.getRegularDescriptionText(concept).isPresent() + || viewCalculator.getFullyQualifiedNameText(concept).isPresent()) { + return; + } + ServiceLifecycleManager.get().getRunningService(RemoteConceptSearchService.class) + .ifPresent(remote -> { + try { + remote.loadConceptWithSemantics(concept.publicId().asUuidList().toList()); + } catch (Exception e) { + LOG.warn("Failed to load {} from remote backend: {}", concept, e.getMessage()); + } + }); + } } diff --git a/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/model/EditorPatternModel.java b/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/model/EditorPatternModel.java index cc74ce62ea..987db5fea8 100644 --- a/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/model/EditorPatternModel.java +++ b/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/model/EditorPatternModel.java @@ -246,7 +246,11 @@ private void savePatternDetails(KometPreferences sectionPreferences) { private String retrieveDisplayName(PatternFacade patternFacade) { Optional optionalStringRegularName = viewCalculator.getRegularDescriptionText(patternFacade); Optional optionalStringFQN = viewCalculator.getFullyQualifiedNameText(patternFacade); - return optionalStringRegularName.orElseGet(optionalStringFQN::get); + // Neither may be present — e.g. a remote-backed provider whose local entity store + // doesn't have this pattern's descriptions loaded — so fall back to the nid rather + // than throw NoSuchElementException. + return optionalStringRegularName.or(() -> optionalStringFQN) + .orElseGet(() -> "Pattern [nid=" + patternFacade.nid() + "]"); } @Override diff --git a/kview/pom.xml b/kview/pom.xml index 84682749b3..8dfa3f8195 100644 --- a/kview/pom.xml +++ b/kview/pom.xml @@ -24,10 +24,6 @@ dev.ikm.tinkar search-provider - - dev.ikm.tinkar - grpc-search-provider - dev.ikm.tinkar composer diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java index 9271839537..50f023a073 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java @@ -30,7 +30,8 @@ import dev.ikm.komet.framework.dnd.KonceptDragSource; import dev.ikm.komet.framework.search.HighlightedSegments; import dev.ikm.komet.framework.search.SearchPanelController; -import dev.ikm.tinkar.provider.grpc.GrpcSearchService; +import dev.ikm.tinkar.common.service.RemoteConceptSearchService; +import dev.ikm.tinkar.common.service.ServiceLifecycleManager; import dev.ikm.komet.framework.view.ViewProperties; import dev.ikm.komet.kview.controls.AutoCompleteTextField; import dev.ikm.komet.layout.controls.FilterOptionsPopup; @@ -92,6 +93,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.OptionalInt; import java.util.TreeMap; import java.util.UUID; @@ -286,6 +288,8 @@ private void doSearch(ActionEvent actionEvent) { clearView(); String queryText = searchField.getText().strip(); currentQueryText = queryText; + Optional remoteSearch = + ServiceLifecycleManager.get().getRunningService(RemoteConceptSearchService.class); try { if (queryText.startsWith("-") && parseInt(queryText).isPresent()) { addComponentFromNid(queryText); @@ -299,21 +303,22 @@ private void doSearch(ActionEvent actionEvent) { UuidUtil.getUUID(queryText).ifPresent(uuid -> { addComponentFromNid(PrimitiveData.nid(PublicIds.of(uuid))); }); - } else if (GrpcSearchService.isActive()) { - final String grpcQuery = queryText; - GrpcSearchService.SortOption sortOption = switch (sortByButton.getText()) { - case BUTTON_TEXT_TOP_COMPONENT_ALPHA -> GrpcSearchService.SortOption.TOP_COMPONENT_ALPHA; - case BUTTON_TEXT_DESCRIPTION_SEMANTIC -> GrpcSearchService.SortOption.SEMANTIC; - case BUTTON_TEXT_DESCRIPTION_SEMANTIC_ALPHA -> GrpcSearchService.SortOption.SEMANTIC_ALPHA; - default -> GrpcSearchService.SortOption.TOP_COMPONENT; + } else if (remoteSearch.isPresent()) { + RemoteConceptSearchService remote = remoteSearch.get(); + final String remoteQuery = queryText; + RemoteConceptSearchService.SortOption sortOption = switch (sortByButton.getText()) { + case BUTTON_TEXT_TOP_COMPONENT_ALPHA -> RemoteConceptSearchService.SortOption.TOP_COMPONENT_ALPHA; + case BUTTON_TEXT_DESCRIPTION_SEMANTIC -> RemoteConceptSearchService.SortOption.SEMANTIC; + case BUTTON_TEXT_DESCRIPTION_SEMANTIC_ALPHA -> RemoteConceptSearchService.SortOption.SEMANTIC_ALPHA; + default -> RemoteConceptSearchService.SortOption.TOP_COMPONENT; }; - boolean isSemanticMode = sortOption == GrpcSearchService.SortOption.SEMANTIC - || sortOption == GrpcSearchService.SortOption.SEMANTIC_ALPHA; + boolean isSemanticMode = sortOption == RemoteConceptSearchService.SortOption.SEMANTIC + || sortOption == RemoteConceptSearchService.SortOption.SEMANTIC_ALPHA; if (isSemanticMode) { setCurrentSearchResultType(SearchResultType.DESCRIPTION_SEMANTICS); - List results = - GrpcSearchService.get().searchFlat(grpcQuery, MAX_RESULT_SIZE, sortOption); - LOG.info("{} gRPC flat results returned for query: {} sortBy: {}", results.size(), grpcQuery, sortOption); + List results = + remote.searchFlat(remoteQuery, MAX_RESULT_SIZE, sortOption); + LOG.info("{} remote flat results returned for query: {} sortBy: {}", results.size(), remoteQuery, sortOption); List converted = results.stream() .map(r -> new LatestVersionSearchResult( new Latest<>(SemanticEntityVersion.class), @@ -324,9 +329,9 @@ private void doSearch(ActionEvent actionEvent) { searchResultsListView.getItems().setAll(converted); } else { setCurrentSearchResultType(SearchResultType.TOP_COMPONENT); - List results = - GrpcSearchService.get().searchGrouped(grpcQuery, MAX_RESULT_SIZE, sortOption); - LOG.info("{} gRPC grouped results returned for query: {} sortBy: {}", results.size(), grpcQuery, sortOption); + List results = + remote.searchGrouped(remoteQuery, MAX_RESULT_SIZE, sortOption); + LOG.info("{} remote grouped results returned for query: {} sortBy: {}", results.size(), remoteQuery, sortOption); List>> entries = results.stream().map(g -> { List uuids = g.publicId().stream().map(UUID::fromString).toList(); diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java index 16bc71d161..2819d15ee2 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java @@ -1,6 +1,7 @@ package dev.ikm.komet.kview.mvvm.view.search; import dev.ikm.komet.framework.Identicon; +import dev.ikm.komet.framework.search.HighlightedSegments; import dev.ikm.komet.framework.view.ObservableViewNoOverride; import dev.ikm.komet.framework.view.ViewProperties; import dev.ikm.tinkar.coordinate.stamp.calculator.LatestVersionSearchResult; @@ -76,7 +77,7 @@ protected void updateItem(Object item, boolean empty) { SemanticEntityVersion semantic = latestVersionSearchResult.latestVersion().get(); controller.setIdenticon(Identicon.generateIdenticonImage(semantic.publicId())); - controller.setSemanticText(formatHighlightedString(latestVersionSearchResult.highlightedString())); + controller.setSemanticText(HighlightedSegments.stripMarkup(latestVersionSearchResult.highlightedString()).replaceAll("\\s+", " ")); controller.setWindowView(observableViewNoOverride); Entity entity = Entity.getConceptForSemantic(semantic.nid()).get(); controller.setData(entity); @@ -92,7 +93,7 @@ protected void updateItem(Object item, boolean empty) { setGraphic(content); } else { // gRPC mode: no local entity, render text only - controller.setSemanticText(formatHighlightedString(latestVersionSearchResult.highlightedString())); + controller.setSemanticText(HighlightedSegments.stripMarkup(latestVersionSearchResult.highlightedString()).replaceAll("\\s+", " ")); controller.setWindowView(observableViewNoOverride); setGraphic(content); } diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java index 49a07ea8c0..3adc6df51f 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java @@ -126,12 +126,12 @@ protected void updateItem(Object item, boolean empty) { setGraphic(parentPane); } else if (!nidTextRecord.publicIds().isEmpty()) { - // gRPC mode: no local entity, render using data carried in NidTextRecord + // Remote-backed result: no local entity, render using data carried in NidTextRecord UUID[] uuids = nidTextRecord.publicIds().toArray(new UUID[0]); controller.setIdenticon(Identicon.generateIdenticonImage(PublicIds.of(uuids))); controller.setWindowView(observableViewNoOverride); controller.setData(null); - controller.setGrpcPublicIds(nidTextRecord.publicIds()); + controller.setRemotePublicIds(nidTextRecord.publicIds()); controller.setComponentText(nidTextRecord.text()); controller.getDescriptionListViewItems().setAll(mapEntry.getValue()); if (nidTextRecord.active()) { diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java index d80e28d1d4..15c2f1039c 100644 --- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java +++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java @@ -37,7 +37,8 @@ import dev.ikm.tinkar.entity.PatternEntity; import dev.ikm.tinkar.events.EvtBus; import dev.ikm.tinkar.events.EvtBusFactory; -import dev.ikm.tinkar.provider.grpc.GrpcSearchService; +import dev.ikm.tinkar.common.service.RemoteConceptSearchService; +import dev.ikm.tinkar.common.service.ServiceLifecycleManager; import dev.ikm.tinkar.terms.EntityFacade; import javafx.application.Platform; import org.slf4j.Logger; @@ -96,8 +97,8 @@ public class SortResultConceptEntryController extends AbstractBasicController { private Entity entity; - /** Public UUIDs carried from a gRPC search result when no local entity is available. */ - private List grpcPublicIds; + /** Public UUIDs carried from a remote search result when no local entity is available. */ + private List remotePublicIds; private ObservableViewNoOverride windowView; @@ -120,10 +121,10 @@ public void initialize() { conceptEntity)); } else if (entity instanceof PatternEntity patternEntity) { eventBus.publish(searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC), new MakePatternWindowEvent(this, MakePatternWindowEvent.OPEN_PATTERN, patternEntity, getViewProperties())); - } else if (grpcPublicIds != null && !grpcPublicIds.isEmpty()) { - // gRPC mode: fetch full entity graph from server, load into ephemeral store, - // then open the concept window as normal. - openGrpcConcept(); + } else if (remotePublicIds != null && !remotePublicIds.isEmpty()) { + // Remote-backed result: fetch full entity graph from the server, load into + // ephemeral store, then open the concept window as normal. + openRemoteConcept(); } } } @@ -209,23 +210,27 @@ public void setData(Entity entity) { } /** - * Sets the public UUIDs from a gRPC search result. Used when no local entity is - * available (gRPC mode) so that double-click can fetch the full concept from the server. + * Sets the public UUIDs from a remote search result. Used when no local entity is + * available so that double-click can fetch the full concept from the remote backend. */ - public void setGrpcPublicIds(List publicIds) { - this.grpcPublicIds = publicIds; + public void setRemotePublicIds(List publicIds) { + this.remotePublicIds = publicIds; } /** - * Background-fetches the concept entity graph via gRPC, loads it into the local - * ephemeral entity store, then fires {@link MakeConceptWindowEvent} on the UI thread. + * Background-fetches the concept entity graph from the active {@link RemoteConceptSearchService}, + * loads it into the local ephemeral entity store, then fires {@link MakeConceptWindowEvent} + * on the UI thread. */ - private void openGrpcConcept() { - List ids = List.copyOf(grpcPublicIds); + private void openRemoteConcept() { + List ids = List.copyOf(remotePublicIds); UUID journalTopic = searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC); Thread.ofVirtual().start(() -> { try { - int nid = GrpcSearchService.get().loadConceptWithSemantics(ids); + RemoteConceptSearchService remote = ServiceLifecycleManager.get() + .getRunningService(RemoteConceptSearchService.class) + .orElseThrow(() -> new IllegalStateException("RemoteConceptSearchService not available")); + int nid = remote.loadConceptWithSemantics(ids); Entity loaded = Entity.getFast(nid); if (loaded instanceof ConceptEntity loadedConcept) { Platform.runLater(() -> @@ -237,7 +242,7 @@ private void openGrpcConcept() { LOG.warn("Loaded entity for {} is not a ConceptEntity: {}", ids, loaded); } } catch (Exception ex) { - LOG.warn("Failed to load concept details from gRPC for {}: {}", ids, ex.getMessage()); + LOG.warn("Failed to load concept details from remote backend for {}: {}", ids, ex.getMessage()); } }); } diff --git a/kview/src/main/java/module-info.java b/kview/src/main/java/module-info.java index 74ea5172c8..05499b418d 100644 --- a/kview/src/main/java/module-info.java +++ b/kview/src/main/java/module-info.java @@ -21,7 +21,6 @@ requires transitive dev.ikm.komet.framework; requires dev.ikm.komet.search; requires dev.ikm.tinkar.provider.search; - requires dev.ikm.tinkar.provider.grpc; requires dev.ikm.komet.navigator; requires dev.ikm.komet.classification; requires dev.ikm.komet.progress; diff --git a/pom.xml b/pom.xml index 598b1272c7..b8ca9329b3 100644 --- a/pom.xml +++ b/pom.xml @@ -70,13 +70,17 @@ kview sync knowledge-layout-editor - grpc-provider application UTF-8 1.7.3 - + + + localhost 1.21.1-r11 @@ -107,14 +111,9 @@ import - dev.ikm.komet - grpc-provider - ${project.version} - - - dev.ikm.tinkar - grpc-search-provider - 1.127.2-SNAPSHOT + network.ike.komet + komet-grpc-plugin + 1-SNAPSHOT