@agentforge/tools 0.12.4 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -4
- package/dist/index.cjs +1818 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2091 -25
- package/dist/index.d.ts +2091 -25
- package/dist/index.js +1771 -70
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { toolBuilder, ToolCategory, LogLevel, createLogger } from '@agentforge/core';
|
|
2
|
-
import
|
|
2
|
+
import axios16 from 'axios';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import * as cheerio from 'cheerio';
|
|
5
5
|
import { WebClient } from '@slack/web-api';
|
|
6
6
|
import { parse } from 'csv-parse/sync';
|
|
7
7
|
import { stringify } from 'csv-stringify/sync';
|
|
8
8
|
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
|
9
|
+
import neo4j, { auth, Integer, Node, Relationship, Path } from 'neo4j-driver';
|
|
9
10
|
import { promises } from 'fs';
|
|
10
11
|
import * as path7 from 'path';
|
|
11
12
|
import { format, parse as parse$1, isValid, add, sub, differenceInDays, differenceInHours, differenceInMinutes, isAfter, isBefore } from 'date-fns';
|
|
@@ -45,7 +46,7 @@ function createHttpClientTool(defaultTimeout = 3e4, defaultHeaders = {}) {
|
|
|
45
46
|
validateStatus: () => true
|
|
46
47
|
// Don't throw on any status code
|
|
47
48
|
};
|
|
48
|
-
const response = await
|
|
49
|
+
const response = await axios16(config);
|
|
49
50
|
return {
|
|
50
51
|
status: response.status,
|
|
51
52
|
statusText: response.statusText,
|
|
@@ -58,7 +59,7 @@ function createHttpClientTool(defaultTimeout = 3e4, defaultHeaders = {}) {
|
|
|
58
59
|
}
|
|
59
60
|
function createHttpGetTool(defaultTimeout = 3e4, defaultHeaders = {}) {
|
|
60
61
|
return toolBuilder().name("http-get").description("Make a simple HTTP GET request to a URL and return the response data.").category(ToolCategory.WEB).tags(["http", "get", "fetch", "web"]).schema(httpGetSchema).implement(async (input) => {
|
|
61
|
-
const response = await
|
|
62
|
+
const response = await axios16.get(input.url, {
|
|
62
63
|
headers: { ...defaultHeaders, ...input.headers },
|
|
63
64
|
params: input.params,
|
|
64
65
|
timeout: defaultTimeout
|
|
@@ -68,7 +69,7 @@ function createHttpGetTool(defaultTimeout = 3e4, defaultHeaders = {}) {
|
|
|
68
69
|
}
|
|
69
70
|
function createHttpPostTool(defaultTimeout = 3e4, defaultHeaders = {}) {
|
|
70
71
|
return toolBuilder().name("http-post").description("Make a simple HTTP POST request with JSON body and return the response data.").category(ToolCategory.WEB).tags(["http", "post", "api", "web"]).schema(httpPostSchema).implement(async (input) => {
|
|
71
|
-
const response = await
|
|
72
|
+
const response = await axios16.post(input.url, input.body, {
|
|
72
73
|
headers: {
|
|
73
74
|
"Content-Type": "application/json",
|
|
74
75
|
...defaultHeaders,
|
|
@@ -105,7 +106,7 @@ var webScraperSchema = z.object({
|
|
|
105
106
|
});
|
|
106
107
|
function createWebScraperTool(defaultTimeout = 3e4, userAgent = "Mozilla/5.0 (compatible; AgentForge/1.0; +https://agentforge.dev)") {
|
|
107
108
|
return toolBuilder().name("web-scraper").description("Scrape and extract data from web pages. Can extract text, HTML, links, images, and use CSS selectors to target specific elements.").category(ToolCategory.WEB).tags(["scraper", "web", "html", "extract", "parse"]).schema(webScraperSchema).implement(async (input) => {
|
|
108
|
-
const response = await
|
|
109
|
+
const response = await axios16.get(input.url, {
|
|
109
110
|
timeout: input.timeout || defaultTimeout,
|
|
110
111
|
headers: {
|
|
111
112
|
"User-Agent": userAgent
|
|
@@ -495,7 +496,7 @@ var DuckDuckGoProvider = class {
|
|
|
495
496
|
async search(query, maxResults, timeout = DEFAULT_TIMEOUT) {
|
|
496
497
|
return retryWithBackoff(async () => {
|
|
497
498
|
try {
|
|
498
|
-
const response = await
|
|
499
|
+
const response = await axios16.get(
|
|
499
500
|
"https://api.duckduckgo.com/",
|
|
500
501
|
{
|
|
501
502
|
params: {
|
|
@@ -599,7 +600,7 @@ var SerperProvider = class {
|
|
|
599
600
|
}
|
|
600
601
|
return retryWithBackoff(async () => {
|
|
601
602
|
try {
|
|
602
|
-
const response = await
|
|
603
|
+
const response = await axios16.post(
|
|
603
604
|
"https://google.serper.dev/search",
|
|
604
605
|
{
|
|
605
606
|
q: query,
|
|
@@ -779,7 +780,7 @@ function getDefaultSlackClient() {
|
|
|
779
780
|
}
|
|
780
781
|
};
|
|
781
782
|
}
|
|
782
|
-
function createSendSlackMessageTool(getSlackClient,
|
|
783
|
+
function createSendSlackMessageTool(getSlackClient, logger8) {
|
|
783
784
|
return toolBuilder().name("send-slack-message").description("Send a message to a Slack channel for team communication and notifications").category(ToolCategory.WEB).tags(["slack", "messaging", "communication"]).usageNotes(
|
|
784
785
|
"Use this for general team communication. For notifications with @mentions, consider using notify-slack instead. Use get-slack-channels first if you need to find the right channel."
|
|
785
786
|
).suggests(["get-slack-channels"]).schema(
|
|
@@ -788,7 +789,7 @@ function createSendSlackMessageTool(getSlackClient, logger4) {
|
|
|
788
789
|
message: z.string().describe("Message content to send")
|
|
789
790
|
})
|
|
790
791
|
).implementSafe(async ({ channel, message }) => {
|
|
791
|
-
|
|
792
|
+
logger8.info("send-slack-message called", { channel, messageLength: message.length });
|
|
792
793
|
try {
|
|
793
794
|
const { client: slack, config } = getSlackClient();
|
|
794
795
|
const result = await slack.chat.postMessage({
|
|
@@ -797,7 +798,7 @@ function createSendSlackMessageTool(getSlackClient, logger4) {
|
|
|
797
798
|
username: config.botName,
|
|
798
799
|
icon_emoji: config.botIcon
|
|
799
800
|
});
|
|
800
|
-
|
|
801
|
+
logger8.info("send-slack-message result", {
|
|
801
802
|
channel: result.channel,
|
|
802
803
|
timestamp: result.ts,
|
|
803
804
|
messageLength: message.length,
|
|
@@ -810,7 +811,7 @@ function createSendSlackMessageTool(getSlackClient, logger4) {
|
|
|
810
811
|
message_id: result.ts
|
|
811
812
|
};
|
|
812
813
|
} catch (error) {
|
|
813
|
-
|
|
814
|
+
logger8.error("send-slack-message failed", {
|
|
814
815
|
channel,
|
|
815
816
|
error: error.message,
|
|
816
817
|
data: error.data
|
|
@@ -819,7 +820,7 @@ function createSendSlackMessageTool(getSlackClient, logger4) {
|
|
|
819
820
|
}
|
|
820
821
|
}).build();
|
|
821
822
|
}
|
|
822
|
-
function createNotifySlackTool(getSlackClient,
|
|
823
|
+
function createNotifySlackTool(getSlackClient, logger8) {
|
|
823
824
|
return toolBuilder().name("notify-slack").description("Send a notification to a Slack channel with optional @mentions for urgent alerts").category(ToolCategory.WEB).tags(["slack", "notification", "alert"]).usageNotes(
|
|
824
825
|
"Use this for urgent notifications that require @mentions. For general messages without mentions, use send-slack-message instead."
|
|
825
826
|
).suggests(["get-slack-channels"]).schema(
|
|
@@ -829,7 +830,7 @@ function createNotifySlackTool(getSlackClient, logger4) {
|
|
|
829
830
|
mentions: z.array(z.string()).optional().describe("List of usernames to mention (without @)")
|
|
830
831
|
})
|
|
831
832
|
).implementSafe(async ({ channel, message, mentions = [] }) => {
|
|
832
|
-
|
|
833
|
+
logger8.info("notify-slack called", {
|
|
833
834
|
channel,
|
|
834
835
|
messageLength: message.length,
|
|
835
836
|
mentionCount: mentions.length
|
|
@@ -843,7 +844,7 @@ function createNotifySlackTool(getSlackClient, logger4) {
|
|
|
843
844
|
username: config.botName,
|
|
844
845
|
icon_emoji: config.botIcon
|
|
845
846
|
});
|
|
846
|
-
|
|
847
|
+
logger8.info("notify-slack result", {
|
|
847
848
|
channel: result.channel,
|
|
848
849
|
timestamp: result.ts,
|
|
849
850
|
mentions: mentions.length
|
|
@@ -857,7 +858,7 @@ function createNotifySlackTool(getSlackClient, logger4) {
|
|
|
857
858
|
};
|
|
858
859
|
}).build();
|
|
859
860
|
}
|
|
860
|
-
function createGetSlackChannelsTool(getSlackClient,
|
|
861
|
+
function createGetSlackChannelsTool(getSlackClient, logger8) {
|
|
861
862
|
return toolBuilder().name("get-slack-channels").description("Get a list of available Slack channels to find the right channel for messaging").category(ToolCategory.WEB).tags(["slack", "channels", "list"]).usageNotes(
|
|
862
863
|
"Use this first to discover available channels before sending messages. Helps ensure you are sending to the correct channel."
|
|
863
864
|
).follows(["send-slack-message", "notify-slack"]).schema(
|
|
@@ -865,7 +866,7 @@ function createGetSlackChannelsTool(getSlackClient, logger4) {
|
|
|
865
866
|
include_private: z.boolean().optional().describe("Include private channels (default: false)")
|
|
866
867
|
})
|
|
867
868
|
).implementSafe(async ({ include_private = false }) => {
|
|
868
|
-
|
|
869
|
+
logger8.info("get-slack-channels called", { include_private });
|
|
869
870
|
const { client: slack } = getSlackClient();
|
|
870
871
|
const publicChannels = await slack.conversations.list({
|
|
871
872
|
types: "public_channel",
|
|
@@ -879,7 +880,7 @@ function createGetSlackChannelsTool(getSlackClient, logger4) {
|
|
|
879
880
|
});
|
|
880
881
|
allChannels = [...allChannels, ...privateChannels.channels || []];
|
|
881
882
|
}
|
|
882
|
-
|
|
883
|
+
logger8.info("get-slack-channels result", {
|
|
883
884
|
channelCount: allChannels.length,
|
|
884
885
|
includePrivate: include_private
|
|
885
886
|
});
|
|
@@ -894,7 +895,7 @@ function createGetSlackChannelsTool(getSlackClient, logger4) {
|
|
|
894
895
|
};
|
|
895
896
|
}).build();
|
|
896
897
|
}
|
|
897
|
-
function createGetSlackMessagesTool(getSlackClient,
|
|
898
|
+
function createGetSlackMessagesTool(getSlackClient, logger8) {
|
|
898
899
|
return toolBuilder().name("get-slack-messages").description("Retrieve message history from a Slack channel to read recent conversations").category(ToolCategory.WEB).tags(["slack", "messages", "history", "read"]).usageNotes(
|
|
899
900
|
"Use this to read recent messages from a channel. Use get-slack-channels first if you need to find the channel ID. Returns messages in reverse chronological order (newest first)."
|
|
900
901
|
).suggests(["get-slack-channels"]).schema(
|
|
@@ -903,7 +904,7 @@ function createGetSlackMessagesTool(getSlackClient, logger4) {
|
|
|
903
904
|
limit: z.number().int().min(1).max(100).optional().describe("Number of messages to retrieve (default: 20, max: 100)")
|
|
904
905
|
})
|
|
905
906
|
).implementSafe(async ({ channel, limit = 20 }) => {
|
|
906
|
-
|
|
907
|
+
logger8.info("get-slack-messages called", { channel, limit });
|
|
907
908
|
try {
|
|
908
909
|
const { client: slack } = getSlackClient();
|
|
909
910
|
let channelId = channel;
|
|
@@ -914,7 +915,7 @@ function createGetSlackMessagesTool(getSlackClient, logger4) {
|
|
|
914
915
|
});
|
|
915
916
|
const found = channels.channels?.find((c) => c.name === channel);
|
|
916
917
|
if (!found) {
|
|
917
|
-
|
|
918
|
+
logger8.error("get-slack-messages: channel not found", { channel });
|
|
918
919
|
throw new Error(
|
|
919
920
|
`Channel '${channel}' not found. Use get-slack-channels to see available channels.`
|
|
920
921
|
);
|
|
@@ -926,7 +927,7 @@ function createGetSlackMessagesTool(getSlackClient, logger4) {
|
|
|
926
927
|
limit: Math.min(limit, 100)
|
|
927
928
|
// Cap at 100 for performance
|
|
928
929
|
});
|
|
929
|
-
|
|
930
|
+
logger8.info("get-slack-messages result", {
|
|
930
931
|
channel: channelId,
|
|
931
932
|
messageCount: result.messages?.length || 0,
|
|
932
933
|
limit
|
|
@@ -944,7 +945,7 @@ function createGetSlackMessagesTool(getSlackClient, logger4) {
|
|
|
944
945
|
})) || []
|
|
945
946
|
};
|
|
946
947
|
} catch (error) {
|
|
947
|
-
|
|
948
|
+
logger8.error("get-slack-messages failed", {
|
|
948
949
|
channel,
|
|
949
950
|
error: error.message,
|
|
950
951
|
data: error.data
|
|
@@ -1007,8 +1008,8 @@ function createGetConfiguredAuth(apiKey, email, siteUrl) {
|
|
|
1007
1008
|
function createGetConfiguredAuthHeader(getConfiguredAuth) {
|
|
1008
1009
|
return function getConfiguredAuthHeader() {
|
|
1009
1010
|
const { ATLASSIAN_API_KEY, ATLASSIAN_EMAIL } = getConfiguredAuth();
|
|
1010
|
-
const
|
|
1011
|
-
return `Basic ${
|
|
1011
|
+
const auth2 = Buffer.from(`${ATLASSIAN_EMAIL}:${ATLASSIAN_API_KEY}`).toString("base64");
|
|
1012
|
+
return `Basic ${auth2}`;
|
|
1012
1013
|
};
|
|
1013
1014
|
}
|
|
1014
1015
|
function getConfig() {
|
|
@@ -1022,18 +1023,18 @@ function getConfig() {
|
|
|
1022
1023
|
}
|
|
1023
1024
|
function getAuthHeader() {
|
|
1024
1025
|
const { ATLASSIAN_API_KEY, ATLASSIAN_EMAIL } = getConfig();
|
|
1025
|
-
const
|
|
1026
|
-
return `Basic ${
|
|
1026
|
+
const auth2 = Buffer.from(`${ATLASSIAN_EMAIL}:${ATLASSIAN_API_KEY}`).toString("base64");
|
|
1027
|
+
return `Basic ${auth2}`;
|
|
1027
1028
|
}
|
|
1028
|
-
function createSearchConfluenceTool(getAuth, getAuthHeader2,
|
|
1029
|
+
function createSearchConfluenceTool(getAuth, getAuthHeader2, logger8) {
|
|
1029
1030
|
return toolBuilder().name("search-confluence").description("Search for pages in Confluence using keywords or CQL (Confluence Query Language). Returns matching pages with titles, IDs, and excerpts.").category(ToolCategory.WEB).tag("confluence").tag("search").tag("knowledge-base").usageNotes("Use this to find relevant documentation, policies, or information in Confluence. You can search by keywords or use CQL for advanced queries (e.g., 'space=AI AND type=page'). Use get-confluence-page to retrieve full content of specific pages.").suggests(["get-confluence-page"]).schema(z.object({
|
|
1030
1031
|
query: z.string().describe("Search query or CQL expression (e.g., 'payment processing' or 'space=BL3 AND title~payment')"),
|
|
1031
1032
|
limit: z.number().optional().describe("Maximum number of results to return (default: 10, max: 25)")
|
|
1032
1033
|
})).implement(async ({ query, limit = 10 }) => {
|
|
1033
|
-
|
|
1034
|
+
logger8.info("search-confluence called", { query, limit });
|
|
1034
1035
|
try {
|
|
1035
1036
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1036
|
-
const response = await
|
|
1037
|
+
const response = await axios16.get(`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/search`, {
|
|
1037
1038
|
headers: {
|
|
1038
1039
|
Authorization: getAuthHeader2(),
|
|
1039
1040
|
Accept: "application/json"
|
|
@@ -1055,13 +1056,13 @@ function createSearchConfluenceTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1055
1056
|
lastModified: page.version?.when || ""
|
|
1056
1057
|
}));
|
|
1057
1058
|
if (results.length === 0) {
|
|
1058
|
-
|
|
1059
|
+
logger8.warn("search-confluence returned NO RESULTS - this is a valid outcome, agent should not retry", {
|
|
1059
1060
|
query,
|
|
1060
1061
|
limit,
|
|
1061
1062
|
totalSize: response.data.totalSize
|
|
1062
1063
|
});
|
|
1063
1064
|
} else {
|
|
1064
|
-
|
|
1065
|
+
logger8.info("search-confluence result", {
|
|
1065
1066
|
query,
|
|
1066
1067
|
resultCount: results.length,
|
|
1067
1068
|
totalSize: response.data.totalSize,
|
|
@@ -1076,7 +1077,7 @@ function createSearchConfluenceTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1076
1077
|
results
|
|
1077
1078
|
});
|
|
1078
1079
|
} catch (error) {
|
|
1079
|
-
|
|
1080
|
+
logger8.error("search-confluence error", {
|
|
1080
1081
|
query,
|
|
1081
1082
|
error: error.response?.data?.message || error.message,
|
|
1082
1083
|
status: error.response?.status
|
|
@@ -1088,14 +1089,14 @@ function createSearchConfluenceTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1088
1089
|
}
|
|
1089
1090
|
}).build();
|
|
1090
1091
|
}
|
|
1091
|
-
function createGetConfluencePageTool(getAuth, getAuthHeader2,
|
|
1092
|
+
function createGetConfluencePageTool(getAuth, getAuthHeader2, logger8) {
|
|
1092
1093
|
return toolBuilder().name("get-confluence-page").description("Get the full content of a specific Confluence page by its ID. Returns the page title, content (in storage format), space, and metadata.").category(ToolCategory.WEB).tag("confluence").tag("page").tag("content").usageNotes("Use this after search-confluence to retrieve the full content of a specific page. The page ID can be found in search results.").requires(["search-confluence"]).schema(z.object({
|
|
1093
1094
|
page_id: z.string().describe("The Confluence page ID (from search results)")
|
|
1094
1095
|
})).implement(async ({ page_id }) => {
|
|
1095
|
-
|
|
1096
|
+
logger8.info("get-confluence-page called", { page_id });
|
|
1096
1097
|
try {
|
|
1097
1098
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1098
|
-
const response = await
|
|
1099
|
+
const response = await axios16.get(`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/${page_id}`, {
|
|
1099
1100
|
headers: {
|
|
1100
1101
|
Authorization: getAuthHeader2(),
|
|
1101
1102
|
Accept: "application/json"
|
|
@@ -1105,7 +1106,7 @@ function createGetConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1105
1106
|
}
|
|
1106
1107
|
});
|
|
1107
1108
|
const page = response.data;
|
|
1108
|
-
|
|
1109
|
+
logger8.info("get-confluence-page result", {
|
|
1109
1110
|
page_id,
|
|
1110
1111
|
title: page.title,
|
|
1111
1112
|
space: page.space?.name,
|
|
@@ -1127,7 +1128,7 @@ function createGetConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1127
1128
|
}
|
|
1128
1129
|
});
|
|
1129
1130
|
} catch (error) {
|
|
1130
|
-
|
|
1131
|
+
logger8.error("get-confluence-page error", {
|
|
1131
1132
|
page_id,
|
|
1132
1133
|
error: error.response?.data?.message || error.message,
|
|
1133
1134
|
status: error.response?.status
|
|
@@ -1139,14 +1140,14 @@ function createGetConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1139
1140
|
}
|
|
1140
1141
|
}).build();
|
|
1141
1142
|
}
|
|
1142
|
-
function createListConfluenceSpacesTool(getAuth, getAuthHeader2,
|
|
1143
|
+
function createListConfluenceSpacesTool(getAuth, getAuthHeader2, logger8) {
|
|
1143
1144
|
return toolBuilder().name("list-confluence-spaces").description("List all available Confluence spaces. Returns space names, keys, types, and descriptions to help identify where to search for information.").category(ToolCategory.WEB).tag("confluence").tag("spaces").tag("list").usageNotes("Use this first to discover available spaces before searching. Helps narrow down searches to specific areas (e.g., 'AI', 'BL3', 'Finance').").follows(["search-confluence"]).schema(z.object({
|
|
1144
1145
|
limit: z.number().optional().describe("Maximum number of spaces to return (default: 25)")
|
|
1145
1146
|
})).implement(async ({ limit = 25 }) => {
|
|
1146
|
-
|
|
1147
|
+
logger8.info("list-confluence-spaces called", { limit });
|
|
1147
1148
|
try {
|
|
1148
1149
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1149
|
-
const response = await
|
|
1150
|
+
const response = await axios16.get(`${ATLASSIAN_SITE_URL}/wiki/rest/api/space`, {
|
|
1150
1151
|
headers: {
|
|
1151
1152
|
Authorization: getAuthHeader2(),
|
|
1152
1153
|
Accept: "application/json"
|
|
@@ -1162,7 +1163,7 @@ function createListConfluenceSpacesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1162
1163
|
description: space.description?.plain?.value || "",
|
|
1163
1164
|
url: `${ATLASSIAN_SITE_URL}/wiki${space._links.webui}`
|
|
1164
1165
|
}));
|
|
1165
|
-
|
|
1166
|
+
logger8.info("list-confluence-spaces result", {
|
|
1166
1167
|
spaceCount: spaces.length,
|
|
1167
1168
|
spaceKeys: spaces.map((s) => s.key).slice(0, 5)
|
|
1168
1169
|
// Log first 5 space keys
|
|
@@ -1173,7 +1174,7 @@ function createListConfluenceSpacesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1173
1174
|
spaces
|
|
1174
1175
|
});
|
|
1175
1176
|
} catch (error) {
|
|
1176
|
-
|
|
1177
|
+
logger8.error("list-confluence-spaces error", {
|
|
1177
1178
|
error: error.response?.data?.message || error.message,
|
|
1178
1179
|
status: error.response?.status
|
|
1179
1180
|
});
|
|
@@ -1184,15 +1185,15 @@ function createListConfluenceSpacesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1184
1185
|
}
|
|
1185
1186
|
}).build();
|
|
1186
1187
|
}
|
|
1187
|
-
function createGetSpacePagesTool(getAuth, getAuthHeader2,
|
|
1188
|
+
function createGetSpacePagesTool(getAuth, getAuthHeader2, logger8) {
|
|
1188
1189
|
return toolBuilder().name("get-space-pages").description("Get all pages from a specific Confluence space by space key. Useful for browsing content in a particular area.").category(ToolCategory.WEB).tag("confluence").tag("space").tag("pages").usageNotes("Use this to explore all pages in a specific space. Get the space key from list-confluence-spaces first.").requires(["list-confluence-spaces"]).schema(z.object({
|
|
1189
1190
|
space_key: z.string().describe("The space key (e.g., 'AI', 'BL3', 'FIN')"),
|
|
1190
1191
|
limit: z.number().optional().describe("Maximum number of pages to return (default: 25)")
|
|
1191
1192
|
})).implement(async ({ space_key, limit = 25 }) => {
|
|
1192
|
-
|
|
1193
|
+
logger8.info("get-space-pages called", { space_key, limit });
|
|
1193
1194
|
try {
|
|
1194
1195
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1195
|
-
const response = await
|
|
1196
|
+
const response = await axios16.get(`${ATLASSIAN_SITE_URL}/wiki/rest/api/content`, {
|
|
1196
1197
|
headers: {
|
|
1197
1198
|
Authorization: getAuthHeader2(),
|
|
1198
1199
|
Accept: "application/json"
|
|
@@ -1211,12 +1212,12 @@ function createGetSpacePagesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1211
1212
|
lastModified: page.version?.when || ""
|
|
1212
1213
|
}));
|
|
1213
1214
|
if (pages.length === 0) {
|
|
1214
|
-
|
|
1215
|
+
logger8.warn("get-space-pages returned NO PAGES - this is a valid outcome, agent should not retry", {
|
|
1215
1216
|
space_key,
|
|
1216
1217
|
limit
|
|
1217
1218
|
});
|
|
1218
1219
|
} else {
|
|
1219
|
-
|
|
1220
|
+
logger8.info("get-space-pages result", {
|
|
1220
1221
|
space_key,
|
|
1221
1222
|
pageCount: pages.length,
|
|
1222
1223
|
titles: pages.map((p) => p.title).slice(0, 3)
|
|
@@ -1230,7 +1231,7 @@ function createGetSpacePagesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1230
1231
|
pages
|
|
1231
1232
|
});
|
|
1232
1233
|
} catch (error) {
|
|
1233
|
-
|
|
1234
|
+
logger8.error("get-space-pages error", {
|
|
1234
1235
|
space_key,
|
|
1235
1236
|
error: error.response?.data?.message || error.message,
|
|
1236
1237
|
status: error.response?.status
|
|
@@ -1242,14 +1243,14 @@ function createGetSpacePagesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1242
1243
|
}
|
|
1243
1244
|
}).build();
|
|
1244
1245
|
}
|
|
1245
|
-
function createCreateConfluencePageTool(getAuth, getAuthHeader2,
|
|
1246
|
+
function createCreateConfluencePageTool(getAuth, getAuthHeader2, logger8) {
|
|
1246
1247
|
return toolBuilder().name("create-confluence-page").description("Create a new page in a Confluence space. Requires space key, page title, and content (in HTML storage format).").category(ToolCategory.WEB).tag("confluence").tag("create").tag("write").usageNotes("Use this to create new documentation pages. Content should be in Confluence storage format (HTML). Get the space key from list-confluence-spaces first. Be mindful of creating duplicate pages.").requires(["list-confluence-spaces"]).schema(z.object({
|
|
1247
1248
|
space_key: z.string().describe("The space key where the page will be created (e.g., 'AI', 'BL3')"),
|
|
1248
1249
|
title: z.string().describe("The title of the new page"),
|
|
1249
1250
|
content: z.string().describe("The page content in HTML format (Confluence storage format)"),
|
|
1250
1251
|
parent_page_id: z.string().optional().describe("Optional parent page ID to create this as a child page")
|
|
1251
1252
|
})).implement(async ({ space_key, title, content, parent_page_id }) => {
|
|
1252
|
-
|
|
1253
|
+
logger8.info("create-confluence-page called", { space_key, title, hasParent: !!parent_page_id });
|
|
1253
1254
|
try {
|
|
1254
1255
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1255
1256
|
const pageData = {
|
|
@@ -1266,7 +1267,7 @@ function createCreateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1266
1267
|
if (parent_page_id) {
|
|
1267
1268
|
pageData.ancestors = [{ id: parent_page_id }];
|
|
1268
1269
|
}
|
|
1269
|
-
const response = await
|
|
1270
|
+
const response = await axios16.post(
|
|
1270
1271
|
`${ATLASSIAN_SITE_URL}/wiki/rest/api/content`,
|
|
1271
1272
|
pageData,
|
|
1272
1273
|
{
|
|
@@ -1276,7 +1277,7 @@ function createCreateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1276
1277
|
}
|
|
1277
1278
|
}
|
|
1278
1279
|
);
|
|
1279
|
-
|
|
1280
|
+
logger8.info("create-confluence-page result", {
|
|
1280
1281
|
page_id: response.data.id,
|
|
1281
1282
|
title: response.data.title,
|
|
1282
1283
|
space: space_key
|
|
@@ -1292,7 +1293,7 @@ function createCreateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1292
1293
|
}
|
|
1293
1294
|
});
|
|
1294
1295
|
} catch (error) {
|
|
1295
|
-
|
|
1296
|
+
logger8.error("create-confluence-page error", {
|
|
1296
1297
|
space_key,
|
|
1297
1298
|
title,
|
|
1298
1299
|
error: error.response?.data?.message || error.message,
|
|
@@ -1305,16 +1306,16 @@ function createCreateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1305
1306
|
}
|
|
1306
1307
|
}).build();
|
|
1307
1308
|
}
|
|
1308
|
-
function createUpdateConfluencePageTool(getAuth, getAuthHeader2,
|
|
1309
|
+
function createUpdateConfluencePageTool(getAuth, getAuthHeader2, logger8) {
|
|
1309
1310
|
return toolBuilder().name("update-confluence-page").description("Update an existing Confluence page's content. Requires page ID, new title, and new content.").category(ToolCategory.WEB).tag("confluence").tag("update").tag("write").usageNotes("Use this to update existing documentation. You must provide the page ID (from search results). The tool will automatically handle version incrementing. Always get the current page content first to avoid overwriting important information.").requires(["get-confluence-page"]).schema(z.object({
|
|
1310
1311
|
page_id: z.string().describe("The ID of the page to update"),
|
|
1311
1312
|
title: z.string().describe("The new title for the page"),
|
|
1312
1313
|
content: z.string().describe("The new content in HTML format (Confluence storage format)")
|
|
1313
1314
|
})).implement(async ({ page_id, title, content }) => {
|
|
1314
|
-
|
|
1315
|
+
logger8.info("update-confluence-page called", { page_id, title });
|
|
1315
1316
|
try {
|
|
1316
1317
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1317
|
-
const getResponse = await
|
|
1318
|
+
const getResponse = await axios16.get(
|
|
1318
1319
|
`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/${page_id}`,
|
|
1319
1320
|
{
|
|
1320
1321
|
headers: {
|
|
@@ -1324,7 +1325,7 @@ function createUpdateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1324
1325
|
}
|
|
1325
1326
|
);
|
|
1326
1327
|
const currentVersion = getResponse.data.version.number;
|
|
1327
|
-
const updateResponse = await
|
|
1328
|
+
const updateResponse = await axios16.put(
|
|
1328
1329
|
`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/${page_id}`,
|
|
1329
1330
|
{
|
|
1330
1331
|
type: "page",
|
|
@@ -1344,7 +1345,7 @@ function createUpdateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1344
1345
|
}
|
|
1345
1346
|
}
|
|
1346
1347
|
);
|
|
1347
|
-
|
|
1348
|
+
logger8.info("update-confluence-page result", {
|
|
1348
1349
|
page_id,
|
|
1349
1350
|
title: updateResponse.data.title,
|
|
1350
1351
|
previousVersion: currentVersion,
|
|
@@ -1361,7 +1362,7 @@ function createUpdateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1361
1362
|
}
|
|
1362
1363
|
});
|
|
1363
1364
|
} catch (error) {
|
|
1364
|
-
|
|
1365
|
+
logger8.error("update-confluence-page error", {
|
|
1365
1366
|
page_id,
|
|
1366
1367
|
title,
|
|
1367
1368
|
error: error.response?.data?.message || error.message,
|
|
@@ -1374,15 +1375,15 @@ function createUpdateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1374
1375
|
}
|
|
1375
1376
|
}).build();
|
|
1376
1377
|
}
|
|
1377
|
-
function createArchiveConfluencePageTool(getAuth, getAuthHeader2,
|
|
1378
|
+
function createArchiveConfluencePageTool(getAuth, getAuthHeader2, logger8) {
|
|
1378
1379
|
return toolBuilder().name("archive-confluence-page").description("Archive a Confluence page by moving it to trash. The page can be restored by space admins. Note: UI may require a note explaining why the page was archived.").category(ToolCategory.WEB).tag("confluence").tag("archive").tag("delete").usageNotes("Use this to archive outdated or obsolete documentation. The page is moved to trash, not permanently deleted. Space admins can restore it if needed. Be very careful - only archive pages that are truly obsolete.").conflicts(["create-confluence-page"]).schema(z.object({
|
|
1379
1380
|
page_id: z.string().describe("The ID of the page to archive"),
|
|
1380
1381
|
reason: z.string().optional().describe("Optional reason for archiving (for audit trail)")
|
|
1381
1382
|
})).implement(async ({ page_id, reason }) => {
|
|
1382
|
-
|
|
1383
|
+
logger8.info("archive-confluence-page called", { page_id, reason });
|
|
1383
1384
|
try {
|
|
1384
1385
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1385
|
-
const getResponse = await
|
|
1386
|
+
const getResponse = await axios16.get(
|
|
1386
1387
|
`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/${page_id}`,
|
|
1387
1388
|
{
|
|
1388
1389
|
headers: {
|
|
@@ -1393,7 +1394,7 @@ function createArchiveConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1393
1394
|
);
|
|
1394
1395
|
const currentVersion = getResponse.data.version.number;
|
|
1395
1396
|
const pageData = getResponse.data;
|
|
1396
|
-
await
|
|
1397
|
+
await axios16.put(
|
|
1397
1398
|
`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/${page_id}`,
|
|
1398
1399
|
{
|
|
1399
1400
|
version: { number: currentVersion + 1 },
|
|
@@ -1410,7 +1411,7 @@ function createArchiveConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1410
1411
|
}
|
|
1411
1412
|
}
|
|
1412
1413
|
);
|
|
1413
|
-
|
|
1414
|
+
logger8.info("archive-confluence-page result", {
|
|
1414
1415
|
page_id,
|
|
1415
1416
|
title: pageData.title,
|
|
1416
1417
|
previousVersion: currentVersion,
|
|
@@ -1428,7 +1429,7 @@ function createArchiveConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1428
1429
|
}
|
|
1429
1430
|
});
|
|
1430
1431
|
} catch (error) {
|
|
1431
|
-
|
|
1432
|
+
logger8.error("archive-confluence-page error", {
|
|
1432
1433
|
page_id,
|
|
1433
1434
|
error: error.response?.data?.message || error.message,
|
|
1434
1435
|
status: error.response?.status
|
|
@@ -2025,6 +2026,1706 @@ function createTransformerTools(config = {}) {
|
|
|
2025
2026
|
createObjectOmitTool()
|
|
2026
2027
|
];
|
|
2027
2028
|
}
|
|
2029
|
+
var neo4jQuerySchema = z.object({
|
|
2030
|
+
cypher: z.string().describe("Cypher query to execute"),
|
|
2031
|
+
parameters: z.record(z.any()).optional().describe("Query parameters for parameterized queries"),
|
|
2032
|
+
database: z.string().optional().describe("Database name (defaults to configured database)")
|
|
2033
|
+
});
|
|
2034
|
+
var neo4jGetSchemaSchema = z.object({
|
|
2035
|
+
database: z.string().optional().describe("Database name (defaults to configured database)")
|
|
2036
|
+
});
|
|
2037
|
+
var neo4jFindNodesSchema = z.object({
|
|
2038
|
+
label: z.string().describe("Node label to search for"),
|
|
2039
|
+
properties: z.record(z.any()).optional().describe("Properties to match (key-value pairs)"),
|
|
2040
|
+
limit: z.number().default(100).describe("Maximum number of nodes to return"),
|
|
2041
|
+
database: z.string().optional().describe("Database name (defaults to configured database)")
|
|
2042
|
+
});
|
|
2043
|
+
var neo4jTraverseSchema = z.object({
|
|
2044
|
+
startNodeId: z.union([
|
|
2045
|
+
z.string().describe("Node ID as string"),
|
|
2046
|
+
z.number().describe("Node ID as number")
|
|
2047
|
+
]).describe("ID of the starting node"),
|
|
2048
|
+
relationshipType: z.string().optional().describe("Type of relationship to follow (optional, follows all if not specified)"),
|
|
2049
|
+
direction: z.enum(["outgoing", "incoming", "both"]).default("outgoing").describe("Direction to traverse"),
|
|
2050
|
+
maxDepth: z.number().default(1).describe("Maximum depth to traverse"),
|
|
2051
|
+
limit: z.number().default(100).describe("Maximum number of nodes to return"),
|
|
2052
|
+
database: z.string().optional().describe("Database name (defaults to configured database)")
|
|
2053
|
+
});
|
|
2054
|
+
var neo4jVectorSearchSchema = z.object({
|
|
2055
|
+
indexName: z.string().describe("Name of the vector index to search"),
|
|
2056
|
+
queryVector: z.array(z.number().describe("Vector dimension value")).describe("Query vector for similarity search"),
|
|
2057
|
+
limit: z.number().default(10).describe("Maximum number of results to return"),
|
|
2058
|
+
database: z.string().optional().describe("Database name (defaults to configured database)")
|
|
2059
|
+
});
|
|
2060
|
+
var neo4jVectorSearchWithEmbeddingSchema = z.object({
|
|
2061
|
+
indexName: z.string().describe("Name of the vector index to search"),
|
|
2062
|
+
queryText: z.string().describe("Text to generate embedding from for similarity search"),
|
|
2063
|
+
limit: z.number().default(10).describe("Maximum number of results to return"),
|
|
2064
|
+
model: z.string().optional().describe("Embedding model to use (defaults to configured model)"),
|
|
2065
|
+
database: z.string().optional().describe("Database name (defaults to configured database)")
|
|
2066
|
+
});
|
|
2067
|
+
var neo4jCreateNodeWithEmbeddingSchema = z.object({
|
|
2068
|
+
label: z.string().describe("Node label"),
|
|
2069
|
+
properties: z.record(z.string(), z.any().describe("Property value")).describe("Node properties (key-value pairs)"),
|
|
2070
|
+
textProperty: z.string().describe("Name of the property containing text to embed"),
|
|
2071
|
+
embeddingProperty: z.string().default("embedding").describe("Name of the property to store the embedding vector"),
|
|
2072
|
+
model: z.string().optional().describe("Embedding model to use (defaults to configured model)"),
|
|
2073
|
+
database: z.string().optional().describe("Database name (defaults to configured database)")
|
|
2074
|
+
});
|
|
2075
|
+
var logger3 = createLogger("agentforge:tools:neo4j:pool");
|
|
2076
|
+
var Neo4jConnectionPool = class {
|
|
2077
|
+
driver = null;
|
|
2078
|
+
config = null;
|
|
2079
|
+
/**
|
|
2080
|
+
* Initialize the connection pool
|
|
2081
|
+
*/
|
|
2082
|
+
async initialize(config) {
|
|
2083
|
+
const startTime = Date.now();
|
|
2084
|
+
logger3.debug("Initializing Neo4j connection pool", {
|
|
2085
|
+
uri: config.uri,
|
|
2086
|
+
username: config.username,
|
|
2087
|
+
database: config.database || "neo4j",
|
|
2088
|
+
maxConnectionPoolSize: config.maxConnectionPoolSize || 50,
|
|
2089
|
+
connectionTimeout: config.connectionTimeout || 3e4
|
|
2090
|
+
});
|
|
2091
|
+
if (this.driver) {
|
|
2092
|
+
logger3.debug("Closing existing connection before reinitializing");
|
|
2093
|
+
await this.close();
|
|
2094
|
+
}
|
|
2095
|
+
this.config = config;
|
|
2096
|
+
this.driver = neo4j.driver(
|
|
2097
|
+
config.uri,
|
|
2098
|
+
auth.basic(config.username, config.password),
|
|
2099
|
+
{
|
|
2100
|
+
maxConnectionPoolSize: config.maxConnectionPoolSize || 50,
|
|
2101
|
+
connectionTimeout: config.connectionTimeout || 3e4
|
|
2102
|
+
}
|
|
2103
|
+
);
|
|
2104
|
+
await this.verifyConnectivity();
|
|
2105
|
+
logger3.info("Neo4j connection pool initialized", {
|
|
2106
|
+
uri: config.uri,
|
|
2107
|
+
database: config.database || "neo4j",
|
|
2108
|
+
duration: Date.now() - startTime
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Verify connectivity to Neo4j
|
|
2113
|
+
*/
|
|
2114
|
+
async verifyConnectivity() {
|
|
2115
|
+
if (!this.driver) {
|
|
2116
|
+
const error = "Neo4j driver not initialized";
|
|
2117
|
+
logger3.error(error);
|
|
2118
|
+
throw new Error(error);
|
|
2119
|
+
}
|
|
2120
|
+
logger3.debug("Verifying Neo4j connectivity");
|
|
2121
|
+
try {
|
|
2122
|
+
await this.driver.verifyConnectivity();
|
|
2123
|
+
logger3.debug("Neo4j connectivity verified");
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
const errorMessage = `Failed to connect to Neo4j: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
2126
|
+
logger3.error("Neo4j connectivity verification failed", {
|
|
2127
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
2128
|
+
uri: this.config?.uri
|
|
2129
|
+
});
|
|
2130
|
+
throw new Error(errorMessage);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Get a session for executing queries
|
|
2135
|
+
*/
|
|
2136
|
+
getSession(database) {
|
|
2137
|
+
if (!this.driver) {
|
|
2138
|
+
throw new Error("Neo4j driver not initialized. Call initialize() first.");
|
|
2139
|
+
}
|
|
2140
|
+
return this.driver.session({
|
|
2141
|
+
database: database || this.config?.database || "neo4j"
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Execute a query with automatic session management
|
|
2146
|
+
*/
|
|
2147
|
+
async executeQuery(cypher, parameters, database) {
|
|
2148
|
+
const session = this.getSession(database);
|
|
2149
|
+
try {
|
|
2150
|
+
const result = await session.run(cypher, parameters);
|
|
2151
|
+
return result.records.map((record) => record.toObject());
|
|
2152
|
+
} finally {
|
|
2153
|
+
await session.close();
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Execute a read query
|
|
2158
|
+
*/
|
|
2159
|
+
async executeReadQuery(cypher, parameters, database) {
|
|
2160
|
+
const session = this.getSession(database);
|
|
2161
|
+
try {
|
|
2162
|
+
const result = await session.executeRead((tx) => tx.run(cypher, parameters));
|
|
2163
|
+
return result.records.map((record) => record.toObject());
|
|
2164
|
+
} finally {
|
|
2165
|
+
await session.close();
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Execute a write query
|
|
2170
|
+
*/
|
|
2171
|
+
async executeWriteQuery(cypher, parameters, database) {
|
|
2172
|
+
const session = this.getSession(database);
|
|
2173
|
+
try {
|
|
2174
|
+
const result = await session.executeWrite((tx) => tx.run(cypher, parameters));
|
|
2175
|
+
return result.records.map((record) => record.toObject());
|
|
2176
|
+
} finally {
|
|
2177
|
+
await session.close();
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Check if driver is initialized
|
|
2182
|
+
*/
|
|
2183
|
+
isInitialized() {
|
|
2184
|
+
return this.driver !== null;
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Get current configuration
|
|
2188
|
+
*/
|
|
2189
|
+
getConfig() {
|
|
2190
|
+
return this.config;
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Close the connection pool
|
|
2194
|
+
*/
|
|
2195
|
+
async close() {
|
|
2196
|
+
if (this.driver) {
|
|
2197
|
+
logger3.debug("Closing Neo4j connection pool");
|
|
2198
|
+
await this.driver.close();
|
|
2199
|
+
this.driver = null;
|
|
2200
|
+
this.config = null;
|
|
2201
|
+
logger3.info("Neo4j connection pool closed");
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
var neo4jPool = new Neo4jConnectionPool();
|
|
2206
|
+
async function initializeFromEnv() {
|
|
2207
|
+
if (!process.env.NEO4J_URI) {
|
|
2208
|
+
logger3.warn("NEO4J_URI environment variable not set, using default", {
|
|
2209
|
+
default: "bolt://localhost:7687"
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
if (!process.env.NEO4J_USER) {
|
|
2213
|
+
logger3.warn("NEO4J_USER environment variable not set, using default", {
|
|
2214
|
+
default: "neo4j"
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
if (!process.env.NEO4J_PASSWORD) {
|
|
2218
|
+
logger3.warn("NEO4J_PASSWORD environment variable not set, using default", {
|
|
2219
|
+
default: "password"
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
if (!process.env.NEO4J_DATABASE) {
|
|
2223
|
+
logger3.debug("NEO4J_DATABASE environment variable not set, using default", {
|
|
2224
|
+
default: "neo4j"
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
const config = {
|
|
2228
|
+
uri: process.env.NEO4J_URI || "bolt://localhost:7687",
|
|
2229
|
+
username: process.env.NEO4J_USER || "neo4j",
|
|
2230
|
+
password: process.env.NEO4J_PASSWORD || "password",
|
|
2231
|
+
database: process.env.NEO4J_DATABASE
|
|
2232
|
+
};
|
|
2233
|
+
await neo4jPool.initialize(config);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// src/data/neo4j/embeddings/utils.ts
|
|
2237
|
+
var DEFAULT_RETRY_CONFIG2 = {
|
|
2238
|
+
maxRetries: 3,
|
|
2239
|
+
initialDelay: 1e3,
|
|
2240
|
+
// 1 second
|
|
2241
|
+
maxDelay: 1e4,
|
|
2242
|
+
// 10 seconds
|
|
2243
|
+
backoffMultiplier: 2
|
|
2244
|
+
};
|
|
2245
|
+
function isRetryableError2(error) {
|
|
2246
|
+
if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT" || error.code === "ENOTFOUND") {
|
|
2247
|
+
return true;
|
|
2248
|
+
}
|
|
2249
|
+
if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
|
|
2250
|
+
return true;
|
|
2251
|
+
}
|
|
2252
|
+
if (error.response?.status >= 500 && error.response?.status < 600) {
|
|
2253
|
+
return true;
|
|
2254
|
+
}
|
|
2255
|
+
if (error.response?.status === 429) {
|
|
2256
|
+
return true;
|
|
2257
|
+
}
|
|
2258
|
+
return false;
|
|
2259
|
+
}
|
|
2260
|
+
function sleep2(ms) {
|
|
2261
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2262
|
+
}
|
|
2263
|
+
async function retryWithBackoff2(fn, config = DEFAULT_RETRY_CONFIG2) {
|
|
2264
|
+
let lastError;
|
|
2265
|
+
let delay = config.initialDelay;
|
|
2266
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
2267
|
+
try {
|
|
2268
|
+
return await fn();
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
lastError = error;
|
|
2271
|
+
if (!isRetryableError2(error)) {
|
|
2272
|
+
throw error;
|
|
2273
|
+
}
|
|
2274
|
+
if (attempt === config.maxRetries) {
|
|
2275
|
+
break;
|
|
2276
|
+
}
|
|
2277
|
+
await sleep2(delay);
|
|
2278
|
+
delay = Math.min(delay * config.backoffMultiplier, config.maxDelay);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
throw lastError;
|
|
2282
|
+
}
|
|
2283
|
+
function getOpenAIApiKey() {
|
|
2284
|
+
return process.env.OPENAI_API_KEY;
|
|
2285
|
+
}
|
|
2286
|
+
function getCohereApiKey() {
|
|
2287
|
+
return process.env.COHERE_API_KEY;
|
|
2288
|
+
}
|
|
2289
|
+
function getHuggingFaceApiKey() {
|
|
2290
|
+
return process.env.HUGGINGFACE_API_KEY;
|
|
2291
|
+
}
|
|
2292
|
+
function getVoyageApiKey() {
|
|
2293
|
+
return process.env.VOYAGE_API_KEY;
|
|
2294
|
+
}
|
|
2295
|
+
function getOllamaBaseUrl() {
|
|
2296
|
+
return process.env.OLLAMA_BASE_URL || "http://localhost:11434";
|
|
2297
|
+
}
|
|
2298
|
+
function getEmbeddingProvider() {
|
|
2299
|
+
return process.env.EMBEDDING_PROVIDER || "openai";
|
|
2300
|
+
}
|
|
2301
|
+
function getEmbeddingModel() {
|
|
2302
|
+
return process.env.EMBEDDING_MODEL;
|
|
2303
|
+
}
|
|
2304
|
+
function validateText(text) {
|
|
2305
|
+
if (!text || text.trim().length === 0) {
|
|
2306
|
+
throw new Error("Text cannot be empty");
|
|
2307
|
+
}
|
|
2308
|
+
if (text.length > 5e4) {
|
|
2309
|
+
throw new Error("Text is too long for embedding generation (max ~50,000 characters)");
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
function validateBatch(texts) {
|
|
2313
|
+
if (!texts || texts.length === 0) {
|
|
2314
|
+
throw new Error("Batch cannot be empty");
|
|
2315
|
+
}
|
|
2316
|
+
if (texts.length > 2048) {
|
|
2317
|
+
throw new Error("Batch size too large (max 2048 texts)");
|
|
2318
|
+
}
|
|
2319
|
+
texts.forEach((text, index) => {
|
|
2320
|
+
try {
|
|
2321
|
+
validateText(text);
|
|
2322
|
+
} catch (error) {
|
|
2323
|
+
throw new Error(`Invalid text at index ${index}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2324
|
+
}
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
var OpenAIEmbeddingProvider = class {
|
|
2328
|
+
name = "openai";
|
|
2329
|
+
defaultModel = "text-embedding-3-small";
|
|
2330
|
+
apiKey;
|
|
2331
|
+
baseURL = "https://api.openai.com/v1";
|
|
2332
|
+
constructor(apiKey) {
|
|
2333
|
+
this.apiKey = apiKey || getOpenAIApiKey();
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Check if OpenAI is available (API key is set)
|
|
2337
|
+
*/
|
|
2338
|
+
isAvailable() {
|
|
2339
|
+
return !!this.apiKey;
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Generate embedding for a single text
|
|
2343
|
+
*/
|
|
2344
|
+
async generateEmbedding(text, model) {
|
|
2345
|
+
if (!this.apiKey) {
|
|
2346
|
+
throw new Error(
|
|
2347
|
+
"OpenAI API key not found. Set OPENAI_API_KEY environment variable. Get your key at https://platform.openai.com/api-keys"
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
validateText(text);
|
|
2351
|
+
const modelToUse = model || this.defaultModel;
|
|
2352
|
+
return retryWithBackoff2(async () => {
|
|
2353
|
+
try {
|
|
2354
|
+
const response = await axios16.post(
|
|
2355
|
+
`${this.baseURL}/embeddings`,
|
|
2356
|
+
{
|
|
2357
|
+
input: text,
|
|
2358
|
+
model: modelToUse
|
|
2359
|
+
},
|
|
2360
|
+
{
|
|
2361
|
+
headers: {
|
|
2362
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2363
|
+
"Content-Type": "application/json"
|
|
2364
|
+
},
|
|
2365
|
+
timeout: 3e4
|
|
2366
|
+
// 30 seconds
|
|
2367
|
+
}
|
|
2368
|
+
);
|
|
2369
|
+
const data = response.data;
|
|
2370
|
+
if (!data.data || data.data.length === 0) {
|
|
2371
|
+
throw new Error("No embedding returned from OpenAI");
|
|
2372
|
+
}
|
|
2373
|
+
return {
|
|
2374
|
+
embedding: data.data[0].embedding,
|
|
2375
|
+
model: data.model,
|
|
2376
|
+
dimensions: data.data[0].embedding.length,
|
|
2377
|
+
usage: {
|
|
2378
|
+
promptTokens: data.usage.prompt_tokens,
|
|
2379
|
+
totalTokens: data.usage.total_tokens
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
let wrappedError;
|
|
2384
|
+
if (error.response?.status === 401) {
|
|
2385
|
+
wrappedError = new Error(
|
|
2386
|
+
"Invalid OpenAI API key. Get your key at https://platform.openai.com/api-keys"
|
|
2387
|
+
);
|
|
2388
|
+
} else if (error.response?.status === 429) {
|
|
2389
|
+
wrappedError = new Error(
|
|
2390
|
+
"OpenAI API rate limit exceeded. Please try again later or upgrade your plan."
|
|
2391
|
+
);
|
|
2392
|
+
} else if (error.response?.status === 400) {
|
|
2393
|
+
wrappedError = new Error(
|
|
2394
|
+
`OpenAI API error: ${error.response.data?.error?.message || "Bad request"}`
|
|
2395
|
+
);
|
|
2396
|
+
} else {
|
|
2397
|
+
wrappedError = new Error(`OpenAI embedding generation failed: ${error.message}`);
|
|
2398
|
+
}
|
|
2399
|
+
if (error.code) wrappedError.code = error.code;
|
|
2400
|
+
if (error.response) wrappedError.response = error.response;
|
|
2401
|
+
throw wrappedError;
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Generate embeddings for multiple texts (batch)
|
|
2407
|
+
*/
|
|
2408
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2409
|
+
if (!this.apiKey) {
|
|
2410
|
+
throw new Error(
|
|
2411
|
+
"OpenAI API key not found. Set OPENAI_API_KEY environment variable. Get your key at https://platform.openai.com/api-keys"
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
validateBatch(texts);
|
|
2415
|
+
const modelToUse = model || this.defaultModel;
|
|
2416
|
+
return retryWithBackoff2(async () => {
|
|
2417
|
+
try {
|
|
2418
|
+
const response = await axios16.post(
|
|
2419
|
+
`${this.baseURL}/embeddings`,
|
|
2420
|
+
{
|
|
2421
|
+
input: texts,
|
|
2422
|
+
model: modelToUse
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
headers: {
|
|
2426
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2427
|
+
"Content-Type": "application/json"
|
|
2428
|
+
},
|
|
2429
|
+
timeout: 6e4
|
|
2430
|
+
// 60 seconds for batch
|
|
2431
|
+
}
|
|
2432
|
+
);
|
|
2433
|
+
const data = response.data;
|
|
2434
|
+
if (!data.data || data.data.length === 0) {
|
|
2435
|
+
throw new Error("No embeddings returned from OpenAI");
|
|
2436
|
+
}
|
|
2437
|
+
const sortedData = data.data.sort((a, b) => a.index - b.index);
|
|
2438
|
+
return {
|
|
2439
|
+
embeddings: sortedData.map((item) => item.embedding),
|
|
2440
|
+
model: data.model,
|
|
2441
|
+
dimensions: sortedData[0].embedding.length,
|
|
2442
|
+
usage: {
|
|
2443
|
+
promptTokens: data.usage.prompt_tokens,
|
|
2444
|
+
totalTokens: data.usage.total_tokens
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
} catch (error) {
|
|
2448
|
+
let wrappedError;
|
|
2449
|
+
if (error.response?.status === 401) {
|
|
2450
|
+
wrappedError = new Error(
|
|
2451
|
+
"Invalid OpenAI API key. Get your key at https://platform.openai.com/api-keys"
|
|
2452
|
+
);
|
|
2453
|
+
} else if (error.response?.status === 429) {
|
|
2454
|
+
wrappedError = new Error(
|
|
2455
|
+
"OpenAI API rate limit exceeded. Please try again later or upgrade your plan."
|
|
2456
|
+
);
|
|
2457
|
+
} else if (error.response?.status === 400) {
|
|
2458
|
+
wrappedError = new Error(
|
|
2459
|
+
`OpenAI API error: ${error.response.data?.error?.message || "Bad request"}`
|
|
2460
|
+
);
|
|
2461
|
+
} else {
|
|
2462
|
+
wrappedError = new Error(`OpenAI batch embedding generation failed: ${error.message}`);
|
|
2463
|
+
}
|
|
2464
|
+
if (error.code) wrappedError.code = error.code;
|
|
2465
|
+
if (error.response) wrappedError.response = error.response;
|
|
2466
|
+
throw wrappedError;
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
};
|
|
2471
|
+
var CohereEmbeddingProvider = class {
|
|
2472
|
+
name = "cohere";
|
|
2473
|
+
defaultModel = "embed-english-v3.0";
|
|
2474
|
+
apiKey;
|
|
2475
|
+
baseUrl = "https://api.cohere.ai/v1";
|
|
2476
|
+
constructor(apiKey, model) {
|
|
2477
|
+
this.apiKey = apiKey;
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Check if Cohere is available (API key is set)
|
|
2481
|
+
*/
|
|
2482
|
+
isAvailable() {
|
|
2483
|
+
return !!this.apiKey;
|
|
2484
|
+
}
|
|
2485
|
+
async generateEmbedding(text, model) {
|
|
2486
|
+
const result = await this.generateBatchEmbeddings([text], model);
|
|
2487
|
+
return {
|
|
2488
|
+
embedding: result.embeddings[0],
|
|
2489
|
+
model: result.model,
|
|
2490
|
+
dimensions: result.dimensions,
|
|
2491
|
+
usage: result.usage
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2495
|
+
const modelToUse = model || this.defaultModel;
|
|
2496
|
+
return retryWithBackoff2(async () => {
|
|
2497
|
+
try {
|
|
2498
|
+
const response = await axios16.post(
|
|
2499
|
+
`${this.baseUrl}/embed`,
|
|
2500
|
+
{
|
|
2501
|
+
texts,
|
|
2502
|
+
model: modelToUse,
|
|
2503
|
+
input_type: "search_document",
|
|
2504
|
+
// For storing in vector DB
|
|
2505
|
+
embedding_types: ["float"]
|
|
2506
|
+
},
|
|
2507
|
+
{
|
|
2508
|
+
headers: {
|
|
2509
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2510
|
+
"Content-Type": "application/json"
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
);
|
|
2514
|
+
const embeddings = response.data.embeddings.float;
|
|
2515
|
+
const dimensions = embeddings[0]?.length || 0;
|
|
2516
|
+
return {
|
|
2517
|
+
embeddings,
|
|
2518
|
+
model: modelToUse,
|
|
2519
|
+
dimensions,
|
|
2520
|
+
usage: response.data.meta?.billed_units ? {
|
|
2521
|
+
promptTokens: response.data.meta.billed_units.input_tokens || 0,
|
|
2522
|
+
totalTokens: response.data.meta.billed_units.input_tokens || 0
|
|
2523
|
+
} : void 0
|
|
2524
|
+
};
|
|
2525
|
+
} catch (error) {
|
|
2526
|
+
if (axios16.isAxiosError(error)) {
|
|
2527
|
+
const status = error.response?.status;
|
|
2528
|
+
const message = error.response?.data?.message || error.message;
|
|
2529
|
+
let wrappedError;
|
|
2530
|
+
if (status === 401) {
|
|
2531
|
+
wrappedError = new Error(`Cohere API authentication failed. Please check your COHERE_API_KEY. ${message}`);
|
|
2532
|
+
} else if (status === 429) {
|
|
2533
|
+
wrappedError = new Error(`Cohere API rate limit exceeded. ${message}`);
|
|
2534
|
+
} else if (status === 400) {
|
|
2535
|
+
wrappedError = new Error(`Cohere API request invalid: ${message}`);
|
|
2536
|
+
} else {
|
|
2537
|
+
wrappedError = new Error(`Cohere API error (${status}): ${message}`);
|
|
2538
|
+
}
|
|
2539
|
+
if (error.code) wrappedError.code = error.code;
|
|
2540
|
+
if (error.response) wrappedError.response = error.response;
|
|
2541
|
+
throw wrappedError;
|
|
2542
|
+
}
|
|
2543
|
+
throw error;
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
var HuggingFaceEmbeddingProvider = class {
|
|
2549
|
+
name = "huggingface";
|
|
2550
|
+
defaultModel = "sentence-transformers/all-MiniLM-L6-v2";
|
|
2551
|
+
apiKey;
|
|
2552
|
+
baseUrl = "https://api-inference.huggingface.co/pipeline/feature-extraction";
|
|
2553
|
+
constructor(apiKey, model) {
|
|
2554
|
+
this.apiKey = apiKey;
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Check if HuggingFace is available (API key is set)
|
|
2558
|
+
*/
|
|
2559
|
+
isAvailable() {
|
|
2560
|
+
return !!this.apiKey;
|
|
2561
|
+
}
|
|
2562
|
+
async generateEmbedding(text, model) {
|
|
2563
|
+
const modelToUse = model || this.defaultModel;
|
|
2564
|
+
return retryWithBackoff2(async () => {
|
|
2565
|
+
try {
|
|
2566
|
+
const response = await axios16.post(
|
|
2567
|
+
`${this.baseUrl}/${modelToUse}`,
|
|
2568
|
+
{
|
|
2569
|
+
inputs: text,
|
|
2570
|
+
options: {
|
|
2571
|
+
wait_for_model: true
|
|
2572
|
+
}
|
|
2573
|
+
},
|
|
2574
|
+
{
|
|
2575
|
+
headers: {
|
|
2576
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2577
|
+
"Content-Type": "application/json"
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
);
|
|
2581
|
+
const embedding = Array.isArray(response.data) ? response.data : response.data[0];
|
|
2582
|
+
return {
|
|
2583
|
+
embedding,
|
|
2584
|
+
model: modelToUse,
|
|
2585
|
+
dimensions: embedding.length
|
|
2586
|
+
};
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
if (axios16.isAxiosError(error)) {
|
|
2589
|
+
const status = error.response?.status;
|
|
2590
|
+
const message = error.response?.data?.error || error.message;
|
|
2591
|
+
let wrappedError;
|
|
2592
|
+
if (status === 401) {
|
|
2593
|
+
wrappedError = new Error(`HuggingFace API authentication failed. Please check your HUGGINGFACE_API_KEY. ${message}`);
|
|
2594
|
+
} else if (status === 429) {
|
|
2595
|
+
wrappedError = new Error(`HuggingFace API rate limit exceeded. ${message}`);
|
|
2596
|
+
} else if (status === 400) {
|
|
2597
|
+
wrappedError = new Error(`HuggingFace API request invalid: ${message}`);
|
|
2598
|
+
} else if (status === 503) {
|
|
2599
|
+
wrappedError = new Error(`HuggingFace model is loading. Please retry in a moment. ${message}`);
|
|
2600
|
+
} else {
|
|
2601
|
+
wrappedError = new Error(`HuggingFace API error (${status}): ${message}`);
|
|
2602
|
+
}
|
|
2603
|
+
if (error.code) wrappedError.code = error.code;
|
|
2604
|
+
if (error.response) wrappedError.response = error.response;
|
|
2605
|
+
throw wrappedError;
|
|
2606
|
+
}
|
|
2607
|
+
throw error;
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2612
|
+
const modelToUse = model || this.defaultModel;
|
|
2613
|
+
return retryWithBackoff2(async () => {
|
|
2614
|
+
try {
|
|
2615
|
+
const response = await axios16.post(
|
|
2616
|
+
`${this.baseUrl}/${modelToUse}`,
|
|
2617
|
+
{
|
|
2618
|
+
inputs: texts,
|
|
2619
|
+
options: {
|
|
2620
|
+
wait_for_model: true
|
|
2621
|
+
}
|
|
2622
|
+
},
|
|
2623
|
+
{
|
|
2624
|
+
headers: {
|
|
2625
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2626
|
+
"Content-Type": "application/json"
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
);
|
|
2630
|
+
const embeddings = response.data;
|
|
2631
|
+
const dimensions = embeddings[0]?.length || 0;
|
|
2632
|
+
return {
|
|
2633
|
+
embeddings,
|
|
2634
|
+
model: modelToUse,
|
|
2635
|
+
dimensions
|
|
2636
|
+
};
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
if (axios16.isAxiosError(error)) {
|
|
2639
|
+
const status = error.response?.status;
|
|
2640
|
+
const message = error.response?.data?.error || error.message;
|
|
2641
|
+
let wrappedError;
|
|
2642
|
+
if (status === 401) {
|
|
2643
|
+
wrappedError = new Error(`HuggingFace API authentication failed. Please check your HUGGINGFACE_API_KEY. ${message}`);
|
|
2644
|
+
} else if (status === 429) {
|
|
2645
|
+
wrappedError = new Error(`HuggingFace API rate limit exceeded. ${message}`);
|
|
2646
|
+
} else if (status === 400) {
|
|
2647
|
+
wrappedError = new Error(`HuggingFace API request invalid: ${message}`);
|
|
2648
|
+
} else if (status === 503) {
|
|
2649
|
+
wrappedError = new Error(`HuggingFace model is loading. Please retry in a moment. ${message}`);
|
|
2650
|
+
} else {
|
|
2651
|
+
wrappedError = new Error(`HuggingFace API error (${status}): ${message}`);
|
|
2652
|
+
}
|
|
2653
|
+
if (error.code) wrappedError.code = error.code;
|
|
2654
|
+
if (error.response) wrappedError.response = error.response;
|
|
2655
|
+
throw wrappedError;
|
|
2656
|
+
}
|
|
2657
|
+
throw error;
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2662
|
+
var VoyageEmbeddingProvider = class {
|
|
2663
|
+
name = "voyage";
|
|
2664
|
+
defaultModel = "voyage-2";
|
|
2665
|
+
apiKey;
|
|
2666
|
+
baseUrl = "https://api.voyageai.com/v1";
|
|
2667
|
+
constructor(apiKey, model) {
|
|
2668
|
+
this.apiKey = apiKey;
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Check if Voyage AI is available (API key is set)
|
|
2672
|
+
*/
|
|
2673
|
+
isAvailable() {
|
|
2674
|
+
return !!this.apiKey;
|
|
2675
|
+
}
|
|
2676
|
+
async generateEmbedding(text, model) {
|
|
2677
|
+
const result = await this.generateBatchEmbeddings([text], model);
|
|
2678
|
+
return {
|
|
2679
|
+
embedding: result.embeddings[0],
|
|
2680
|
+
model: result.model,
|
|
2681
|
+
dimensions: result.dimensions,
|
|
2682
|
+
usage: result.usage
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2686
|
+
const modelToUse = model || this.defaultModel;
|
|
2687
|
+
return retryWithBackoff2(async () => {
|
|
2688
|
+
try {
|
|
2689
|
+
const response = await axios16.post(
|
|
2690
|
+
`${this.baseUrl}/embeddings`,
|
|
2691
|
+
{
|
|
2692
|
+
input: texts,
|
|
2693
|
+
model: modelToUse,
|
|
2694
|
+
input_type: "document"
|
|
2695
|
+
// For storing in vector DB
|
|
2696
|
+
},
|
|
2697
|
+
{
|
|
2698
|
+
headers: {
|
|
2699
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2700
|
+
"Content-Type": "application/json"
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
);
|
|
2704
|
+
const embeddings = response.data.data.map((item) => item.embedding);
|
|
2705
|
+
const dimensions = embeddings[0]?.length || 0;
|
|
2706
|
+
return {
|
|
2707
|
+
embeddings,
|
|
2708
|
+
model: modelToUse,
|
|
2709
|
+
dimensions,
|
|
2710
|
+
usage: response.data.usage ? {
|
|
2711
|
+
promptTokens: response.data.usage.total_tokens || 0,
|
|
2712
|
+
totalTokens: response.data.usage.total_tokens || 0
|
|
2713
|
+
} : void 0
|
|
2714
|
+
};
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
if (axios16.isAxiosError(error)) {
|
|
2717
|
+
const status = error.response?.status;
|
|
2718
|
+
const message = error.response?.data?.message || error.message;
|
|
2719
|
+
let wrappedError;
|
|
2720
|
+
if (status === 401) {
|
|
2721
|
+
wrappedError = new Error(`Voyage AI API authentication failed. Please check your VOYAGE_API_KEY. ${message}`);
|
|
2722
|
+
} else if (status === 429) {
|
|
2723
|
+
wrappedError = new Error(`Voyage AI API rate limit exceeded. ${message}`);
|
|
2724
|
+
} else if (status === 400) {
|
|
2725
|
+
wrappedError = new Error(`Voyage AI API request invalid: ${message}`);
|
|
2726
|
+
} else {
|
|
2727
|
+
wrappedError = new Error(`Voyage AI API error (${status}): ${message}`);
|
|
2728
|
+
}
|
|
2729
|
+
if (error.code) wrappedError.code = error.code;
|
|
2730
|
+
if (error.response) wrappedError.response = error.response;
|
|
2731
|
+
throw wrappedError;
|
|
2732
|
+
}
|
|
2733
|
+
throw error;
|
|
2734
|
+
}
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2738
|
+
var OllamaEmbeddingProvider = class {
|
|
2739
|
+
name = "ollama";
|
|
2740
|
+
defaultModel = "nomic-embed-text";
|
|
2741
|
+
baseUrl;
|
|
2742
|
+
constructor(model, baseUrl = "http://localhost:11434") {
|
|
2743
|
+
this.baseUrl = baseUrl;
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Check if Ollama is available (always true for local service)
|
|
2747
|
+
*/
|
|
2748
|
+
isAvailable() {
|
|
2749
|
+
return true;
|
|
2750
|
+
}
|
|
2751
|
+
async generateEmbedding(text, model) {
|
|
2752
|
+
const modelToUse = model || this.defaultModel;
|
|
2753
|
+
return retryWithBackoff2(async () => {
|
|
2754
|
+
try {
|
|
2755
|
+
const response = await axios16.post(
|
|
2756
|
+
`${this.baseUrl}/api/embeddings`,
|
|
2757
|
+
{
|
|
2758
|
+
model: modelToUse,
|
|
2759
|
+
prompt: text
|
|
2760
|
+
},
|
|
2761
|
+
{
|
|
2762
|
+
headers: {
|
|
2763
|
+
"Content-Type": "application/json"
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
);
|
|
2767
|
+
const embedding = response.data.embedding;
|
|
2768
|
+
return {
|
|
2769
|
+
embedding,
|
|
2770
|
+
model: modelToUse,
|
|
2771
|
+
dimensions: embedding.length
|
|
2772
|
+
};
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
if (axios16.isAxiosError(error)) {
|
|
2775
|
+
const status = error.response?.status;
|
|
2776
|
+
const message = error.response?.data?.error || error.message;
|
|
2777
|
+
let wrappedError;
|
|
2778
|
+
if (error.code === "ECONNREFUSED") {
|
|
2779
|
+
wrappedError = new Error(`Cannot connect to Ollama at ${this.baseUrl}. Make sure Ollama is running locally.`);
|
|
2780
|
+
} else if (status === 404) {
|
|
2781
|
+
wrappedError = new Error(`Ollama model not found. Pull it with: ollama pull <model-name>`);
|
|
2782
|
+
} else if (status === 400) {
|
|
2783
|
+
wrappedError = new Error(`Ollama API request invalid: ${message}`);
|
|
2784
|
+
} else {
|
|
2785
|
+
wrappedError = new Error(`Ollama API error (${status}): ${message}`);
|
|
2786
|
+
}
|
|
2787
|
+
if (error.code) wrappedError.code = error.code;
|
|
2788
|
+
if (error.response) wrappedError.response = error.response;
|
|
2789
|
+
throw wrappedError;
|
|
2790
|
+
}
|
|
2791
|
+
throw error;
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2796
|
+
const modelToUse = model || this.defaultModel;
|
|
2797
|
+
const embeddings = [];
|
|
2798
|
+
for (const text of texts) {
|
|
2799
|
+
const result = await this.generateEmbedding(text, modelToUse);
|
|
2800
|
+
embeddings.push(result.embedding);
|
|
2801
|
+
}
|
|
2802
|
+
const dimensions = embeddings[0]?.length || 0;
|
|
2803
|
+
return {
|
|
2804
|
+
embeddings,
|
|
2805
|
+
model: modelToUse,
|
|
2806
|
+
dimensions
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
2810
|
+
|
|
2811
|
+
// src/data/neo4j/embeddings/embedding-manager.ts
|
|
2812
|
+
var logger4 = createLogger("agentforge:tools:neo4j:embeddings");
|
|
2813
|
+
var EmbeddingManager = class {
|
|
2814
|
+
provider = null;
|
|
2815
|
+
config = null;
|
|
2816
|
+
/**
|
|
2817
|
+
* Initialize with configuration
|
|
2818
|
+
*/
|
|
2819
|
+
initialize(config) {
|
|
2820
|
+
logger4.debug("Initializing embedding manager", {
|
|
2821
|
+
provider: config.provider,
|
|
2822
|
+
model: config.model
|
|
2823
|
+
});
|
|
2824
|
+
this.config = config;
|
|
2825
|
+
this.provider = this.createProvider(config.provider, config.apiKey);
|
|
2826
|
+
logger4.info("Embedding manager initialized", {
|
|
2827
|
+
provider: config.provider,
|
|
2828
|
+
model: config.model || this.getDefaultModel(config.provider)
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Initialize from environment variables
|
|
2833
|
+
*/
|
|
2834
|
+
initializeFromEnv() {
|
|
2835
|
+
const providerName = getEmbeddingProvider();
|
|
2836
|
+
const model = getEmbeddingModel();
|
|
2837
|
+
logger4.debug("Initializing embedding manager from environment", {
|
|
2838
|
+
provider: providerName,
|
|
2839
|
+
model: model || this.getDefaultModel(providerName)
|
|
2840
|
+
});
|
|
2841
|
+
let apiKey = "";
|
|
2842
|
+
let baseUrl = "";
|
|
2843
|
+
switch (providerName) {
|
|
2844
|
+
case "openai": {
|
|
2845
|
+
const key = getOpenAIApiKey();
|
|
2846
|
+
if (!key) {
|
|
2847
|
+
logger4.error("OPENAI_API_KEY environment variable not set", {
|
|
2848
|
+
provider: "openai",
|
|
2849
|
+
required: true
|
|
2850
|
+
});
|
|
2851
|
+
throw new Error("OPENAI_API_KEY environment variable is required for OpenAI embeddings");
|
|
2852
|
+
}
|
|
2853
|
+
apiKey = key;
|
|
2854
|
+
logger4.debug("OpenAI API key found");
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
case "cohere": {
|
|
2858
|
+
const key = getCohereApiKey();
|
|
2859
|
+
if (!key) {
|
|
2860
|
+
logger4.error("COHERE_API_KEY environment variable not set", {
|
|
2861
|
+
provider: "cohere",
|
|
2862
|
+
required: true
|
|
2863
|
+
});
|
|
2864
|
+
throw new Error("COHERE_API_KEY environment variable is required for Cohere embeddings");
|
|
2865
|
+
}
|
|
2866
|
+
apiKey = key;
|
|
2867
|
+
logger4.debug("Cohere API key found");
|
|
2868
|
+
break;
|
|
2869
|
+
}
|
|
2870
|
+
case "huggingface": {
|
|
2871
|
+
const key = getHuggingFaceApiKey();
|
|
2872
|
+
if (!key) {
|
|
2873
|
+
logger4.error("HUGGINGFACE_API_KEY environment variable not set", {
|
|
2874
|
+
provider: "huggingface",
|
|
2875
|
+
required: true
|
|
2876
|
+
});
|
|
2877
|
+
throw new Error("HUGGINGFACE_API_KEY environment variable is required for HuggingFace embeddings");
|
|
2878
|
+
}
|
|
2879
|
+
apiKey = key;
|
|
2880
|
+
logger4.debug("HuggingFace API key found");
|
|
2881
|
+
break;
|
|
2882
|
+
}
|
|
2883
|
+
case "voyage": {
|
|
2884
|
+
const key = getVoyageApiKey();
|
|
2885
|
+
if (!key) {
|
|
2886
|
+
logger4.error("VOYAGE_API_KEY environment variable not set", {
|
|
2887
|
+
provider: "voyage",
|
|
2888
|
+
required: true
|
|
2889
|
+
});
|
|
2890
|
+
throw new Error("VOYAGE_API_KEY environment variable is required for Voyage AI embeddings");
|
|
2891
|
+
}
|
|
2892
|
+
apiKey = key;
|
|
2893
|
+
logger4.debug("Voyage API key found");
|
|
2894
|
+
break;
|
|
2895
|
+
}
|
|
2896
|
+
case "ollama":
|
|
2897
|
+
baseUrl = getOllamaBaseUrl();
|
|
2898
|
+
logger4.debug("Using Ollama (local, no API key required)", {
|
|
2899
|
+
baseUrl: baseUrl || "http://localhost:11434"
|
|
2900
|
+
});
|
|
2901
|
+
break;
|
|
2902
|
+
default:
|
|
2903
|
+
logger4.error("Unknown embedding provider", {
|
|
2904
|
+
provider: providerName
|
|
2905
|
+
});
|
|
2906
|
+
throw new Error(`Unknown embedding provider: ${providerName}`);
|
|
2907
|
+
}
|
|
2908
|
+
this.provider = this.createProvider(providerName, apiKey, model, baseUrl);
|
|
2909
|
+
this.config = {
|
|
2910
|
+
provider: providerName,
|
|
2911
|
+
model: model || this.getDefaultModel(providerName),
|
|
2912
|
+
apiKey: ""
|
|
2913
|
+
// Not stored when using env
|
|
2914
|
+
};
|
|
2915
|
+
logger4.info("Embedding manager initialized from environment", {
|
|
2916
|
+
provider: providerName,
|
|
2917
|
+
model: this.config.model
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
/**
|
|
2921
|
+
* Check if manager is initialized
|
|
2922
|
+
*/
|
|
2923
|
+
isInitialized() {
|
|
2924
|
+
return this.provider !== null && this.config !== null;
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Get current provider
|
|
2928
|
+
*/
|
|
2929
|
+
getProvider() {
|
|
2930
|
+
if (!this.provider) {
|
|
2931
|
+
throw new Error("Embedding manager not initialized. Call initialize() or initializeFromEnv() first.");
|
|
2932
|
+
}
|
|
2933
|
+
return this.provider;
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Get current configuration
|
|
2937
|
+
*/
|
|
2938
|
+
getConfig() {
|
|
2939
|
+
if (!this.config) {
|
|
2940
|
+
throw new Error("Embedding manager not initialized. Call initialize() or initializeFromEnv() first.");
|
|
2941
|
+
}
|
|
2942
|
+
return this.config;
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Generate embedding for a single text
|
|
2946
|
+
*/
|
|
2947
|
+
async generateEmbedding(text, model) {
|
|
2948
|
+
const startTime = Date.now();
|
|
2949
|
+
const provider = this.getProvider();
|
|
2950
|
+
const config = this.getConfig();
|
|
2951
|
+
const modelToUse = model || config.model;
|
|
2952
|
+
logger4.debug("Generating embedding", {
|
|
2953
|
+
provider: provider.name,
|
|
2954
|
+
model: modelToUse,
|
|
2955
|
+
textLength: text.length
|
|
2956
|
+
});
|
|
2957
|
+
const result = await provider.generateEmbedding(text, modelToUse);
|
|
2958
|
+
logger4.info("Embedding generated", {
|
|
2959
|
+
provider: provider.name,
|
|
2960
|
+
model: result.model,
|
|
2961
|
+
dimensions: result.dimensions,
|
|
2962
|
+
duration: Date.now() - startTime
|
|
2963
|
+
});
|
|
2964
|
+
return result;
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Generate embeddings for multiple texts (batch)
|
|
2968
|
+
*/
|
|
2969
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2970
|
+
const startTime = Date.now();
|
|
2971
|
+
const provider = this.getProvider();
|
|
2972
|
+
const config = this.getConfig();
|
|
2973
|
+
const modelToUse = model || config.model;
|
|
2974
|
+
logger4.debug("Generating batch embeddings", {
|
|
2975
|
+
provider: provider.name,
|
|
2976
|
+
model: modelToUse,
|
|
2977
|
+
count: texts.length
|
|
2978
|
+
});
|
|
2979
|
+
const result = await provider.generateBatchEmbeddings(texts, modelToUse);
|
|
2980
|
+
logger4.info("Batch embeddings generated", {
|
|
2981
|
+
provider: provider.name,
|
|
2982
|
+
model: result.model,
|
|
2983
|
+
dimensions: result.dimensions,
|
|
2984
|
+
count: texts.length,
|
|
2985
|
+
duration: Date.now() - startTime
|
|
2986
|
+
});
|
|
2987
|
+
return result;
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Get default model for a provider
|
|
2991
|
+
*/
|
|
2992
|
+
getDefaultModel(provider) {
|
|
2993
|
+
switch (provider) {
|
|
2994
|
+
case "openai":
|
|
2995
|
+
return "text-embedding-3-small";
|
|
2996
|
+
case "cohere":
|
|
2997
|
+
return "embed-english-v3.0";
|
|
2998
|
+
case "huggingface":
|
|
2999
|
+
return "sentence-transformers/all-MiniLM-L6-v2";
|
|
3000
|
+
case "voyage":
|
|
3001
|
+
return "voyage-2";
|
|
3002
|
+
case "ollama":
|
|
3003
|
+
return "nomic-embed-text";
|
|
3004
|
+
default:
|
|
3005
|
+
return "text-embedding-3-small";
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
/**
|
|
3009
|
+
* Create a provider instance
|
|
3010
|
+
*/
|
|
3011
|
+
createProvider(providerName, apiKey, model, baseUrl) {
|
|
3012
|
+
switch (providerName) {
|
|
3013
|
+
case "openai":
|
|
3014
|
+
return new OpenAIEmbeddingProvider(apiKey);
|
|
3015
|
+
case "cohere":
|
|
3016
|
+
return new CohereEmbeddingProvider(apiKey);
|
|
3017
|
+
case "huggingface":
|
|
3018
|
+
return new HuggingFaceEmbeddingProvider(apiKey);
|
|
3019
|
+
case "voyage":
|
|
3020
|
+
return new VoyageEmbeddingProvider(apiKey);
|
|
3021
|
+
case "ollama":
|
|
3022
|
+
return new OllamaEmbeddingProvider(model, baseUrl || "http://localhost:11434");
|
|
3023
|
+
default:
|
|
3024
|
+
throw new Error(`Unknown embedding provider: ${providerName}`);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Reset the manager (for testing)
|
|
3029
|
+
*/
|
|
3030
|
+
reset() {
|
|
3031
|
+
this.provider = null;
|
|
3032
|
+
this.config = null;
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
var embeddingManager = new EmbeddingManager();
|
|
3036
|
+
function initializeEmbeddings() {
|
|
3037
|
+
embeddingManager.initializeFromEnv();
|
|
3038
|
+
}
|
|
3039
|
+
function initializeEmbeddingsWithConfig(config) {
|
|
3040
|
+
embeddingManager.initialize(config);
|
|
3041
|
+
}
|
|
3042
|
+
async function generateEmbedding(text, model) {
|
|
3043
|
+
return embeddingManager.generateEmbedding(text, model);
|
|
3044
|
+
}
|
|
3045
|
+
async function generateBatchEmbeddings(texts, model) {
|
|
3046
|
+
return embeddingManager.generateBatchEmbeddings(texts, model);
|
|
3047
|
+
}
|
|
3048
|
+
function formatInteger(value) {
|
|
3049
|
+
if (value.inSafeRange()) {
|
|
3050
|
+
return value.toNumber();
|
|
3051
|
+
}
|
|
3052
|
+
return value.toString();
|
|
3053
|
+
}
|
|
3054
|
+
function formatValue(value) {
|
|
3055
|
+
if (value === null || value === void 0) {
|
|
3056
|
+
return value;
|
|
3057
|
+
}
|
|
3058
|
+
if (value instanceof Integer) {
|
|
3059
|
+
return formatInteger(value);
|
|
3060
|
+
}
|
|
3061
|
+
if (value instanceof Node) {
|
|
3062
|
+
return formatNode(value);
|
|
3063
|
+
}
|
|
3064
|
+
if (value instanceof Relationship) {
|
|
3065
|
+
return formatRelationship(value);
|
|
3066
|
+
}
|
|
3067
|
+
if (value instanceof Path) {
|
|
3068
|
+
return formatPath(value);
|
|
3069
|
+
}
|
|
3070
|
+
if (Array.isArray(value)) {
|
|
3071
|
+
return value.map(formatValue);
|
|
3072
|
+
}
|
|
3073
|
+
if (typeof value === "object") {
|
|
3074
|
+
const formatted = {};
|
|
3075
|
+
for (const [key, val] of Object.entries(value)) {
|
|
3076
|
+
formatted[key] = formatValue(val);
|
|
3077
|
+
}
|
|
3078
|
+
return formatted;
|
|
3079
|
+
}
|
|
3080
|
+
return value;
|
|
3081
|
+
}
|
|
3082
|
+
function formatNode(node) {
|
|
3083
|
+
return {
|
|
3084
|
+
identity: formatInteger(node.identity),
|
|
3085
|
+
labels: node.labels,
|
|
3086
|
+
properties: formatValue(node.properties)
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
function formatRelationship(rel) {
|
|
3090
|
+
return {
|
|
3091
|
+
identity: formatInteger(rel.identity),
|
|
3092
|
+
type: rel.type,
|
|
3093
|
+
start: formatInteger(rel.start),
|
|
3094
|
+
end: formatInteger(rel.end),
|
|
3095
|
+
properties: formatValue(rel.properties)
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
function formatPath(path12) {
|
|
3099
|
+
return {
|
|
3100
|
+
start: formatNode(path12.start),
|
|
3101
|
+
end: formatNode(path12.end),
|
|
3102
|
+
segments: path12.segments.map((segment) => ({
|
|
3103
|
+
start: formatNode(segment.start),
|
|
3104
|
+
relationship: formatRelationship(segment.relationship),
|
|
3105
|
+
end: formatNode(segment.end)
|
|
3106
|
+
})),
|
|
3107
|
+
length: path12.length
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
function formatResults(records) {
|
|
3111
|
+
return records.map((record) => {
|
|
3112
|
+
const formatted = {};
|
|
3113
|
+
for (const key of record.keys) {
|
|
3114
|
+
formatted[key] = formatValue(record.get(key));
|
|
3115
|
+
}
|
|
3116
|
+
return formatted;
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
// src/data/neo4j/tools/neo4j-query.ts
|
|
3121
|
+
var logger5 = createLogger("agentforge:tools:neo4j:query");
|
|
3122
|
+
function createNeo4jQueryTool() {
|
|
3123
|
+
return toolBuilder().name("neo4j-query").description(
|
|
3124
|
+
"Execute a Cypher query against the Neo4j graph database. Supports parameterized queries for safety. Use this for complex queries, graph traversals, and custom operations."
|
|
3125
|
+
).category(ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "cypher", "query"]).schema(neo4jQuerySchema).implement(async (input) => {
|
|
3126
|
+
if (!neo4jPool.isInitialized()) {
|
|
3127
|
+
logger5.warn("Neo4j query attempted but connection not initialized");
|
|
3128
|
+
return {
|
|
3129
|
+
success: false,
|
|
3130
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
const startTime = Date.now();
|
|
3134
|
+
logger5.debug("Executing Neo4j query", {
|
|
3135
|
+
cypherPreview: input.cypher.substring(0, 100),
|
|
3136
|
+
parameterCount: Object.keys(input.parameters || {}).length,
|
|
3137
|
+
database: input.database
|
|
3138
|
+
});
|
|
3139
|
+
try {
|
|
3140
|
+
const session = neo4jPool.getSession(input.database);
|
|
3141
|
+
try {
|
|
3142
|
+
const result = await session.run(input.cypher, input.parameters || {});
|
|
3143
|
+
const formattedResults = formatResults(result.records);
|
|
3144
|
+
const duration = Date.now() - startTime;
|
|
3145
|
+
logger5.info("Neo4j query executed successfully", {
|
|
3146
|
+
recordCount: result.records.length,
|
|
3147
|
+
nodesCreated: result.summary.counters.updates().nodesCreated,
|
|
3148
|
+
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
|
3149
|
+
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
|
3150
|
+
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
|
3151
|
+
propertiesSet: result.summary.counters.updates().propertiesSet,
|
|
3152
|
+
duration
|
|
3153
|
+
});
|
|
3154
|
+
return {
|
|
3155
|
+
success: true,
|
|
3156
|
+
data: formattedResults,
|
|
3157
|
+
recordCount: result.records.length,
|
|
3158
|
+
summary: {
|
|
3159
|
+
query: input.cypher,
|
|
3160
|
+
parameters: input.parameters,
|
|
3161
|
+
counters: {
|
|
3162
|
+
nodesCreated: result.summary.counters.updates().nodesCreated,
|
|
3163
|
+
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
|
3164
|
+
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
|
3165
|
+
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
|
3166
|
+
propertiesSet: result.summary.counters.updates().propertiesSet
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
};
|
|
3170
|
+
} finally {
|
|
3171
|
+
await session.close();
|
|
3172
|
+
}
|
|
3173
|
+
} catch (error) {
|
|
3174
|
+
const duration = Date.now() - startTime;
|
|
3175
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to execute query";
|
|
3176
|
+
logger5.error("Neo4j query execution failed", {
|
|
3177
|
+
error: errorMessage,
|
|
3178
|
+
cypherPreview: input.cypher.substring(0, 100),
|
|
3179
|
+
duration
|
|
3180
|
+
});
|
|
3181
|
+
return {
|
|
3182
|
+
success: false,
|
|
3183
|
+
error: errorMessage,
|
|
3184
|
+
query: input.cypher
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
}).build();
|
|
3188
|
+
}
|
|
3189
|
+
function createNeo4jGetSchemaTool() {
|
|
3190
|
+
return toolBuilder().name("neo4j-get-schema").description(
|
|
3191
|
+
"Get the schema of the Neo4j graph database including node labels, relationship types, property keys, constraints, and indexes. This helps understand the structure of the graph before querying."
|
|
3192
|
+
).category(ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "schema", "introspection"]).schema(neo4jGetSchemaSchema).implement(async (input) => {
|
|
3193
|
+
if (!neo4jPool.isInitialized()) {
|
|
3194
|
+
return {
|
|
3195
|
+
success: false,
|
|
3196
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
try {
|
|
3200
|
+
const session = neo4jPool.getSession(input.database);
|
|
3201
|
+
try {
|
|
3202
|
+
const labelsResult = await session.run("CALL db.labels()");
|
|
3203
|
+
const nodeLabels = labelsResult.records.map((r) => r.get("label"));
|
|
3204
|
+
const relTypesResult = await session.run("CALL db.relationshipTypes()");
|
|
3205
|
+
const relationshipTypes = relTypesResult.records.map((r) => r.get("relationshipType"));
|
|
3206
|
+
const propsResult = await session.run("CALL db.propertyKeys()");
|
|
3207
|
+
const propertyKeys = propsResult.records.map((r) => r.get("propertyKey"));
|
|
3208
|
+
const constraintsResult = await session.run("SHOW CONSTRAINTS");
|
|
3209
|
+
const constraints = constraintsResult.records.map((r) => ({
|
|
3210
|
+
name: r.get("name"),
|
|
3211
|
+
type: r.get("type"),
|
|
3212
|
+
entityType: r.get("entityType"),
|
|
3213
|
+
labelsOrTypes: r.get("labelsOrTypes"),
|
|
3214
|
+
properties: r.get("properties")
|
|
3215
|
+
}));
|
|
3216
|
+
const indexesResult = await session.run("SHOW INDEXES");
|
|
3217
|
+
const indexes = indexesResult.records.map((r) => ({
|
|
3218
|
+
name: r.get("name"),
|
|
3219
|
+
type: r.get("type"),
|
|
3220
|
+
entityType: r.get("entityType"),
|
|
3221
|
+
labelsOrTypes: r.get("labelsOrTypes"),
|
|
3222
|
+
properties: r.get("properties")
|
|
3223
|
+
}));
|
|
3224
|
+
return {
|
|
3225
|
+
success: true,
|
|
3226
|
+
schema: {
|
|
3227
|
+
nodeLabels,
|
|
3228
|
+
relationshipTypes,
|
|
3229
|
+
propertyKeys,
|
|
3230
|
+
constraints,
|
|
3231
|
+
indexes
|
|
3232
|
+
},
|
|
3233
|
+
summary: {
|
|
3234
|
+
totalLabels: nodeLabels.length,
|
|
3235
|
+
totalRelationshipTypes: relationshipTypes.length,
|
|
3236
|
+
totalPropertyKeys: propertyKeys.length,
|
|
3237
|
+
totalConstraints: constraints.length,
|
|
3238
|
+
totalIndexes: indexes.length
|
|
3239
|
+
}
|
|
3240
|
+
};
|
|
3241
|
+
} finally {
|
|
3242
|
+
await session.close();
|
|
3243
|
+
}
|
|
3244
|
+
} catch (error) {
|
|
3245
|
+
return {
|
|
3246
|
+
success: false,
|
|
3247
|
+
error: error instanceof Error ? error.message : "Failed to get schema"
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
}).build();
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// src/data/neo4j/utils/cypher-sanitizer.ts
|
|
3254
|
+
function escapeCypherIdentifier(identifier, type = "identifier") {
|
|
3255
|
+
if (!identifier || typeof identifier !== "string") {
|
|
3256
|
+
throw new Error(`Invalid ${type}: must be a non-empty string`);
|
|
3257
|
+
}
|
|
3258
|
+
const trimmed = identifier.trim();
|
|
3259
|
+
if (trimmed.length === 0) {
|
|
3260
|
+
throw new Error(`Invalid ${type}: cannot be empty or whitespace`);
|
|
3261
|
+
}
|
|
3262
|
+
if (trimmed.includes("\0")) {
|
|
3263
|
+
throw new Error(`Invalid ${type}: cannot contain null bytes`);
|
|
3264
|
+
}
|
|
3265
|
+
const simplePattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3266
|
+
if (simplePattern.test(trimmed)) {
|
|
3267
|
+
return trimmed;
|
|
3268
|
+
}
|
|
3269
|
+
const escaped = trimmed.replace(/`/g, "``");
|
|
3270
|
+
return `\`${escaped}\``;
|
|
3271
|
+
}
|
|
3272
|
+
function validateLabel(label) {
|
|
3273
|
+
return escapeCypherIdentifier(label, "label");
|
|
3274
|
+
}
|
|
3275
|
+
function validatePropertyKey(key) {
|
|
3276
|
+
return escapeCypherIdentifier(key, "property key");
|
|
3277
|
+
}
|
|
3278
|
+
function validateRelationshipType(type) {
|
|
3279
|
+
return escapeCypherIdentifier(type, "relationship type");
|
|
3280
|
+
}
|
|
3281
|
+
function validateDirection(direction) {
|
|
3282
|
+
const normalized = direction.toUpperCase();
|
|
3283
|
+
if (normalized !== "OUTGOING" && normalized !== "INCOMING" && normalized !== "BOTH") {
|
|
3284
|
+
throw new Error(`Invalid direction: must be 'OUTGOING', 'INCOMING', or 'BOTH'`);
|
|
3285
|
+
}
|
|
3286
|
+
return normalized;
|
|
3287
|
+
}
|
|
3288
|
+
function buildPropertyFilter(properties, nodeVar = "n") {
|
|
3289
|
+
const keys = Object.keys(properties);
|
|
3290
|
+
if (keys.length === 0) {
|
|
3291
|
+
return { whereClause: "", parameters: {} };
|
|
3292
|
+
}
|
|
3293
|
+
const safeNodeVar = escapeCypherIdentifier(nodeVar, "node variable");
|
|
3294
|
+
const conditions = keys.map((key, index) => {
|
|
3295
|
+
const safeKey = validatePropertyKey(key);
|
|
3296
|
+
const paramName = `prop_${index}`;
|
|
3297
|
+
return `${safeNodeVar}.${safeKey} = $${paramName}`;
|
|
3298
|
+
});
|
|
3299
|
+
const parameters = {};
|
|
3300
|
+
keys.forEach((key, index) => {
|
|
3301
|
+
parameters[`prop_${index}`] = properties[key];
|
|
3302
|
+
});
|
|
3303
|
+
return {
|
|
3304
|
+
whereClause: `WHERE ${conditions.join(" AND ")}`,
|
|
3305
|
+
parameters
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
// src/data/neo4j/tools/neo4j-find-nodes.ts
|
|
3310
|
+
function createNeo4jFindNodesTool() {
|
|
3311
|
+
return toolBuilder().name("neo4j-find-nodes").description(
|
|
3312
|
+
"Find nodes in the Neo4j graph by label and optional property filters. This is a simplified interface for common node lookup operations. Returns nodes matching the specified criteria."
|
|
3313
|
+
).category(ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "nodes", "search"]).schema(neo4jFindNodesSchema).implement(async (input) => {
|
|
3314
|
+
if (!neo4jPool.isInitialized()) {
|
|
3315
|
+
return {
|
|
3316
|
+
success: false,
|
|
3317
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
try {
|
|
3321
|
+
const safeLabel = validateLabel(input.label);
|
|
3322
|
+
const session = neo4jPool.getSession(input.database);
|
|
3323
|
+
try {
|
|
3324
|
+
let cypher = `MATCH (n:${safeLabel})`;
|
|
3325
|
+
let parameters = {};
|
|
3326
|
+
if (input.properties && Object.keys(input.properties).length > 0) {
|
|
3327
|
+
const { whereClause, parameters: filterParams } = buildPropertyFilter(input.properties, "n");
|
|
3328
|
+
cypher += ` ${whereClause}`;
|
|
3329
|
+
parameters = { ...parameters, ...filterParams };
|
|
3330
|
+
}
|
|
3331
|
+
cypher += ` RETURN n LIMIT $limit`;
|
|
3332
|
+
parameters.limit = input.limit;
|
|
3333
|
+
const result = await session.run(cypher, parameters);
|
|
3334
|
+
const formattedResults = formatResults(result.records);
|
|
3335
|
+
return {
|
|
3336
|
+
success: true,
|
|
3337
|
+
nodes: formattedResults.map((r) => r.n),
|
|
3338
|
+
count: result.records.length,
|
|
3339
|
+
query: {
|
|
3340
|
+
label: input.label,
|
|
3341
|
+
properties: input.properties,
|
|
3342
|
+
limit: input.limit
|
|
3343
|
+
}
|
|
3344
|
+
};
|
|
3345
|
+
} finally {
|
|
3346
|
+
await session.close();
|
|
3347
|
+
}
|
|
3348
|
+
} catch (error) {
|
|
3349
|
+
return {
|
|
3350
|
+
success: false,
|
|
3351
|
+
error: error instanceof Error ? error.message : "Failed to find nodes",
|
|
3352
|
+
query: {
|
|
3353
|
+
label: input.label,
|
|
3354
|
+
properties: input.properties
|
|
3355
|
+
}
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
}).build();
|
|
3359
|
+
}
|
|
3360
|
+
function createNeo4jTraverseTool() {
|
|
3361
|
+
return toolBuilder().name("neo4j-traverse").description(
|
|
3362
|
+
"Traverse the Neo4j graph from a starting node by following relationships. Supports filtering by relationship type, direction, and maximum depth. Returns connected nodes and their relationships."
|
|
3363
|
+
).category(ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "traverse", "relationships"]).schema(neo4jTraverseSchema).implement(async (input) => {
|
|
3364
|
+
if (!neo4jPool.isInitialized()) {
|
|
3365
|
+
return {
|
|
3366
|
+
success: false,
|
|
3367
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
try {
|
|
3371
|
+
const safeDirection = validateDirection(input.direction || "outgoing");
|
|
3372
|
+
const safeRelType = input.relationshipType ? validateRelationshipType(input.relationshipType) : null;
|
|
3373
|
+
const session = neo4jPool.getSession(input.database);
|
|
3374
|
+
try {
|
|
3375
|
+
let relPattern = "";
|
|
3376
|
+
if (safeDirection === "OUTGOING") {
|
|
3377
|
+
relPattern = safeRelType ? `-[r:${safeRelType}*1..${input.maxDepth}]->` : `-[r*1..${input.maxDepth}]->`;
|
|
3378
|
+
} else if (safeDirection === "INCOMING") {
|
|
3379
|
+
relPattern = safeRelType ? `<-[r:${safeRelType}*1..${input.maxDepth}]-` : `<-[r*1..${input.maxDepth}]-`;
|
|
3380
|
+
} else {
|
|
3381
|
+
relPattern = safeRelType ? `-[r:${safeRelType}*1..${input.maxDepth}]-` : `-[r*1..${input.maxDepth}]-`;
|
|
3382
|
+
}
|
|
3383
|
+
const cypher = `
|
|
3384
|
+
MATCH path = (start)${relPattern}(end)
|
|
3385
|
+
WHERE id(start) = $startNodeId
|
|
3386
|
+
RETURN start, end, relationships(path) as rels, length(path) as depth
|
|
3387
|
+
LIMIT $limit
|
|
3388
|
+
`;
|
|
3389
|
+
const parameters = {
|
|
3390
|
+
startNodeId: typeof input.startNodeId === "string" ? parseInt(input.startNodeId, 10) : input.startNodeId,
|
|
3391
|
+
limit: input.limit
|
|
3392
|
+
};
|
|
3393
|
+
const result = await session.run(cypher, parameters);
|
|
3394
|
+
const formattedResults = formatResults(result.records);
|
|
3395
|
+
return {
|
|
3396
|
+
success: true,
|
|
3397
|
+
paths: formattedResults.map((r) => ({
|
|
3398
|
+
start: r.start,
|
|
3399
|
+
end: r.end,
|
|
3400
|
+
relationships: r.rels,
|
|
3401
|
+
depth: r.depth
|
|
3402
|
+
})),
|
|
3403
|
+
count: result.records.length,
|
|
3404
|
+
query: {
|
|
3405
|
+
startNodeId: input.startNodeId,
|
|
3406
|
+
relationshipType: input.relationshipType,
|
|
3407
|
+
direction: input.direction,
|
|
3408
|
+
maxDepth: input.maxDepth
|
|
3409
|
+
}
|
|
3410
|
+
};
|
|
3411
|
+
} finally {
|
|
3412
|
+
await session.close();
|
|
3413
|
+
}
|
|
3414
|
+
} catch (error) {
|
|
3415
|
+
return {
|
|
3416
|
+
success: false,
|
|
3417
|
+
error: error instanceof Error ? error.message : "Failed to traverse graph",
|
|
3418
|
+
query: {
|
|
3419
|
+
startNodeId: input.startNodeId,
|
|
3420
|
+
relationshipType: input.relationshipType,
|
|
3421
|
+
direction: input.direction
|
|
3422
|
+
}
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
}).build();
|
|
3426
|
+
}
|
|
3427
|
+
function createNeo4jVectorSearchTool() {
|
|
3428
|
+
return toolBuilder().name("neo4j-vector-search").description(
|
|
3429
|
+
"Perform semantic similarity search using vector indexes in Neo4j. Essential for GraphRAG applications - finds nodes with similar embeddings. Requires a vector index to be created in advance."
|
|
3430
|
+
).category(ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "vector", "search", "semantic", "graphrag"]).schema(neo4jVectorSearchSchema).implement(async (input) => {
|
|
3431
|
+
if (!neo4jPool.isInitialized()) {
|
|
3432
|
+
return {
|
|
3433
|
+
success: false,
|
|
3434
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
try {
|
|
3438
|
+
const session = neo4jPool.getSession(input.database);
|
|
3439
|
+
try {
|
|
3440
|
+
const cypher = `
|
|
3441
|
+
CALL db.index.vector.queryNodes($indexName, $limit, $queryVector)
|
|
3442
|
+
YIELD node, score
|
|
3443
|
+
RETURN node, score
|
|
3444
|
+
ORDER BY score DESC
|
|
3445
|
+
`;
|
|
3446
|
+
const parameters = {
|
|
3447
|
+
indexName: input.indexName,
|
|
3448
|
+
limit: input.limit,
|
|
3449
|
+
queryVector: input.queryVector
|
|
3450
|
+
};
|
|
3451
|
+
const result = await session.run(cypher, parameters);
|
|
3452
|
+
const formattedResults = formatResults(result.records);
|
|
3453
|
+
return {
|
|
3454
|
+
success: true,
|
|
3455
|
+
results: formattedResults.map((r) => ({
|
|
3456
|
+
node: r.node,
|
|
3457
|
+
score: r.score
|
|
3458
|
+
})),
|
|
3459
|
+
count: result.records.length,
|
|
3460
|
+
query: {
|
|
3461
|
+
indexName: input.indexName,
|
|
3462
|
+
vectorDimension: input.queryVector.length,
|
|
3463
|
+
limit: input.limit
|
|
3464
|
+
}
|
|
3465
|
+
};
|
|
3466
|
+
} finally {
|
|
3467
|
+
await session.close();
|
|
3468
|
+
}
|
|
3469
|
+
} catch (error) {
|
|
3470
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to perform vector search";
|
|
3471
|
+
let helpText = "";
|
|
3472
|
+
if (errorMessage.includes("index") || errorMessage.includes("not found")) {
|
|
3473
|
+
helpText = " Make sure the vector index exists. Create one with: CREATE VECTOR INDEX <name> FOR (n:Label) ON (n.embedding)";
|
|
3474
|
+
}
|
|
3475
|
+
return {
|
|
3476
|
+
success: false,
|
|
3477
|
+
error: errorMessage + helpText,
|
|
3478
|
+
query: {
|
|
3479
|
+
indexName: input.indexName,
|
|
3480
|
+
vectorDimension: input.queryVector.length
|
|
3481
|
+
}
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
}).build();
|
|
3485
|
+
}
|
|
3486
|
+
var logger6 = createLogger("agentforge:tools:neo4j:vector-search");
|
|
3487
|
+
function createNeo4jVectorSearchWithEmbeddingTool() {
|
|
3488
|
+
return toolBuilder().name("neo4j-vector-search-with-embedding").description(
|
|
3489
|
+
"Perform semantic similarity search in Neo4j by automatically generating embeddings from text. This tool takes text input, generates an embedding vector, and searches for similar nodes. Essential for GraphRAG applications - no need to manually generate embeddings. Requires a vector index and embedding provider (OpenAI) to be configured."
|
|
3490
|
+
).category(ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "vector", "search", "semantic", "graphrag", "embedding", "ai"]).schema(neo4jVectorSearchWithEmbeddingSchema).implement(async (input) => {
|
|
3491
|
+
if (!neo4jPool.isInitialized()) {
|
|
3492
|
+
logger6.warn("Vector search attempted but Neo4j connection not initialized");
|
|
3493
|
+
return {
|
|
3494
|
+
success: false,
|
|
3495
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
if (!embeddingManager.isInitialized()) {
|
|
3499
|
+
logger6.warn("Vector search attempted but embedding manager not initialized");
|
|
3500
|
+
return {
|
|
3501
|
+
success: false,
|
|
3502
|
+
error: "Embedding manager not initialized. Please configure embedding provider (set OPENAI_API_KEY and optionally EMBEDDING_MODEL)."
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
const startTime = Date.now();
|
|
3506
|
+
logger6.debug("Performing vector search with embedding", {
|
|
3507
|
+
queryTextLength: input.queryText.length,
|
|
3508
|
+
indexName: input.indexName,
|
|
3509
|
+
limit: input.limit,
|
|
3510
|
+
model: input.model
|
|
3511
|
+
});
|
|
3512
|
+
try {
|
|
3513
|
+
const embeddingResult = await embeddingManager.generateEmbedding(input.queryText, input.model);
|
|
3514
|
+
const session = neo4jPool.getSession(input.database);
|
|
3515
|
+
try {
|
|
3516
|
+
const cypher = `
|
|
3517
|
+
CALL db.index.vector.queryNodes($indexName, $limit, $queryVector)
|
|
3518
|
+
YIELD node, score
|
|
3519
|
+
RETURN node, score
|
|
3520
|
+
ORDER BY score DESC
|
|
3521
|
+
`;
|
|
3522
|
+
const parameters = {
|
|
3523
|
+
indexName: input.indexName,
|
|
3524
|
+
limit: input.limit,
|
|
3525
|
+
queryVector: embeddingResult.embedding
|
|
3526
|
+
};
|
|
3527
|
+
const result = await session.run(cypher, parameters);
|
|
3528
|
+
const formattedResults = formatResults(result.records);
|
|
3529
|
+
const duration = Date.now() - startTime;
|
|
3530
|
+
logger6.info("Vector search completed successfully", {
|
|
3531
|
+
resultCount: result.records.length,
|
|
3532
|
+
indexName: input.indexName,
|
|
3533
|
+
embeddingModel: embeddingResult.model,
|
|
3534
|
+
embeddingDimensions: embeddingResult.dimensions,
|
|
3535
|
+
duration
|
|
3536
|
+
});
|
|
3537
|
+
return {
|
|
3538
|
+
success: true,
|
|
3539
|
+
results: formattedResults.map((r) => ({
|
|
3540
|
+
node: r.node,
|
|
3541
|
+
score: r.score
|
|
3542
|
+
})),
|
|
3543
|
+
count: result.records.length,
|
|
3544
|
+
query: {
|
|
3545
|
+
text: input.queryText,
|
|
3546
|
+
indexName: input.indexName,
|
|
3547
|
+
embeddingModel: embeddingResult.model,
|
|
3548
|
+
vectorDimension: embeddingResult.dimensions,
|
|
3549
|
+
limit: input.limit
|
|
3550
|
+
},
|
|
3551
|
+
embedding: {
|
|
3552
|
+
model: embeddingResult.model,
|
|
3553
|
+
dimensions: embeddingResult.dimensions,
|
|
3554
|
+
usage: embeddingResult.usage
|
|
3555
|
+
}
|
|
3556
|
+
};
|
|
3557
|
+
} finally {
|
|
3558
|
+
await session.close();
|
|
3559
|
+
}
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
const duration = Date.now() - startTime;
|
|
3562
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to perform vector search with embedding";
|
|
3563
|
+
let helpText = "";
|
|
3564
|
+
if (errorMessage.includes("index") || errorMessage.includes("not found")) {
|
|
3565
|
+
helpText = " Make sure the vector index exists. Create one with: CREATE VECTOR INDEX <name> FOR (n:Label) ON (n.embedding)";
|
|
3566
|
+
} else if (errorMessage.includes("API key") || errorMessage.includes("not initialized")) {
|
|
3567
|
+
helpText = " Make sure OPENAI_API_KEY is set in your environment variables.";
|
|
3568
|
+
} else if (errorMessage.includes("dimension")) {
|
|
3569
|
+
helpText = " Make sure the vector index dimensions match your embedding model dimensions.";
|
|
3570
|
+
}
|
|
3571
|
+
logger6.error("Vector search failed", {
|
|
3572
|
+
error: errorMessage,
|
|
3573
|
+
indexName: input.indexName,
|
|
3574
|
+
queryTextLength: input.queryText.length,
|
|
3575
|
+
duration
|
|
3576
|
+
});
|
|
3577
|
+
return {
|
|
3578
|
+
success: false,
|
|
3579
|
+
error: errorMessage + helpText,
|
|
3580
|
+
query: {
|
|
3581
|
+
text: input.queryText,
|
|
3582
|
+
indexName: input.indexName
|
|
3583
|
+
}
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
}).build();
|
|
3587
|
+
}
|
|
3588
|
+
function createNeo4jCreateNodeWithEmbeddingTool() {
|
|
3589
|
+
return toolBuilder().name("neo4j-create-node-with-embedding").description(
|
|
3590
|
+
"Create a Neo4j node with automatic embedding generation from text content. This tool extracts text from a specified property, generates an embedding vector, and stores both the original properties and the embedding in the node. Perfect for building GraphRAG knowledge bases. Requires embedding provider (OpenAI) to be configured."
|
|
3591
|
+
).category(ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "create", "node", "embedding", "graphrag", "ai"]).schema(neo4jCreateNodeWithEmbeddingSchema).implement(async (input) => {
|
|
3592
|
+
if (!neo4jPool.isInitialized()) {
|
|
3593
|
+
return {
|
|
3594
|
+
success: false,
|
|
3595
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
if (!embeddingManager.isInitialized()) {
|
|
3599
|
+
return {
|
|
3600
|
+
success: false,
|
|
3601
|
+
error: "Embedding manager not initialized. Please configure embedding provider (set OPENAI_API_KEY and optionally EMBEDDING_MODEL)."
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
try {
|
|
3605
|
+
const textToEmbed = input.properties[input.textProperty];
|
|
3606
|
+
if (!textToEmbed || typeof textToEmbed !== "string") {
|
|
3607
|
+
return {
|
|
3608
|
+
success: false,
|
|
3609
|
+
error: `Property '${input.textProperty}' not found or is not a string in the provided properties.`
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
const embeddingResult = await embeddingManager.generateEmbedding(textToEmbed, input.model);
|
|
3613
|
+
const safeLabel = validateLabel(input.label);
|
|
3614
|
+
const safeEmbeddingProp = validatePropertyKey(input.embeddingProperty || "embedding");
|
|
3615
|
+
const session = neo4jPool.getSession(input.database);
|
|
3616
|
+
try {
|
|
3617
|
+
const allProperties = {
|
|
3618
|
+
...input.properties
|
|
3619
|
+
};
|
|
3620
|
+
allProperties[input.embeddingProperty || "embedding"] = embeddingResult.embedding;
|
|
3621
|
+
const cypher = `
|
|
3622
|
+
CREATE (n:${safeLabel})
|
|
3623
|
+
SET n = $properties
|
|
3624
|
+
RETURN n, id(n) as nodeId
|
|
3625
|
+
`;
|
|
3626
|
+
const parameters = {
|
|
3627
|
+
properties: allProperties
|
|
3628
|
+
};
|
|
3629
|
+
const result = await session.run(cypher, parameters);
|
|
3630
|
+
const formattedResults = formatResults(result.records);
|
|
3631
|
+
if (formattedResults.length === 0) {
|
|
3632
|
+
return {
|
|
3633
|
+
success: false,
|
|
3634
|
+
error: "Failed to create node"
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
const createdNode = formattedResults[0];
|
|
3638
|
+
return {
|
|
3639
|
+
success: true,
|
|
3640
|
+
node: createdNode.n,
|
|
3641
|
+
nodeId: createdNode.nodeId,
|
|
3642
|
+
embedding: {
|
|
3643
|
+
model: embeddingResult.model,
|
|
3644
|
+
dimensions: embeddingResult.dimensions,
|
|
3645
|
+
property: safeEmbeddingProp,
|
|
3646
|
+
usage: embeddingResult.usage
|
|
3647
|
+
},
|
|
3648
|
+
message: `Created node with label '${input.label}' and ${embeddingResult.dimensions}-dimensional embedding`
|
|
3649
|
+
};
|
|
3650
|
+
} finally {
|
|
3651
|
+
await session.close();
|
|
3652
|
+
}
|
|
3653
|
+
} catch (error) {
|
|
3654
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to create node with embedding";
|
|
3655
|
+
let helpText = "";
|
|
3656
|
+
if (errorMessage.includes("API key") || errorMessage.includes("not initialized")) {
|
|
3657
|
+
helpText = " Make sure OPENAI_API_KEY is set in your environment variables.";
|
|
3658
|
+
} else if (errorMessage.includes("Syntax error") || errorMessage.includes("Invalid")) {
|
|
3659
|
+
helpText = " Check that the label and property names are valid.";
|
|
3660
|
+
}
|
|
3661
|
+
return {
|
|
3662
|
+
success: false,
|
|
3663
|
+
error: errorMessage + helpText
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
}).build();
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// src/data/neo4j/index.ts
|
|
3670
|
+
var neo4jQuery = createNeo4jQueryTool();
|
|
3671
|
+
var neo4jGetSchema = createNeo4jGetSchemaTool();
|
|
3672
|
+
var neo4jFindNodes = createNeo4jFindNodesTool();
|
|
3673
|
+
var neo4jTraverse = createNeo4jTraverseTool();
|
|
3674
|
+
var neo4jVectorSearch = createNeo4jVectorSearchTool();
|
|
3675
|
+
var neo4jVectorSearchWithEmbedding = createNeo4jVectorSearchWithEmbeddingTool();
|
|
3676
|
+
var neo4jCreateNodeWithEmbedding = createNeo4jCreateNodeWithEmbeddingTool();
|
|
3677
|
+
var neo4jTools = [
|
|
3678
|
+
neo4jQuery,
|
|
3679
|
+
neo4jGetSchema,
|
|
3680
|
+
neo4jFindNodes,
|
|
3681
|
+
neo4jTraverse,
|
|
3682
|
+
neo4jVectorSearch,
|
|
3683
|
+
neo4jVectorSearchWithEmbedding,
|
|
3684
|
+
neo4jCreateNodeWithEmbedding
|
|
3685
|
+
];
|
|
3686
|
+
var neo4jCoreTools = [
|
|
3687
|
+
neo4jQuery,
|
|
3688
|
+
neo4jGetSchema,
|
|
3689
|
+
neo4jFindNodes,
|
|
3690
|
+
neo4jTraverse,
|
|
3691
|
+
neo4jVectorSearch
|
|
3692
|
+
];
|
|
3693
|
+
function createNeo4jTools(config = {}, includeEmbeddingTools = true) {
|
|
3694
|
+
if (config.uri && config.username && config.password) {
|
|
3695
|
+
neo4jPool.initialize({
|
|
3696
|
+
uri: config.uri,
|
|
3697
|
+
username: config.username,
|
|
3698
|
+
password: config.password,
|
|
3699
|
+
database: config.database,
|
|
3700
|
+
maxConnectionPoolSize: config.maxConnectionPoolSize,
|
|
3701
|
+
connectionTimeout: config.connectionTimeout
|
|
3702
|
+
}).catch((error) => {
|
|
3703
|
+
console.error("Failed to initialize Neo4j connection:", error);
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
const coreTools = [
|
|
3707
|
+
createNeo4jQueryTool(),
|
|
3708
|
+
createNeo4jGetSchemaTool(),
|
|
3709
|
+
createNeo4jFindNodesTool(),
|
|
3710
|
+
createNeo4jTraverseTool(),
|
|
3711
|
+
createNeo4jVectorSearchTool()
|
|
3712
|
+
];
|
|
3713
|
+
if (includeEmbeddingTools) {
|
|
3714
|
+
return [
|
|
3715
|
+
...coreTools,
|
|
3716
|
+
createNeo4jVectorSearchWithEmbeddingTool(),
|
|
3717
|
+
createNeo4jCreateNodeWithEmbeddingTool()
|
|
3718
|
+
];
|
|
3719
|
+
}
|
|
3720
|
+
return coreTools;
|
|
3721
|
+
}
|
|
3722
|
+
async function initializeNeo4jTools() {
|
|
3723
|
+
await initializeFromEnv();
|
|
3724
|
+
try {
|
|
3725
|
+
embeddingManager.initializeFromEnv();
|
|
3726
|
+
} catch (error) {
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
2028
3729
|
var fileReaderSchema = z.object({
|
|
2029
3730
|
path: z.string().describe("Path to the file to read"),
|
|
2030
3731
|
encoding: z.enum(["utf8", "utf-8", "ascii", "base64", "hex", "binary"]).default("utf8").describe("File encoding")
|
|
@@ -3174,7 +4875,7 @@ var AskHumanInputSchema = z.object({
|
|
|
3174
4875
|
suggestions: z.array(z.string()).optional().describe("Suggested responses for the human")
|
|
3175
4876
|
});
|
|
3176
4877
|
var logLevel3 = process.env.LOG_LEVEL?.toLowerCase() || LogLevel.INFO;
|
|
3177
|
-
var
|
|
4878
|
+
var logger7 = createLogger("askHuman", { level: logLevel3 });
|
|
3178
4879
|
function createAskHumanTool() {
|
|
3179
4880
|
return toolBuilder().name("ask-human").description(
|
|
3180
4881
|
"Ask a human for input or approval. Use this when you need human guidance, approval for a critical action, or clarification on ambiguous requirements. The agent execution will pause until the human responds."
|
|
@@ -3207,13 +4908,13 @@ function createAskHumanTool() {
|
|
|
3207
4908
|
suggestions: validatedInput.suggestions,
|
|
3208
4909
|
status: "pending"
|
|
3209
4910
|
};
|
|
3210
|
-
|
|
4911
|
+
logger7.debug("About to call interrupt()", { humanRequest });
|
|
3211
4912
|
let response;
|
|
3212
4913
|
try {
|
|
3213
4914
|
response = interrupt(humanRequest);
|
|
3214
|
-
|
|
4915
|
+
logger7.debug("interrupt() returned successfully", { response, responseType: typeof response });
|
|
3215
4916
|
} catch (error) {
|
|
3216
|
-
|
|
4917
|
+
logger7.debug("interrupt() threw error (expected for GraphInterrupt)", {
|
|
3217
4918
|
errorType: error?.constructor?.name,
|
|
3218
4919
|
error: error instanceof Error ? error.message : String(error)
|
|
3219
4920
|
});
|
|
@@ -3238,6 +4939,6 @@ function createAskHumanTool() {
|
|
|
3238
4939
|
}
|
|
3239
4940
|
var askHumanTool = createAskHumanTool();
|
|
3240
4941
|
|
|
3241
|
-
export { AskHumanInputSchema, CalculatorSchema, CreditCardValidatorSchema, CurrentDateTimeSchema, DateArithmeticSchema, DateComparisonSchema, DateDifferenceSchema, DateFormatterSchema, DuckDuckGoProvider, EmailValidatorSchema, HttpMethod, IpValidatorSchema, MathFunctionsSchema, PhoneValidatorSchema, RandomNumberSchema, SerperProvider, StatisticsSchema, StringCaseConverterSchema, StringJoinSchema, StringLengthSchema, StringReplaceSchema, StringSplitSchema, StringSubstringSchema, StringTrimSchema, UrlValidatorSimpleSchema, UuidValidatorSchema, archiveConfluencePage, arrayFilter, arrayFilterSchema, arrayGroupBy, arrayGroupBySchema, arrayMap, arrayMapSchema, arraySort, arraySortSchema, askHumanTool, calculator, confluenceTools, createArrayFilterTool, createArrayGroupByTool, createArrayMapTool, createArraySortTool, createAskHumanTool, createCalculatorTool, createConfluencePage, createConfluenceTools, createCreditCardValidatorTool, createCsvGeneratorTool, createCsvParserTool, createCsvToJsonTool, createCsvTools, createCurrentDateTimeTool, createDateArithmeticTool, createDateComparisonTool, createDateDifferenceTool, createDateFormatterTool, createDateTimeTools, createDirectoryCreateTool, createDirectoryDeleteTool, createDirectoryListTool, createDirectoryOperationTools, createDuckDuckGoProvider, createEmailValidatorTool, createExtractImagesTool, createExtractLinksTool, createFileAppendTool, createFileDeleteTool, createFileExistsTool, createFileOperationTools, createFileReaderTool, createFileSearchTool, createFileWriterTool, createHtmlParserTool, createHtmlParserTools, createHttpTools, createIpValidatorTool, createJsonMergeTool, createJsonParserTool, createJsonQueryTool, createJsonStringifyTool, createJsonToCsvTool, createJsonToXmlTool, createJsonTools, createJsonValidatorTool, createMathFunctionsTool, createMathOperationTools, createObjectOmitTool, createObjectPickTool, createPathBasenameTool, createPathDirnameTool, createPathExtensionTool, createPathJoinTool, createPathNormalizeTool, createPathParseTool, createPathRelativeTool, createPathResolveTool, createPathUtilityTools, createPhoneValidatorTool, createRandomNumberTool, createScraperTools, createSerperProvider, createSlackTools, createStatisticsTool, createStringCaseConverterTool, createStringJoinTool, createStringLengthTool, createStringReplaceTool, createStringSplitTool, createStringSubstringTool, createStringTrimTool, createStringUtilityTools, createTransformerTools, createUrlBuilderTool, createUrlQueryParserTool, createUrlValidatorSimpleTool, createUrlValidatorTool, createUrlValidatorTools, createUuidValidatorTool, createValidationTools, createWebScraperTool, createXmlGeneratorTool, createXmlParserTool, createXmlToJsonTool, createXmlTools, creditCardValidator, csvGenerator, csvGeneratorSchema, csvParser, csvParserSchema, csvToJson, csvToJsonSchema, csvTools, currentDateTime, dateArithmetic, dateComparison, dateDifference, dateFormatter, dateTimeTools, directoryCreate, directoryCreateSchema, directoryDelete, directoryDeleteSchema, directoryList, directoryListSchema, directoryOperationTools, emailValidator, extractImages, extractImagesSchema, extractLinks, extractLinksSchema, fileAppend, fileAppendSchema, fileDelete, fileDeleteSchema, fileExists, fileExistsSchema, fileOperationTools, fileReader, fileReaderSchema, fileSearch, fileSearchSchema, fileWriter, fileWriterSchema, getConfluencePage, getSlackChannels, getSlackMessages, getSpacePages, htmlParser, htmlParserSchema, htmlParserTools, httpClient, httpGet, httpGetSchema, httpPost, httpPostSchema, httpRequestSchema, httpTools, ipValidator, jsonMerge, jsonMergeSchema, jsonParser, jsonParserSchema, jsonQuery, jsonQuerySchema, jsonStringify, jsonStringifySchema, jsonToCsv, jsonToCsvSchema, jsonToXml, jsonToXmlSchema, jsonTools, jsonValidator, jsonValidatorSchema, listConfluenceSpaces, mathFunctions, mathOperationTools, notifySlack, objectOmit, objectOmitSchema, objectPick, objectPickSchema, pathBasename, pathBasenameSchema, pathDirname, pathDirnameSchema, pathExtension, pathExtensionSchema, pathJoin, pathJoinSchema, pathNormalize, pathNormalizeSchema, pathParse, pathParseSchema, pathRelative, pathRelativeSchema, pathResolve, pathResolveSchema, pathUtilityTools, phoneValidator, randomNumber, scraperTools, searchConfluence, searchResultSchema, sendSlackMessage, slackTools, statistics, stringCaseConverter, stringJoin, stringLength, stringReplace, stringSplit, stringSubstring, stringTrim, stringUtilityTools, transformerTools, updateConfluencePage, urlBuilder, urlBuilderSchema, urlQueryParser, urlQueryParserSchema, urlValidator, urlValidatorSchema, urlValidatorSimple, urlValidatorTools, uuidValidator, validationTools, webScraper, webScraperSchema, webSearch, webSearchOutputSchema, webSearchSchema, xmlGenerator, xmlGeneratorSchema, xmlParser, xmlParserSchema, xmlToJson, xmlToJsonSchema, xmlTools };
|
|
4942
|
+
export { AskHumanInputSchema, CalculatorSchema, CreditCardValidatorSchema, CurrentDateTimeSchema, DEFAULT_RETRY_CONFIG2 as DEFAULT_RETRY_CONFIG, DateArithmeticSchema, DateComparisonSchema, DateDifferenceSchema, DateFormatterSchema, DuckDuckGoProvider, EmailValidatorSchema, EmbeddingManager, HttpMethod, IpValidatorSchema, MathFunctionsSchema, OpenAIEmbeddingProvider, PhoneValidatorSchema, RandomNumberSchema, SerperProvider, StatisticsSchema, StringCaseConverterSchema, StringJoinSchema, StringLengthSchema, StringReplaceSchema, StringSplitSchema, StringSubstringSchema, StringTrimSchema, UrlValidatorSimpleSchema, UuidValidatorSchema, archiveConfluencePage, arrayFilter, arrayFilterSchema, arrayGroupBy, arrayGroupBySchema, arrayMap, arrayMapSchema, arraySort, arraySortSchema, askHumanTool, calculator, confluenceTools, createArrayFilterTool, createArrayGroupByTool, createArrayMapTool, createArraySortTool, createAskHumanTool, createCalculatorTool, createConfluencePage, createConfluenceTools, createCreditCardValidatorTool, createCsvGeneratorTool, createCsvParserTool, createCsvToJsonTool, createCsvTools, createCurrentDateTimeTool, createDateArithmeticTool, createDateComparisonTool, createDateDifferenceTool, createDateFormatterTool, createDateTimeTools, createDirectoryCreateTool, createDirectoryDeleteTool, createDirectoryListTool, createDirectoryOperationTools, createDuckDuckGoProvider, createEmailValidatorTool, createExtractImagesTool, createExtractLinksTool, createFileAppendTool, createFileDeleteTool, createFileExistsTool, createFileOperationTools, createFileReaderTool, createFileSearchTool, createFileWriterTool, createHtmlParserTool, createHtmlParserTools, createHttpTools, createIpValidatorTool, createJsonMergeTool, createJsonParserTool, createJsonQueryTool, createJsonStringifyTool, createJsonToCsvTool, createJsonToXmlTool, createJsonTools, createJsonValidatorTool, createMathFunctionsTool, createMathOperationTools, createNeo4jCreateNodeWithEmbeddingTool, createNeo4jFindNodesTool, createNeo4jGetSchemaTool, createNeo4jQueryTool, createNeo4jTools, createNeo4jTraverseTool, createNeo4jVectorSearchTool, createNeo4jVectorSearchWithEmbeddingTool, createObjectOmitTool, createObjectPickTool, createPathBasenameTool, createPathDirnameTool, createPathExtensionTool, createPathJoinTool, createPathNormalizeTool, createPathParseTool, createPathRelativeTool, createPathResolveTool, createPathUtilityTools, createPhoneValidatorTool, createRandomNumberTool, createScraperTools, createSerperProvider, createSlackTools, createStatisticsTool, createStringCaseConverterTool, createStringJoinTool, createStringLengthTool, createStringReplaceTool, createStringSplitTool, createStringSubstringTool, createStringTrimTool, createStringUtilityTools, createTransformerTools, createUrlBuilderTool, createUrlQueryParserTool, createUrlValidatorSimpleTool, createUrlValidatorTool, createUrlValidatorTools, createUuidValidatorTool, createValidationTools, createWebScraperTool, createXmlGeneratorTool, createXmlParserTool, createXmlToJsonTool, createXmlTools, creditCardValidator, csvGenerator, csvGeneratorSchema, csvParser, csvParserSchema, csvToJson, csvToJsonSchema, csvTools, currentDateTime, dateArithmetic, dateComparison, dateDifference, dateFormatter, dateTimeTools, directoryCreate, directoryCreateSchema, directoryDelete, directoryDeleteSchema, directoryList, directoryListSchema, directoryOperationTools, emailValidator, embeddingManager, extractImages, extractImagesSchema, extractLinks, extractLinksSchema, fileAppend, fileAppendSchema, fileDelete, fileDeleteSchema, fileExists, fileExistsSchema, fileOperationTools, fileReader, fileReaderSchema, fileSearch, fileSearchSchema, fileWriter, fileWriterSchema, generateBatchEmbeddings, generateEmbedding, getCohereApiKey, getConfluencePage, getEmbeddingModel, getEmbeddingProvider, getHuggingFaceApiKey, getOllamaBaseUrl, getOpenAIApiKey, getSlackChannels, getSlackMessages, getSpacePages, getVoyageApiKey, htmlParser, htmlParserSchema, htmlParserTools, httpClient, httpGet, httpGetSchema, httpPost, httpPostSchema, httpRequestSchema, httpTools, initializeEmbeddings, initializeEmbeddingsWithConfig, initializeFromEnv, initializeNeo4jTools, ipValidator, isRetryableError2 as isRetryableError, jsonMerge, jsonMergeSchema, jsonParser, jsonParserSchema, jsonQuery, jsonQuerySchema, jsonStringify, jsonStringifySchema, jsonToCsv, jsonToCsvSchema, jsonToXml, jsonToXmlSchema, jsonTools, jsonValidator, jsonValidatorSchema, listConfluenceSpaces, mathFunctions, mathOperationTools, neo4jCoreTools, neo4jCreateNodeWithEmbedding, neo4jCreateNodeWithEmbeddingSchema, neo4jFindNodes, neo4jFindNodesSchema, neo4jGetSchema, neo4jGetSchemaSchema, neo4jPool, neo4jQuery, neo4jQuerySchema, neo4jTools, neo4jTraverse, neo4jTraverseSchema, neo4jVectorSearch, neo4jVectorSearchSchema, neo4jVectorSearchWithEmbedding, neo4jVectorSearchWithEmbeddingSchema, notifySlack, objectOmit, objectOmitSchema, objectPick, objectPickSchema, pathBasename, pathBasenameSchema, pathDirname, pathDirnameSchema, pathExtension, pathExtensionSchema, pathJoin, pathJoinSchema, pathNormalize, pathNormalizeSchema, pathParse, pathParseSchema, pathRelative, pathRelativeSchema, pathResolve, pathResolveSchema, pathUtilityTools, phoneValidator, randomNumber, retryWithBackoff2 as retryWithBackoff, scraperTools, searchConfluence, searchResultSchema, sendSlackMessage, slackTools, statistics, stringCaseConverter, stringJoin, stringLength, stringReplace, stringSplit, stringSubstring, stringTrim, stringUtilityTools, transformerTools, updateConfluencePage, urlBuilder, urlBuilderSchema, urlQueryParser, urlQueryParserSchema, urlValidator, urlValidatorSchema, urlValidatorSimple, urlValidatorTools, uuidValidator, validateBatch, validateText, validationTools, webScraper, webScraperSchema, webSearch, webSearchOutputSchema, webSearchSchema, xmlGenerator, xmlGeneratorSchema, xmlParser, xmlParserSchema, xmlToJson, xmlToJsonSchema, xmlTools };
|
|
3242
4943
|
//# sourceMappingURL=index.js.map
|
|
3243
4944
|
//# sourceMappingURL=index.js.map
|