vectra 0.1.1 → 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.
Files changed (78) hide show
  1. package/README.md +3 -3
  2. package/bin/vectra.js +3 -0
  3. package/lib/GPT3Tokenizer.d.ts +9 -0
  4. package/lib/GPT3Tokenizer.d.ts.map +1 -0
  5. package/lib/GPT3Tokenizer.js +17 -0
  6. package/lib/GPT3Tokenizer.js.map +1 -0
  7. package/lib/ItemSelector.d.ts +41 -0
  8. package/lib/ItemSelector.d.ts.map +1 -0
  9. package/lib/ItemSelector.js +156 -0
  10. package/lib/ItemSelector.js.map +1 -0
  11. package/lib/LocalDocument.d.ts +16 -0
  12. package/lib/LocalDocument.d.ts.map +1 -0
  13. package/lib/LocalDocument.js +99 -0
  14. package/lib/LocalDocument.js.map +1 -0
  15. package/lib/LocalDocumentIndex.d.ts +48 -0
  16. package/lib/LocalDocumentIndex.d.ts.map +1 -0
  17. package/lib/LocalDocumentIndex.js +367 -0
  18. package/lib/LocalDocumentIndex.js.map +1 -0
  19. package/lib/LocalDocumentResult.d.ts +12 -0
  20. package/lib/LocalDocumentResult.d.ts.map +1 -0
  21. package/lib/LocalDocumentResult.js +186 -0
  22. package/lib/LocalDocumentResult.js.map +1 -0
  23. package/lib/LocalIndex.d.ts +130 -0
  24. package/lib/LocalIndex.d.ts.map +1 -0
  25. package/lib/LocalIndex.js +405 -0
  26. package/lib/LocalIndex.js.map +1 -0
  27. package/lib/OpenAIEmbeddings.d.ts +98 -0
  28. package/lib/OpenAIEmbeddings.d.ts.map +1 -0
  29. package/lib/OpenAIEmbeddings.js +139 -0
  30. package/lib/OpenAIEmbeddings.js.map +1 -0
  31. package/lib/TextSplitter.d.ts +17 -0
  32. package/lib/TextSplitter.d.ts.map +1 -0
  33. package/lib/TextSplitter.js +460 -0
  34. package/lib/TextSplitter.js.map +1 -0
  35. package/lib/WebFetcher.d.ts +16 -0
  36. package/lib/WebFetcher.d.ts.map +1 -0
  37. package/lib/WebFetcher.js +144 -0
  38. package/lib/WebFetcher.js.map +1 -0
  39. package/lib/index.d.ts +11 -0
  40. package/lib/index.d.ts.map +1 -0
  41. package/lib/index.js +27 -0
  42. package/lib/index.js.map +1 -0
  43. package/lib/internals/Colorize.d.ts +14 -0
  44. package/lib/internals/Colorize.d.ts.map +1 -0
  45. package/lib/internals/Colorize.js +64 -0
  46. package/lib/internals/Colorize.js.map +1 -0
  47. package/lib/internals/index.d.ts +3 -0
  48. package/lib/internals/index.d.ts.map +1 -0
  49. package/lib/internals/index.js +19 -0
  50. package/lib/internals/index.js.map +1 -0
  51. package/lib/internals/types.d.ts +42 -0
  52. package/lib/internals/types.d.ts.map +1 -0
  53. package/lib/internals/types.js +3 -0
  54. package/lib/internals/types.js.map +1 -0
  55. package/lib/types.d.ts +133 -0
  56. package/lib/types.d.ts.map +1 -0
  57. package/lib/types.js +3 -0
  58. package/lib/types.js.map +1 -0
  59. package/lib/vectra-cli.d.ts +2 -0
  60. package/lib/vectra-cli.d.ts.map +1 -0
  61. package/lib/vectra-cli.js +276 -0
  62. package/lib/vectra-cli.js.map +1 -0
  63. package/package.json +21 -3
  64. package/src/GPT3Tokenizer.ts +15 -0
  65. package/src/ItemSelector.ts +9 -9
  66. package/src/LocalDocument.ts +70 -0
  67. package/src/LocalDocumentIndex.ts +355 -0
  68. package/src/LocalDocumentResult.ts +206 -0
  69. package/src/LocalIndex.ts +12 -78
  70. package/src/OpenAIEmbeddings.ts +205 -0
  71. package/src/TextSplitter.ts +480 -0
  72. package/src/WebFetcher.ts +128 -0
  73. package/src/index.ts +8 -0
  74. package/src/internals/Colorize.ts +64 -0
  75. package/src/internals/index.ts +2 -0
  76. package/src/internals/types.ts +46 -0
  77. package/src/types.ts +160 -0
  78. package/src/vectra-cli.ts +238 -0
