Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The current `golemcore/*` modules in this repository are:
| Plugin | Purpose |
| --- | --- |
| `golemcore/brave-search` | Brave Search web search tool plugin with API-key backed queries. |
| `golemcore/brain` | GolemCore Brain wiki tool plugin for CRUD operations and optional dynamic relevance search. |
| `golemcore/brain` | GolemCore Brain wiki tool plugin for CRUD operations, hybrid search, intellisearch, and reindexing. |
| `golemcore/browser` | Playwright-backed browser automation tool with screenshot support. |
| `golemcore/browserless` | Browserless smart scrape plugin for rendered HTML, markdown, and link extraction. |
| `golemcore/elevenlabs` | ElevenLabs-backed STT/TTS provider plugin. |
Expand Down
4 changes: 2 additions & 2 deletions golemcore/brain/plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
id: golemcore/brain
provider: golemcore
name: brain
version: 1.0.2
version: 1.0.3
pluginApiVersion: 1
engineVersion: ">=0.0.0 <2.0.0"
entrypoint: me.golemcore.plugins.golemcore.brain.BrainPluginBootstrap
description: "GolemCore Brain wiki plugin for CRUD operations and optional dynamic relevance search."
description: "GolemCore Brain wiki plugin for CRUD, section-level patching, atomic transactions, link-graph insights, top-read tracking, hybrid search, intellisearch, and reindexing."
sourceUrl: "https://github.com/alexk-dev/golemcore-plugins/tree/main/golemcore/brain"
license: "Apache-2.0"
maintainers:
Expand Down
2 changes: 1 addition & 1 deletion golemcore/brain/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<relativePath>../../pom.xml</relativePath>
</parent>

