doc2vec 2.0.0 → 2.3.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/README.md +64 -5
- package/dist/code-chunker.js +179 -0
- package/dist/content-processor.js +469 -83
- package/dist/database.js +197 -7
- package/dist/doc2vec.js +474 -4
- package/dist/vitest.config.js +16 -0
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -22,13 +22,16 @@ The primary goal is to prepare documentation content for Retrieval-Augmented Gen
|
|
|
22
22
|
* **Local Directory Processing:** Scans local directories for files, converts content to searchable chunks.
|
|
23
23
|
* **PDF Support:** Automatically extracts text from PDF files and converts them to Markdown format using Mozilla's PDF.js.
|
|
24
24
|
* **Word Document Support:** Processes both legacy `.doc` and modern `.docx` files, extracting text and formatting.
|
|
25
|
+
* **Code Source Processing:** Ingests code from local directories or GitHub repositories using Chonkie code chunking.
|
|
26
|
+
* **AST-aware Chunking:** Uses Chonkie-based code chunking with Tree-sitter to preserve code structure.
|
|
27
|
+
* **Repository Support:** Clones GitHub repos for code ingestion and maps files to GitHub URLs.
|
|
25
28
|
* **Content Extraction:** Uses Puppeteer for rendering JavaScript-heavy pages and `@mozilla/readability` to extract the main article content.
|
|
26
29
|
* **Smart H1 Preservation:** Automatically extracts and preserves page titles (H1 headings) that Readability might strip as "page chrome", ensuring proper heading hierarchy.
|
|
27
30
|
* **Flexible Content Selectors:** Supports multiple content container patterns (`.docs-content`, `.doc-content`, `.markdown-body`, `article`, etc.) for better compatibility with various documentation sites.
|
|
28
31
|
* **HTML to Markdown:** Converts extracted HTML to clean Markdown using `turndown`, preserving code blocks and basic formatting.
|
|
29
32
|
* **Clean Heading Text:** Automatically removes anchor links (like `[](#section-id)`) from heading text for cleaner hierarchy display.
|
|
30
33
|
* **Intelligent Chunking:** Splits Markdown content into manageable chunks based on headings and token limits, preserving context.
|
|
31
|
-
* **Vector Embeddings:** Generates embeddings for each chunk using OpenAI
|
|
34
|
+
* **Vector Embeddings:** Generates embeddings for each chunk using OpenAI or Azure OpenAI (configurable).
|
|
32
35
|
* **Vector Storage:** Supports storing chunks, metadata, and embeddings in:
|
|
33
36
|
* **SQLite:** Using `better-sqlite3` and the `sqlite-vec` extension for efficient vector search.
|
|
34
37
|
* **Qdrant:** A dedicated vector database, using the `@qdrant/js-client-rest`.
|
|
@@ -95,7 +98,7 @@ This ensures that searches for parent topics (like "Installation") will also mat
|
|
|
95
98
|
* **Node.js:** Version 18 or higher recommended (check `.nvmrc` if available).
|
|
96
99
|
* **npm:** Node Package Manager (usually comes with Node.js).
|
|
97
100
|
* **TypeScript:** As the project is written in TypeScript (`ts-node` is used for execution via `npm start`).
|
|
98
|
-
* **OpenAI API Key:** You need an API key
|
|
101
|
+
* **OpenAI API Key or Azure OpenAI Credentials:** You need either an OpenAI API key or Azure OpenAI credentials to generate embeddings.
|
|
99
102
|
* **GitHub Personal Access Token:** Required for accessing GitHub issues (set as `GITHUB_PERSONAL_ACCESS_TOKEN` in your environment).
|
|
100
103
|
* **Zendesk API Token:** Required for accessing Zendesk tickets and articles (set as `ZENDESK_API_TOKEN` in your environment).
|
|
101
104
|
* **(Optional) Qdrant Instance:** If using the `qdrant` database type, you need a running Qdrant instance accessible from where you run the script.
|
|
@@ -126,8 +129,20 @@ Configuration is managed through two files:
|
|
|
126
129
|
```dotenv
|
|
127
130
|
# .env
|
|
128
131
|
|
|
129
|
-
#
|
|
132
|
+
# Embedding Provider Configuration
|
|
133
|
+
# Optional: Specify which provider to use (defaults to 'openai' if not set)
|
|
134
|
+
# Can also be configured in config.yaml
|
|
135
|
+
EMBEDDING_PROVIDER="azure" # or "openai"
|
|
136
|
+
|
|
137
|
+
# Required: Your OpenAI API Key (if using OpenAI provider)
|
|
130
138
|
OPENAI_API_KEY="sk-..."
|
|
139
|
+
OPENAI_MODEL="text-embedding-3-large" # Optional, defaults to text-embedding-3-large
|
|
140
|
+
|
|
141
|
+
# Required: Your Azure OpenAI credentials (if using Azure provider)
|
|
142
|
+
AZURE_OPENAI_KEY="your-azure-key"
|
|
143
|
+
AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com"
|
|
144
|
+
AZURE_OPENAI_DEPLOYMENT_NAME="text-embedding-3-large"
|
|
145
|
+
AZURE_OPENAI_API_VERSION="2024-10-21"
|
|
131
146
|
|
|
132
147
|
# Required for GitHub sources
|
|
133
148
|
GITHUB_PERSONAL_ACCESS_TOKEN="ghp_..."
|
|
@@ -145,7 +160,7 @@ Configuration is managed through two files:
|
|
|
145
160
|
**Structure:**
|
|
146
161
|
|
|
147
162
|
* `sources`: An array of source configurations.
|
|
148
|
-
* `type`: Either `'website'`, `'github'`, `'local_directory'`, or `'zendesk'`
|
|
163
|
+
* `type`: Either `'website'`, `'github'`, `'local_directory'`, `'code'`, or `'zendesk'`
|
|
149
164
|
|
|
150
165
|
For websites (`type: 'website'`):
|
|
151
166
|
* `url`: The starting URL for crawling the documentation site.
|
|
@@ -162,6 +177,20 @@ Configuration is managed through two files:
|
|
|
162
177
|
* `recursive`: (Optional) Whether to traverse subdirectories (defaults to `true`).
|
|
163
178
|
* `url_rewrite_prefix` (Optional) URL prefix to rewrite `file://` URLs (e.g., `https://mydomain.com`)
|
|
164
179
|
* `encoding`: (Optional) File encoding to use (defaults to `'utf8'`). Note: PDF files are processed as binary and this setting doesn't apply to them.
|
|
180
|
+
|
|
181
|
+
For code sources (`type: 'code'`):
|
|
182
|
+
* `source`: Either `'local_directory'` or `'github'`.
|
|
183
|
+
* `path`: Path to the local directory (required when `source: 'local_directory'`).
|
|
184
|
+
* `repo`: Repository name in the format `'owner/repo'` (required when `source: 'github'`).
|
|
185
|
+
* `branch`: (Optional) Branch to clone for GitHub sources.
|
|
186
|
+
* `include_extensions`: (Optional) Array of file extensions to include (defaults to common code extensions).
|
|
187
|
+
* `exclude_extensions`: (Optional) Array of file extensions to exclude.
|
|
188
|
+
* `recursive`: (Optional) Whether to traverse subdirectories (defaults to `true`).
|
|
189
|
+
* `url_rewrite_prefix`: (Optional) URL prefix to rewrite `file://` URLs for local sources.
|
|
190
|
+
* `encoding`: (Optional) File encoding to use (defaults to `'utf8'`).
|
|
191
|
+
* `chunk_size`: (Optional) Chonkie chunk size for code files.
|
|
192
|
+
* `version` is optional for code sources; if omitted it defaults to `branch` (or `local` for local directories).
|
|
193
|
+
* `branch` is stored in the database and used by `query_code` filtering.
|
|
165
194
|
|
|
166
195
|
For Zendesk (`type: 'zendesk'`):
|
|
167
196
|
* `zendesk_subdomain`: Your Zendesk subdomain (e.g., `'mycompany'` for mycompany.zendesk.com).
|
|
@@ -189,6 +218,21 @@ Configuration is managed through two files:
|
|
|
189
218
|
|
|
190
219
|
**Example (`config.yaml`):**
|
|
191
220
|
```yaml
|
|
221
|
+
# Optional: Configure embedding provider
|
|
222
|
+
# Can also be set via EMBEDDING_PROVIDER environment variable
|
|
223
|
+
# Defaults to OpenAI if not specified
|
|
224
|
+
embedding:
|
|
225
|
+
provider: 'openai' # or 'azure'
|
|
226
|
+
openai:
|
|
227
|
+
api_key: '${OPENAI_API_KEY}' # Optional, uses env var by default
|
|
228
|
+
model: 'text-embedding-3-large' # Optional, defaults to text-embedding-3-large
|
|
229
|
+
# For Azure OpenAI, use this instead:
|
|
230
|
+
# azure:
|
|
231
|
+
# api_key: '${AZURE_OPENAI_KEY}'
|
|
232
|
+
# endpoint: '${AZURE_OPENAI_ENDPOINT}'
|
|
233
|
+
# deployment_name: 'text-embedding-3-large'
|
|
234
|
+
# api_version: '2024-10-21' # Optional
|
|
235
|
+
|
|
192
236
|
sources:
|
|
193
237
|
# Website source example
|
|
194
238
|
- type: 'website'
|
|
@@ -226,6 +270,21 @@ Configuration is managed through two files:
|
|
|
226
270
|
type: 'sqlite'
|
|
227
271
|
params:
|
|
228
272
|
db_path: './project-docs.db'
|
|
273
|
+
|
|
274
|
+
# Code source example (GitHub)
|
|
275
|
+
- type: 'code'
|
|
276
|
+
source: 'github'
|
|
277
|
+
product_name: 'doc2vec'
|
|
278
|
+
version: 'main'
|
|
279
|
+
repo: 'kagent-dev/doc2vec'
|
|
280
|
+
branch: 'main'
|
|
281
|
+
include_extensions: ['.ts', '.tsx', '.md']
|
|
282
|
+
max_size: 1048576
|
|
283
|
+
chunk_size: 2048
|
|
284
|
+
database_config:
|
|
285
|
+
type: 'sqlite'
|
|
286
|
+
params:
|
|
287
|
+
db_path: './doc2vec-code.db'
|
|
229
288
|
|
|
230
289
|
# Zendesk example
|
|
231
290
|
- type: 'zendesk'
|
|
@@ -494,4 +553,4 @@ If you don't specify a config path, it will look for config.yaml in the current
|
|
|
494
553
|
### Heading Hierarchy Improvements
|
|
495
554
|
- Fixed sparse array issues that caused `NULL` values in heading hierarchy
|
|
496
555
|
- Proper breadcrumb generation for nested sections
|
|
497
|
-
- Hierarchical context preserved across chunk boundaries
|
|
556
|
+
- Hierarchical context preserved across chunk boundaries
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CodeChunker = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const web_tree_sitter_1 = require("web-tree-sitter");
|
|
40
|
+
class CodeChunker {
|
|
41
|
+
constructor(lang, chunkSize, tokenCounter) {
|
|
42
|
+
this.lang = lang;
|
|
43
|
+
this.chunkSize = chunkSize;
|
|
44
|
+
this.tokenCounter = tokenCounter;
|
|
45
|
+
}
|
|
46
|
+
static async create(options) {
|
|
47
|
+
if (!CodeChunker.treeSitterInitialized && web_tree_sitter_1.Parser.init) {
|
|
48
|
+
try {
|
|
49
|
+
await web_tree_sitter_1.Parser.init();
|
|
50
|
+
CodeChunker.treeSitterInitialized = true;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.warn('Failed to initialize tree-sitter parser:', error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const chunkSize = options.chunkSize ?? 512;
|
|
57
|
+
if (chunkSize <= 0) {
|
|
58
|
+
throw new Error('chunkSize must be greater than 0');
|
|
59
|
+
}
|
|
60
|
+
const tokenCounter = options.tokenCounter ?? (async (text) => text.length);
|
|
61
|
+
const chunker = new CodeChunker(options.lang, chunkSize, tokenCounter);
|
|
62
|
+
return chunker;
|
|
63
|
+
}
|
|
64
|
+
async chunk(text) {
|
|
65
|
+
if (!text.trim()) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const parser = await CodeChunker.getParser(this.lang);
|
|
69
|
+
const originalTextBytes = Buffer.from(text, 'utf-8');
|
|
70
|
+
const tree = parser.parse(originalTextBytes.toString());
|
|
71
|
+
if (!tree) {
|
|
72
|
+
throw new Error('Failed to parse code');
|
|
73
|
+
}
|
|
74
|
+
const chunks = [];
|
|
75
|
+
await this.recursiveChunk(tree.rootNode, originalTextBytes.toString(), chunks);
|
|
76
|
+
return this.mergeChunks(chunks);
|
|
77
|
+
}
|
|
78
|
+
static async getParser(lang) {
|
|
79
|
+
const formattedLang = lang.toLowerCase().replace(/-/g, '_');
|
|
80
|
+
const cached = this.parserCache.get(formattedLang);
|
|
81
|
+
if (cached) {
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
const parserPromise = (async () => {
|
|
85
|
+
if (!CodeChunker.treeSitterInitialized && web_tree_sitter_1.Parser.init) {
|
|
86
|
+
try {
|
|
87
|
+
await web_tree_sitter_1.Parser.init();
|
|
88
|
+
CodeChunker.treeSitterInitialized = true;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.warn('Failed to initialize tree-sitter parser:', error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const wasmPath = CodeChunker.resolveWasmPath(formattedLang);
|
|
95
|
+
const wasmBuffer = fs.readFileSync(wasmPath);
|
|
96
|
+
const language = await web_tree_sitter_1.Language.load(wasmBuffer);
|
|
97
|
+
const parser = new web_tree_sitter_1.Parser();
|
|
98
|
+
parser.setLanguage(language);
|
|
99
|
+
return parser;
|
|
100
|
+
})();
|
|
101
|
+
this.parserCache.set(formattedLang, parserPromise);
|
|
102
|
+
return parserPromise;
|
|
103
|
+
}
|
|
104
|
+
static resolveWasmPath(formattedLang) {
|
|
105
|
+
const nodeModulesPath = CodeChunker.findNearestNodeModules(__dirname);
|
|
106
|
+
if (!nodeModulesPath) {
|
|
107
|
+
throw new Error('node_modules directory not found.');
|
|
108
|
+
}
|
|
109
|
+
const wasmPath = path.join(nodeModulesPath, `tree-sitter-wasms/out/tree-sitter-${formattedLang}.wasm`);
|
|
110
|
+
if (!fs.existsSync(wasmPath)) {
|
|
111
|
+
throw new Error(`Tree-sitter WASM file for language "${formattedLang}" not found at ${wasmPath}.`);
|
|
112
|
+
}
|
|
113
|
+
return wasmPath;
|
|
114
|
+
}
|
|
115
|
+
static findNearestNodeModules(startDir) {
|
|
116
|
+
let dir = path.resolve(startDir);
|
|
117
|
+
while (true) {
|
|
118
|
+
const candidate = path.join(dir, 'node_modules');
|
|
119
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
const parent = path.dirname(dir);
|
|
123
|
+
if (parent === dir)
|
|
124
|
+
break;
|
|
125
|
+
dir = parent;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
async recursiveChunk(node, source, chunks) {
|
|
130
|
+
const nodeText = source.substring(node.startIndex, node.endIndex);
|
|
131
|
+
const tokenCount = await this.tokenCounter(nodeText);
|
|
132
|
+
const children = (node.children || []).filter((child) => Boolean(child));
|
|
133
|
+
if (tokenCount <= this.chunkSize || children.length === 0) {
|
|
134
|
+
if (nodeText.trim()) {
|
|
135
|
+
chunks.push({ text: nodeText, tokenCount });
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const beforeCount = chunks.length;
|
|
140
|
+
for (const child of children) {
|
|
141
|
+
await this.recursiveChunk(child, source, chunks);
|
|
142
|
+
}
|
|
143
|
+
if (chunks.length === beforeCount && nodeText.trim()) {
|
|
144
|
+
chunks.push({ text: nodeText, tokenCount });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
mergeChunks(chunks) {
|
|
148
|
+
const merged = [];
|
|
149
|
+
let currentText = '';
|
|
150
|
+
let currentTokens = 0;
|
|
151
|
+
const separatorTokens = 1; // Account for the '\n' separator between merged chunks
|
|
152
|
+
for (const chunk of chunks) {
|
|
153
|
+
if (!chunk.text.trim()) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const nextTokens = currentTokens + separatorTokens + chunk.tokenCount;
|
|
157
|
+
if (currentTokens === 0) {
|
|
158
|
+
currentText = chunk.text;
|
|
159
|
+
currentTokens = chunk.tokenCount;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (nextTokens <= this.chunkSize) {
|
|
163
|
+
currentText = `${currentText}\n${chunk.text}`;
|
|
164
|
+
currentTokens = nextTokens;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
merged.push({ text: currentText, tokenCount: currentTokens });
|
|
168
|
+
currentText = chunk.text;
|
|
169
|
+
currentTokens = chunk.tokenCount;
|
|
170
|
+
}
|
|
171
|
+
if (currentTokens > 0) {
|
|
172
|
+
merged.push({ text: currentText, tokenCount: currentTokens });
|
|
173
|
+
}
|
|
174
|
+
return merged;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
exports.CodeChunker = CodeChunker;
|
|
178
|
+
CodeChunker.treeSitterInitialized = false;
|
|
179
|
+
CodeChunker.parserCache = new Map();
|