@@ -0,0 +1,128 @@
1
+ import axios, { AxiosRequestConfig } from "axios";
2
+ import * as cheerio from "cheerio";
3
+ import { TextFetcher } from './types';
4
+
5
+
6
+ const ALLOWED_CONTENT_TYPES = [
7
+ "text/html",
8
+ "application/json",
9
+ "application/xml",
10
+ "application/javascript",
11
+ "text/plain",
12
+ ];
13
+
14
+
15
+ const DEFAULT_HEADERS = {
16
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
17
+ "Accept-Encoding": "gzip, deflate",
18
+ "Accept-Language": "en-US,en;q=0.5",
19
+ "Alt-Used": "LEAVE-THIS-KEY-SET-BY-TOOL",
20
+ Connection: "keep-alive",
21
+ Host: "LEAVE-THIS-KEY-SET-BY-TOOL",
22
+ Referer: "https://www.google.com/",
23
+ "Sec-Fetch-Dest": "document",
24
+ "Sec-Fetch-Mode": "navigate",
25
+ "Sec-Fetch-Site": "cross-site",
26
+ "Upgrade-Insecure-Requests": "1",
27
+ "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0",
28
+ };
29
+
30
+ export interface WebFetcherConfig {
31
+ headers?: Record<string,string>;
32
+ requestConfig?: AxiosRequestConfig;
33
+ htmlToText: boolean;
34
+ summarizeHtml: boolean;
35
+ }
36
+
37
+ export class WebFetcher implements TextFetcher {
38
+ private readonly _config: WebFetcherConfig;
39
+
40
+ public constructor(config?: Partial<WebFetcherConfig>) {
41
+ this._config = Object.assign({
42
+ htmlToText: true,
43
+ summarizeHtml: false,
44
+ } as WebFetcherConfig, config);
45
+ }
46
+
47
+ public async fetch(uri: string): Promise<string> {
48
+ const {data, contentType} = await this.fetchPage(uri);
49
+ if (contentType === "text/html" && this._config.htmlToText) {
50
+ return this.extractText(data, uri, this._config.summarizeHtml);
51
+ } else {
52
+ return data;
53
+ }
54
+ }
55
+
56
+ private extractText(html: string, baseUrl: string, summarize: boolean): string {
57
+ // Parse all elements including <noscript> tags
58
+ const $ = cheerio.load(html, { scriptingEnabled: true });
59
+
60
+ // If we want a summary, just get use the <body/>
61
+ let text = '';
62
+ $(`${summarize ? 'body ' : '*'}:not(style):not(script):not(svg)`).each((i, elem: any) => {
63
+ // Remove any children to avoid duplicate text
64
+ let content = $(elem).clone().children().remove().end().text().trim();
65
+ const $el = $(elem);
66
+
67
+ // Print links in markdown format
68
+ let href = $el.attr("href");
69
+ if ($el.prop("tagName")?.toLowerCase() === "a" && href) {
70
+ if (!href.startsWith("http")) {
71
+ // Try converting to a relevant link
72
+ try {
73
+ href = new URL(href, baseUrl).toString();
74
+ } catch {
75
+ // Leave as is
76
+ }
77
+ }
78
+
79
+ // If the link has content, use that as the text
80
+ const altText = $el.find("img[alt]").attr("alt")?.trim();
81
+ if (altText) {
82
+ content += ` ${altText}`;
83
+ }
84
+
85
+ text += ` [${content}](${href})`;
86
+ }
87
+ // otherwise just print the content
88
+ else if (content !== "") {
89
+ text += ` ${content}`;
90
+ }
91
+ });
92
+
93
+ // Remove newlines
94
+ return text.trim().replace(/\n+/g, ' ');
95
+ }
96
+
97
+ private async fetchPage(baseUrl: string): Promise<{data: string; contentType: string;}> {
98
+ const httpClient = axios.create({
99
+ validateStatus: () => true,
100
+ });
101
+
102
+ // Clone headers to avoid mutating the original
103
+ const headers = Object.assign({}, DEFAULT_HEADERS, this._config.headers)
104
+
105
+ // get hostname from url
106
+ const host = new URL(baseUrl).hostname;
107
+ headers['Host'] = host;
108
+ headers['Alt-Used'] = host;
109
+
110
+ // Fetch page and check for errors
111
+ const response = await httpClient.get(baseUrl, {
112
+ headers,
113
+ ...this._config.requestConfig,
114
+ });
115
+ if (response.status >= 400) {
116
+ throw new Error(`Site returned an HTTP status of ${response.status}`);
117
+ }
118
+
119
+ // Check for valid content type
120
+ const contentType = response.headers['content-type'];
121
+ const contentTypeArray = contentType.split(';');
122
+ if (!contentTypeArray[0] || !ALLOWED_CONTENT_TYPES.includes(contentTypeArray[0])) {
123
+ throw new Error(`Site returned an invalid content type of ${contentType}`);
124
+ }
125
+
126
+ return {data: response.data, contentType: contentTypeArray[0]};
127
+ }
128
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,10 @@
1
+ export * from './GPT3Tokenizer';
1
2
  export * from './ItemSelector';
2
3
  export * from './LocalIndex';
4
+ export * from './LocalDocument';
5
+ export * from './LocalDocumentIndex';
6
+ export * from './LocalDocumentResult';
7
+ export * from './OpenAIEmbeddings';
8
+ export * from './TextSplitter';
9
+ export * from './types';
10
+ export * from './WebFetcher';
@@ -0,0 +1,64 @@
1
+ const colorizer = require('json-colorizer');
2
+
3
+ /**
4
+ * @private
5
+ */
6
+ export class Colorize {
7
+ public static replaceLine(text: string): string {
8
+ return '\x1b[A\x1b[2K' + text;
9
+ }
10
+
11
+ public static error(error: Error|string): string {
12
+ if (typeof error === 'string') {
13
+ return `\x1b[31;1m${error}\x1b[0m`;
14
+ } else {
15
+ return `\x1b[31;1m${error.message}\x1b[0m`;
16
+ }
17
+ }
18
+
19
+ public static output(output: object | string, quote: string = '', units: string = ''): string {
20
+ if (typeof output === 'string') {
21
+ return `\x1b[32m${quote}${output}${quote}\x1b[0m`;
22
+ } else if (typeof output === 'object' && output !== null) {
23
+ return colorizer(output, {
24
+ pretty: true,
25
+ colors: {
26
+ BRACE: 'white',
27
+ BRACKET: 'white',
28
+ COLON: 'white',
29
+ COMMA: 'white',
30
+ STRING_KEY: 'white',
31
+ STRING_LITERAL: 'green',
32
+ NUMBER_LITERAL: 'blue',
33
+ BOOLEAN_LITERAL: 'blue',
34
+ NULL_LITERAL: 'blue'
35
+ }
36
+ });
37
+ } else if (typeof output == 'number') {
38
+ return `\x1b[34m${output}${units}\x1b[0m`;
39
+ } else {
40
+ return `\x1b[34m${output}\x1b[0m`;
41
+ }
42
+ }
43
+
44
+ public static progress(message: string): string {
45
+ return message;
46
+ }
47
+
48
+ public static success(message: string): string {
49
+ return `\x1b[32;1m${message}\x1b[0m`;
50
+ }
51
+
52
+ public static title(title: string): string {
53
+ return `\x1b[35;1m${title}\x1b[0m`;
54
+ }
55
+
56
+ public static value(field: string, value: any, units: string = ''): string {
57
+ return `${field}: ${Colorize.output(value, '"', units)}`;
58
+ }
59
+
60
+ public static warning(warning: string): string {
61
+ return `\x1b[33m${warning}\x1b[0m`;
62
+
63
+ }
64
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./Colorize";
2
+ export * from "./types";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @private
3
+ */
4
+ export interface CreateEmbeddingRequest {
5
+ input: CreateEmbeddingRequestInput;
6
+ user?: string;
7
+ }
8
+
9
+ /**
10
+ * @private
11
+ */
12
+ export interface OpenAICreateEmbeddingRequest extends CreateEmbeddingRequest {
13
+ model: string;
14
+ }
15
+
16
+ /**
17
+ * @private
18
+ */
19
+ export interface CreateEmbeddingResponse {
20
+ object: string;
21
+ model: string;
22
+ data: Array<CreateEmbeddingResponseDataInner>;
23
+ usage: CreateEmbeddingResponseUsage;
24
+ }
25
+
26
+ /**
27
+ * @private
28
+ */
29
+ export interface CreateEmbeddingResponseDataInner {
30
+ index: number;
31
+ object: string;
32
+ embedding: Array<number>;
33
+ }
34
+
35
+ /**
36
+ * @private
37
+ */
38
+ export interface CreateEmbeddingResponseUsage {
39
+ prompt_tokens: number;
40
+ total_tokens: number;
41
+ }
42
+
43
+ /**
44
+ * @private
45
+ */
46
+ export type CreateEmbeddingRequestInput = Array<any> | Array<number> | Array<string> | string;
package/src/types.ts ADDED
@@ -0,0 +1,160 @@
1
+
2
+
3
+ /**
4
+ * An AI model that can be used to create embeddings.
5
+ */
6
+ export interface EmbeddingsModel {
7
+ /**
8
+ * Creates embeddings for the given inputs.
9
+ * @param inputs Text inputs to create embeddings for.
10
+ * @returns A `EmbeddingsResponse` with a status and the generated embeddings or a message when an error occurs.
11
+ */
12
+ createEmbeddings(inputs: string|string[]): Promise<EmbeddingsResponse>;
13
+ }
14
+
15
+ /**
16
+ * Status of the embeddings response.
17
+ * @remarks
18
+ * `success` - The embeddings were successfully created.
19
+ * `error` - An error occurred while creating the embeddings.
20
+ * `rate_limited` - The request was rate limited.
21
+ */
22
+ export type EmbeddingsResponseStatus = 'success' | 'error' | 'rate_limited';
23
+
24
+ /**
25
+ * Response returned by a `EmbeddingsClient`.
26
+ */
27
+ export interface EmbeddingsResponse {
28
+ /**
29
+ * Status of the embeddings response.
30
+ */
31
+ status: EmbeddingsResponseStatus;
32
+
33
+ /**
34
+ * Optional. Embeddings for the given inputs.
35
+ */
36
+ output?: number[][];
37
+
38
+ /**
39
+ * Optional. Message when status is not equal to `success`.
40
+ */
41
+ message?: string;
42
+ }
43
+
44
+ export interface TextChunk {
45
+ text: string;
46
+ tokens: number[];
47
+ startPos: number;
48
+ endPos: number;
49
+ startOverlap: number[];
50
+ endOverlap: number[];
51
+ }
52
+
53
+ export interface TextFetcher {
54
+ fetch(uri: string): Promise<string>;
55
+ }
56
+
57
+ export interface IndexStats {
58
+ version: number;
59
+ metadata_config: {
60
+ indexed?: string[];
61
+ };
62
+ items: number;
63
+ }
64
+
65
+ export interface IndexItem<TMetadata = Record<string,MetadataTypes>> {
66
+ id: string;
67
+ metadata: TMetadata;
68
+ vector: number[];
69
+ norm: number;
70
+ metadataFile?: string;
71
+ }
72
+
73
+ export interface MetadataFilter {
74
+
75
+ /**
76
+ * Equal to (number, string, boolean)
77
+ */
78
+ '$eq'?: number|string|boolean;
79
+
80
+ /**
81
+ * Not equal to (number, string, boolean)
82
+ */
83
+ '$ne'?: number|string|boolean;
84
+
85
+ /**
86
+ * Greater than (number)
87
+ */
88
+ '$gt'?: number;
89
+
90
+ /**
91
+ * Greater than or equal to (number)
92
+ */
93
+ '$gte'?: number;
94
+
95
+ /**
96
+ * Less than (number)
97
+ */
98
+ '$lt'?: number;
99
+
100
+ /**
101
+ * Less than or equal to (number)
102
+ */
103
+ '$lte'?: number;
104
+
105
+ /**
106
+ * In array (string or number)
107
+ */
108
+ '$in'?: (number|string)[];
109
+
110
+ /**
111
+ * Not in array (string or number)
112
+ */
113
+ '$nin'?: (number|string)[];
114
+
115
+ /**
116
+ * AND (MetadataFilter[])
117
+ */
118
+ '$and'?: MetadataFilter[];
119
+
120
+ /**
121
+ * OR (MetadataFilter[])
122
+ */
123
+ '$or'?: MetadataFilter[];
124
+
125
+ [key: string]: unknown;
126
+ }
127
+
128
+ export type MetadataTypes = number|string|boolean;
129
+
130
+ export interface QueryResult<TMetadata = Record<string,MetadataTypes>> {
131
+ item: IndexItem<TMetadata>;
132
+ score: number;
133
+ }
134
+
135
+ export interface Tokenizer {
136
+ decode(tokens: number[]): string;
137
+ encode(text: string): number[];
138
+ }
139
+
140
+ export interface DocumentChunkMetadata {
141
+ documentId: string;
142
+ startPos: number;
143
+ endPos: number;
144
+ [key: string]: MetadataTypes;
145
+ }
146
+
147
+ export interface DocumentCatalogStats {
148
+ version: number;
149
+ documents: number;
150
+ chunks: number;
151
+ metadata_config: {
152
+ indexed?: string[];
153
+ };
154
+ }
155
+
156
+ export interface DocumentTextSection {
157
+ text: string;
158
+ tokenCount: number;
159
+ score: number;
160
+ }
@@ -0,0 +1,238 @@
1
+ import * as fs from 'fs/promises';
2
+ import yargs from "yargs/yargs";
3
+ import { hideBin } from "yargs/helpers";
4
+ import { LocalDocumentIndex } from "./LocalDocumentIndex";
5
+ import { WebFetcher } from './WebFetcher';
6
+ import { OpenAIEmbeddings } from './OpenAIEmbeddings';
7
+ import { Colorize } from './internals';
8
+ import { Console } from 'console';
9
+
10
+ export async function run() {
11
+ // prettier-ignore
12
+ const args = await yargs(hideBin(process.argv))
13
+ .command('create <index>', `create a new local index`, {}, async (args) => {
14
+ const folderPath = args.index as string;
15
+ const index = new LocalDocumentIndex({ folderPath });
16
+ console.log(Colorize.output(`creating index at ${folderPath}`));
17
+ await index.createIndex({ version: 1, deleteIfExists: true });
18
+ })
19
+ .command('delete <index>', `delete an existing local index`, {}, async (args) => {
20
+ const folderPath = args.index as string;
21
+ console.log(Colorize.output(`deleting index at ${folderPath}`));
22
+ const index = new LocalDocumentIndex({ folderPath });
23
+ await index.deleteIndex();
24
+ })
25
+ .command('add-web <index>', `adds one or more web pages to an index`, (yargs) => {
26
+ return yargs
27
+ .option('keys', {
28
+ alias: 'k',
29
+ describe: 'path of a JSON file containing the model keys to use for generating embeddings',
30
+ type: 'string'
31
+ })
32
+ .option('uri', {
33
+ alias: 'u',
34
+ array: true,
35
+ describe: 'http/https link to a web page to add',
36
+ type: 'string'
37
+ })
38
+ .option('list', {
39
+ alias: 'l',
40
+ describe: 'path to a file containing a list of web pages to add',
41
+ type: 'string'
42
+ })
43
+ .option('chunk-size', {
44
+ alias: 'cs',
45
+ describe: 'size of the generated chunks in tokens (defaults to 512)',
46
+ type: 'number',
47
+ default: 512
48
+ })
49
+ .check((argv) => {
50
+ if (Array.isArray(argv.uri) && argv.uri.length > 0) {
51
+ return true;
52
+ } else if (typeof argv.list == 'string' && argv.list.trim().length > 0) {
53
+ return true;
54
+ } else {
55
+ throw new Error(`you must specify either one or more "--uri <link>" for the pages to add or a "--list <file path>" for a file containing the list of pages to add.`);
56
+ }
57
+ })
58
+ .demandOption(['keys']);
59
+ }, async (args) => {
60
+ console.log(Colorize.title('Adding Web Pages to Index'));
61
+
62
+ // Create embeddings
63
+ const keys = JSON.parse(await fs.readFile(args.keys as string, 'utf-8'));
64
+ const embeddings = new OpenAIEmbeddings(Object.assign({ model: 'text-embedding-ada-002' }, keys));
65
+
66
+ // Initialize index
67
+ const folderPath = args.index as string;
68
+ const index = new LocalDocumentIndex({
69
+ folderPath,
70
+ embeddings,
71
+ chunkingConfig: {
72
+ chunkSize: args.chunkSize
73
+ }
74
+ });
75
+
76
+ // Get list of url's
77
+ const uris = await getItemList(args.uri as string[], args.list as string, 'web page');
78
+
79
+ // Fetch web pages
80
+ const fetcher = new WebFetcher();
81
+ for (const uri of uris) {
82
+ try {
83
+ console.log(Colorize.progress(`fetching ${uri}`));
84
+ const content = await fetcher.fetch(uri);
85
+ console.log(Colorize.replaceLine(Colorize.progress(`indexing ${uri}`)));
86
+ await index.upsertDocument(uri, content);
87
+ console.log(Colorize.replaceLine(Colorize.success(`added ${uri}`)));
88
+ } catch (err: unknown) {
89
+ console.log(Colorize.replaceLine(Colorize.error(`Error adding: ${uri}\n${(err as Error).message}`)));
90
+ }
91
+ }
92
+ })
93
+ .command('remove <index>', `removes one or more documents from an index`, (yargs) => {
94
+ return yargs
95
+ .option('uri', {
96
+ alias: 'u',
97
+ array: true,
98
+ describe: 'uri of a document to remove',
99
+ type: 'string'
100
+ })
101
+ .option('list', {
102
+ alias: 'l',
103
+ describe: 'path to a file containing a list of documents to remove',
104
+ type: 'string'
105
+ })
106
+ .check((argv) => {
107
+ if (Array.isArray(argv.uri) && argv.uri.length > 0) {
108
+ return true;
109
+ } else if (typeof argv.list == 'string' && argv.list.trim().length > 0) {
110
+ return true;
111
+ } else {
112
+ throw new Error(`you must specify either one or more "--uri <link>" for the pages to add or a "--list <file path>" for a file containing the list of pages to add.`);
113
+ }
114
+ });
115
+ }, async (args) => {
116
+ // Initialize index
117
+ const folderPath = args.index as string;
118
+ const index = new LocalDocumentIndex({ folderPath });
119
+
120
+ // Get list of uri's
121
+ const uris = await getItemList(args.uri as string[], args.list as string, 'document');
122
+
123
+ // Remove documents
124
+ for (const uri of uris) {
125
+ console.log(`removing ${uri}`);
126
+ await index.deleteDocument(uri);
127
+ }
128
+ })
129
+ .command('stats <index>', `prints the stats for a local index`, {}, async (args) => {
130
+ const folderPath = args.index as string;
131
+ const index = new LocalDocumentIndex({ folderPath });
132
+ const stats = await index.getCatalogStats();
133
+ console.log(Colorize.title('Index Stats'));
134
+ console.log(Colorize.output(stats));
135
+ })
136
+ .command('query <index> <query>', `queries a local index`, (yargs) => {
137
+ return yargs
138
+ .option('keys', {
139
+ alias: 'k',
140
+ describe: 'path of a JSON file containing the model keys to use for generating embeddings'
141
+ })
142
+ .option('document-count', {
143
+ alias: 'dc',
144
+ describe: 'max number of documents to return (defaults to 10)',
145
+ type: 'count',
146
+ default: 10
147
+ })
148
+ .option('chunk-count', {
149
+ alias: 'cc',
150
+ describe: 'max number of chunks to return (defaults to 50)',
151
+ type: 'count',
152
+ default: 50
153
+ })
154
+ .option('section-count', {
155
+ alias: 'sc',
156
+ describe: 'max number of document sections to render (defaults to 1)',
157
+ type: 'count',
158
+ default: 1
159
+ })
160
+ .option('tokens', {
161
+ alias: 't',
162
+ describe: 'max number of tokens to render for each document section (defaults to 2000)',
163
+ type: 'count',
164
+ default: 2000
165
+ })
166
+ .option('format', {
167
+ alias: 'f',
168
+ describe: `format of the rendered results. Defaults to 'sections'`,
169
+ choices: ['sections', 'stats', 'chunks'],
170
+ default: 'sections'
171
+ })
172
+ .demandOption(['keys']);
173
+ }, async (args) => {
174
+ console.log(Colorize.title('Querying Index'));
175
+
176
+ // Create embeddings
177
+ const keys = JSON.parse(await fs.readFile(args.keys as string, 'utf-8'));
178
+ const embeddings = new OpenAIEmbeddings(Object.assign({ model: 'text-embedding-ada-002' }, keys));
179
+
180
+ // Initialize index
181
+ const folderPath = args.index as string;
182
+ const index = new LocalDocumentIndex({
183
+ folderPath,
184
+ embeddings
185
+ });
186
+
187
+ // Query index
188
+ const query = args.query as string;
189
+ const results = await index.queryDocuments(query, {
190
+ maxDocuments: args.documentCount,
191
+ maxChunks: args.chunkCount,
192
+ });
193
+
194
+ // Render results
195
+ for (const result of results) {
196
+ console.log(Colorize.output(result.uri));
197
+ console.log(Colorize.value('score', result.score));
198
+ console.log(Colorize.value('chunks', result.chunks.length));
199
+ if (args.format == 'sections') {
200
+ const sections = await result.renderSections(args.tokens, args.sectionCount);
201
+ for (let i = 0; i < sections.length; i++) {
202
+ const section = sections[i];
203
+ console.log(Colorize.title(args.sectionCount > 1 ? 'Section' : `Section ${1}`));
204
+ console.log(Colorize.value('score', section.score));
205
+ console.log(Colorize.value('tokens', section.tokenCount));
206
+ console.log(Colorize.output(section.text));
207
+ }
208
+ } else if (args.format == 'chunks') {
209
+ const text = await result.loadText();
210
+ for (let i = 0; i < result.chunks.length; i++) {
211
+ const chunk = result.chunks[i];
212
+ const startPos = chunk.item.metadata.startPos;
213
+ const endPos = chunk.item.metadata.endPos;
214
+ console.log(Colorize.title(`Chunk ${i + 1}`));
215
+ console.log(Colorize.value('score', chunk.score));
216
+ console.log(Colorize.value('startPos', startPos));
217
+ console.log(Colorize.value('endPos', endPos));
218
+ console.log(Colorize.output(text.substring(startPos, endPos + 1)));
219
+ }
220
+ }
221
+ }
222
+ })
223
+ .help()
224
+ .demandCommand()
225
+ .parseAsync();
226
+ }
227
+
228
+
229
+ async function getItemList(items: string[], listFile: string, uriType: string): Promise<string[]> {
230
+ if (Array.isArray(items) && items.length > 0) {
231
+ return items;
232
+ } else if (typeof listFile == 'string' && listFile.trim().length > 0) {
233
+ const list = await fs.readFile(listFile, 'utf-8');
234
+ return list.split('\n').map((item) => item.trim()).filter((item) => item.length > 0);
235
+ } else {
236
+ throw new Error(`you must specify either one or more "--uri <${uriType}>" for the items or a "--list <file path>" for a file containing the items.`)
237
+ }
238
+ }