@agentforge/tools 0.12.5 → 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.cjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var core = require('@agentforge/core');
|
|
4
|
-
var
|
|
4
|
+
var axios16 = require('axios');
|
|
5
5
|
var zod = require('zod');
|
|
6
6
|
var cheerio = require('cheerio');
|
|
7
7
|
var webApi = require('@slack/web-api');
|
|
8
8
|
var sync = require('csv-parse/sync');
|
|
9
9
|
var sync$1 = require('csv-stringify/sync');
|
|
10
10
|
var fastXmlParser = require('fast-xml-parser');
|
|
11
|
+
var neo4j = require('neo4j-driver');
|
|
11
12
|
var fs = require('fs');
|
|
12
13
|
var path7 = require('path');
|
|
13
14
|
var dateFns = require('date-fns');
|
|
@@ -33,8 +34,9 @@ function _interopNamespace(e) {
|
|
|
33
34
|
return Object.freeze(n);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
var
|
|
37
|
+
var axios16__default = /*#__PURE__*/_interopDefault(axios16);
|
|
37
38
|
var cheerio__namespace = /*#__PURE__*/_interopNamespace(cheerio);
|
|
39
|
+
var neo4j__default = /*#__PURE__*/_interopDefault(neo4j);
|
|
38
40
|
var path7__namespace = /*#__PURE__*/_interopNamespace(path7);
|
|
39
41
|
|
|
40
42
|
// src/web/http/tools/http-client.ts
|
|
@@ -71,7 +73,7 @@ function createHttpClientTool(defaultTimeout = 3e4, defaultHeaders = {}) {
|
|
|
71
73
|
validateStatus: () => true
|
|
72
74
|
// Don't throw on any status code
|
|
73
75
|
};
|
|
74
|
-
const response = await
|
|
76
|
+
const response = await axios16__default.default(config);
|
|
75
77
|
return {
|
|
76
78
|
status: response.status,
|
|
77
79
|
statusText: response.statusText,
|
|
@@ -84,7 +86,7 @@ function createHttpClientTool(defaultTimeout = 3e4, defaultHeaders = {}) {
|
|
|
84
86
|
}
|
|
85
87
|
function createHttpGetTool(defaultTimeout = 3e4, defaultHeaders = {}) {
|
|
86
88
|
return core.toolBuilder().name("http-get").description("Make a simple HTTP GET request to a URL and return the response data.").category(core.ToolCategory.WEB).tags(["http", "get", "fetch", "web"]).schema(httpGetSchema).implement(async (input) => {
|
|
87
|
-
const response = await
|
|
89
|
+
const response = await axios16__default.default.get(input.url, {
|
|
88
90
|
headers: { ...defaultHeaders, ...input.headers },
|
|
89
91
|
params: input.params,
|
|
90
92
|
timeout: defaultTimeout
|
|
@@ -94,7 +96,7 @@ function createHttpGetTool(defaultTimeout = 3e4, defaultHeaders = {}) {
|
|
|
94
96
|
}
|
|
95
97
|
function createHttpPostTool(defaultTimeout = 3e4, defaultHeaders = {}) {
|
|
96
98
|
return core.toolBuilder().name("http-post").description("Make a simple HTTP POST request with JSON body and return the response data.").category(core.ToolCategory.WEB).tags(["http", "post", "api", "web"]).schema(httpPostSchema).implement(async (input) => {
|
|
97
|
-
const response = await
|
|
99
|
+
const response = await axios16__default.default.post(input.url, input.body, {
|
|
98
100
|
headers: {
|
|
99
101
|
"Content-Type": "application/json",
|
|
100
102
|
...defaultHeaders,
|
|
@@ -131,7 +133,7 @@ var webScraperSchema = zod.z.object({
|
|
|
131
133
|
});
|
|
132
134
|
function createWebScraperTool(defaultTimeout = 3e4, userAgent = "Mozilla/5.0 (compatible; AgentForge/1.0; +https://agentforge.dev)") {
|
|
133
135
|
return core.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(core.ToolCategory.WEB).tags(["scraper", "web", "html", "extract", "parse"]).schema(webScraperSchema).implement(async (input) => {
|
|
134
|
-
const response = await
|
|
136
|
+
const response = await axios16__default.default.get(input.url, {
|
|
135
137
|
timeout: input.timeout || defaultTimeout,
|
|
136
138
|
headers: {
|
|
137
139
|
"User-Agent": userAgent
|
|
@@ -521,7 +523,7 @@ var DuckDuckGoProvider = class {
|
|
|
521
523
|
async search(query, maxResults, timeout = DEFAULT_TIMEOUT) {
|
|
522
524
|
return retryWithBackoff(async () => {
|
|
523
525
|
try {
|
|
524
|
-
const response = await
|
|
526
|
+
const response = await axios16__default.default.get(
|
|
525
527
|
"https://api.duckduckgo.com/",
|
|
526
528
|
{
|
|
527
529
|
params: {
|
|
@@ -625,7 +627,7 @@ var SerperProvider = class {
|
|
|
625
627
|
}
|
|
626
628
|
return retryWithBackoff(async () => {
|
|
627
629
|
try {
|
|
628
|
-
const response = await
|
|
630
|
+
const response = await axios16__default.default.post(
|
|
629
631
|
"https://google.serper.dev/search",
|
|
630
632
|
{
|
|
631
633
|
q: query,
|
|
@@ -805,7 +807,7 @@ function getDefaultSlackClient() {
|
|
|
805
807
|
}
|
|
806
808
|
};
|
|
807
809
|
}
|
|
808
|
-
function createSendSlackMessageTool(getSlackClient,
|
|
810
|
+
function createSendSlackMessageTool(getSlackClient, logger8) {
|
|
809
811
|
return core.toolBuilder().name("send-slack-message").description("Send a message to a Slack channel for team communication and notifications").category(core.ToolCategory.WEB).tags(["slack", "messaging", "communication"]).usageNotes(
|
|
810
812
|
"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."
|
|
811
813
|
).suggests(["get-slack-channels"]).schema(
|
|
@@ -814,7 +816,7 @@ function createSendSlackMessageTool(getSlackClient, logger4) {
|
|
|
814
816
|
message: zod.z.string().describe("Message content to send")
|
|
815
817
|
})
|
|
816
818
|
).implementSafe(async ({ channel, message }) => {
|
|
817
|
-
|
|
819
|
+
logger8.info("send-slack-message called", { channel, messageLength: message.length });
|
|
818
820
|
try {
|
|
819
821
|
const { client: slack, config } = getSlackClient();
|
|
820
822
|
const result = await slack.chat.postMessage({
|
|
@@ -823,7 +825,7 @@ function createSendSlackMessageTool(getSlackClient, logger4) {
|
|
|
823
825
|
username: config.botName,
|
|
824
826
|
icon_emoji: config.botIcon
|
|
825
827
|
});
|
|
826
|
-
|
|
828
|
+
logger8.info("send-slack-message result", {
|
|
827
829
|
channel: result.channel,
|
|
828
830
|
timestamp: result.ts,
|
|
829
831
|
messageLength: message.length,
|
|
@@ -836,7 +838,7 @@ function createSendSlackMessageTool(getSlackClient, logger4) {
|
|
|
836
838
|
message_id: result.ts
|
|
837
839
|
};
|
|
838
840
|
} catch (error) {
|
|
839
|
-
|
|
841
|
+
logger8.error("send-slack-message failed", {
|
|
840
842
|
channel,
|
|
841
843
|
error: error.message,
|
|
842
844
|
data: error.data
|
|
@@ -845,7 +847,7 @@ function createSendSlackMessageTool(getSlackClient, logger4) {
|
|
|
845
847
|
}
|
|
846
848
|
}).build();
|
|
847
849
|
}
|
|
848
|
-
function createNotifySlackTool(getSlackClient,
|
|
850
|
+
function createNotifySlackTool(getSlackClient, logger8) {
|
|
849
851
|
return core.toolBuilder().name("notify-slack").description("Send a notification to a Slack channel with optional @mentions for urgent alerts").category(core.ToolCategory.WEB).tags(["slack", "notification", "alert"]).usageNotes(
|
|
850
852
|
"Use this for urgent notifications that require @mentions. For general messages without mentions, use send-slack-message instead."
|
|
851
853
|
).suggests(["get-slack-channels"]).schema(
|
|
@@ -855,7 +857,7 @@ function createNotifySlackTool(getSlackClient, logger4) {
|
|
|
855
857
|
mentions: zod.z.array(zod.z.string()).optional().describe("List of usernames to mention (without @)")
|
|
856
858
|
})
|
|
857
859
|
).implementSafe(async ({ channel, message, mentions = [] }) => {
|
|
858
|
-
|
|
860
|
+
logger8.info("notify-slack called", {
|
|
859
861
|
channel,
|
|
860
862
|
messageLength: message.length,
|
|
861
863
|
mentionCount: mentions.length
|
|
@@ -869,7 +871,7 @@ function createNotifySlackTool(getSlackClient, logger4) {
|
|
|
869
871
|
username: config.botName,
|
|
870
872
|
icon_emoji: config.botIcon
|
|
871
873
|
});
|
|
872
|
-
|
|
874
|
+
logger8.info("notify-slack result", {
|
|
873
875
|
channel: result.channel,
|
|
874
876
|
timestamp: result.ts,
|
|
875
877
|
mentions: mentions.length
|
|
@@ -883,7 +885,7 @@ function createNotifySlackTool(getSlackClient, logger4) {
|
|
|
883
885
|
};
|
|
884
886
|
}).build();
|
|
885
887
|
}
|
|
886
|
-
function createGetSlackChannelsTool(getSlackClient,
|
|
888
|
+
function createGetSlackChannelsTool(getSlackClient, logger8) {
|
|
887
889
|
return core.toolBuilder().name("get-slack-channels").description("Get a list of available Slack channels to find the right channel for messaging").category(core.ToolCategory.WEB).tags(["slack", "channels", "list"]).usageNotes(
|
|
888
890
|
"Use this first to discover available channels before sending messages. Helps ensure you are sending to the correct channel."
|
|
889
891
|
).follows(["send-slack-message", "notify-slack"]).schema(
|
|
@@ -891,7 +893,7 @@ function createGetSlackChannelsTool(getSlackClient, logger4) {
|
|
|
891
893
|
include_private: zod.z.boolean().optional().describe("Include private channels (default: false)")
|
|
892
894
|
})
|
|
893
895
|
).implementSafe(async ({ include_private = false }) => {
|
|
894
|
-
|
|
896
|
+
logger8.info("get-slack-channels called", { include_private });
|
|
895
897
|
const { client: slack } = getSlackClient();
|
|
896
898
|
const publicChannels = await slack.conversations.list({
|
|
897
899
|
types: "public_channel",
|
|
@@ -905,7 +907,7 @@ function createGetSlackChannelsTool(getSlackClient, logger4) {
|
|
|
905
907
|
});
|
|
906
908
|
allChannels = [...allChannels, ...privateChannels.channels || []];
|
|
907
909
|
}
|
|
908
|
-
|
|
910
|
+
logger8.info("get-slack-channels result", {
|
|
909
911
|
channelCount: allChannels.length,
|
|
910
912
|
includePrivate: include_private
|
|
911
913
|
});
|
|
@@ -920,7 +922,7 @@ function createGetSlackChannelsTool(getSlackClient, logger4) {
|
|
|
920
922
|
};
|
|
921
923
|
}).build();
|
|
922
924
|
}
|
|
923
|
-
function createGetSlackMessagesTool(getSlackClient,
|
|
925
|
+
function createGetSlackMessagesTool(getSlackClient, logger8) {
|
|
924
926
|
return core.toolBuilder().name("get-slack-messages").description("Retrieve message history from a Slack channel to read recent conversations").category(core.ToolCategory.WEB).tags(["slack", "messages", "history", "read"]).usageNotes(
|
|
925
927
|
"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)."
|
|
926
928
|
).suggests(["get-slack-channels"]).schema(
|
|
@@ -929,7 +931,7 @@ function createGetSlackMessagesTool(getSlackClient, logger4) {
|
|
|
929
931
|
limit: zod.z.number().int().min(1).max(100).optional().describe("Number of messages to retrieve (default: 20, max: 100)")
|
|
930
932
|
})
|
|
931
933
|
).implementSafe(async ({ channel, limit = 20 }) => {
|
|
932
|
-
|
|
934
|
+
logger8.info("get-slack-messages called", { channel, limit });
|
|
933
935
|
try {
|
|
934
936
|
const { client: slack } = getSlackClient();
|
|
935
937
|
let channelId = channel;
|
|
@@ -940,7 +942,7 @@ function createGetSlackMessagesTool(getSlackClient, logger4) {
|
|
|
940
942
|
});
|
|
941
943
|
const found = channels.channels?.find((c) => c.name === channel);
|
|
942
944
|
if (!found) {
|
|
943
|
-
|
|
945
|
+
logger8.error("get-slack-messages: channel not found", { channel });
|
|
944
946
|
throw new Error(
|
|
945
947
|
`Channel '${channel}' not found. Use get-slack-channels to see available channels.`
|
|
946
948
|
);
|
|
@@ -952,7 +954,7 @@ function createGetSlackMessagesTool(getSlackClient, logger4) {
|
|
|
952
954
|
limit: Math.min(limit, 100)
|
|
953
955
|
// Cap at 100 for performance
|
|
954
956
|
});
|
|
955
|
-
|
|
957
|
+
logger8.info("get-slack-messages result", {
|
|
956
958
|
channel: channelId,
|
|
957
959
|
messageCount: result.messages?.length || 0,
|
|
958
960
|
limit
|
|
@@ -970,7 +972,7 @@ function createGetSlackMessagesTool(getSlackClient, logger4) {
|
|
|
970
972
|
})) || []
|
|
971
973
|
};
|
|
972
974
|
} catch (error) {
|
|
973
|
-
|
|
975
|
+
logger8.error("get-slack-messages failed", {
|
|
974
976
|
channel,
|
|
975
977
|
error: error.message,
|
|
976
978
|
data: error.data
|
|
@@ -1033,8 +1035,8 @@ function createGetConfiguredAuth(apiKey, email, siteUrl) {
|
|
|
1033
1035
|
function createGetConfiguredAuthHeader(getConfiguredAuth) {
|
|
1034
1036
|
return function getConfiguredAuthHeader() {
|
|
1035
1037
|
const { ATLASSIAN_API_KEY, ATLASSIAN_EMAIL } = getConfiguredAuth();
|
|
1036
|
-
const
|
|
1037
|
-
return `Basic ${
|
|
1038
|
+
const auth2 = Buffer.from(`${ATLASSIAN_EMAIL}:${ATLASSIAN_API_KEY}`).toString("base64");
|
|
1039
|
+
return `Basic ${auth2}`;
|
|
1038
1040
|
};
|
|
1039
1041
|
}
|
|
1040
1042
|
function getConfig() {
|
|
@@ -1048,18 +1050,18 @@ function getConfig() {
|
|
|
1048
1050
|
}
|
|
1049
1051
|
function getAuthHeader() {
|
|
1050
1052
|
const { ATLASSIAN_API_KEY, ATLASSIAN_EMAIL } = getConfig();
|
|
1051
|
-
const
|
|
1052
|
-
return `Basic ${
|
|
1053
|
+
const auth2 = Buffer.from(`${ATLASSIAN_EMAIL}:${ATLASSIAN_API_KEY}`).toString("base64");
|
|
1054
|
+
return `Basic ${auth2}`;
|
|
1053
1055
|
}
|
|
1054
|
-
function createSearchConfluenceTool(getAuth, getAuthHeader2,
|
|
1056
|
+
function createSearchConfluenceTool(getAuth, getAuthHeader2, logger8) {
|
|
1055
1057
|
return core.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(core.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(zod.z.object({
|
|
1056
1058
|
query: zod.z.string().describe("Search query or CQL expression (e.g., 'payment processing' or 'space=BL3 AND title~payment')"),
|
|
1057
1059
|
limit: zod.z.number().optional().describe("Maximum number of results to return (default: 10, max: 25)")
|
|
1058
1060
|
})).implement(async ({ query, limit = 10 }) => {
|
|
1059
|
-
|
|
1061
|
+
logger8.info("search-confluence called", { query, limit });
|
|
1060
1062
|
try {
|
|
1061
1063
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1062
|
-
const response = await
|
|
1064
|
+
const response = await axios16__default.default.get(`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/search`, {
|
|
1063
1065
|
headers: {
|
|
1064
1066
|
Authorization: getAuthHeader2(),
|
|
1065
1067
|
Accept: "application/json"
|
|
@@ -1081,13 +1083,13 @@ function createSearchConfluenceTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1081
1083
|
lastModified: page.version?.when || ""
|
|
1082
1084
|
}));
|
|
1083
1085
|
if (results.length === 0) {
|
|
1084
|
-
|
|
1086
|
+
logger8.warn("search-confluence returned NO RESULTS - this is a valid outcome, agent should not retry", {
|
|
1085
1087
|
query,
|
|
1086
1088
|
limit,
|
|
1087
1089
|
totalSize: response.data.totalSize
|
|
1088
1090
|
});
|
|
1089
1091
|
} else {
|
|
1090
|
-
|
|
1092
|
+
logger8.info("search-confluence result", {
|
|
1091
1093
|
query,
|
|
1092
1094
|
resultCount: results.length,
|
|
1093
1095
|
totalSize: response.data.totalSize,
|
|
@@ -1102,7 +1104,7 @@ function createSearchConfluenceTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1102
1104
|
results
|
|
1103
1105
|
});
|
|
1104
1106
|
} catch (error) {
|
|
1105
|
-
|
|
1107
|
+
logger8.error("search-confluence error", {
|
|
1106
1108
|
query,
|
|
1107
1109
|
error: error.response?.data?.message || error.message,
|
|
1108
1110
|
status: error.response?.status
|
|
@@ -1114,14 +1116,14 @@ function createSearchConfluenceTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1114
1116
|
}
|
|
1115
1117
|
}).build();
|
|
1116
1118
|
}
|
|
1117
|
-
function createGetConfluencePageTool(getAuth, getAuthHeader2,
|
|
1119
|
+
function createGetConfluencePageTool(getAuth, getAuthHeader2, logger8) {
|
|
1118
1120
|
return core.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(core.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(zod.z.object({
|
|
1119
1121
|
page_id: zod.z.string().describe("The Confluence page ID (from search results)")
|
|
1120
1122
|
})).implement(async ({ page_id }) => {
|
|
1121
|
-
|
|
1123
|
+
logger8.info("get-confluence-page called", { page_id });
|
|
1122
1124
|
try {
|
|
1123
1125
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1124
|
-
const response = await
|
|
1126
|
+
const response = await axios16__default.default.get(`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/${page_id}`, {
|
|
1125
1127
|
headers: {
|
|
1126
1128
|
Authorization: getAuthHeader2(),
|
|
1127
1129
|
Accept: "application/json"
|
|
@@ -1131,7 +1133,7 @@ function createGetConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1131
1133
|
}
|
|
1132
1134
|
});
|
|
1133
1135
|
const page = response.data;
|
|
1134
|
-
|
|
1136
|
+
logger8.info("get-confluence-page result", {
|
|
1135
1137
|
page_id,
|
|
1136
1138
|
title: page.title,
|
|
1137
1139
|
space: page.space?.name,
|
|
@@ -1153,7 +1155,7 @@ function createGetConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1153
1155
|
}
|
|
1154
1156
|
});
|
|
1155
1157
|
} catch (error) {
|
|
1156
|
-
|
|
1158
|
+
logger8.error("get-confluence-page error", {
|
|
1157
1159
|
page_id,
|
|
1158
1160
|
error: error.response?.data?.message || error.message,
|
|
1159
1161
|
status: error.response?.status
|
|
@@ -1165,14 +1167,14 @@ function createGetConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1165
1167
|
}
|
|
1166
1168
|
}).build();
|
|
1167
1169
|
}
|
|
1168
|
-
function createListConfluenceSpacesTool(getAuth, getAuthHeader2,
|
|
1170
|
+
function createListConfluenceSpacesTool(getAuth, getAuthHeader2, logger8) {
|
|
1169
1171
|
return core.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(core.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(zod.z.object({
|
|
1170
1172
|
limit: zod.z.number().optional().describe("Maximum number of spaces to return (default: 25)")
|
|
1171
1173
|
})).implement(async ({ limit = 25 }) => {
|
|
1172
|
-
|
|
1174
|
+
logger8.info("list-confluence-spaces called", { limit });
|
|
1173
1175
|
try {
|
|
1174
1176
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1175
|
-
const response = await
|
|
1177
|
+
const response = await axios16__default.default.get(`${ATLASSIAN_SITE_URL}/wiki/rest/api/space`, {
|
|
1176
1178
|
headers: {
|
|
1177
1179
|
Authorization: getAuthHeader2(),
|
|
1178
1180
|
Accept: "application/json"
|
|
@@ -1188,7 +1190,7 @@ function createListConfluenceSpacesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1188
1190
|
description: space.description?.plain?.value || "",
|
|
1189
1191
|
url: `${ATLASSIAN_SITE_URL}/wiki${space._links.webui}`
|
|
1190
1192
|
}));
|
|
1191
|
-
|
|
1193
|
+
logger8.info("list-confluence-spaces result", {
|
|
1192
1194
|
spaceCount: spaces.length,
|
|
1193
1195
|
spaceKeys: spaces.map((s) => s.key).slice(0, 5)
|
|
1194
1196
|
// Log first 5 space keys
|
|
@@ -1199,7 +1201,7 @@ function createListConfluenceSpacesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1199
1201
|
spaces
|
|
1200
1202
|
});
|
|
1201
1203
|
} catch (error) {
|
|
1202
|
-
|
|
1204
|
+
logger8.error("list-confluence-spaces error", {
|
|
1203
1205
|
error: error.response?.data?.message || error.message,
|
|
1204
1206
|
status: error.response?.status
|
|
1205
1207
|
});
|
|
@@ -1210,15 +1212,15 @@ function createListConfluenceSpacesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1210
1212
|
}
|
|
1211
1213
|
}).build();
|
|
1212
1214
|
}
|
|
1213
|
-
function createGetSpacePagesTool(getAuth, getAuthHeader2,
|
|
1215
|
+
function createGetSpacePagesTool(getAuth, getAuthHeader2, logger8) {
|
|
1214
1216
|
return core.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(core.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(zod.z.object({
|
|
1215
1217
|
space_key: zod.z.string().describe("The space key (e.g., 'AI', 'BL3', 'FIN')"),
|
|
1216
1218
|
limit: zod.z.number().optional().describe("Maximum number of pages to return (default: 25)")
|
|
1217
1219
|
})).implement(async ({ space_key, limit = 25 }) => {
|
|
1218
|
-
|
|
1220
|
+
logger8.info("get-space-pages called", { space_key, limit });
|
|
1219
1221
|
try {
|
|
1220
1222
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1221
|
-
const response = await
|
|
1223
|
+
const response = await axios16__default.default.get(`${ATLASSIAN_SITE_URL}/wiki/rest/api/content`, {
|
|
1222
1224
|
headers: {
|
|
1223
1225
|
Authorization: getAuthHeader2(),
|
|
1224
1226
|
Accept: "application/json"
|
|
@@ -1237,12 +1239,12 @@ function createGetSpacePagesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1237
1239
|
lastModified: page.version?.when || ""
|
|
1238
1240
|
}));
|
|
1239
1241
|
if (pages.length === 0) {
|
|
1240
|
-
|
|
1242
|
+
logger8.warn("get-space-pages returned NO PAGES - this is a valid outcome, agent should not retry", {
|
|
1241
1243
|
space_key,
|
|
1242
1244
|
limit
|
|
1243
1245
|
});
|
|
1244
1246
|
} else {
|
|
1245
|
-
|
|
1247
|
+
logger8.info("get-space-pages result", {
|
|
1246
1248
|
space_key,
|
|
1247
1249
|
pageCount: pages.length,
|
|
1248
1250
|
titles: pages.map((p) => p.title).slice(0, 3)
|
|
@@ -1256,7 +1258,7 @@ function createGetSpacePagesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1256
1258
|
pages
|
|
1257
1259
|
});
|
|
1258
1260
|
} catch (error) {
|
|
1259
|
-
|
|
1261
|
+
logger8.error("get-space-pages error", {
|
|
1260
1262
|
space_key,
|
|
1261
1263
|
error: error.response?.data?.message || error.message,
|
|
1262
1264
|
status: error.response?.status
|
|
@@ -1268,14 +1270,14 @@ function createGetSpacePagesTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1268
1270
|
}
|
|
1269
1271
|
}).build();
|
|
1270
1272
|
}
|
|
1271
|
-
function createCreateConfluencePageTool(getAuth, getAuthHeader2,
|
|
1273
|
+
function createCreateConfluencePageTool(getAuth, getAuthHeader2, logger8) {
|
|
1272
1274
|
return core.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(core.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(zod.z.object({
|
|
1273
1275
|
space_key: zod.z.string().describe("The space key where the page will be created (e.g., 'AI', 'BL3')"),
|
|
1274
1276
|
title: zod.z.string().describe("The title of the new page"),
|
|
1275
1277
|
content: zod.z.string().describe("The page content in HTML format (Confluence storage format)"),
|
|
1276
1278
|
parent_page_id: zod.z.string().optional().describe("Optional parent page ID to create this as a child page")
|
|
1277
1279
|
})).implement(async ({ space_key, title, content, parent_page_id }) => {
|
|
1278
|
-
|
|
1280
|
+
logger8.info("create-confluence-page called", { space_key, title, hasParent: !!parent_page_id });
|
|
1279
1281
|
try {
|
|
1280
1282
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1281
1283
|
const pageData = {
|
|
@@ -1292,7 +1294,7 @@ function createCreateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1292
1294
|
if (parent_page_id) {
|
|
1293
1295
|
pageData.ancestors = [{ id: parent_page_id }];
|
|
1294
1296
|
}
|
|
1295
|
-
const response = await
|
|
1297
|
+
const response = await axios16__default.default.post(
|
|
1296
1298
|
`${ATLASSIAN_SITE_URL}/wiki/rest/api/content`,
|
|
1297
1299
|
pageData,
|
|
1298
1300
|
{
|
|
@@ -1302,7 +1304,7 @@ function createCreateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1302
1304
|
}
|
|
1303
1305
|
}
|
|
1304
1306
|
);
|
|
1305
|
-
|
|
1307
|
+
logger8.info("create-confluence-page result", {
|
|
1306
1308
|
page_id: response.data.id,
|
|
1307
1309
|
title: response.data.title,
|
|
1308
1310
|
space: space_key
|
|
@@ -1318,7 +1320,7 @@ function createCreateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1318
1320
|
}
|
|
1319
1321
|
});
|
|
1320
1322
|
} catch (error) {
|
|
1321
|
-
|
|
1323
|
+
logger8.error("create-confluence-page error", {
|
|
1322
1324
|
space_key,
|
|
1323
1325
|
title,
|
|
1324
1326
|
error: error.response?.data?.message || error.message,
|
|
@@ -1331,16 +1333,16 @@ function createCreateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1331
1333
|
}
|
|
1332
1334
|
}).build();
|
|
1333
1335
|
}
|
|
1334
|
-
function createUpdateConfluencePageTool(getAuth, getAuthHeader2,
|
|
1336
|
+
function createUpdateConfluencePageTool(getAuth, getAuthHeader2, logger8) {
|
|
1335
1337
|
return core.toolBuilder().name("update-confluence-page").description("Update an existing Confluence page's content. Requires page ID, new title, and new content.").category(core.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(zod.z.object({
|
|
1336
1338
|
page_id: zod.z.string().describe("The ID of the page to update"),
|
|
1337
1339
|
title: zod.z.string().describe("The new title for the page"),
|
|
1338
1340
|
content: zod.z.string().describe("The new content in HTML format (Confluence storage format)")
|
|
1339
1341
|
})).implement(async ({ page_id, title, content }) => {
|
|
1340
|
-
|
|
1342
|
+
logger8.info("update-confluence-page called", { page_id, title });
|
|
1341
1343
|
try {
|
|
1342
1344
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1343
|
-
const getResponse = await
|
|
1345
|
+
const getResponse = await axios16__default.default.get(
|
|
1344
1346
|
`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/${page_id}`,
|
|
1345
1347
|
{
|
|
1346
1348
|
headers: {
|
|
@@ -1350,7 +1352,7 @@ function createUpdateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1350
1352
|
}
|
|
1351
1353
|
);
|
|
1352
1354
|
const currentVersion = getResponse.data.version.number;
|
|
1353
|
-
const updateResponse = await
|
|
1355
|
+
const updateResponse = await axios16__default.default.put(
|
|
1354
1356
|
`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/${page_id}`,
|
|
1355
1357
|
{
|
|
1356
1358
|
type: "page",
|
|
@@ -1370,7 +1372,7 @@ function createUpdateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1370
1372
|
}
|
|
1371
1373
|
}
|
|
1372
1374
|
);
|
|
1373
|
-
|
|
1375
|
+
logger8.info("update-confluence-page result", {
|
|
1374
1376
|
page_id,
|
|
1375
1377
|
title: updateResponse.data.title,
|
|
1376
1378
|
previousVersion: currentVersion,
|
|
@@ -1387,7 +1389,7 @@ function createUpdateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1387
1389
|
}
|
|
1388
1390
|
});
|
|
1389
1391
|
} catch (error) {
|
|
1390
|
-
|
|
1392
|
+
logger8.error("update-confluence-page error", {
|
|
1391
1393
|
page_id,
|
|
1392
1394
|
title,
|
|
1393
1395
|
error: error.response?.data?.message || error.message,
|
|
@@ -1400,15 +1402,15 @@ function createUpdateConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1400
1402
|
}
|
|
1401
1403
|
}).build();
|
|
1402
1404
|
}
|
|
1403
|
-
function createArchiveConfluencePageTool(getAuth, getAuthHeader2,
|
|
1405
|
+
function createArchiveConfluencePageTool(getAuth, getAuthHeader2, logger8) {
|
|
1404
1406
|
return core.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(core.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(zod.z.object({
|
|
1405
1407
|
page_id: zod.z.string().describe("The ID of the page to archive"),
|
|
1406
1408
|
reason: zod.z.string().optional().describe("Optional reason for archiving (for audit trail)")
|
|
1407
1409
|
})).implement(async ({ page_id, reason }) => {
|
|
1408
|
-
|
|
1410
|
+
logger8.info("archive-confluence-page called", { page_id, reason });
|
|
1409
1411
|
try {
|
|
1410
1412
|
const { ATLASSIAN_SITE_URL } = getAuth();
|
|
1411
|
-
const getResponse = await
|
|
1413
|
+
const getResponse = await axios16__default.default.get(
|
|
1412
1414
|
`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/${page_id}`,
|
|
1413
1415
|
{
|
|
1414
1416
|
headers: {
|
|
@@ -1419,7 +1421,7 @@ function createArchiveConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1419
1421
|
);
|
|
1420
1422
|
const currentVersion = getResponse.data.version.number;
|
|
1421
1423
|
const pageData = getResponse.data;
|
|
1422
|
-
await
|
|
1424
|
+
await axios16__default.default.put(
|
|
1423
1425
|
`${ATLASSIAN_SITE_URL}/wiki/rest/api/content/${page_id}`,
|
|
1424
1426
|
{
|
|
1425
1427
|
version: { number: currentVersion + 1 },
|
|
@@ -1436,7 +1438,7 @@ function createArchiveConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1436
1438
|
}
|
|
1437
1439
|
}
|
|
1438
1440
|
);
|
|
1439
|
-
|
|
1441
|
+
logger8.info("archive-confluence-page result", {
|
|
1440
1442
|
page_id,
|
|
1441
1443
|
title: pageData.title,
|
|
1442
1444
|
previousVersion: currentVersion,
|
|
@@ -1454,7 +1456,7 @@ function createArchiveConfluencePageTool(getAuth, getAuthHeader2, logger4) {
|
|
|
1454
1456
|
}
|
|
1455
1457
|
});
|
|
1456
1458
|
} catch (error) {
|
|
1457
|
-
|
|
1459
|
+
logger8.error("archive-confluence-page error", {
|
|
1458
1460
|
page_id,
|
|
1459
1461
|
error: error.response?.data?.message || error.message,
|
|
1460
1462
|
status: error.response?.status
|
|
@@ -2051,6 +2053,1706 @@ function createTransformerTools(config = {}) {
|
|
|
2051
2053
|
createObjectOmitTool()
|
|
2052
2054
|
];
|
|
2053
2055
|
}
|
|
2056
|
+
var neo4jQuerySchema = zod.z.object({
|
|
2057
|
+
cypher: zod.z.string().describe("Cypher query to execute"),
|
|
2058
|
+
parameters: zod.z.record(zod.z.any()).optional().describe("Query parameters for parameterized queries"),
|
|
2059
|
+
database: zod.z.string().optional().describe("Database name (defaults to configured database)")
|
|
2060
|
+
});
|
|
2061
|
+
var neo4jGetSchemaSchema = zod.z.object({
|
|
2062
|
+
database: zod.z.string().optional().describe("Database name (defaults to configured database)")
|
|
2063
|
+
});
|
|
2064
|
+
var neo4jFindNodesSchema = zod.z.object({
|
|
2065
|
+
label: zod.z.string().describe("Node label to search for"),
|
|
2066
|
+
properties: zod.z.record(zod.z.any()).optional().describe("Properties to match (key-value pairs)"),
|
|
2067
|
+
limit: zod.z.number().default(100).describe("Maximum number of nodes to return"),
|
|
2068
|
+
database: zod.z.string().optional().describe("Database name (defaults to configured database)")
|
|
2069
|
+
});
|
|
2070
|
+
var neo4jTraverseSchema = zod.z.object({
|
|
2071
|
+
startNodeId: zod.z.union([
|
|
2072
|
+
zod.z.string().describe("Node ID as string"),
|
|
2073
|
+
zod.z.number().describe("Node ID as number")
|
|
2074
|
+
]).describe("ID of the starting node"),
|
|
2075
|
+
relationshipType: zod.z.string().optional().describe("Type of relationship to follow (optional, follows all if not specified)"),
|
|
2076
|
+
direction: zod.z.enum(["outgoing", "incoming", "both"]).default("outgoing").describe("Direction to traverse"),
|
|
2077
|
+
maxDepth: zod.z.number().default(1).describe("Maximum depth to traverse"),
|
|
2078
|
+
limit: zod.z.number().default(100).describe("Maximum number of nodes to return"),
|
|
2079
|
+
database: zod.z.string().optional().describe("Database name (defaults to configured database)")
|
|
2080
|
+
});
|
|
2081
|
+
var neo4jVectorSearchSchema = zod.z.object({
|
|
2082
|
+
indexName: zod.z.string().describe("Name of the vector index to search"),
|
|
2083
|
+
queryVector: zod.z.array(zod.z.number().describe("Vector dimension value")).describe("Query vector for similarity search"),
|
|
2084
|
+
limit: zod.z.number().default(10).describe("Maximum number of results to return"),
|
|
2085
|
+
database: zod.z.string().optional().describe("Database name (defaults to configured database)")
|
|
2086
|
+
});
|
|
2087
|
+
var neo4jVectorSearchWithEmbeddingSchema = zod.z.object({
|
|
2088
|
+
indexName: zod.z.string().describe("Name of the vector index to search"),
|
|
2089
|
+
queryText: zod.z.string().describe("Text to generate embedding from for similarity search"),
|
|
2090
|
+
limit: zod.z.number().default(10).describe("Maximum number of results to return"),
|
|
2091
|
+
model: zod.z.string().optional().describe("Embedding model to use (defaults to configured model)"),
|
|
2092
|
+
database: zod.z.string().optional().describe("Database name (defaults to configured database)")
|
|
2093
|
+
});
|
|
2094
|
+
var neo4jCreateNodeWithEmbeddingSchema = zod.z.object({
|
|
2095
|
+
label: zod.z.string().describe("Node label"),
|
|
2096
|
+
properties: zod.z.record(zod.z.string(), zod.z.any().describe("Property value")).describe("Node properties (key-value pairs)"),
|
|
2097
|
+
textProperty: zod.z.string().describe("Name of the property containing text to embed"),
|
|
2098
|
+
embeddingProperty: zod.z.string().default("embedding").describe("Name of the property to store the embedding vector"),
|
|
2099
|
+
model: zod.z.string().optional().describe("Embedding model to use (defaults to configured model)"),
|
|
2100
|
+
database: zod.z.string().optional().describe("Database name (defaults to configured database)")
|
|
2101
|
+
});
|
|
2102
|
+
var logger3 = core.createLogger("agentforge:tools:neo4j:pool");
|
|
2103
|
+
var Neo4jConnectionPool = class {
|
|
2104
|
+
driver = null;
|
|
2105
|
+
config = null;
|
|
2106
|
+
/**
|
|
2107
|
+
* Initialize the connection pool
|
|
2108
|
+
*/
|
|
2109
|
+
async initialize(config) {
|
|
2110
|
+
const startTime = Date.now();
|
|
2111
|
+
logger3.debug("Initializing Neo4j connection pool", {
|
|
2112
|
+
uri: config.uri,
|
|
2113
|
+
username: config.username,
|
|
2114
|
+
database: config.database || "neo4j",
|
|
2115
|
+
maxConnectionPoolSize: config.maxConnectionPoolSize || 50,
|
|
2116
|
+
connectionTimeout: config.connectionTimeout || 3e4
|
|
2117
|
+
});
|
|
2118
|
+
if (this.driver) {
|
|
2119
|
+
logger3.debug("Closing existing connection before reinitializing");
|
|
2120
|
+
await this.close();
|
|
2121
|
+
}
|
|
2122
|
+
this.config = config;
|
|
2123
|
+
this.driver = neo4j__default.default.driver(
|
|
2124
|
+
config.uri,
|
|
2125
|
+
neo4j.auth.basic(config.username, config.password),
|
|
2126
|
+
{
|
|
2127
|
+
maxConnectionPoolSize: config.maxConnectionPoolSize || 50,
|
|
2128
|
+
connectionTimeout: config.connectionTimeout || 3e4
|
|
2129
|
+
}
|
|
2130
|
+
);
|
|
2131
|
+
await this.verifyConnectivity();
|
|
2132
|
+
logger3.info("Neo4j connection pool initialized", {
|
|
2133
|
+
uri: config.uri,
|
|
2134
|
+
database: config.database || "neo4j",
|
|
2135
|
+
duration: Date.now() - startTime
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Verify connectivity to Neo4j
|
|
2140
|
+
*/
|
|
2141
|
+
async verifyConnectivity() {
|
|
2142
|
+
if (!this.driver) {
|
|
2143
|
+
const error = "Neo4j driver not initialized";
|
|
2144
|
+
logger3.error(error);
|
|
2145
|
+
throw new Error(error);
|
|
2146
|
+
}
|
|
2147
|
+
logger3.debug("Verifying Neo4j connectivity");
|
|
2148
|
+
try {
|
|
2149
|
+
await this.driver.verifyConnectivity();
|
|
2150
|
+
logger3.debug("Neo4j connectivity verified");
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
const errorMessage = `Failed to connect to Neo4j: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
2153
|
+
logger3.error("Neo4j connectivity verification failed", {
|
|
2154
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
2155
|
+
uri: this.config?.uri
|
|
2156
|
+
});
|
|
2157
|
+
throw new Error(errorMessage);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Get a session for executing queries
|
|
2162
|
+
*/
|
|
2163
|
+
getSession(database) {
|
|
2164
|
+
if (!this.driver) {
|
|
2165
|
+
throw new Error("Neo4j driver not initialized. Call initialize() first.");
|
|
2166
|
+
}
|
|
2167
|
+
return this.driver.session({
|
|
2168
|
+
database: database || this.config?.database || "neo4j"
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Execute a query with automatic session management
|
|
2173
|
+
*/
|
|
2174
|
+
async executeQuery(cypher, parameters, database) {
|
|
2175
|
+
const session = this.getSession(database);
|
|
2176
|
+
try {
|
|
2177
|
+
const result = await session.run(cypher, parameters);
|
|
2178
|
+
return result.records.map((record) => record.toObject());
|
|
2179
|
+
} finally {
|
|
2180
|
+
await session.close();
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* Execute a read query
|
|
2185
|
+
*/
|
|
2186
|
+
async executeReadQuery(cypher, parameters, database) {
|
|
2187
|
+
const session = this.getSession(database);
|
|
2188
|
+
try {
|
|
2189
|
+
const result = await session.executeRead((tx) => tx.run(cypher, parameters));
|
|
2190
|
+
return result.records.map((record) => record.toObject());
|
|
2191
|
+
} finally {
|
|
2192
|
+
await session.close();
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Execute a write query
|
|
2197
|
+
*/
|
|
2198
|
+
async executeWriteQuery(cypher, parameters, database) {
|
|
2199
|
+
const session = this.getSession(database);
|
|
2200
|
+
try {
|
|
2201
|
+
const result = await session.executeWrite((tx) => tx.run(cypher, parameters));
|
|
2202
|
+
return result.records.map((record) => record.toObject());
|
|
2203
|
+
} finally {
|
|
2204
|
+
await session.close();
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Check if driver is initialized
|
|
2209
|
+
*/
|
|
2210
|
+
isInitialized() {
|
|
2211
|
+
return this.driver !== null;
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Get current configuration
|
|
2215
|
+
*/
|
|
2216
|
+
getConfig() {
|
|
2217
|
+
return this.config;
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Close the connection pool
|
|
2221
|
+
*/
|
|
2222
|
+
async close() {
|
|
2223
|
+
if (this.driver) {
|
|
2224
|
+
logger3.debug("Closing Neo4j connection pool");
|
|
2225
|
+
await this.driver.close();
|
|
2226
|
+
this.driver = null;
|
|
2227
|
+
this.config = null;
|
|
2228
|
+
logger3.info("Neo4j connection pool closed");
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
var neo4jPool = new Neo4jConnectionPool();
|
|
2233
|
+
async function initializeFromEnv() {
|
|
2234
|
+
if (!process.env.NEO4J_URI) {
|
|
2235
|
+
logger3.warn("NEO4J_URI environment variable not set, using default", {
|
|
2236
|
+
default: "bolt://localhost:7687"
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
if (!process.env.NEO4J_USER) {
|
|
2240
|
+
logger3.warn("NEO4J_USER environment variable not set, using default", {
|
|
2241
|
+
default: "neo4j"
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
if (!process.env.NEO4J_PASSWORD) {
|
|
2245
|
+
logger3.warn("NEO4J_PASSWORD environment variable not set, using default", {
|
|
2246
|
+
default: "password"
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
if (!process.env.NEO4J_DATABASE) {
|
|
2250
|
+
logger3.debug("NEO4J_DATABASE environment variable not set, using default", {
|
|
2251
|
+
default: "neo4j"
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
const config = {
|
|
2255
|
+
uri: process.env.NEO4J_URI || "bolt://localhost:7687",
|
|
2256
|
+
username: process.env.NEO4J_USER || "neo4j",
|
|
2257
|
+
password: process.env.NEO4J_PASSWORD || "password",
|
|
2258
|
+
database: process.env.NEO4J_DATABASE
|
|
2259
|
+
};
|
|
2260
|
+
await neo4jPool.initialize(config);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// src/data/neo4j/embeddings/utils.ts
|
|
2264
|
+
var DEFAULT_RETRY_CONFIG2 = {
|
|
2265
|
+
maxRetries: 3,
|
|
2266
|
+
initialDelay: 1e3,
|
|
2267
|
+
// 1 second
|
|
2268
|
+
maxDelay: 1e4,
|
|
2269
|
+
// 10 seconds
|
|
2270
|
+
backoffMultiplier: 2
|
|
2271
|
+
};
|
|
2272
|
+
function isRetryableError2(error) {
|
|
2273
|
+
if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT" || error.code === "ENOTFOUND") {
|
|
2274
|
+
return true;
|
|
2275
|
+
}
|
|
2276
|
+
if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
|
|
2277
|
+
return true;
|
|
2278
|
+
}
|
|
2279
|
+
if (error.response?.status >= 500 && error.response?.status < 600) {
|
|
2280
|
+
return true;
|
|
2281
|
+
}
|
|
2282
|
+
if (error.response?.status === 429) {
|
|
2283
|
+
return true;
|
|
2284
|
+
}
|
|
2285
|
+
return false;
|
|
2286
|
+
}
|
|
2287
|
+
function sleep2(ms) {
|
|
2288
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2289
|
+
}
|
|
2290
|
+
async function retryWithBackoff2(fn, config = DEFAULT_RETRY_CONFIG2) {
|
|
2291
|
+
let lastError;
|
|
2292
|
+
let delay = config.initialDelay;
|
|
2293
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
2294
|
+
try {
|
|
2295
|
+
return await fn();
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
lastError = error;
|
|
2298
|
+
if (!isRetryableError2(error)) {
|
|
2299
|
+
throw error;
|
|
2300
|
+
}
|
|
2301
|
+
if (attempt === config.maxRetries) {
|
|
2302
|
+
break;
|
|
2303
|
+
}
|
|
2304
|
+
await sleep2(delay);
|
|
2305
|
+
delay = Math.min(delay * config.backoffMultiplier, config.maxDelay);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
throw lastError;
|
|
2309
|
+
}
|
|
2310
|
+
function getOpenAIApiKey() {
|
|
2311
|
+
return process.env.OPENAI_API_KEY;
|
|
2312
|
+
}
|
|
2313
|
+
function getCohereApiKey() {
|
|
2314
|
+
return process.env.COHERE_API_KEY;
|
|
2315
|
+
}
|
|
2316
|
+
function getHuggingFaceApiKey() {
|
|
2317
|
+
return process.env.HUGGINGFACE_API_KEY;
|
|
2318
|
+
}
|
|
2319
|
+
function getVoyageApiKey() {
|
|
2320
|
+
return process.env.VOYAGE_API_KEY;
|
|
2321
|
+
}
|
|
2322
|
+
function getOllamaBaseUrl() {
|
|
2323
|
+
return process.env.OLLAMA_BASE_URL || "http://localhost:11434";
|
|
2324
|
+
}
|
|
2325
|
+
function getEmbeddingProvider() {
|
|
2326
|
+
return process.env.EMBEDDING_PROVIDER || "openai";
|
|
2327
|
+
}
|
|
2328
|
+
function getEmbeddingModel() {
|
|
2329
|
+
return process.env.EMBEDDING_MODEL;
|
|
2330
|
+
}
|
|
2331
|
+
function validateText(text) {
|
|
2332
|
+
if (!text || text.trim().length === 0) {
|
|
2333
|
+
throw new Error("Text cannot be empty");
|
|
2334
|
+
}
|
|
2335
|
+
if (text.length > 5e4) {
|
|
2336
|
+
throw new Error("Text is too long for embedding generation (max ~50,000 characters)");
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
function validateBatch(texts) {
|
|
2340
|
+
if (!texts || texts.length === 0) {
|
|
2341
|
+
throw new Error("Batch cannot be empty");
|
|
2342
|
+
}
|
|
2343
|
+
if (texts.length > 2048) {
|
|
2344
|
+
throw new Error("Batch size too large (max 2048 texts)");
|
|
2345
|
+
}
|
|
2346
|
+
texts.forEach((text, index) => {
|
|
2347
|
+
try {
|
|
2348
|
+
validateText(text);
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
throw new Error(`Invalid text at index ${index}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
var OpenAIEmbeddingProvider = class {
|
|
2355
|
+
name = "openai";
|
|
2356
|
+
defaultModel = "text-embedding-3-small";
|
|
2357
|
+
apiKey;
|
|
2358
|
+
baseURL = "https://api.openai.com/v1";
|
|
2359
|
+
constructor(apiKey) {
|
|
2360
|
+
this.apiKey = apiKey || getOpenAIApiKey();
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Check if OpenAI is available (API key is set)
|
|
2364
|
+
*/
|
|
2365
|
+
isAvailable() {
|
|
2366
|
+
return !!this.apiKey;
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Generate embedding for a single text
|
|
2370
|
+
*/
|
|
2371
|
+
async generateEmbedding(text, model) {
|
|
2372
|
+
if (!this.apiKey) {
|
|
2373
|
+
throw new Error(
|
|
2374
|
+
"OpenAI API key not found. Set OPENAI_API_KEY environment variable. Get your key at https://platform.openai.com/api-keys"
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
validateText(text);
|
|
2378
|
+
const modelToUse = model || this.defaultModel;
|
|
2379
|
+
return retryWithBackoff2(async () => {
|
|
2380
|
+
try {
|
|
2381
|
+
const response = await axios16__default.default.post(
|
|
2382
|
+
`${this.baseURL}/embeddings`,
|
|
2383
|
+
{
|
|
2384
|
+
input: text,
|
|
2385
|
+
model: modelToUse
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
headers: {
|
|
2389
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2390
|
+
"Content-Type": "application/json"
|
|
2391
|
+
},
|
|
2392
|
+
timeout: 3e4
|
|
2393
|
+
// 30 seconds
|
|
2394
|
+
}
|
|
2395
|
+
);
|
|
2396
|
+
const data = response.data;
|
|
2397
|
+
if (!data.data || data.data.length === 0) {
|
|
2398
|
+
throw new Error("No embedding returned from OpenAI");
|
|
2399
|
+
}
|
|
2400
|
+
return {
|
|
2401
|
+
embedding: data.data[0].embedding,
|
|
2402
|
+
model: data.model,
|
|
2403
|
+
dimensions: data.data[0].embedding.length,
|
|
2404
|
+
usage: {
|
|
2405
|
+
promptTokens: data.usage.prompt_tokens,
|
|
2406
|
+
totalTokens: data.usage.total_tokens
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
} catch (error) {
|
|
2410
|
+
let wrappedError;
|
|
2411
|
+
if (error.response?.status === 401) {
|
|
2412
|
+
wrappedError = new Error(
|
|
2413
|
+
"Invalid OpenAI API key. Get your key at https://platform.openai.com/api-keys"
|
|
2414
|
+
);
|
|
2415
|
+
} else if (error.response?.status === 429) {
|
|
2416
|
+
wrappedError = new Error(
|
|
2417
|
+
"OpenAI API rate limit exceeded. Please try again later or upgrade your plan."
|
|
2418
|
+
);
|
|
2419
|
+
} else if (error.response?.status === 400) {
|
|
2420
|
+
wrappedError = new Error(
|
|
2421
|
+
`OpenAI API error: ${error.response.data?.error?.message || "Bad request"}`
|
|
2422
|
+
);
|
|
2423
|
+
} else {
|
|
2424
|
+
wrappedError = new Error(`OpenAI embedding generation failed: ${error.message}`);
|
|
2425
|
+
}
|
|
2426
|
+
if (error.code) wrappedError.code = error.code;
|
|
2427
|
+
if (error.response) wrappedError.response = error.response;
|
|
2428
|
+
throw wrappedError;
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Generate embeddings for multiple texts (batch)
|
|
2434
|
+
*/
|
|
2435
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2436
|
+
if (!this.apiKey) {
|
|
2437
|
+
throw new Error(
|
|
2438
|
+
"OpenAI API key not found. Set OPENAI_API_KEY environment variable. Get your key at https://platform.openai.com/api-keys"
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
validateBatch(texts);
|
|
2442
|
+
const modelToUse = model || this.defaultModel;
|
|
2443
|
+
return retryWithBackoff2(async () => {
|
|
2444
|
+
try {
|
|
2445
|
+
const response = await axios16__default.default.post(
|
|
2446
|
+
`${this.baseURL}/embeddings`,
|
|
2447
|
+
{
|
|
2448
|
+
input: texts,
|
|
2449
|
+
model: modelToUse
|
|
2450
|
+
},
|
|
2451
|
+
{
|
|
2452
|
+
headers: {
|
|
2453
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2454
|
+
"Content-Type": "application/json"
|
|
2455
|
+
},
|
|
2456
|
+
timeout: 6e4
|
|
2457
|
+
// 60 seconds for batch
|
|
2458
|
+
}
|
|
2459
|
+
);
|
|
2460
|
+
const data = response.data;
|
|
2461
|
+
if (!data.data || data.data.length === 0) {
|
|
2462
|
+
throw new Error("No embeddings returned from OpenAI");
|
|
2463
|
+
}
|
|
2464
|
+
const sortedData = data.data.sort((a, b) => a.index - b.index);
|
|
2465
|
+
return {
|
|
2466
|
+
embeddings: sortedData.map((item) => item.embedding),
|
|
2467
|
+
model: data.model,
|
|
2468
|
+
dimensions: sortedData[0].embedding.length,
|
|
2469
|
+
usage: {
|
|
2470
|
+
promptTokens: data.usage.prompt_tokens,
|
|
2471
|
+
totalTokens: data.usage.total_tokens
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
} catch (error) {
|
|
2475
|
+
let wrappedError;
|
|
2476
|
+
if (error.response?.status === 401) {
|
|
2477
|
+
wrappedError = new Error(
|
|
2478
|
+
"Invalid OpenAI API key. Get your key at https://platform.openai.com/api-keys"
|
|
2479
|
+
);
|
|
2480
|
+
} else if (error.response?.status === 429) {
|
|
2481
|
+
wrappedError = new Error(
|
|
2482
|
+
"OpenAI API rate limit exceeded. Please try again later or upgrade your plan."
|
|
2483
|
+
);
|
|
2484
|
+
} else if (error.response?.status === 400) {
|
|
2485
|
+
wrappedError = new Error(
|
|
2486
|
+
`OpenAI API error: ${error.response.data?.error?.message || "Bad request"}`
|
|
2487
|
+
);
|
|
2488
|
+
} else {
|
|
2489
|
+
wrappedError = new Error(`OpenAI batch embedding generation failed: ${error.message}`);
|
|
2490
|
+
}
|
|
2491
|
+
if (error.code) wrappedError.code = error.code;
|
|
2492
|
+
if (error.response) wrappedError.response = error.response;
|
|
2493
|
+
throw wrappedError;
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
};
|
|
2498
|
+
var CohereEmbeddingProvider = class {
|
|
2499
|
+
name = "cohere";
|
|
2500
|
+
defaultModel = "embed-english-v3.0";
|
|
2501
|
+
apiKey;
|
|
2502
|
+
baseUrl = "https://api.cohere.ai/v1";
|
|
2503
|
+
constructor(apiKey, model) {
|
|
2504
|
+
this.apiKey = apiKey;
|
|
2505
|
+
}
|
|
2506
|
+
/**
|
|
2507
|
+
* Check if Cohere is available (API key is set)
|
|
2508
|
+
*/
|
|
2509
|
+
isAvailable() {
|
|
2510
|
+
return !!this.apiKey;
|
|
2511
|
+
}
|
|
2512
|
+
async generateEmbedding(text, model) {
|
|
2513
|
+
const result = await this.generateBatchEmbeddings([text], model);
|
|
2514
|
+
return {
|
|
2515
|
+
embedding: result.embeddings[0],
|
|
2516
|
+
model: result.model,
|
|
2517
|
+
dimensions: result.dimensions,
|
|
2518
|
+
usage: result.usage
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2522
|
+
const modelToUse = model || this.defaultModel;
|
|
2523
|
+
return retryWithBackoff2(async () => {
|
|
2524
|
+
try {
|
|
2525
|
+
const response = await axios16__default.default.post(
|
|
2526
|
+
`${this.baseUrl}/embed`,
|
|
2527
|
+
{
|
|
2528
|
+
texts,
|
|
2529
|
+
model: modelToUse,
|
|
2530
|
+
input_type: "search_document",
|
|
2531
|
+
// For storing in vector DB
|
|
2532
|
+
embedding_types: ["float"]
|
|
2533
|
+
},
|
|
2534
|
+
{
|
|
2535
|
+
headers: {
|
|
2536
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2537
|
+
"Content-Type": "application/json"
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
);
|
|
2541
|
+
const embeddings = response.data.embeddings.float;
|
|
2542
|
+
const dimensions = embeddings[0]?.length || 0;
|
|
2543
|
+
return {
|
|
2544
|
+
embeddings,
|
|
2545
|
+
model: modelToUse,
|
|
2546
|
+
dimensions,
|
|
2547
|
+
usage: response.data.meta?.billed_units ? {
|
|
2548
|
+
promptTokens: response.data.meta.billed_units.input_tokens || 0,
|
|
2549
|
+
totalTokens: response.data.meta.billed_units.input_tokens || 0
|
|
2550
|
+
} : void 0
|
|
2551
|
+
};
|
|
2552
|
+
} catch (error) {
|
|
2553
|
+
if (axios16__default.default.isAxiosError(error)) {
|
|
2554
|
+
const status = error.response?.status;
|
|
2555
|
+
const message = error.response?.data?.message || error.message;
|
|
2556
|
+
let wrappedError;
|
|
2557
|
+
if (status === 401) {
|
|
2558
|
+
wrappedError = new Error(`Cohere API authentication failed. Please check your COHERE_API_KEY. ${message}`);
|
|
2559
|
+
} else if (status === 429) {
|
|
2560
|
+
wrappedError = new Error(`Cohere API rate limit exceeded. ${message}`);
|
|
2561
|
+
} else if (status === 400) {
|
|
2562
|
+
wrappedError = new Error(`Cohere API request invalid: ${message}`);
|
|
2563
|
+
} else {
|
|
2564
|
+
wrappedError = new Error(`Cohere API error (${status}): ${message}`);
|
|
2565
|
+
}
|
|
2566
|
+
if (error.code) wrappedError.code = error.code;
|
|
2567
|
+
if (error.response) wrappedError.response = error.response;
|
|
2568
|
+
throw wrappedError;
|
|
2569
|
+
}
|
|
2570
|
+
throw error;
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
var HuggingFaceEmbeddingProvider = class {
|
|
2576
|
+
name = "huggingface";
|
|
2577
|
+
defaultModel = "sentence-transformers/all-MiniLM-L6-v2";
|
|
2578
|
+
apiKey;
|
|
2579
|
+
baseUrl = "https://api-inference.huggingface.co/pipeline/feature-extraction";
|
|
2580
|
+
constructor(apiKey, model) {
|
|
2581
|
+
this.apiKey = apiKey;
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Check if HuggingFace is available (API key is set)
|
|
2585
|
+
*/
|
|
2586
|
+
isAvailable() {
|
|
2587
|
+
return !!this.apiKey;
|
|
2588
|
+
}
|
|
2589
|
+
async generateEmbedding(text, model) {
|
|
2590
|
+
const modelToUse = model || this.defaultModel;
|
|
2591
|
+
return retryWithBackoff2(async () => {
|
|
2592
|
+
try {
|
|
2593
|
+
const response = await axios16__default.default.post(
|
|
2594
|
+
`${this.baseUrl}/${modelToUse}`,
|
|
2595
|
+
{
|
|
2596
|
+
inputs: text,
|
|
2597
|
+
options: {
|
|
2598
|
+
wait_for_model: true
|
|
2599
|
+
}
|
|
2600
|
+
},
|
|
2601
|
+
{
|
|
2602
|
+
headers: {
|
|
2603
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2604
|
+
"Content-Type": "application/json"
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
);
|
|
2608
|
+
const embedding = Array.isArray(response.data) ? response.data : response.data[0];
|
|
2609
|
+
return {
|
|
2610
|
+
embedding,
|
|
2611
|
+
model: modelToUse,
|
|
2612
|
+
dimensions: embedding.length
|
|
2613
|
+
};
|
|
2614
|
+
} catch (error) {
|
|
2615
|
+
if (axios16__default.default.isAxiosError(error)) {
|
|
2616
|
+
const status = error.response?.status;
|
|
2617
|
+
const message = error.response?.data?.error || error.message;
|
|
2618
|
+
let wrappedError;
|
|
2619
|
+
if (status === 401) {
|
|
2620
|
+
wrappedError = new Error(`HuggingFace API authentication failed. Please check your HUGGINGFACE_API_KEY. ${message}`);
|
|
2621
|
+
} else if (status === 429) {
|
|
2622
|
+
wrappedError = new Error(`HuggingFace API rate limit exceeded. ${message}`);
|
|
2623
|
+
} else if (status === 400) {
|
|
2624
|
+
wrappedError = new Error(`HuggingFace API request invalid: ${message}`);
|
|
2625
|
+
} else if (status === 503) {
|
|
2626
|
+
wrappedError = new Error(`HuggingFace model is loading. Please retry in a moment. ${message}`);
|
|
2627
|
+
} else {
|
|
2628
|
+
wrappedError = new Error(`HuggingFace API error (${status}): ${message}`);
|
|
2629
|
+
}
|
|
2630
|
+
if (error.code) wrappedError.code = error.code;
|
|
2631
|
+
if (error.response) wrappedError.response = error.response;
|
|
2632
|
+
throw wrappedError;
|
|
2633
|
+
}
|
|
2634
|
+
throw error;
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2639
|
+
const modelToUse = model || this.defaultModel;
|
|
2640
|
+
return retryWithBackoff2(async () => {
|
|
2641
|
+
try {
|
|
2642
|
+
const response = await axios16__default.default.post(
|
|
2643
|
+
`${this.baseUrl}/${modelToUse}`,
|
|
2644
|
+
{
|
|
2645
|
+
inputs: texts,
|
|
2646
|
+
options: {
|
|
2647
|
+
wait_for_model: true
|
|
2648
|
+
}
|
|
2649
|
+
},
|
|
2650
|
+
{
|
|
2651
|
+
headers: {
|
|
2652
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2653
|
+
"Content-Type": "application/json"
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
);
|
|
2657
|
+
const embeddings = response.data;
|
|
2658
|
+
const dimensions = embeddings[0]?.length || 0;
|
|
2659
|
+
return {
|
|
2660
|
+
embeddings,
|
|
2661
|
+
model: modelToUse,
|
|
2662
|
+
dimensions
|
|
2663
|
+
};
|
|
2664
|
+
} catch (error) {
|
|
2665
|
+
if (axios16__default.default.isAxiosError(error)) {
|
|
2666
|
+
const status = error.response?.status;
|
|
2667
|
+
const message = error.response?.data?.error || error.message;
|
|
2668
|
+
let wrappedError;
|
|
2669
|
+
if (status === 401) {
|
|
2670
|
+
wrappedError = new Error(`HuggingFace API authentication failed. Please check your HUGGINGFACE_API_KEY. ${message}`);
|
|
2671
|
+
} else if (status === 429) {
|
|
2672
|
+
wrappedError = new Error(`HuggingFace API rate limit exceeded. ${message}`);
|
|
2673
|
+
} else if (status === 400) {
|
|
2674
|
+
wrappedError = new Error(`HuggingFace API request invalid: ${message}`);
|
|
2675
|
+
} else if (status === 503) {
|
|
2676
|
+
wrappedError = new Error(`HuggingFace model is loading. Please retry in a moment. ${message}`);
|
|
2677
|
+
} else {
|
|
2678
|
+
wrappedError = new Error(`HuggingFace API error (${status}): ${message}`);
|
|
2679
|
+
}
|
|
2680
|
+
if (error.code) wrappedError.code = error.code;
|
|
2681
|
+
if (error.response) wrappedError.response = error.response;
|
|
2682
|
+
throw wrappedError;
|
|
2683
|
+
}
|
|
2684
|
+
throw error;
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
};
|
|
2689
|
+
var VoyageEmbeddingProvider = class {
|
|
2690
|
+
name = "voyage";
|
|
2691
|
+
defaultModel = "voyage-2";
|
|
2692
|
+
apiKey;
|
|
2693
|
+
baseUrl = "https://api.voyageai.com/v1";
|
|
2694
|
+
constructor(apiKey, model) {
|
|
2695
|
+
this.apiKey = apiKey;
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Check if Voyage AI is available (API key is set)
|
|
2699
|
+
*/
|
|
2700
|
+
isAvailable() {
|
|
2701
|
+
return !!this.apiKey;
|
|
2702
|
+
}
|
|
2703
|
+
async generateEmbedding(text, model) {
|
|
2704
|
+
const result = await this.generateBatchEmbeddings([text], model);
|
|
2705
|
+
return {
|
|
2706
|
+
embedding: result.embeddings[0],
|
|
2707
|
+
model: result.model,
|
|
2708
|
+
dimensions: result.dimensions,
|
|
2709
|
+
usage: result.usage
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2713
|
+
const modelToUse = model || this.defaultModel;
|
|
2714
|
+
return retryWithBackoff2(async () => {
|
|
2715
|
+
try {
|
|
2716
|
+
const response = await axios16__default.default.post(
|
|
2717
|
+
`${this.baseUrl}/embeddings`,
|
|
2718
|
+
{
|
|
2719
|
+
input: texts,
|
|
2720
|
+
model: modelToUse,
|
|
2721
|
+
input_type: "document"
|
|
2722
|
+
// For storing in vector DB
|
|
2723
|
+
},
|
|
2724
|
+
{
|
|
2725
|
+
headers: {
|
|
2726
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2727
|
+
"Content-Type": "application/json"
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
);
|
|
2731
|
+
const embeddings = response.data.data.map((item) => item.embedding);
|
|
2732
|
+
const dimensions = embeddings[0]?.length || 0;
|
|
2733
|
+
return {
|
|
2734
|
+
embeddings,
|
|
2735
|
+
model: modelToUse,
|
|
2736
|
+
dimensions,
|
|
2737
|
+
usage: response.data.usage ? {
|
|
2738
|
+
promptTokens: response.data.usage.total_tokens || 0,
|
|
2739
|
+
totalTokens: response.data.usage.total_tokens || 0
|
|
2740
|
+
} : void 0
|
|
2741
|
+
};
|
|
2742
|
+
} catch (error) {
|
|
2743
|
+
if (axios16__default.default.isAxiosError(error)) {
|
|
2744
|
+
const status = error.response?.status;
|
|
2745
|
+
const message = error.response?.data?.message || error.message;
|
|
2746
|
+
let wrappedError;
|
|
2747
|
+
if (status === 401) {
|
|
2748
|
+
wrappedError = new Error(`Voyage AI API authentication failed. Please check your VOYAGE_API_KEY. ${message}`);
|
|
2749
|
+
} else if (status === 429) {
|
|
2750
|
+
wrappedError = new Error(`Voyage AI API rate limit exceeded. ${message}`);
|
|
2751
|
+
} else if (status === 400) {
|
|
2752
|
+
wrappedError = new Error(`Voyage AI API request invalid: ${message}`);
|
|
2753
|
+
} else {
|
|
2754
|
+
wrappedError = new Error(`Voyage AI API error (${status}): ${message}`);
|
|
2755
|
+
}
|
|
2756
|
+
if (error.code) wrappedError.code = error.code;
|
|
2757
|
+
if (error.response) wrappedError.response = error.response;
|
|
2758
|
+
throw wrappedError;
|
|
2759
|
+
}
|
|
2760
|
+
throw error;
|
|
2761
|
+
}
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
var OllamaEmbeddingProvider = class {
|
|
2766
|
+
name = "ollama";
|
|
2767
|
+
defaultModel = "nomic-embed-text";
|
|
2768
|
+
baseUrl;
|
|
2769
|
+
constructor(model, baseUrl = "http://localhost:11434") {
|
|
2770
|
+
this.baseUrl = baseUrl;
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Check if Ollama is available (always true for local service)
|
|
2774
|
+
*/
|
|
2775
|
+
isAvailable() {
|
|
2776
|
+
return true;
|
|
2777
|
+
}
|
|
2778
|
+
async generateEmbedding(text, model) {
|
|
2779
|
+
const modelToUse = model || this.defaultModel;
|
|
2780
|
+
return retryWithBackoff2(async () => {
|
|
2781
|
+
try {
|
|
2782
|
+
const response = await axios16__default.default.post(
|
|
2783
|
+
`${this.baseUrl}/api/embeddings`,
|
|
2784
|
+
{
|
|
2785
|
+
model: modelToUse,
|
|
2786
|
+
prompt: text
|
|
2787
|
+
},
|
|
2788
|
+
{
|
|
2789
|
+
headers: {
|
|
2790
|
+
"Content-Type": "application/json"
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
);
|
|
2794
|
+
const embedding = response.data.embedding;
|
|
2795
|
+
return {
|
|
2796
|
+
embedding,
|
|
2797
|
+
model: modelToUse,
|
|
2798
|
+
dimensions: embedding.length
|
|
2799
|
+
};
|
|
2800
|
+
} catch (error) {
|
|
2801
|
+
if (axios16__default.default.isAxiosError(error)) {
|
|
2802
|
+
const status = error.response?.status;
|
|
2803
|
+
const message = error.response?.data?.error || error.message;
|
|
2804
|
+
let wrappedError;
|
|
2805
|
+
if (error.code === "ECONNREFUSED") {
|
|
2806
|
+
wrappedError = new Error(`Cannot connect to Ollama at ${this.baseUrl}. Make sure Ollama is running locally.`);
|
|
2807
|
+
} else if (status === 404) {
|
|
2808
|
+
wrappedError = new Error(`Ollama model not found. Pull it with: ollama pull <model-name>`);
|
|
2809
|
+
} else if (status === 400) {
|
|
2810
|
+
wrappedError = new Error(`Ollama API request invalid: ${message}`);
|
|
2811
|
+
} else {
|
|
2812
|
+
wrappedError = new Error(`Ollama API error (${status}): ${message}`);
|
|
2813
|
+
}
|
|
2814
|
+
if (error.code) wrappedError.code = error.code;
|
|
2815
|
+
if (error.response) wrappedError.response = error.response;
|
|
2816
|
+
throw wrappedError;
|
|
2817
|
+
}
|
|
2818
|
+
throw error;
|
|
2819
|
+
}
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2823
|
+
const modelToUse = model || this.defaultModel;
|
|
2824
|
+
const embeddings = [];
|
|
2825
|
+
for (const text of texts) {
|
|
2826
|
+
const result = await this.generateEmbedding(text, modelToUse);
|
|
2827
|
+
embeddings.push(result.embedding);
|
|
2828
|
+
}
|
|
2829
|
+
const dimensions = embeddings[0]?.length || 0;
|
|
2830
|
+
return {
|
|
2831
|
+
embeddings,
|
|
2832
|
+
model: modelToUse,
|
|
2833
|
+
dimensions
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
};
|
|
2837
|
+
|
|
2838
|
+
// src/data/neo4j/embeddings/embedding-manager.ts
|
|
2839
|
+
var logger4 = core.createLogger("agentforge:tools:neo4j:embeddings");
|
|
2840
|
+
var EmbeddingManager = class {
|
|
2841
|
+
provider = null;
|
|
2842
|
+
config = null;
|
|
2843
|
+
/**
|
|
2844
|
+
* Initialize with configuration
|
|
2845
|
+
*/
|
|
2846
|
+
initialize(config) {
|
|
2847
|
+
logger4.debug("Initializing embedding manager", {
|
|
2848
|
+
provider: config.provider,
|
|
2849
|
+
model: config.model
|
|
2850
|
+
});
|
|
2851
|
+
this.config = config;
|
|
2852
|
+
this.provider = this.createProvider(config.provider, config.apiKey);
|
|
2853
|
+
logger4.info("Embedding manager initialized", {
|
|
2854
|
+
provider: config.provider,
|
|
2855
|
+
model: config.model || this.getDefaultModel(config.provider)
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Initialize from environment variables
|
|
2860
|
+
*/
|
|
2861
|
+
initializeFromEnv() {
|
|
2862
|
+
const providerName = getEmbeddingProvider();
|
|
2863
|
+
const model = getEmbeddingModel();
|
|
2864
|
+
logger4.debug("Initializing embedding manager from environment", {
|
|
2865
|
+
provider: providerName,
|
|
2866
|
+
model: model || this.getDefaultModel(providerName)
|
|
2867
|
+
});
|
|
2868
|
+
let apiKey = "";
|
|
2869
|
+
let baseUrl = "";
|
|
2870
|
+
switch (providerName) {
|
|
2871
|
+
case "openai": {
|
|
2872
|
+
const key = getOpenAIApiKey();
|
|
2873
|
+
if (!key) {
|
|
2874
|
+
logger4.error("OPENAI_API_KEY environment variable not set", {
|
|
2875
|
+
provider: "openai",
|
|
2876
|
+
required: true
|
|
2877
|
+
});
|
|
2878
|
+
throw new Error("OPENAI_API_KEY environment variable is required for OpenAI embeddings");
|
|
2879
|
+
}
|
|
2880
|
+
apiKey = key;
|
|
2881
|
+
logger4.debug("OpenAI API key found");
|
|
2882
|
+
break;
|
|
2883
|
+
}
|
|
2884
|
+
case "cohere": {
|
|
2885
|
+
const key = getCohereApiKey();
|
|
2886
|
+
if (!key) {
|
|
2887
|
+
logger4.error("COHERE_API_KEY environment variable not set", {
|
|
2888
|
+
provider: "cohere",
|
|
2889
|
+
required: true
|
|
2890
|
+
});
|
|
2891
|
+
throw new Error("COHERE_API_KEY environment variable is required for Cohere embeddings");
|
|
2892
|
+
}
|
|
2893
|
+
apiKey = key;
|
|
2894
|
+
logger4.debug("Cohere API key found");
|
|
2895
|
+
break;
|
|
2896
|
+
}
|
|
2897
|
+
case "huggingface": {
|
|
2898
|
+
const key = getHuggingFaceApiKey();
|
|
2899
|
+
if (!key) {
|
|
2900
|
+
logger4.error("HUGGINGFACE_API_KEY environment variable not set", {
|
|
2901
|
+
provider: "huggingface",
|
|
2902
|
+
required: true
|
|
2903
|
+
});
|
|
2904
|
+
throw new Error("HUGGINGFACE_API_KEY environment variable is required for HuggingFace embeddings");
|
|
2905
|
+
}
|
|
2906
|
+
apiKey = key;
|
|
2907
|
+
logger4.debug("HuggingFace API key found");
|
|
2908
|
+
break;
|
|
2909
|
+
}
|
|
2910
|
+
case "voyage": {
|
|
2911
|
+
const key = getVoyageApiKey();
|
|
2912
|
+
if (!key) {
|
|
2913
|
+
logger4.error("VOYAGE_API_KEY environment variable not set", {
|
|
2914
|
+
provider: "voyage",
|
|
2915
|
+
required: true
|
|
2916
|
+
});
|
|
2917
|
+
throw new Error("VOYAGE_API_KEY environment variable is required for Voyage AI embeddings");
|
|
2918
|
+
}
|
|
2919
|
+
apiKey = key;
|
|
2920
|
+
logger4.debug("Voyage API key found");
|
|
2921
|
+
break;
|
|
2922
|
+
}
|
|
2923
|
+
case "ollama":
|
|
2924
|
+
baseUrl = getOllamaBaseUrl();
|
|
2925
|
+
logger4.debug("Using Ollama (local, no API key required)", {
|
|
2926
|
+
baseUrl: baseUrl || "http://localhost:11434"
|
|
2927
|
+
});
|
|
2928
|
+
break;
|
|
2929
|
+
default:
|
|
2930
|
+
logger4.error("Unknown embedding provider", {
|
|
2931
|
+
provider: providerName
|
|
2932
|
+
});
|
|
2933
|
+
throw new Error(`Unknown embedding provider: ${providerName}`);
|
|
2934
|
+
}
|
|
2935
|
+
this.provider = this.createProvider(providerName, apiKey, model, baseUrl);
|
|
2936
|
+
this.config = {
|
|
2937
|
+
provider: providerName,
|
|
2938
|
+
model: model || this.getDefaultModel(providerName),
|
|
2939
|
+
apiKey: ""
|
|
2940
|
+
// Not stored when using env
|
|
2941
|
+
};
|
|
2942
|
+
logger4.info("Embedding manager initialized from environment", {
|
|
2943
|
+
provider: providerName,
|
|
2944
|
+
model: this.config.model
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Check if manager is initialized
|
|
2949
|
+
*/
|
|
2950
|
+
isInitialized() {
|
|
2951
|
+
return this.provider !== null && this.config !== null;
|
|
2952
|
+
}
|
|
2953
|
+
/**
|
|
2954
|
+
* Get current provider
|
|
2955
|
+
*/
|
|
2956
|
+
getProvider() {
|
|
2957
|
+
if (!this.provider) {
|
|
2958
|
+
throw new Error("Embedding manager not initialized. Call initialize() or initializeFromEnv() first.");
|
|
2959
|
+
}
|
|
2960
|
+
return this.provider;
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Get current configuration
|
|
2964
|
+
*/
|
|
2965
|
+
getConfig() {
|
|
2966
|
+
if (!this.config) {
|
|
2967
|
+
throw new Error("Embedding manager not initialized. Call initialize() or initializeFromEnv() first.");
|
|
2968
|
+
}
|
|
2969
|
+
return this.config;
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Generate embedding for a single text
|
|
2973
|
+
*/
|
|
2974
|
+
async generateEmbedding(text, model) {
|
|
2975
|
+
const startTime = Date.now();
|
|
2976
|
+
const provider = this.getProvider();
|
|
2977
|
+
const config = this.getConfig();
|
|
2978
|
+
const modelToUse = model || config.model;
|
|
2979
|
+
logger4.debug("Generating embedding", {
|
|
2980
|
+
provider: provider.name,
|
|
2981
|
+
model: modelToUse,
|
|
2982
|
+
textLength: text.length
|
|
2983
|
+
});
|
|
2984
|
+
const result = await provider.generateEmbedding(text, modelToUse);
|
|
2985
|
+
logger4.info("Embedding generated", {
|
|
2986
|
+
provider: provider.name,
|
|
2987
|
+
model: result.model,
|
|
2988
|
+
dimensions: result.dimensions,
|
|
2989
|
+
duration: Date.now() - startTime
|
|
2990
|
+
});
|
|
2991
|
+
return result;
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Generate embeddings for multiple texts (batch)
|
|
2995
|
+
*/
|
|
2996
|
+
async generateBatchEmbeddings(texts, model) {
|
|
2997
|
+
const startTime = Date.now();
|
|
2998
|
+
const provider = this.getProvider();
|
|
2999
|
+
const config = this.getConfig();
|
|
3000
|
+
const modelToUse = model || config.model;
|
|
3001
|
+
logger4.debug("Generating batch embeddings", {
|
|
3002
|
+
provider: provider.name,
|
|
3003
|
+
model: modelToUse,
|
|
3004
|
+
count: texts.length
|
|
3005
|
+
});
|
|
3006
|
+
const result = await provider.generateBatchEmbeddings(texts, modelToUse);
|
|
3007
|
+
logger4.info("Batch embeddings generated", {
|
|
3008
|
+
provider: provider.name,
|
|
3009
|
+
model: result.model,
|
|
3010
|
+
dimensions: result.dimensions,
|
|
3011
|
+
count: texts.length,
|
|
3012
|
+
duration: Date.now() - startTime
|
|
3013
|
+
});
|
|
3014
|
+
return result;
|
|
3015
|
+
}
|
|
3016
|
+
/**
|
|
3017
|
+
* Get default model for a provider
|
|
3018
|
+
*/
|
|
3019
|
+
getDefaultModel(provider) {
|
|
3020
|
+
switch (provider) {
|
|
3021
|
+
case "openai":
|
|
3022
|
+
return "text-embedding-3-small";
|
|
3023
|
+
case "cohere":
|
|
3024
|
+
return "embed-english-v3.0";
|
|
3025
|
+
case "huggingface":
|
|
3026
|
+
return "sentence-transformers/all-MiniLM-L6-v2";
|
|
3027
|
+
case "voyage":
|
|
3028
|
+
return "voyage-2";
|
|
3029
|
+
case "ollama":
|
|
3030
|
+
return "nomic-embed-text";
|
|
3031
|
+
default:
|
|
3032
|
+
return "text-embedding-3-small";
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Create a provider instance
|
|
3037
|
+
*/
|
|
3038
|
+
createProvider(providerName, apiKey, model, baseUrl) {
|
|
3039
|
+
switch (providerName) {
|
|
3040
|
+
case "openai":
|
|
3041
|
+
return new OpenAIEmbeddingProvider(apiKey);
|
|
3042
|
+
case "cohere":
|
|
3043
|
+
return new CohereEmbeddingProvider(apiKey);
|
|
3044
|
+
case "huggingface":
|
|
3045
|
+
return new HuggingFaceEmbeddingProvider(apiKey);
|
|
3046
|
+
case "voyage":
|
|
3047
|
+
return new VoyageEmbeddingProvider(apiKey);
|
|
3048
|
+
case "ollama":
|
|
3049
|
+
return new OllamaEmbeddingProvider(model, baseUrl || "http://localhost:11434");
|
|
3050
|
+
default:
|
|
3051
|
+
throw new Error(`Unknown embedding provider: ${providerName}`);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Reset the manager (for testing)
|
|
3056
|
+
*/
|
|
3057
|
+
reset() {
|
|
3058
|
+
this.provider = null;
|
|
3059
|
+
this.config = null;
|
|
3060
|
+
}
|
|
3061
|
+
};
|
|
3062
|
+
var embeddingManager = new EmbeddingManager();
|
|
3063
|
+
function initializeEmbeddings() {
|
|
3064
|
+
embeddingManager.initializeFromEnv();
|
|
3065
|
+
}
|
|
3066
|
+
function initializeEmbeddingsWithConfig(config) {
|
|
3067
|
+
embeddingManager.initialize(config);
|
|
3068
|
+
}
|
|
3069
|
+
async function generateEmbedding(text, model) {
|
|
3070
|
+
return embeddingManager.generateEmbedding(text, model);
|
|
3071
|
+
}
|
|
3072
|
+
async function generateBatchEmbeddings(texts, model) {
|
|
3073
|
+
return embeddingManager.generateBatchEmbeddings(texts, model);
|
|
3074
|
+
}
|
|
3075
|
+
function formatInteger(value) {
|
|
3076
|
+
if (value.inSafeRange()) {
|
|
3077
|
+
return value.toNumber();
|
|
3078
|
+
}
|
|
3079
|
+
return value.toString();
|
|
3080
|
+
}
|
|
3081
|
+
function formatValue(value) {
|
|
3082
|
+
if (value === null || value === void 0) {
|
|
3083
|
+
return value;
|
|
3084
|
+
}
|
|
3085
|
+
if (value instanceof neo4j.Integer) {
|
|
3086
|
+
return formatInteger(value);
|
|
3087
|
+
}
|
|
3088
|
+
if (value instanceof neo4j.Node) {
|
|
3089
|
+
return formatNode(value);
|
|
3090
|
+
}
|
|
3091
|
+
if (value instanceof neo4j.Relationship) {
|
|
3092
|
+
return formatRelationship(value);
|
|
3093
|
+
}
|
|
3094
|
+
if (value instanceof neo4j.Path) {
|
|
3095
|
+
return formatPath(value);
|
|
3096
|
+
}
|
|
3097
|
+
if (Array.isArray(value)) {
|
|
3098
|
+
return value.map(formatValue);
|
|
3099
|
+
}
|
|
3100
|
+
if (typeof value === "object") {
|
|
3101
|
+
const formatted = {};
|
|
3102
|
+
for (const [key, val] of Object.entries(value)) {
|
|
3103
|
+
formatted[key] = formatValue(val);
|
|
3104
|
+
}
|
|
3105
|
+
return formatted;
|
|
3106
|
+
}
|
|
3107
|
+
return value;
|
|
3108
|
+
}
|
|
3109
|
+
function formatNode(node) {
|
|
3110
|
+
return {
|
|
3111
|
+
identity: formatInteger(node.identity),
|
|
3112
|
+
labels: node.labels,
|
|
3113
|
+
properties: formatValue(node.properties)
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
function formatRelationship(rel) {
|
|
3117
|
+
return {
|
|
3118
|
+
identity: formatInteger(rel.identity),
|
|
3119
|
+
type: rel.type,
|
|
3120
|
+
start: formatInteger(rel.start),
|
|
3121
|
+
end: formatInteger(rel.end),
|
|
3122
|
+
properties: formatValue(rel.properties)
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
function formatPath(path12) {
|
|
3126
|
+
return {
|
|
3127
|
+
start: formatNode(path12.start),
|
|
3128
|
+
end: formatNode(path12.end),
|
|
3129
|
+
segments: path12.segments.map((segment) => ({
|
|
3130
|
+
start: formatNode(segment.start),
|
|
3131
|
+
relationship: formatRelationship(segment.relationship),
|
|
3132
|
+
end: formatNode(segment.end)
|
|
3133
|
+
})),
|
|
3134
|
+
length: path12.length
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
function formatResults(records) {
|
|
3138
|
+
return records.map((record) => {
|
|
3139
|
+
const formatted = {};
|
|
3140
|
+
for (const key of record.keys) {
|
|
3141
|
+
formatted[key] = formatValue(record.get(key));
|
|
3142
|
+
}
|
|
3143
|
+
return formatted;
|
|
3144
|
+
});
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
// src/data/neo4j/tools/neo4j-query.ts
|
|
3148
|
+
var logger5 = core.createLogger("agentforge:tools:neo4j:query");
|
|
3149
|
+
function createNeo4jQueryTool() {
|
|
3150
|
+
return core.toolBuilder().name("neo4j-query").description(
|
|
3151
|
+
"Execute a Cypher query against the Neo4j graph database. Supports parameterized queries for safety. Use this for complex queries, graph traversals, and custom operations."
|
|
3152
|
+
).category(core.ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "cypher", "query"]).schema(neo4jQuerySchema).implement(async (input) => {
|
|
3153
|
+
if (!neo4jPool.isInitialized()) {
|
|
3154
|
+
logger5.warn("Neo4j query attempted but connection not initialized");
|
|
3155
|
+
return {
|
|
3156
|
+
success: false,
|
|
3157
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
const startTime = Date.now();
|
|
3161
|
+
logger5.debug("Executing Neo4j query", {
|
|
3162
|
+
cypherPreview: input.cypher.substring(0, 100),
|
|
3163
|
+
parameterCount: Object.keys(input.parameters || {}).length,
|
|
3164
|
+
database: input.database
|
|
3165
|
+
});
|
|
3166
|
+
try {
|
|
3167
|
+
const session = neo4jPool.getSession(input.database);
|
|
3168
|
+
try {
|
|
3169
|
+
const result = await session.run(input.cypher, input.parameters || {});
|
|
3170
|
+
const formattedResults = formatResults(result.records);
|
|
3171
|
+
const duration = Date.now() - startTime;
|
|
3172
|
+
logger5.info("Neo4j query executed successfully", {
|
|
3173
|
+
recordCount: result.records.length,
|
|
3174
|
+
nodesCreated: result.summary.counters.updates().nodesCreated,
|
|
3175
|
+
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
|
3176
|
+
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
|
3177
|
+
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
|
3178
|
+
propertiesSet: result.summary.counters.updates().propertiesSet,
|
|
3179
|
+
duration
|
|
3180
|
+
});
|
|
3181
|
+
return {
|
|
3182
|
+
success: true,
|
|
3183
|
+
data: formattedResults,
|
|
3184
|
+
recordCount: result.records.length,
|
|
3185
|
+
summary: {
|
|
3186
|
+
query: input.cypher,
|
|
3187
|
+
parameters: input.parameters,
|
|
3188
|
+
counters: {
|
|
3189
|
+
nodesCreated: result.summary.counters.updates().nodesCreated,
|
|
3190
|
+
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
|
3191
|
+
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
|
3192
|
+
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
|
3193
|
+
propertiesSet: result.summary.counters.updates().propertiesSet
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
};
|
|
3197
|
+
} finally {
|
|
3198
|
+
await session.close();
|
|
3199
|
+
}
|
|
3200
|
+
} catch (error) {
|
|
3201
|
+
const duration = Date.now() - startTime;
|
|
3202
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to execute query";
|
|
3203
|
+
logger5.error("Neo4j query execution failed", {
|
|
3204
|
+
error: errorMessage,
|
|
3205
|
+
cypherPreview: input.cypher.substring(0, 100),
|
|
3206
|
+
duration
|
|
3207
|
+
});
|
|
3208
|
+
return {
|
|
3209
|
+
success: false,
|
|
3210
|
+
error: errorMessage,
|
|
3211
|
+
query: input.cypher
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
}).build();
|
|
3215
|
+
}
|
|
3216
|
+
function createNeo4jGetSchemaTool() {
|
|
3217
|
+
return core.toolBuilder().name("neo4j-get-schema").description(
|
|
3218
|
+
"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."
|
|
3219
|
+
).category(core.ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "schema", "introspection"]).schema(neo4jGetSchemaSchema).implement(async (input) => {
|
|
3220
|
+
if (!neo4jPool.isInitialized()) {
|
|
3221
|
+
return {
|
|
3222
|
+
success: false,
|
|
3223
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
try {
|
|
3227
|
+
const session = neo4jPool.getSession(input.database);
|
|
3228
|
+
try {
|
|
3229
|
+
const labelsResult = await session.run("CALL db.labels()");
|
|
3230
|
+
const nodeLabels = labelsResult.records.map((r) => r.get("label"));
|
|
3231
|
+
const relTypesResult = await session.run("CALL db.relationshipTypes()");
|
|
3232
|
+
const relationshipTypes = relTypesResult.records.map((r) => r.get("relationshipType"));
|
|
3233
|
+
const propsResult = await session.run("CALL db.propertyKeys()");
|
|
3234
|
+
const propertyKeys = propsResult.records.map((r) => r.get("propertyKey"));
|
|
3235
|
+
const constraintsResult = await session.run("SHOW CONSTRAINTS");
|
|
3236
|
+
const constraints = constraintsResult.records.map((r) => ({
|
|
3237
|
+
name: r.get("name"),
|
|
3238
|
+
type: r.get("type"),
|
|
3239
|
+
entityType: r.get("entityType"),
|
|
3240
|
+
labelsOrTypes: r.get("labelsOrTypes"),
|
|
3241
|
+
properties: r.get("properties")
|
|
3242
|
+
}));
|
|
3243
|
+
const indexesResult = await session.run("SHOW INDEXES");
|
|
3244
|
+
const indexes = indexesResult.records.map((r) => ({
|
|
3245
|
+
name: r.get("name"),
|
|
3246
|
+
type: r.get("type"),
|
|
3247
|
+
entityType: r.get("entityType"),
|
|
3248
|
+
labelsOrTypes: r.get("labelsOrTypes"),
|
|
3249
|
+
properties: r.get("properties")
|
|
3250
|
+
}));
|
|
3251
|
+
return {
|
|
3252
|
+
success: true,
|
|
3253
|
+
schema: {
|
|
3254
|
+
nodeLabels,
|
|
3255
|
+
relationshipTypes,
|
|
3256
|
+
propertyKeys,
|
|
3257
|
+
constraints,
|
|
3258
|
+
indexes
|
|
3259
|
+
},
|
|
3260
|
+
summary: {
|
|
3261
|
+
totalLabels: nodeLabels.length,
|
|
3262
|
+
totalRelationshipTypes: relationshipTypes.length,
|
|
3263
|
+
totalPropertyKeys: propertyKeys.length,
|
|
3264
|
+
totalConstraints: constraints.length,
|
|
3265
|
+
totalIndexes: indexes.length
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
} finally {
|
|
3269
|
+
await session.close();
|
|
3270
|
+
}
|
|
3271
|
+
} catch (error) {
|
|
3272
|
+
return {
|
|
3273
|
+
success: false,
|
|
3274
|
+
error: error instanceof Error ? error.message : "Failed to get schema"
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
}).build();
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
// src/data/neo4j/utils/cypher-sanitizer.ts
|
|
3281
|
+
function escapeCypherIdentifier(identifier, type = "identifier") {
|
|
3282
|
+
if (!identifier || typeof identifier !== "string") {
|
|
3283
|
+
throw new Error(`Invalid ${type}: must be a non-empty string`);
|
|
3284
|
+
}
|
|
3285
|
+
const trimmed = identifier.trim();
|
|
3286
|
+
if (trimmed.length === 0) {
|
|
3287
|
+
throw new Error(`Invalid ${type}: cannot be empty or whitespace`);
|
|
3288
|
+
}
|
|
3289
|
+
if (trimmed.includes("\0")) {
|
|
3290
|
+
throw new Error(`Invalid ${type}: cannot contain null bytes`);
|
|
3291
|
+
}
|
|
3292
|
+
const simplePattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3293
|
+
if (simplePattern.test(trimmed)) {
|
|
3294
|
+
return trimmed;
|
|
3295
|
+
}
|
|
3296
|
+
const escaped = trimmed.replace(/`/g, "``");
|
|
3297
|
+
return `\`${escaped}\``;
|
|
3298
|
+
}
|
|
3299
|
+
function validateLabel(label) {
|
|
3300
|
+
return escapeCypherIdentifier(label, "label");
|
|
3301
|
+
}
|
|
3302
|
+
function validatePropertyKey(key) {
|
|
3303
|
+
return escapeCypherIdentifier(key, "property key");
|
|
3304
|
+
}
|
|
3305
|
+
function validateRelationshipType(type) {
|
|
3306
|
+
return escapeCypherIdentifier(type, "relationship type");
|
|
3307
|
+
}
|
|
3308
|
+
function validateDirection(direction) {
|
|
3309
|
+
const normalized = direction.toUpperCase();
|
|
3310
|
+
if (normalized !== "OUTGOING" && normalized !== "INCOMING" && normalized !== "BOTH") {
|
|
3311
|
+
throw new Error(`Invalid direction: must be 'OUTGOING', 'INCOMING', or 'BOTH'`);
|
|
3312
|
+
}
|
|
3313
|
+
return normalized;
|
|
3314
|
+
}
|
|
3315
|
+
function buildPropertyFilter(properties, nodeVar = "n") {
|
|
3316
|
+
const keys = Object.keys(properties);
|
|
3317
|
+
if (keys.length === 0) {
|
|
3318
|
+
return { whereClause: "", parameters: {} };
|
|
3319
|
+
}
|
|
3320
|
+
const safeNodeVar = escapeCypherIdentifier(nodeVar, "node variable");
|
|
3321
|
+
const conditions = keys.map((key, index) => {
|
|
3322
|
+
const safeKey = validatePropertyKey(key);
|
|
3323
|
+
const paramName = `prop_${index}`;
|
|
3324
|
+
return `${safeNodeVar}.${safeKey} = $${paramName}`;
|
|
3325
|
+
});
|
|
3326
|
+
const parameters = {};
|
|
3327
|
+
keys.forEach((key, index) => {
|
|
3328
|
+
parameters[`prop_${index}`] = properties[key];
|
|
3329
|
+
});
|
|
3330
|
+
return {
|
|
3331
|
+
whereClause: `WHERE ${conditions.join(" AND ")}`,
|
|
3332
|
+
parameters
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
// src/data/neo4j/tools/neo4j-find-nodes.ts
|
|
3337
|
+
function createNeo4jFindNodesTool() {
|
|
3338
|
+
return core.toolBuilder().name("neo4j-find-nodes").description(
|
|
3339
|
+
"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."
|
|
3340
|
+
).category(core.ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "nodes", "search"]).schema(neo4jFindNodesSchema).implement(async (input) => {
|
|
3341
|
+
if (!neo4jPool.isInitialized()) {
|
|
3342
|
+
return {
|
|
3343
|
+
success: false,
|
|
3344
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
try {
|
|
3348
|
+
const safeLabel = validateLabel(input.label);
|
|
3349
|
+
const session = neo4jPool.getSession(input.database);
|
|
3350
|
+
try {
|
|
3351
|
+
let cypher = `MATCH (n:${safeLabel})`;
|
|
3352
|
+
let parameters = {};
|
|
3353
|
+
if (input.properties && Object.keys(input.properties).length > 0) {
|
|
3354
|
+
const { whereClause, parameters: filterParams } = buildPropertyFilter(input.properties, "n");
|
|
3355
|
+
cypher += ` ${whereClause}`;
|
|
3356
|
+
parameters = { ...parameters, ...filterParams };
|
|
3357
|
+
}
|
|
3358
|
+
cypher += ` RETURN n LIMIT $limit`;
|
|
3359
|
+
parameters.limit = input.limit;
|
|
3360
|
+
const result = await session.run(cypher, parameters);
|
|
3361
|
+
const formattedResults = formatResults(result.records);
|
|
3362
|
+
return {
|
|
3363
|
+
success: true,
|
|
3364
|
+
nodes: formattedResults.map((r) => r.n),
|
|
3365
|
+
count: result.records.length,
|
|
3366
|
+
query: {
|
|
3367
|
+
label: input.label,
|
|
3368
|
+
properties: input.properties,
|
|
3369
|
+
limit: input.limit
|
|
3370
|
+
}
|
|
3371
|
+
};
|
|
3372
|
+
} finally {
|
|
3373
|
+
await session.close();
|
|
3374
|
+
}
|
|
3375
|
+
} catch (error) {
|
|
3376
|
+
return {
|
|
3377
|
+
success: false,
|
|
3378
|
+
error: error instanceof Error ? error.message : "Failed to find nodes",
|
|
3379
|
+
query: {
|
|
3380
|
+
label: input.label,
|
|
3381
|
+
properties: input.properties
|
|
3382
|
+
}
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
}).build();
|
|
3386
|
+
}
|
|
3387
|
+
function createNeo4jTraverseTool() {
|
|
3388
|
+
return core.toolBuilder().name("neo4j-traverse").description(
|
|
3389
|
+
"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."
|
|
3390
|
+
).category(core.ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "traverse", "relationships"]).schema(neo4jTraverseSchema).implement(async (input) => {
|
|
3391
|
+
if (!neo4jPool.isInitialized()) {
|
|
3392
|
+
return {
|
|
3393
|
+
success: false,
|
|
3394
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
try {
|
|
3398
|
+
const safeDirection = validateDirection(input.direction || "outgoing");
|
|
3399
|
+
const safeRelType = input.relationshipType ? validateRelationshipType(input.relationshipType) : null;
|
|
3400
|
+
const session = neo4jPool.getSession(input.database);
|
|
3401
|
+
try {
|
|
3402
|
+
let relPattern = "";
|
|
3403
|
+
if (safeDirection === "OUTGOING") {
|
|
3404
|
+
relPattern = safeRelType ? `-[r:${safeRelType}*1..${input.maxDepth}]->` : `-[r*1..${input.maxDepth}]->`;
|
|
3405
|
+
} else if (safeDirection === "INCOMING") {
|
|
3406
|
+
relPattern = safeRelType ? `<-[r:${safeRelType}*1..${input.maxDepth}]-` : `<-[r*1..${input.maxDepth}]-`;
|
|
3407
|
+
} else {
|
|
3408
|
+
relPattern = safeRelType ? `-[r:${safeRelType}*1..${input.maxDepth}]-` : `-[r*1..${input.maxDepth}]-`;
|
|
3409
|
+
}
|
|
3410
|
+
const cypher = `
|
|
3411
|
+
MATCH path = (start)${relPattern}(end)
|
|
3412
|
+
WHERE id(start) = $startNodeId
|
|
3413
|
+
RETURN start, end, relationships(path) as rels, length(path) as depth
|
|
3414
|
+
LIMIT $limit
|
|
3415
|
+
`;
|
|
3416
|
+
const parameters = {
|
|
3417
|
+
startNodeId: typeof input.startNodeId === "string" ? parseInt(input.startNodeId, 10) : input.startNodeId,
|
|
3418
|
+
limit: input.limit
|
|
3419
|
+
};
|
|
3420
|
+
const result = await session.run(cypher, parameters);
|
|
3421
|
+
const formattedResults = formatResults(result.records);
|
|
3422
|
+
return {
|
|
3423
|
+
success: true,
|
|
3424
|
+
paths: formattedResults.map((r) => ({
|
|
3425
|
+
start: r.start,
|
|
3426
|
+
end: r.end,
|
|
3427
|
+
relationships: r.rels,
|
|
3428
|
+
depth: r.depth
|
|
3429
|
+
})),
|
|
3430
|
+
count: result.records.length,
|
|
3431
|
+
query: {
|
|
3432
|
+
startNodeId: input.startNodeId,
|
|
3433
|
+
relationshipType: input.relationshipType,
|
|
3434
|
+
direction: input.direction,
|
|
3435
|
+
maxDepth: input.maxDepth
|
|
3436
|
+
}
|
|
3437
|
+
};
|
|
3438
|
+
} finally {
|
|
3439
|
+
await session.close();
|
|
3440
|
+
}
|
|
3441
|
+
} catch (error) {
|
|
3442
|
+
return {
|
|
3443
|
+
success: false,
|
|
3444
|
+
error: error instanceof Error ? error.message : "Failed to traverse graph",
|
|
3445
|
+
query: {
|
|
3446
|
+
startNodeId: input.startNodeId,
|
|
3447
|
+
relationshipType: input.relationshipType,
|
|
3448
|
+
direction: input.direction
|
|
3449
|
+
}
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
}).build();
|
|
3453
|
+
}
|
|
3454
|
+
function createNeo4jVectorSearchTool() {
|
|
3455
|
+
return core.toolBuilder().name("neo4j-vector-search").description(
|
|
3456
|
+
"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."
|
|
3457
|
+
).category(core.ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "vector", "search", "semantic", "graphrag"]).schema(neo4jVectorSearchSchema).implement(async (input) => {
|
|
3458
|
+
if (!neo4jPool.isInitialized()) {
|
|
3459
|
+
return {
|
|
3460
|
+
success: false,
|
|
3461
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
try {
|
|
3465
|
+
const session = neo4jPool.getSession(input.database);
|
|
3466
|
+
try {
|
|
3467
|
+
const cypher = `
|
|
3468
|
+
CALL db.index.vector.queryNodes($indexName, $limit, $queryVector)
|
|
3469
|
+
YIELD node, score
|
|
3470
|
+
RETURN node, score
|
|
3471
|
+
ORDER BY score DESC
|
|
3472
|
+
`;
|
|
3473
|
+
const parameters = {
|
|
3474
|
+
indexName: input.indexName,
|
|
3475
|
+
limit: input.limit,
|
|
3476
|
+
queryVector: input.queryVector
|
|
3477
|
+
};
|
|
3478
|
+
const result = await session.run(cypher, parameters);
|
|
3479
|
+
const formattedResults = formatResults(result.records);
|
|
3480
|
+
return {
|
|
3481
|
+
success: true,
|
|
3482
|
+
results: formattedResults.map((r) => ({
|
|
3483
|
+
node: r.node,
|
|
3484
|
+
score: r.score
|
|
3485
|
+
})),
|
|
3486
|
+
count: result.records.length,
|
|
3487
|
+
query: {
|
|
3488
|
+
indexName: input.indexName,
|
|
3489
|
+
vectorDimension: input.queryVector.length,
|
|
3490
|
+
limit: input.limit
|
|
3491
|
+
}
|
|
3492
|
+
};
|
|
3493
|
+
} finally {
|
|
3494
|
+
await session.close();
|
|
3495
|
+
}
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to perform vector search";
|
|
3498
|
+
let helpText = "";
|
|
3499
|
+
if (errorMessage.includes("index") || errorMessage.includes("not found")) {
|
|
3500
|
+
helpText = " Make sure the vector index exists. Create one with: CREATE VECTOR INDEX <name> FOR (n:Label) ON (n.embedding)";
|
|
3501
|
+
}
|
|
3502
|
+
return {
|
|
3503
|
+
success: false,
|
|
3504
|
+
error: errorMessage + helpText,
|
|
3505
|
+
query: {
|
|
3506
|
+
indexName: input.indexName,
|
|
3507
|
+
vectorDimension: input.queryVector.length
|
|
3508
|
+
}
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
}).build();
|
|
3512
|
+
}
|
|
3513
|
+
var logger6 = core.createLogger("agentforge:tools:neo4j:vector-search");
|
|
3514
|
+
function createNeo4jVectorSearchWithEmbeddingTool() {
|
|
3515
|
+
return core.toolBuilder().name("neo4j-vector-search-with-embedding").description(
|
|
3516
|
+
"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."
|
|
3517
|
+
).category(core.ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "vector", "search", "semantic", "graphrag", "embedding", "ai"]).schema(neo4jVectorSearchWithEmbeddingSchema).implement(async (input) => {
|
|
3518
|
+
if (!neo4jPool.isInitialized()) {
|
|
3519
|
+
logger6.warn("Vector search attempted but Neo4j connection not initialized");
|
|
3520
|
+
return {
|
|
3521
|
+
success: false,
|
|
3522
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
if (!embeddingManager.isInitialized()) {
|
|
3526
|
+
logger6.warn("Vector search attempted but embedding manager not initialized");
|
|
3527
|
+
return {
|
|
3528
|
+
success: false,
|
|
3529
|
+
error: "Embedding manager not initialized. Please configure embedding provider (set OPENAI_API_KEY and optionally EMBEDDING_MODEL)."
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
const startTime = Date.now();
|
|
3533
|
+
logger6.debug("Performing vector search with embedding", {
|
|
3534
|
+
queryTextLength: input.queryText.length,
|
|
3535
|
+
indexName: input.indexName,
|
|
3536
|
+
limit: input.limit,
|
|
3537
|
+
model: input.model
|
|
3538
|
+
});
|
|
3539
|
+
try {
|
|
3540
|
+
const embeddingResult = await embeddingManager.generateEmbedding(input.queryText, input.model);
|
|
3541
|
+
const session = neo4jPool.getSession(input.database);
|
|
3542
|
+
try {
|
|
3543
|
+
const cypher = `
|
|
3544
|
+
CALL db.index.vector.queryNodes($indexName, $limit, $queryVector)
|
|
3545
|
+
YIELD node, score
|
|
3546
|
+
RETURN node, score
|
|
3547
|
+
ORDER BY score DESC
|
|
3548
|
+
`;
|
|
3549
|
+
const parameters = {
|
|
3550
|
+
indexName: input.indexName,
|
|
3551
|
+
limit: input.limit,
|
|
3552
|
+
queryVector: embeddingResult.embedding
|
|
3553
|
+
};
|
|
3554
|
+
const result = await session.run(cypher, parameters);
|
|
3555
|
+
const formattedResults = formatResults(result.records);
|
|
3556
|
+
const duration = Date.now() - startTime;
|
|
3557
|
+
logger6.info("Vector search completed successfully", {
|
|
3558
|
+
resultCount: result.records.length,
|
|
3559
|
+
indexName: input.indexName,
|
|
3560
|
+
embeddingModel: embeddingResult.model,
|
|
3561
|
+
embeddingDimensions: embeddingResult.dimensions,
|
|
3562
|
+
duration
|
|
3563
|
+
});
|
|
3564
|
+
return {
|
|
3565
|
+
success: true,
|
|
3566
|
+
results: formattedResults.map((r) => ({
|
|
3567
|
+
node: r.node,
|
|
3568
|
+
score: r.score
|
|
3569
|
+
})),
|
|
3570
|
+
count: result.records.length,
|
|
3571
|
+
query: {
|
|
3572
|
+
text: input.queryText,
|
|
3573
|
+
indexName: input.indexName,
|
|
3574
|
+
embeddingModel: embeddingResult.model,
|
|
3575
|
+
vectorDimension: embeddingResult.dimensions,
|
|
3576
|
+
limit: input.limit
|
|
3577
|
+
},
|
|
3578
|
+
embedding: {
|
|
3579
|
+
model: embeddingResult.model,
|
|
3580
|
+
dimensions: embeddingResult.dimensions,
|
|
3581
|
+
usage: embeddingResult.usage
|
|
3582
|
+
}
|
|
3583
|
+
};
|
|
3584
|
+
} finally {
|
|
3585
|
+
await session.close();
|
|
3586
|
+
}
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
const duration = Date.now() - startTime;
|
|
3589
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to perform vector search with embedding";
|
|
3590
|
+
let helpText = "";
|
|
3591
|
+
if (errorMessage.includes("index") || errorMessage.includes("not found")) {
|
|
3592
|
+
helpText = " Make sure the vector index exists. Create one with: CREATE VECTOR INDEX <name> FOR (n:Label) ON (n.embedding)";
|
|
3593
|
+
} else if (errorMessage.includes("API key") || errorMessage.includes("not initialized")) {
|
|
3594
|
+
helpText = " Make sure OPENAI_API_KEY is set in your environment variables.";
|
|
3595
|
+
} else if (errorMessage.includes("dimension")) {
|
|
3596
|
+
helpText = " Make sure the vector index dimensions match your embedding model dimensions.";
|
|
3597
|
+
}
|
|
3598
|
+
logger6.error("Vector search failed", {
|
|
3599
|
+
error: errorMessage,
|
|
3600
|
+
indexName: input.indexName,
|
|
3601
|
+
queryTextLength: input.queryText.length,
|
|
3602
|
+
duration
|
|
3603
|
+
});
|
|
3604
|
+
return {
|
|
3605
|
+
success: false,
|
|
3606
|
+
error: errorMessage + helpText,
|
|
3607
|
+
query: {
|
|
3608
|
+
text: input.queryText,
|
|
3609
|
+
indexName: input.indexName
|
|
3610
|
+
}
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
}).build();
|
|
3614
|
+
}
|
|
3615
|
+
function createNeo4jCreateNodeWithEmbeddingTool() {
|
|
3616
|
+
return core.toolBuilder().name("neo4j-create-node-with-embedding").description(
|
|
3617
|
+
"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."
|
|
3618
|
+
).category(core.ToolCategory.DATABASE).tags(["neo4j", "graph", "database", "create", "node", "embedding", "graphrag", "ai"]).schema(neo4jCreateNodeWithEmbeddingSchema).implement(async (input) => {
|
|
3619
|
+
if (!neo4jPool.isInitialized()) {
|
|
3620
|
+
return {
|
|
3621
|
+
success: false,
|
|
3622
|
+
error: "Neo4j connection not initialized. Please configure Neo4j connection first."
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
if (!embeddingManager.isInitialized()) {
|
|
3626
|
+
return {
|
|
3627
|
+
success: false,
|
|
3628
|
+
error: "Embedding manager not initialized. Please configure embedding provider (set OPENAI_API_KEY and optionally EMBEDDING_MODEL)."
|
|
3629
|
+
};
|
|
3630
|
+
}
|
|
3631
|
+
try {
|
|
3632
|
+
const textToEmbed = input.properties[input.textProperty];
|
|
3633
|
+
if (!textToEmbed || typeof textToEmbed !== "string") {
|
|
3634
|
+
return {
|
|
3635
|
+
success: false,
|
|
3636
|
+
error: `Property '${input.textProperty}' not found or is not a string in the provided properties.`
|
|
3637
|
+
};
|
|
3638
|
+
}
|
|
3639
|
+
const embeddingResult = await embeddingManager.generateEmbedding(textToEmbed, input.model);
|
|
3640
|
+
const safeLabel = validateLabel(input.label);
|
|
3641
|
+
const safeEmbeddingProp = validatePropertyKey(input.embeddingProperty || "embedding");
|
|
3642
|
+
const session = neo4jPool.getSession(input.database);
|
|
3643
|
+
try {
|
|
3644
|
+
const allProperties = {
|
|
3645
|
+
...input.properties
|
|
3646
|
+
};
|
|
3647
|
+
allProperties[input.embeddingProperty || "embedding"] = embeddingResult.embedding;
|
|
3648
|
+
const cypher = `
|
|
3649
|
+
CREATE (n:${safeLabel})
|
|
3650
|
+
SET n = $properties
|
|
3651
|
+
RETURN n, id(n) as nodeId
|
|
3652
|
+
`;
|
|
3653
|
+
const parameters = {
|
|
3654
|
+
properties: allProperties
|
|
3655
|
+
};
|
|
3656
|
+
const result = await session.run(cypher, parameters);
|
|
3657
|
+
const formattedResults = formatResults(result.records);
|
|
3658
|
+
if (formattedResults.length === 0) {
|
|
3659
|
+
return {
|
|
3660
|
+
success: false,
|
|
3661
|
+
error: "Failed to create node"
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
const createdNode = formattedResults[0];
|
|
3665
|
+
return {
|
|
3666
|
+
success: true,
|
|
3667
|
+
node: createdNode.n,
|
|
3668
|
+
nodeId: createdNode.nodeId,
|
|
3669
|
+
embedding: {
|
|
3670
|
+
model: embeddingResult.model,
|
|
3671
|
+
dimensions: embeddingResult.dimensions,
|
|
3672
|
+
property: safeEmbeddingProp,
|
|
3673
|
+
usage: embeddingResult.usage
|
|
3674
|
+
},
|
|
3675
|
+
message: `Created node with label '${input.label}' and ${embeddingResult.dimensions}-dimensional embedding`
|
|
3676
|
+
};
|
|
3677
|
+
} finally {
|
|
3678
|
+
await session.close();
|
|
3679
|
+
}
|
|
3680
|
+
} catch (error) {
|
|
3681
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to create node with embedding";
|
|
3682
|
+
let helpText = "";
|
|
3683
|
+
if (errorMessage.includes("API key") || errorMessage.includes("not initialized")) {
|
|
3684
|
+
helpText = " Make sure OPENAI_API_KEY is set in your environment variables.";
|
|
3685
|
+
} else if (errorMessage.includes("Syntax error") || errorMessage.includes("Invalid")) {
|
|
3686
|
+
helpText = " Check that the label and property names are valid.";
|
|
3687
|
+
}
|
|
3688
|
+
return {
|
|
3689
|
+
success: false,
|
|
3690
|
+
error: errorMessage + helpText
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
}).build();
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
// src/data/neo4j/index.ts
|
|
3697
|
+
var neo4jQuery = createNeo4jQueryTool();
|
|
3698
|
+
var neo4jGetSchema = createNeo4jGetSchemaTool();
|
|
3699
|
+
var neo4jFindNodes = createNeo4jFindNodesTool();
|
|
3700
|
+
var neo4jTraverse = createNeo4jTraverseTool();
|
|
3701
|
+
var neo4jVectorSearch = createNeo4jVectorSearchTool();
|
|
3702
|
+
var neo4jVectorSearchWithEmbedding = createNeo4jVectorSearchWithEmbeddingTool();
|
|
3703
|
+
var neo4jCreateNodeWithEmbedding = createNeo4jCreateNodeWithEmbeddingTool();
|
|
3704
|
+
var neo4jTools = [
|
|
3705
|
+
neo4jQuery,
|
|
3706
|
+
neo4jGetSchema,
|
|
3707
|
+
neo4jFindNodes,
|
|
3708
|
+
neo4jTraverse,
|
|
3709
|
+
neo4jVectorSearch,
|
|
3710
|
+
neo4jVectorSearchWithEmbedding,
|
|
3711
|
+
neo4jCreateNodeWithEmbedding
|
|
3712
|
+
];
|
|
3713
|
+
var neo4jCoreTools = [
|
|
3714
|
+
neo4jQuery,
|
|
3715
|
+
neo4jGetSchema,
|
|
3716
|
+
neo4jFindNodes,
|
|
3717
|
+
neo4jTraverse,
|
|
3718
|
+
neo4jVectorSearch
|
|
3719
|
+
];
|
|
3720
|
+
function createNeo4jTools(config = {}, includeEmbeddingTools = true) {
|
|
3721
|
+
if (config.uri && config.username && config.password) {
|
|
3722
|
+
neo4jPool.initialize({
|
|
3723
|
+
uri: config.uri,
|
|
3724
|
+
username: config.username,
|
|
3725
|
+
password: config.password,
|
|
3726
|
+
database: config.database,
|
|
3727
|
+
maxConnectionPoolSize: config.maxConnectionPoolSize,
|
|
3728
|
+
connectionTimeout: config.connectionTimeout
|
|
3729
|
+
}).catch((error) => {
|
|
3730
|
+
console.error("Failed to initialize Neo4j connection:", error);
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
const coreTools = [
|
|
3734
|
+
createNeo4jQueryTool(),
|
|
3735
|
+
createNeo4jGetSchemaTool(),
|
|
3736
|
+
createNeo4jFindNodesTool(),
|
|
3737
|
+
createNeo4jTraverseTool(),
|
|
3738
|
+
createNeo4jVectorSearchTool()
|
|
3739
|
+
];
|
|
3740
|
+
if (includeEmbeddingTools) {
|
|
3741
|
+
return [
|
|
3742
|
+
...coreTools,
|
|
3743
|
+
createNeo4jVectorSearchWithEmbeddingTool(),
|
|
3744
|
+
createNeo4jCreateNodeWithEmbeddingTool()
|
|
3745
|
+
];
|
|
3746
|
+
}
|
|
3747
|
+
return coreTools;
|
|
3748
|
+
}
|
|
3749
|
+
async function initializeNeo4jTools() {
|
|
3750
|
+
await initializeFromEnv();
|
|
3751
|
+
try {
|
|
3752
|
+
embeddingManager.initializeFromEnv();
|
|
3753
|
+
} catch (error) {
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
2054
3756
|
var fileReaderSchema = zod.z.object({
|
|
2055
3757
|
path: zod.z.string().describe("Path to the file to read"),
|
|
2056
3758
|
encoding: zod.z.enum(["utf8", "utf-8", "ascii", "base64", "hex", "binary"]).default("utf8").describe("File encoding")
|
|
@@ -3200,7 +4902,7 @@ var AskHumanInputSchema = zod.z.object({
|
|
|
3200
4902
|
suggestions: zod.z.array(zod.z.string()).optional().describe("Suggested responses for the human")
|
|
3201
4903
|
});
|
|
3202
4904
|
var logLevel3 = process.env.LOG_LEVEL?.toLowerCase() || core.LogLevel.INFO;
|
|
3203
|
-
var
|
|
4905
|
+
var logger7 = core.createLogger("askHuman", { level: logLevel3 });
|
|
3204
4906
|
function createAskHumanTool() {
|
|
3205
4907
|
return core.toolBuilder().name("ask-human").description(
|
|
3206
4908
|
"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."
|
|
@@ -3233,13 +4935,13 @@ function createAskHumanTool() {
|
|
|
3233
4935
|
suggestions: validatedInput.suggestions,
|
|
3234
4936
|
status: "pending"
|
|
3235
4937
|
};
|
|
3236
|
-
|
|
4938
|
+
logger7.debug("About to call interrupt()", { humanRequest });
|
|
3237
4939
|
let response;
|
|
3238
4940
|
try {
|
|
3239
4941
|
response = interrupt(humanRequest);
|
|
3240
|
-
|
|
4942
|
+
logger7.debug("interrupt() returned successfully", { response, responseType: typeof response });
|
|
3241
4943
|
} catch (error) {
|
|
3242
|
-
|
|
4944
|
+
logger7.debug("interrupt() threw error (expected for GraphInterrupt)", {
|
|
3243
4945
|
errorType: error?.constructor?.name,
|
|
3244
4946
|
error: error instanceof Error ? error.message : String(error)
|
|
3245
4947
|
});
|
|
@@ -3268,15 +4970,18 @@ exports.AskHumanInputSchema = AskHumanInputSchema;
|
|
|
3268
4970
|
exports.CalculatorSchema = CalculatorSchema;
|
|
3269
4971
|
exports.CreditCardValidatorSchema = CreditCardValidatorSchema;
|
|
3270
4972
|
exports.CurrentDateTimeSchema = CurrentDateTimeSchema;
|
|
4973
|
+
exports.DEFAULT_RETRY_CONFIG = DEFAULT_RETRY_CONFIG2;
|
|
3271
4974
|
exports.DateArithmeticSchema = DateArithmeticSchema;
|
|
3272
4975
|
exports.DateComparisonSchema = DateComparisonSchema;
|
|
3273
4976
|
exports.DateDifferenceSchema = DateDifferenceSchema;
|
|
3274
4977
|
exports.DateFormatterSchema = DateFormatterSchema;
|
|
3275
4978
|
exports.DuckDuckGoProvider = DuckDuckGoProvider;
|
|
3276
4979
|
exports.EmailValidatorSchema = EmailValidatorSchema;
|
|
4980
|
+
exports.EmbeddingManager = EmbeddingManager;
|
|
3277
4981
|
exports.HttpMethod = HttpMethod;
|
|
3278
4982
|
exports.IpValidatorSchema = IpValidatorSchema;
|
|
3279
4983
|
exports.MathFunctionsSchema = MathFunctionsSchema;
|
|
4984
|
+
exports.OpenAIEmbeddingProvider = OpenAIEmbeddingProvider;
|
|
3280
4985
|
exports.PhoneValidatorSchema = PhoneValidatorSchema;
|
|
3281
4986
|
exports.RandomNumberSchema = RandomNumberSchema;
|
|
3282
4987
|
exports.SerperProvider = SerperProvider;
|
|
@@ -3350,6 +5055,14 @@ exports.createJsonTools = createJsonTools;
|
|
|
3350
5055
|
exports.createJsonValidatorTool = createJsonValidatorTool;
|
|
3351
5056
|
exports.createMathFunctionsTool = createMathFunctionsTool;
|
|
3352
5057
|
exports.createMathOperationTools = createMathOperationTools;
|
|
5058
|
+
exports.createNeo4jCreateNodeWithEmbeddingTool = createNeo4jCreateNodeWithEmbeddingTool;
|
|
5059
|
+
exports.createNeo4jFindNodesTool = createNeo4jFindNodesTool;
|
|
5060
|
+
exports.createNeo4jGetSchemaTool = createNeo4jGetSchemaTool;
|
|
5061
|
+
exports.createNeo4jQueryTool = createNeo4jQueryTool;
|
|
5062
|
+
exports.createNeo4jTools = createNeo4jTools;
|
|
5063
|
+
exports.createNeo4jTraverseTool = createNeo4jTraverseTool;
|
|
5064
|
+
exports.createNeo4jVectorSearchTool = createNeo4jVectorSearchTool;
|
|
5065
|
+
exports.createNeo4jVectorSearchWithEmbeddingTool = createNeo4jVectorSearchWithEmbeddingTool;
|
|
3353
5066
|
exports.createObjectOmitTool = createObjectOmitTool;
|
|
3354
5067
|
exports.createObjectPickTool = createObjectPickTool;
|
|
3355
5068
|
exports.createPathBasenameTool = createPathBasenameTool;
|
|
@@ -3410,6 +5123,7 @@ exports.directoryList = directoryList;
|
|
|
3410
5123
|
exports.directoryListSchema = directoryListSchema;
|
|
3411
5124
|
exports.directoryOperationTools = directoryOperationTools;
|
|
3412
5125
|
exports.emailValidator = emailValidator;
|
|
5126
|
+
exports.embeddingManager = embeddingManager;
|
|
3413
5127
|
exports.extractImages = extractImages;
|
|
3414
5128
|
exports.extractImagesSchema = extractImagesSchema;
|
|
3415
5129
|
exports.extractLinks = extractLinks;
|
|
@@ -3427,10 +5141,19 @@ exports.fileSearch = fileSearch;
|
|
|
3427
5141
|
exports.fileSearchSchema = fileSearchSchema;
|
|
3428
5142
|
exports.fileWriter = fileWriter;
|
|
3429
5143
|
exports.fileWriterSchema = fileWriterSchema;
|
|
5144
|
+
exports.generateBatchEmbeddings = generateBatchEmbeddings;
|
|
5145
|
+
exports.generateEmbedding = generateEmbedding;
|
|
5146
|
+
exports.getCohereApiKey = getCohereApiKey;
|
|
3430
5147
|
exports.getConfluencePage = getConfluencePage;
|
|
5148
|
+
exports.getEmbeddingModel = getEmbeddingModel;
|
|
5149
|
+
exports.getEmbeddingProvider = getEmbeddingProvider;
|
|
5150
|
+
exports.getHuggingFaceApiKey = getHuggingFaceApiKey;
|
|
5151
|
+
exports.getOllamaBaseUrl = getOllamaBaseUrl;
|
|
5152
|
+
exports.getOpenAIApiKey = getOpenAIApiKey;
|
|
3431
5153
|
exports.getSlackChannels = getSlackChannels;
|
|
3432
5154
|
exports.getSlackMessages = getSlackMessages;
|
|
3433
5155
|
exports.getSpacePages = getSpacePages;
|
|
5156
|
+
exports.getVoyageApiKey = getVoyageApiKey;
|
|
3434
5157
|
exports.htmlParser = htmlParser;
|
|
3435
5158
|
exports.htmlParserSchema = htmlParserSchema;
|
|
3436
5159
|
exports.htmlParserTools = htmlParserTools;
|
|
@@ -3441,7 +5164,12 @@ exports.httpPost = httpPost;
|
|
|
3441
5164
|
exports.httpPostSchema = httpPostSchema;
|
|
3442
5165
|
exports.httpRequestSchema = httpRequestSchema;
|
|
3443
5166
|
exports.httpTools = httpTools;
|
|
5167
|
+
exports.initializeEmbeddings = initializeEmbeddings;
|
|
5168
|
+
exports.initializeEmbeddingsWithConfig = initializeEmbeddingsWithConfig;
|
|
5169
|
+
exports.initializeFromEnv = initializeFromEnv;
|
|
5170
|
+
exports.initializeNeo4jTools = initializeNeo4jTools;
|
|
3444
5171
|
exports.ipValidator = ipValidator;
|
|
5172
|
+
exports.isRetryableError = isRetryableError2;
|
|
3445
5173
|
exports.jsonMerge = jsonMerge;
|
|
3446
5174
|
exports.jsonMergeSchema = jsonMergeSchema;
|
|
3447
5175
|
exports.jsonParser = jsonParser;
|
|
@@ -3460,6 +5188,23 @@ exports.jsonValidatorSchema = jsonValidatorSchema;
|
|
|
3460
5188
|
exports.listConfluenceSpaces = listConfluenceSpaces;
|
|
3461
5189
|
exports.mathFunctions = mathFunctions;
|
|
3462
5190
|
exports.mathOperationTools = mathOperationTools;
|
|
5191
|
+
exports.neo4jCoreTools = neo4jCoreTools;
|
|
5192
|
+
exports.neo4jCreateNodeWithEmbedding = neo4jCreateNodeWithEmbedding;
|
|
5193
|
+
exports.neo4jCreateNodeWithEmbeddingSchema = neo4jCreateNodeWithEmbeddingSchema;
|
|
5194
|
+
exports.neo4jFindNodes = neo4jFindNodes;
|
|
5195
|
+
exports.neo4jFindNodesSchema = neo4jFindNodesSchema;
|
|
5196
|
+
exports.neo4jGetSchema = neo4jGetSchema;
|
|
5197
|
+
exports.neo4jGetSchemaSchema = neo4jGetSchemaSchema;
|
|
5198
|
+
exports.neo4jPool = neo4jPool;
|
|
5199
|
+
exports.neo4jQuery = neo4jQuery;
|
|
5200
|
+
exports.neo4jQuerySchema = neo4jQuerySchema;
|
|
5201
|
+
exports.neo4jTools = neo4jTools;
|
|
5202
|
+
exports.neo4jTraverse = neo4jTraverse;
|
|
5203
|
+
exports.neo4jTraverseSchema = neo4jTraverseSchema;
|
|
5204
|
+
exports.neo4jVectorSearch = neo4jVectorSearch;
|
|
5205
|
+
exports.neo4jVectorSearchSchema = neo4jVectorSearchSchema;
|
|
5206
|
+
exports.neo4jVectorSearchWithEmbedding = neo4jVectorSearchWithEmbedding;
|
|
5207
|
+
exports.neo4jVectorSearchWithEmbeddingSchema = neo4jVectorSearchWithEmbeddingSchema;
|
|
3463
5208
|
exports.notifySlack = notifySlack;
|
|
3464
5209
|
exports.objectOmit = objectOmit;
|
|
3465
5210
|
exports.objectOmitSchema = objectOmitSchema;
|
|
@@ -3484,6 +5229,7 @@ exports.pathResolveSchema = pathResolveSchema;
|
|
|
3484
5229
|
exports.pathUtilityTools = pathUtilityTools;
|
|
3485
5230
|
exports.phoneValidator = phoneValidator;
|
|
3486
5231
|
exports.randomNumber = randomNumber;
|
|
5232
|
+
exports.retryWithBackoff = retryWithBackoff2;
|
|
3487
5233
|
exports.scraperTools = scraperTools;
|
|
3488
5234
|
exports.searchConfluence = searchConfluence;
|
|
3489
5235
|
exports.searchResultSchema = searchResultSchema;
|
|
@@ -3509,6 +5255,8 @@ exports.urlValidatorSchema = urlValidatorSchema;
|
|
|
3509
5255
|
exports.urlValidatorSimple = urlValidatorSimple;
|
|
3510
5256
|
exports.urlValidatorTools = urlValidatorTools;
|
|
3511
5257
|
exports.uuidValidator = uuidValidator;
|
|
5258
|
+
exports.validateBatch = validateBatch;
|
|
5259
|
+
exports.validateText = validateText;
|
|
3512
5260
|
exports.validationTools = validationTools;
|
|
3513
5261
|
exports.webScraper = webScraper;
|
|
3514
5262
|
exports.webScraperSchema = webScraperSchema;
|