<version>1.0.2</version>
<version>1.0.3</version>
<artifactId>golemcore-brain-plugin</artifactId>
<name>golemcore/brain</name>
<description>GolemCore Brain wiki tool plugin for GolemCore</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public PluginSettingsSection getSection(String sectionKey) {
values.put("allowWrite", Boolean.TRUE.equals(config.getAllowWrite()));
return PluginSettingsSection.builder()
.title("GolemCore Brain")
.description("Configure the Brain wiki API tool and optional dynamic relevance endpoint.")
.description("Configure the Brain wiki API tool and optional dynamic intellisearch endpoint.")
.fields(List.of(
PluginSettingsField.builder()
.key("enabled")
Expand Down Expand Up @@ -108,8 +108,8 @@ public PluginSettingsSection getSection(String sectionKey) {
PluginSettingsField.builder()
.key("allowWrite")
.type("boolean")
.label("Allow Write Operations")
.description("Allow create, update, delete, ensure, move, and copy operations.")
.label("Allow Mutating Operations")
.description("Allow page mutations and search reindex requests.")
.build()))
.values(values)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class BrainService {

private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
private static final int DEFAULT_SEARCH_LIMIT = 5;
private static final String SEARCH_MODE_AUTO = "auto";
private static final String SEARCH_MODE_FTS = "fts";
private static final String SEARCH_MODE_HYBRID = "hybrid";
private static final Pattern SLUG_SEPARATOR_PATTERN = Pattern.compile("[^a-z0-9]+");

private final BrainPluginConfigService configService;
Expand Down Expand Up @@ -69,18 +72,34 @@ public ToolResult listTree(String spaceSlug) {
}
}

public ToolResult searchPages(String spaceSlug, String query, Integer limit) {
public ToolResult searchPages(String spaceSlug, String query, String searchMode, Integer limit) {
String resolvedQuery = requireText(query, "query");
String space = resolveSpace(spaceSlug);
String resolvedMode = resolveSearchMode(searchMode, SEARCH_MODE_AUTO);
int resolvedLimit = resolveLimit(limit, DEFAULT_SEARCH_LIMIT);
try {
JsonNode node = executeJson("GET", searchUrl(space, resolvedQuery, resolvedLimit), null);
JsonNode node = executeJson("POST", searchUrl(space),
searchPayload(resolvedQuery, resolvedMode, resolvedLimit));
return ToolResult.success(formatSearchResults(resolvedQuery, node), nodeToData(node));
} catch (IOException | IllegalStateException exception) {
return executionFailure(exception.getMessage());
}
}

public ToolResult getSearchStatus(String spaceSlug) {
String space = resolveSpace(spaceSlug);
try {
JsonNode node = executeJson("GET", searchStatusUrl(space), null);
String output = "Brain search status for " + space
+ ": ready=" + node.path("ready").asBoolean(false)
+ ", indexedDocuments=" + node.path("indexedDocuments").asInt(0)
+ ", embeddingsReady=" + node.path("embeddingsReady").asBoolean(false);
return ToolResult.success(output, nodeToData(node));
} catch (IOException | IllegalStateException exception) {
return executionFailure(exception.getMessage());
}
}

public ToolResult intellisearch(
String spaceSlug,
String context,
Expand Down Expand Up @@ -117,7 +136,7 @@ public ToolResult readPage(String spaceSlug, String path) {
}

public ToolResult createPage(String spaceSlug, String parentPath, String title, String slug, String content,
String kind) {
String kind, List<String> tags, String summary) {
requireWriteAllowed();
String space = resolveSpace(spaceSlug);
try {
Expand All @@ -127,6 +146,12 @@ public ToolResult createPage(String spaceSlug, String parentPath, String title,
payload.put("slug", trimToNull(slug));
payload.put("content", valueOrEmpty(content));
payload.put("kind", trimToNull(kind) != null ? kind : "PAGE");
if (tags != null && !tags.isEmpty()) {
payload.put("tags", tags);
}
if (trimToNull(summary) != null) {
payload.put("summary", summary);
}
JsonNode node = executeJson("POST", spaceUrl(space, "/pages"), payload);
return ToolResult.success("Created Brain page " + node.path("path").asText(""), nodeToData(node));
} catch (IOException | IllegalStateException exception) {
Expand All @@ -135,7 +160,7 @@ public ToolResult createPage(String spaceSlug, String parentPath, String title,
}

public ToolResult updatePage(String spaceSlug, String path, String title, String slug, String content,
String revision) {
String revision, List<String> tags, String summary) {
requireWriteAllowed();
String pagePath = requireText(path, "path");
String space = resolveSpace(spaceSlug);
Expand All @@ -144,6 +169,12 @@ public ToolResult updatePage(String spaceSlug, String path, String title, String
payload.put("slug", trimToNull(slug));
payload.put("content", valueOrEmpty(content));
payload.put("revision", trimToNull(revision));
if (tags != null && !tags.isEmpty()) {
payload.put("tags", tags);
}
if (trimToNull(summary) != null) {
payload.put("summary", summary);
}
try {
JsonNode node = executeJson("PUT", pageUrl(space, pagePath), payload);
return ToolResult.success("Updated Brain page " + node.path("path").asText(pagePath), nodeToData(node));
Expand All @@ -152,6 +183,73 @@ public ToolResult updatePage(String spaceSlug, String path, String title, String
}
}

public ToolResult patchPage(String spaceSlug, String path, String operation, String heading, String content,
String expectedRevision) {
requireWriteAllowed();
String pagePath = requireText(path, "path");
String resolvedOperation = requireText(operation, "patch_operation");
String resolvedRevision = requireText(expectedRevision, "expected_revision");
String space = resolveSpace(spaceSlug);
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("operation", resolvedOperation.toUpperCase(Locale.ROOT));
payload.put("expectedRevision", resolvedRevision);
payload.put("content", valueOrEmpty(content));
payload.put("heading", trimToNull(heading));
try {
JsonNode node = executeJson("PATCH", pageUrl(space, pagePath), payload);
return ToolResult.success("Patched Brain page " + pagePath + " (" + resolvedOperation + ")",
nodeToData(node));
} catch (IOException | IllegalStateException exception) {
return executionFailure(exception.getMessage());
}
}

public ToolResult getWikiGraph(String spaceSlug) {
String space = resolveSpace(spaceSlug);
try {
JsonNode node = executeJson("GET", spaceUrl(space, "/wiki/graph"), null);
int orphanCount = node.path("orphans").isArray() ? node.path("orphans").size() : 0;
int danglingCount = node.path("dangling").isArray() ? node.path("dangling").size() : 0;
return ToolResult.success(
"Brain graph for " + space + ": " + orphanCount + " orphan(s), " + danglingCount + " dangling",
nodeToData(node));
} catch (IOException | IllegalStateException exception) {
return executionFailure(exception.getMessage());
}
}

public ToolResult listTopAccessed(String spaceSlug, Integer limit) {
String space = resolveSpace(spaceSlug);
int resolvedLimit = resolveLimit(limit, DEFAULT_SEARCH_LIMIT);
try {
JsonNode node = executeJson("GET",
spaceUrl(space, "/wiki/access/top").newBuilder()
.addQueryParameter("limit", Integer.toString(resolvedLimit)).build(),
null);
int count = node.path("items").isArray() ? node.path("items").size() : 0;
return ToolResult.success("Listed " + count + " top Brain page(s) by access count", nodeToData(node));
} catch (IOException | IllegalStateException exception) {
return executionFailure(exception.getMessage());
}
}

public ToolResult applyTransaction(String spaceSlug, List<Map<String, Object>> operations) {
requireWriteAllowed();
if (operations == null || operations.isEmpty()) {
return executionFailure("operations is required and must be non-empty");
}
String space = resolveSpace(spaceSlug);
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("operations", operations);
try {
JsonNode node = executeJson("POST", spaceUrl(space, "/wiki/tx"), payload);
int applied = node.path("operations").isArray() ? node.path("operations").size() : 0;
return ToolResult.success("Applied Brain transaction with " + applied + " operation(s)", nodeToData(node));
} catch (IOException | IllegalStateException exception) {
return executionFailure(exception.getMessage());
}
}

public ToolResult deletePage(String spaceSlug, String path) {
requireWriteAllowed();
String pagePath = requireText(path, "path");
Expand Down Expand Up @@ -224,6 +322,28 @@ public ToolResult listAssets(String spaceSlug, String path) {
}
}

public ToolResult reindexSpace(String spaceSlug) {
requireWriteAllowed();
String space = resolveSpace(spaceSlug);
try {
JsonNode node = executeJson("POST", adminSpaceReindexUrl(space), null);
return ToolResult.success("Queued Brain reindex for space " + space, nodeToData(node));
} catch (IOException | IllegalStateException exception) {
return executionFailure(exception.getMessage());
}
}

public ToolResult reindexAllSpaces() {
requireWriteAllowed();
try {
JsonNode node = executeJson("POST", adminSpacesReindexUrl(), null);
int spacesQueued = node.path("spacesQueued").asInt(0);
return ToolResult.success("Queued Brain reindex for " + spacesQueued + " space(s)", nodeToData(node));
} catch (IOException | IllegalStateException exception) {
return executionFailure(exception.getMessage());
}
}

private ToolResult runDynamicIntellisearch(String space, String dynamicApiSlug, String context, String query,
int limit) {
Map<String, Object> payload = new LinkedHashMap<>();
Expand Down Expand Up @@ -310,7 +430,9 @@ private String slugifySegment(String segment) {

private ToolResult fallbackIntellisearch(String space, String context, String query, int limit) {
try {
JsonNode hits = executeJson("GET", searchUrl(space, query, limit), null);
JsonNode searchResult = executeJson("POST", searchUrl(space),
searchPayload(query, SEARCH_MODE_HYBRID, limit));
JsonNode hits = searchHits(searchResult);
List<Map<String, Object>> documents = new ArrayList<>();
if (hits.isArray()) {
for (JsonNode hit : hits) {
Expand Down Expand Up @@ -370,10 +492,18 @@ private Request buildRequest(String method, HttpUrl url, Map<String, Object> pay
if (apiToken != null && !apiToken.isBlank()) {
builder.header("Authorization", "Bearer " + apiToken);
}
RequestBody body = payload == null ? null : RequestBody.create(objectMapper.writeValueAsString(payload), JSON);
RequestBody body = payload == null
? emptyRequestBody(method)
: RequestBody.create(objectMapper.writeValueAsString(payload), JSON);
return builder.method(method, body).build();
}

private RequestBody emptyRequestBody(String method) {
return "POST".equals(method) || "PUT".equals(method) || "PATCH".equals(method)
? RequestBody.create("", JSON)
: null;
}

private HttpUrl apiUrl(String path) {
HttpUrl base = parseBaseUrl();
return base.newBuilder()
Expand All @@ -397,11 +527,20 @@ private HttpUrl pageActionUrl(String space, String path, String actionPath) {
.build();
}

private HttpUrl searchUrl(String space, String query, int limit) {
return spaceUrl(space, "/search").newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("limit", Integer.toString(limit))
.build();
private HttpUrl searchUrl(String space) {
return spaceUrl(space, "/search");
}

private HttpUrl searchStatusUrl(String space) {
return spaceUrl(space, "/search/status");
}

private HttpUrl adminSpaceReindexUrl(String space) {
return apiUrl("/api/admin/spaces/" + encodePathSegment(space) + "/reindex");
}

private HttpUrl adminSpacesReindexUrl() {
return apiUrl("/api/admin/spaces/reindex");
}

private HttpUrl dynamicApiUrl(String space, String dynamicApiSlug) {
Expand All @@ -428,26 +567,52 @@ private int resolveLimit(Integer value, int defaultValue) {

private void requireWriteAllowed() {
if (!Boolean.TRUE.equals(configService.getConfig().getAllowWrite())) {
throw new IllegalStateException("Brain write operations are disabled in plugin settings");
throw new IllegalStateException("Brain mutating operations are disabled in plugin settings");
}
}

private Map<String, Object> searchPayload(String query, String mode, int limit) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("query", query);
payload.put("mode", mode);
payload.put("limit", limit);
return payload;
}

private Object nodeToData(JsonNode node) throws JsonProcessingException {
return objectMapper.convertValue(node, new TypeReference<>() {
});
}

private String formatSearchResults(String query, JsonNode node) {
if (!node.isArray() || node.isEmpty()) {
JsonNode hits = searchHits(node);
if (!hits.isArray() || hits.isEmpty()) {
return "No Brain pages found for: " + query;
}
String mode = node.path("mode").asText("");
String modeSuffix = mode.isBlank() ? "" : " (" + mode + ")";
List<String> rows = new ArrayList<>();
for (JsonNode hit : node) {
for (JsonNode hit : hits) {
rows.add("- " + hit.path("title").asText(hit.path("path").asText(""))
+ " (`" + hit.path("path").asText("") + "`): "
+ hit.path("excerpt").asText(""));
}
return "Brain search results for \"" + query + "\":\n" + String.join("\n", rows);
return "Brain search results for \"" + query + "\"" + modeSuffix + ":\n" + String.join("\n", rows);
}

private JsonNode searchHits(JsonNode node) {
return node.isArray() ? node : node.path("hits");
}

private String resolveSearchMode(String value, String defaultMode) {
String resolved = firstNonBlank(value, defaultMode);
String normalized = resolved.toLowerCase(Locale.ROOT).replace('_', '-');
if (SEARCH_MODE_AUTO.equals(normalized) || SEARCH_MODE_FTS.equals(normalized)
|| SEARCH_MODE_HYBRID.equals(normalized)) {
return normalized;
}
throw new IllegalArgumentException("Unsupported Brain search_mode: " + value
+ " (expected auto, fts, or hybrid)");
}

private String formatIntellisearchDocuments(List<Map<String, Object>> documents) {
Expand Down
Loading
Loading