@templmf/temp-solf-lmf 0.0.137 → 0.0.139
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/confMCP.js +551 -0
- package/confluenceTools.json +151 -0
- package/langgraphConfluenceTools.js +324 -0
- package/package.json +1 -1
- package/.tiktoken_cache/gpt2.json +0 -1
- package/demo.js +0 -77
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { DynamicStructuredTool } from "@langchain/core/tools";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import { htmlToText } from "html-to-text";
|
|
5
|
+
import BM25 from "wink-bm25-text-search";
|
|
6
|
+
import LRUCache from "lru-cache";
|
|
7
|
+
import express from "express";
|
|
8
|
+
|
|
9
|
+
export class ConfluenceClient {
|
|
10
|
+
constructor(baseURL, authorization) {
|
|
11
|
+
if (!baseURL || !authorization) {
|
|
12
|
+
throw new Error("CONFLUENCE_BASE_URL and CONFLUENCE_AUTHORIZATION are required");
|
|
13
|
+
}
|
|
14
|
+
this.baseURL = baseURL;
|
|
15
|
+
this.client = axios.create({
|
|
16
|
+
baseURL,
|
|
17
|
+
timeout: 30000,
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: authorization,
|
|
20
|
+
Accept: "application/json"
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
this.pageCache = new LRUCache({ max: 100, ttl: 1000 * 60 * 10 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
stripHtml(html = "") {
|
|
27
|
+
return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
buildCql({ keywords = [], spaces = [], contributors = [] }) {
|
|
31
|
+
const clauses = [];
|
|
32
|
+
if (spaces.length) {
|
|
33
|
+
clauses.push("(" + spaces.map((v) => `space="${v}"`).join(" OR ") + ")");
|
|
34
|
+
}
|
|
35
|
+
if (contributors.length) {
|
|
36
|
+
clauses.push("(" + contributors.map((v) => `contributor="${v}"`).join(" OR ") + ")");
|
|
37
|
+
}
|
|
38
|
+
if (keywords.length) {
|
|
39
|
+
clauses.push("(" + keywords.map((v) => `text~"${v}"`).join(" OR ") + ")");
|
|
40
|
+
}
|
|
41
|
+
return clauses.join(" AND ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getPageById(pageId) {
|
|
45
|
+
const cached = this.pageCache.get(pageId);
|
|
46
|
+
if (cached) return cached;
|
|
47
|
+
|
|
48
|
+
const { data } = await this.client.get(`/rest/api/content/${pageId}`, {
|
|
49
|
+
params: { expand: "body.storage,version,space" }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = {
|
|
53
|
+
id: data.id,
|
|
54
|
+
title: data.title,
|
|
55
|
+
space: data.space?.key,
|
|
56
|
+
version: data.version?.number,
|
|
57
|
+
url: `${this.baseURL}${data._links?.webui}`,
|
|
58
|
+
content: data.body?.storage?.value || ""
|
|
59
|
+
};
|
|
60
|
+
this.pageCache.set(pageId, result);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getPageByTitle(title, space) {
|
|
65
|
+
const params = { title, expand: "body.storage" };
|
|
66
|
+
if (space) params.spaceKey = space;
|
|
67
|
+
|
|
68
|
+
const { data } = await this.client.get("/rest/api/content", { params });
|
|
69
|
+
const page = data.results?.[0];
|
|
70
|
+
if (!page) return null;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
id: page.id,
|
|
74
|
+
title: page.title,
|
|
75
|
+
url: `${this.baseURL}${page._links?.webui}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getSpaces() {
|
|
80
|
+
const { data } = await this.client.get("/rest/api/space");
|
|
81
|
+
return data.results.map((v) => ({ key: v.key, name: v.name }));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async searchPages({ keywords, spaces = [], contributors = [], limit = 10 }) {
|
|
85
|
+
const cql = this.buildCql({ keywords, spaces, contributors });
|
|
86
|
+
const { data } = await this.client.get("/rest/api/search", {
|
|
87
|
+
params: { cql, limit }
|
|
88
|
+
});
|
|
89
|
+
return data.results.map((item) => ({
|
|
90
|
+
pageId: item.content?.id,
|
|
91
|
+
title: item.title,
|
|
92
|
+
url: `${this.baseURL}${item.url}`
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getChildren(pageId) {
|
|
97
|
+
const { data } = await this.client.get(`/rest/api/content/${pageId}/child/page`);
|
|
98
|
+
return data.results.map((v) => ({ id: v.id, title: v.title }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async buildTree(pageId, depth) {
|
|
102
|
+
const page = await this.getPageById(pageId);
|
|
103
|
+
if (depth <= 0) return { id: page.id, title: page.title };
|
|
104
|
+
|
|
105
|
+
const children = await this.getChildren(pageId);
|
|
106
|
+
return {
|
|
107
|
+
id: page.id,
|
|
108
|
+
title: page.title,
|
|
109
|
+
children: await Promise.all(children.map((v) => this.buildTree(v.id, depth - 1)))
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
chunkText(text, size = 1200, overlap = 200) {
|
|
114
|
+
const chunks = [];
|
|
115
|
+
let start = 0;
|
|
116
|
+
while (start < text.length) {
|
|
117
|
+
chunks.push(text.slice(start, start + size));
|
|
118
|
+
start += size - overlap;
|
|
119
|
+
}
|
|
120
|
+
return chunks;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
buildQueries(question) {
|
|
124
|
+
const queries = [question];
|
|
125
|
+
queries.push(
|
|
126
|
+
...question.split(/[ ,,、]/).map((v) => v.trim()).filter(Boolean)
|
|
127
|
+
);
|
|
128
|
+
return [...new Set(queries)];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async hybridSearch({ question, spaces, contributors, maxPages = 10 }) {
|
|
132
|
+
const queries = this.buildQueries(question);
|
|
133
|
+
const weights = [1, 0.8, 0.6, 0.4, 0.2];
|
|
134
|
+
const scoreMap = new Map();
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < queries.length; i++) {
|
|
137
|
+
const pages = await this.searchPages({
|
|
138
|
+
keywords: [queries[i]],
|
|
139
|
+
spaces,
|
|
140
|
+
contributors,
|
|
141
|
+
limit: 20
|
|
142
|
+
});
|
|
143
|
+
for (const page of pages) {
|
|
144
|
+
const old = scoreMap.get(page.pageId) || { ...page, score: 0 };
|
|
145
|
+
old.score += weights[i] || 0.1;
|
|
146
|
+
scoreMap.set(page.pageId, old);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return [...scoreMap.values()]
|
|
151
|
+
.sort((a, b) => b.score - a.score)
|
|
152
|
+
.slice(0, maxPages);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async askConfluence({ question, spaces, contributors, maxPages = 10, maxChunks = 15 }) {
|
|
156
|
+
const pages = await this.hybridSearch({ question, spaces, contributors, maxPages });
|
|
157
|
+
const documents = [];
|
|
158
|
+
|
|
159
|
+
for (const page of pages) {
|
|
160
|
+
const detail = await this.getPageById(page.pageId);
|
|
161
|
+
const text = htmlToText(detail.content, { wordwrap: false });
|
|
162
|
+
const chunks = this.chunkText(text);
|
|
163
|
+
chunks.forEach((chunk) => {
|
|
164
|
+
documents.push({ pageId: detail.id, title: detail.title, url: detail.url, chunk });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const engine = BM25();
|
|
169
|
+
engine.defineConfig({ fldWeights: { chunk: 1 } });
|
|
170
|
+
engine.definePrepTasks([]);
|
|
171
|
+
engine.defineField("chunk");
|
|
172
|
+
engine.defineRef("id");
|
|
173
|
+
engine.configure();
|
|
174
|
+
documents.forEach((doc, index) => engine.addDoc(index, { chunk: doc.chunk }));
|
|
175
|
+
engine.consolidate();
|
|
176
|
+
|
|
177
|
+
return engine.search(question, maxChunks).map((v) => {
|
|
178
|
+
const doc = documents[v[0]];
|
|
179
|
+
return { title: doc.title, url: doc.url, score: v[1], content: doc.chunk };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async exploreConfluence(topic) {
|
|
184
|
+
const pages = await this.searchPages({ keywords: [topic], limit: 5 });
|
|
185
|
+
const result = [];
|
|
186
|
+
for (const page of pages) {
|
|
187
|
+
const children = await this.getChildren(page.pageId);
|
|
188
|
+
result.push({ page, children });
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function createConfluenceTools(baseURL, authorization) {
|
|
195
|
+
const client = new ConfluenceClient(baseURL, authorization);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
getPageById: new DynamicStructuredTool({
|
|
199
|
+
name: "get_page_by_id",
|
|
200
|
+
description: "Get Confluence page content by page id",
|
|
201
|
+
schema: z.object({ pageId: z.string() }),
|
|
202
|
+
func: async ({ pageId }) => JSON.stringify(await client.getPageById(pageId))
|
|
203
|
+
}),
|
|
204
|
+
|
|
205
|
+
getPageByTitle: new DynamicStructuredTool({
|
|
206
|
+
name: "get_page_by_title",
|
|
207
|
+
description: "Find page by title",
|
|
208
|
+
schema: z.object({
|
|
209
|
+
title: z.string(),
|
|
210
|
+
space: z.string().optional()
|
|
211
|
+
}),
|
|
212
|
+
func: async ({ title, space }) => JSON.stringify(await client.getPageByTitle(title, space))
|
|
213
|
+
}),
|
|
214
|
+
|
|
215
|
+
getSpaces: new DynamicStructuredTool({
|
|
216
|
+
name: "get_spaces",
|
|
217
|
+
description: "List all spaces",
|
|
218
|
+
schema: z.object({}),
|
|
219
|
+
func: async () => JSON.stringify(await client.getSpaces())
|
|
220
|
+
}),
|
|
221
|
+
|
|
222
|
+
searchPages: new DynamicStructuredTool({
|
|
223
|
+
name: "search_pages",
|
|
224
|
+
description: "Search pages by keywords, spaces and contributors",
|
|
225
|
+
schema: z.object({
|
|
226
|
+
keywords: z.array(z.string()).min(1),
|
|
227
|
+
spaces: z.array(z.string()).optional(),
|
|
228
|
+
contributors: z.array(z.string()).optional(),
|
|
229
|
+
limit: z.number().default(10)
|
|
230
|
+
}),
|
|
231
|
+
func: async (args) => JSON.stringify(await client.searchPages(args))
|
|
232
|
+
}),
|
|
233
|
+
|
|
234
|
+
getChildren: new DynamicStructuredTool({
|
|
235
|
+
name: "get_children",
|
|
236
|
+
description: "Get child pages of a given page",
|
|
237
|
+
schema: z.object({ pageId: z.string() }),
|
|
238
|
+
func: async ({ pageId }) => JSON.stringify(await client.getChildren(pageId))
|
|
239
|
+
}),
|
|
240
|
+
|
|
241
|
+
getPageTree: new DynamicStructuredTool({
|
|
242
|
+
name: "get_page_tree",
|
|
243
|
+
description: "Get page tree recursively",
|
|
244
|
+
schema: z.object({
|
|
245
|
+
rootPageId: z.string(),
|
|
246
|
+
depth: z.number().default(2)
|
|
247
|
+
}),
|
|
248
|
+
func: async ({ rootPageId, depth }) => JSON.stringify(await client.buildTree(rootPageId, depth))
|
|
249
|
+
}),
|
|
250
|
+
|
|
251
|
+
askConfluence: new DynamicStructuredTool({
|
|
252
|
+
name: "ask_confluence",
|
|
253
|
+
description: "Retrieve relevant contexts from Confluence using hybrid search + BM25 reranking",
|
|
254
|
+
schema: z.object({
|
|
255
|
+
question: z.string(),
|
|
256
|
+
spaces: z.array(z.string()).optional(),
|
|
257
|
+
contributors: z.array(z.string()).optional(),
|
|
258
|
+
maxPages: z.number().default(10),
|
|
259
|
+
maxChunks: z.number().default(15)
|
|
260
|
+
}),
|
|
261
|
+
func: async (args) => JSON.stringify(await client.askConfluence(args))
|
|
262
|
+
}),
|
|
263
|
+
|
|
264
|
+
exploreConfluence: new DynamicStructuredTool({
|
|
265
|
+
name: "explore_confluence",
|
|
266
|
+
description: "Explore topic related pages and their children",
|
|
267
|
+
schema: z.object({ topic: z.string() }),
|
|
268
|
+
func: async ({ topic }) => JSON.stringify(await client.exploreConfluence(topic))
|
|
269
|
+
})
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function startServer(port = 3000) {
|
|
274
|
+
const app = express();
|
|
275
|
+
app.use(express.json());
|
|
276
|
+
|
|
277
|
+
app.post("/tools/:toolName", async (req, res) => {
|
|
278
|
+
const { baseURL, authorization, ...args } = req.body;
|
|
279
|
+
|
|
280
|
+
if (!baseURL || !authorization) {
|
|
281
|
+
return res.status(400).json({
|
|
282
|
+
error: "baseURL and authorization are required in request body"
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const tools = createConfluenceTools(baseURL, authorization);
|
|
288
|
+
const tool = tools[req.params.toolName];
|
|
289
|
+
if (!tool) {
|
|
290
|
+
return res.status(404).json({
|
|
291
|
+
error: `Tool "${req.params.toolName}" not found`,
|
|
292
|
+
availableTools: Object.keys(tools)
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
const result = await tool.invoke(args);
|
|
296
|
+
res.json({ result: JSON.parse(result) });
|
|
297
|
+
} catch (err) {
|
|
298
|
+
res.status(500).json({ error: err.message });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
app.get("/tools", (_req, res) => {
|
|
303
|
+
res.json({
|
|
304
|
+
availableTools: Object.keys(createConfluenceTools("http://dummy", "dummy"))
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
app.listen(port, () => {
|
|
309
|
+
console.log(`Confluence tools HTTP server running on http://localhost:${port}`);
|
|
310
|
+
console.log(`Usage: curl -X POST http://localhost:${port}/tools/get_spaces \\`);
|
|
311
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
312
|
+
console.log(` -d '{"baseURL":"<base>","authorization":"Bearer <token>"}'`);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const isMain = process.argv[1] && (
|
|
317
|
+
process.argv[1] === new URL(import.meta.url).pathname ||
|
|
318
|
+
process.argv[1].endsWith("/langgraphConfluenceTools.js")
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (isMain) {
|
|
322
|
+
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
|
323
|
+
startServer(port);
|
|
324
|
+
}
|