@templmf/temp-solf-lmf 0.0.140 → 0.0.141

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.
@@ -1,151 +0,0 @@
1
- {
2
- "tools": [
3
- {
4
- "name": "get_page_by_id",
5
- "description": "根据页面 ID 获取 Confluence 页面内容",
6
- "parameters": [
7
- {
8
- "name": "pageId",
9
- "type": "string",
10
- "required": true,
11
- "description": "Confluence 页面 ID"
12
- }
13
- ]
14
- },
15
- {
16
- "name": "get_page_by_title",
17
- "description": "根据标题查找页面",
18
- "parameters": [
19
- {
20
- "name": "title",
21
- "type": "string",
22
- "required": true,
23
- "description": "要查找的页面标题"
24
- },
25
- {
26
- "name": "space",
27
- "type": "string",
28
- "required": false,
29
- "description": "空间标识,缩小搜索范围(如 DEV, OPS)"
30
- }
31
- ]
32
- },
33
- {
34
- "name": "get_spaces",
35
- "description": "列出所有空间",
36
- "parameters": []
37
- },
38
- {
39
- "name": "search_pages",
40
- "description": "按关键词、空间和贡献者搜索页面",
41
- "parameters": [
42
- {
43
- "name": "keywords",
44
- "type": "array<string>",
45
- "required": true,
46
- "description": "要在页面内容中搜索的关键词"
47
- },
48
- {
49
- "name": "spaces",
50
- "type": "array<string>",
51
- "required": false,
52
- "description": "按空间标识过滤"
53
- },
54
- {
55
- "name": "contributors",
56
- "type": "array<string>",
57
- "required": false,
58
- "description": "按贡献者用户名过滤"
59
- },
60
- {
61
- "name": "limit",
62
- "type": "number",
63
- "required": false,
64
- "default": 10,
65
- "description": "最大返回结果数"
66
- }
67
- ]
68
- },
69
- {
70
- "name": "get_children",
71
- "description": "获取指定页面的子页面",
72
- "parameters": [
73
- {
74
- "name": "pageId",
75
- "type": "string",
76
- "required": true,
77
- "description": "父页面 ID"
78
- }
79
- ]
80
- },
81
- {
82
- "name": "get_page_tree",
83
- "description": "递归获取页面树",
84
- "parameters": [
85
- {
86
- "name": "rootPageId",
87
- "type": "string",
88
- "required": true,
89
- "description": "根页面 ID"
90
- },
91
- {
92
- "name": "depth",
93
- "type": "number",
94
- "required": false,
95
- "default": 2,
96
- "description": "递归深度"
97
- }
98
- ]
99
- },
100
- {
101
- "name": "ask_confluence",
102
- "description": "使用混合搜索 + BM25 重排序检索 Confluence 相关内容",
103
- "parameters": [
104
- {
105
- "name": "question",
106
- "type": "string",
107
- "required": true,
108
- "description": "自然语言问题,用于搜索 Confluence"
109
- },
110
- {
111
- "name": "spaces",
112
- "type": "array<string>",
113
- "required": false,
114
- "description": "按空间标识过滤"
115
- },
116
- {
117
- "name": "contributors",
118
- "type": "array<string>",
119
- "required": false,
120
- "description": "按贡献者用户名过滤"
121
- },
122
- {
123
- "name": "maxPages",
124
- "type": "number",
125
- "required": false,
126
- "default": 10,
127
- "description": "在分块前最多获取的页面数"
128
- },
129
- {
130
- "name": "maxChunks",
131
- "type": "number",
132
- "required": false,
133
- "default": 15,
134
- "description": "BM25 重排序后返回的最大文本块数"
135
- }
136
- ]
137
- },
138
- {
139
- "name": "explore_confluence",
140
- "description": "探索与主题相关的页面及其子页面",
141
- "parameters": [
142
- {
143
- "name": "topic",
144
- "type": "string",
145
- "required": true,
146
- "description": "要探索的主题关键词"
147
- }
148
- ]
149
- }
150
- ]
151
- }
@@ -1,324 +0,0 @@
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
- }