@dreki-gg/pi-doc-search 0.2.0
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/.github/workflows/release.yml +46 -0
- package/.release-please-manifest.json +3 -0
- package/CHANGELOG.md +82 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/bun.lock +382 -0
- package/extensions/doc-search/README.md +50 -0
- package/extensions/doc-search/api.ts +143 -0
- package/extensions/doc-search/cache.ts +415 -0
- package/extensions/doc-search/config.example.json +7 -0
- package/extensions/doc-search/config.ts +52 -0
- package/extensions/doc-search/curation.ts +192 -0
- package/extensions/doc-search/index.ts +6 -0
- package/extensions/doc-search/tools.ts +615 -0
- package/extensions/doc-search/types.ts +142 -0
- package/package.json +50 -0
- package/release-please-config.json +9 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { CuratedDocResult } from './types';
|
|
2
|
+
|
|
3
|
+
const MAX_CURATED_CHARS = 12_000;
|
|
4
|
+
const STOPWORDS = new Set([
|
|
5
|
+
'the',
|
|
6
|
+
'and',
|
|
7
|
+
'for',
|
|
8
|
+
'with',
|
|
9
|
+
'that',
|
|
10
|
+
'this',
|
|
11
|
+
'from',
|
|
12
|
+
'into',
|
|
13
|
+
'your',
|
|
14
|
+
'their',
|
|
15
|
+
'about',
|
|
16
|
+
'using',
|
|
17
|
+
'use',
|
|
18
|
+
'how',
|
|
19
|
+
'what',
|
|
20
|
+
'when',
|
|
21
|
+
'where',
|
|
22
|
+
'why',
|
|
23
|
+
'are',
|
|
24
|
+
'was',
|
|
25
|
+
'were',
|
|
26
|
+
'can',
|
|
27
|
+
'you',
|
|
28
|
+
'need',
|
|
29
|
+
'docs',
|
|
30
|
+
'doc',
|
|
31
|
+
'library',
|
|
32
|
+
'page',
|
|
33
|
+
'requested',
|
|
34
|
+
'focus',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
interface Section {
|
|
38
|
+
text: string;
|
|
39
|
+
score: number;
|
|
40
|
+
index: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeWhitespace(text: string): string {
|
|
44
|
+
return text
|
|
45
|
+
.replace(/\r\n/g, '\n')
|
|
46
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
47
|
+
.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extractKeywords(query?: string, topic?: string): string[] {
|
|
51
|
+
const source = `${query ?? ''} ${topic ?? ''}`.toLowerCase();
|
|
52
|
+
const words = source.match(/[a-z0-9._/-]{3,}/g) ?? [];
|
|
53
|
+
const unique = new Set<string>();
|
|
54
|
+
|
|
55
|
+
for (const word of words) {
|
|
56
|
+
if (STOPWORDS.has(word)) continue;
|
|
57
|
+
unique.add(word);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Array.from(unique).slice(0, 12);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function splitIntoSections(text: string): string[] {
|
|
64
|
+
const normalized = normalizeWhitespace(text);
|
|
65
|
+
if (!normalized) return [];
|
|
66
|
+
|
|
67
|
+
const lines = normalized.split('\n');
|
|
68
|
+
const sections: string[] = [];
|
|
69
|
+
let buffer: string[] = [];
|
|
70
|
+
|
|
71
|
+
const flush = () => {
|
|
72
|
+
const chunk = buffer.join('\n').trim();
|
|
73
|
+
if (chunk) sections.push(chunk);
|
|
74
|
+
buffer = [];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
const isHeading =
|
|
79
|
+
/^#{1,6}\s+/.test(line) || (/^[A-Z][^\n]{0,100}:$/.test(line) && line.length < 100);
|
|
80
|
+
if (isHeading && buffer.length > 0) flush();
|
|
81
|
+
buffer.push(line);
|
|
82
|
+
if (!line.trim()) flush();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
flush();
|
|
86
|
+
return sections.filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function scoreSection(section: string, keywords: string[], index: number): number {
|
|
90
|
+
if (keywords.length === 0) return Math.max(0, 10 - index);
|
|
91
|
+
|
|
92
|
+
const lower = section.toLowerCase();
|
|
93
|
+
const lines = section.split('\n');
|
|
94
|
+
const heading = lines[0]?.toLowerCase() ?? '';
|
|
95
|
+
|
|
96
|
+
let score = Math.max(0, 8 - index);
|
|
97
|
+
for (const keyword of keywords) {
|
|
98
|
+
const occurrences = lower.split(keyword).length - 1;
|
|
99
|
+
if (occurrences > 0) score += occurrences * 4;
|
|
100
|
+
if (heading.includes(keyword)) score += 6;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return score;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function chooseSections(text: string, query?: string, topic?: string): Section[] {
|
|
107
|
+
const keywords = extractKeywords(query, topic);
|
|
108
|
+
const sections = splitIntoSections(text).map((section, index) => ({
|
|
109
|
+
text: section,
|
|
110
|
+
score: scoreSection(section, keywords, index),
|
|
111
|
+
index,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
if (sections.length === 0) return [];
|
|
115
|
+
|
|
116
|
+
const highSignal = sections
|
|
117
|
+
.filter((section) => section.score > 8)
|
|
118
|
+
.sort((a, b) => b.score - a.score || a.index - b.index);
|
|
119
|
+
const picked = (
|
|
120
|
+
highSignal.length > 0 ? highSignal : sections.slice().sort((a, b) => a.index - b.index)
|
|
121
|
+
).slice(0, 12);
|
|
122
|
+
|
|
123
|
+
return picked.sort((a, b) => a.index - b.index);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function curateDocText(input: {
|
|
127
|
+
rawText: string;
|
|
128
|
+
libraryId: string;
|
|
129
|
+
libraryName: string;
|
|
130
|
+
libraryVersion?: string;
|
|
131
|
+
query?: string;
|
|
132
|
+
topic?: string;
|
|
133
|
+
page: number;
|
|
134
|
+
docRef: string;
|
|
135
|
+
}): CuratedDocResult {
|
|
136
|
+
const sections = chooseSections(input.rawText, input.query, input.topic);
|
|
137
|
+
|
|
138
|
+
const headerLines = [
|
|
139
|
+
`Library: ${input.libraryName}`,
|
|
140
|
+
`Library ID: ${input.libraryId}`,
|
|
141
|
+
input.libraryVersion ? `Version: ${input.libraryVersion}` : undefined,
|
|
142
|
+
input.query ? `Query: ${input.query}` : undefined,
|
|
143
|
+
input.topic ? `Topic: ${input.topic}` : undefined,
|
|
144
|
+
`Page: ${input.page}`,
|
|
145
|
+
'',
|
|
146
|
+
'Relevant documentation:',
|
|
147
|
+
'',
|
|
148
|
+
].filter(Boolean) as string[];
|
|
149
|
+
|
|
150
|
+
const footerLines = ['', `Raw cached document available via docRef: ${input.docRef}`];
|
|
151
|
+
|
|
152
|
+
let selectedSectionCount = 0;
|
|
153
|
+
let body = '';
|
|
154
|
+
let truncated = false;
|
|
155
|
+
const budget = Math.max(
|
|
156
|
+
2_000,
|
|
157
|
+
MAX_CURATED_CHARS - headerLines.join('\n').length - footerLines.join('\n').length,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
for (const section of sections) {
|
|
161
|
+
const next = body ? `${body}\n\n${section.text}` : section.text;
|
|
162
|
+
if (next.length > budget) {
|
|
163
|
+
if (!body) {
|
|
164
|
+
body = section.text.slice(0, budget);
|
|
165
|
+
truncated = true;
|
|
166
|
+
selectedSectionCount = 1;
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
body = next;
|
|
171
|
+
selectedSectionCount += 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!body) {
|
|
175
|
+
body = normalizeWhitespace(input.rawText).slice(0, budget);
|
|
176
|
+
truncated = normalizeWhitespace(input.rawText).length > budget;
|
|
177
|
+
selectedSectionCount = body ? 1 : 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!truncated && normalizeWhitespace(input.rawText).length > body.length) {
|
|
181
|
+
truncated = body.length < normalizeWhitespace(input.rawText).length;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const text = `${headerLines.join('\n')}${body}${footerLines.join('\n')}`.trim();
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
text,
|
|
188
|
+
truncated,
|
|
189
|
+
selectedSectionCount,
|
|
190
|
+
rawLength: normalizeWhitespace(input.rawText).length,
|
|
191
|
+
};
|
|
192
|
+
}
|