vectra 0.1.2 → 0.2.1
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 +5 -0
- package/bin/vectra.js +3 -0
- package/lib/GPT3Tokenizer.d.ts +9 -0
- package/lib/GPT3Tokenizer.d.ts.map +1 -0
- package/lib/GPT3Tokenizer.js +17 -0
- package/lib/GPT3Tokenizer.js.map +1 -0
- package/lib/ItemSelector.d.ts +1 -1
- package/lib/ItemSelector.d.ts.map +1 -1
- package/lib/ItemSelector.js.map +1 -1
- package/lib/LocalDocument.d.ts +16 -0
- package/lib/LocalDocument.d.ts.map +1 -0
- package/lib/LocalDocument.js +99 -0
- package/lib/LocalDocument.js.map +1 -0
- package/lib/LocalDocumentIndex.d.ts +48 -0
- package/lib/LocalDocumentIndex.d.ts.map +1 -0
- package/lib/LocalDocumentIndex.js +367 -0
- package/lib/LocalDocumentIndex.js.map +1 -0
- package/lib/LocalDocumentResult.d.ts +12 -0
- package/lib/LocalDocumentResult.d.ts.map +1 -0
- package/lib/LocalDocumentResult.js +186 -0
- package/lib/LocalDocumentResult.js.map +1 -0
- package/lib/LocalIndex.d.ts +9 -63
- package/lib/LocalIndex.d.ts.map +1 -1
- package/lib/LocalIndex.js +14 -1
- package/lib/LocalIndex.js.map +1 -1
- package/lib/OpenAIEmbeddings.d.ts +98 -0
- package/lib/OpenAIEmbeddings.d.ts.map +1 -0
- package/lib/OpenAIEmbeddings.js +139 -0
- package/lib/OpenAIEmbeddings.js.map +1 -0
- package/lib/TextSplitter.d.ts +17 -0
- package/lib/TextSplitter.d.ts.map +1 -0
- package/lib/TextSplitter.js +460 -0
- package/lib/TextSplitter.js.map +1 -0
- package/lib/WebFetcher.d.ts +16 -0
- package/lib/WebFetcher.d.ts.map +1 -0
- package/lib/WebFetcher.js +144 -0
- package/lib/WebFetcher.js.map +1 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +13 -1
- package/lib/index.js.map +1 -1
- package/lib/internals/Colorize.d.ts +14 -0
- package/lib/internals/Colorize.d.ts.map +1 -0
- package/lib/internals/Colorize.js +64 -0
- package/lib/internals/Colorize.js.map +1 -0
- package/lib/internals/index.d.ts +3 -0
- package/lib/internals/index.d.ts.map +1 -0
- package/lib/internals/index.js +19 -0
- package/lib/internals/index.js.map +1 -0
- package/lib/internals/types.d.ts +42 -0
- package/lib/internals/types.d.ts.map +1 -0
- package/lib/internals/types.js +3 -0
- package/lib/internals/types.js.map +1 -0
- package/lib/types.d.ts +133 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -0
- package/lib/vectra-cli.d.ts +2 -0
- package/lib/vectra-cli.d.ts.map +1 -0
- package/lib/vectra-cli.js +277 -0
- package/lib/vectra-cli.js.map +1 -0
- package/package.json +21 -3
- package/src/GPT3Tokenizer.ts +15 -0
- package/src/ItemSelector.ts +9 -9
- package/src/LocalDocument.ts +70 -0
- package/src/LocalDocumentIndex.ts +355 -0
- package/src/LocalDocumentResult.ts +206 -0
- package/src/LocalIndex.ts +12 -78
- package/src/OpenAIEmbeddings.ts +205 -0
- package/src/TextSplitter.ts +480 -0
- package/src/WebFetcher.ts +128 -0
- package/src/index.ts +8 -0
- package/src/internals/Colorize.ts +64 -0
- package/src/internals/index.ts +2 -0
- package/src/internals/types.ts +46 -0
- package/src/types.ts +160 -0
- package/src/vectra-cli.ts +238 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { LocalDocument } from "./LocalDocument";
|
|
2
|
+
import { QueryResult, DocumentChunkMetadata, Tokenizer, DocumentTextSection } from "./types";
|
|
3
|
+
|
|
4
|
+
export class LocalDocumentResult extends LocalDocument {
|
|
5
|
+
private readonly _chunks: QueryResult<DocumentChunkMetadata>[];
|
|
6
|
+
private readonly _tokenizer: Tokenizer;
|
|
7
|
+
private readonly _score: number;
|
|
8
|
+
|
|
9
|
+
public constructor(folderPath: string, id: string, uri: string, chunks: QueryResult<DocumentChunkMetadata>[], tokenizer: Tokenizer) {
|
|
10
|
+
super(folderPath, id, uri);
|
|
11
|
+
this._chunks = chunks;
|
|
12
|
+
this._tokenizer = tokenizer;
|
|
13
|
+
|
|
14
|
+
// Compute average score
|
|
15
|
+
let score = 0;
|
|
16
|
+
this._chunks.forEach(chunk => score += chunk.score);
|
|
17
|
+
this._score = score / this._chunks.length;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public get chunks(): QueryResult<DocumentChunkMetadata>[] {
|
|
21
|
+
return this._chunks;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public get score(): number {
|
|
25
|
+
return this._score;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public async renderSections(maxTokens: number, maxSections: number): Promise<DocumentTextSection[]> {
|
|
29
|
+
// Load text from disk
|
|
30
|
+
const text = await this.loadText();
|
|
31
|
+
|
|
32
|
+
// First check to see if the entire document is less than maxTokens
|
|
33
|
+
const tokens = this._tokenizer.encode(text);
|
|
34
|
+
if (tokens.length < maxTokens) {
|
|
35
|
+
return [{
|
|
36
|
+
text,
|
|
37
|
+
tokenCount: tokens.length,
|
|
38
|
+
score: 1.0
|
|
39
|
+
}];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Otherwise, we need to split the document into sections
|
|
43
|
+
// - Add each chunk to a temp array and filter out any chunk that's longer then maxTokens.
|
|
44
|
+
// - Sort the array by startPos to arrange chunks in document order.
|
|
45
|
+
// - Generate a new array of sections by combining chunks until the maxTokens is reached for each section.
|
|
46
|
+
// - Generate an aggregate score for each section by averaging the score of each chunk in the section.
|
|
47
|
+
// - Sort the sections by score and limit to maxSections.
|
|
48
|
+
// - For each remaining section combine adjacent chunks of text.
|
|
49
|
+
// - Dynamically add overlapping chunks of text to each section until the maxTokens is reached.
|
|
50
|
+
const chunks: SectionChunk[] = this._chunks.map(chunk => {
|
|
51
|
+
const startPos = chunk.item.metadata.startPos;
|
|
52
|
+
const endPos = chunk.item.metadata.endPos;
|
|
53
|
+
const chunkText = text.substring(startPos, endPos + 1);
|
|
54
|
+
return {
|
|
55
|
+
text: chunkText,
|
|
56
|
+
startPos,
|
|
57
|
+
endPos,
|
|
58
|
+
score: chunk.score,
|
|
59
|
+
tokenCount: this._tokenizer.encode(chunkText).length
|
|
60
|
+
};
|
|
61
|
+
}).filter(chunk => chunk.tokenCount <= maxTokens).sort((a, b) => a.startPos - b.startPos);
|
|
62
|
+
|
|
63
|
+
// Check for no chunks
|
|
64
|
+
if (chunks.length === 0) {
|
|
65
|
+
// Take the top chunk and return a subset of its text
|
|
66
|
+
const topChunk = this._chunks[0];
|
|
67
|
+
const startPos = topChunk.item.metadata.startPos;
|
|
68
|
+
const endPos = topChunk.item.metadata.endPos;
|
|
69
|
+
const chunkText = text.substring(startPos, endPos + 1);
|
|
70
|
+
const tokens = this._tokenizer.encode(chunkText);
|
|
71
|
+
return [{
|
|
72
|
+
text: this._tokenizer.decode(tokens.slice(0, maxTokens)),
|
|
73
|
+
tokenCount: maxTokens,
|
|
74
|
+
score: topChunk.score
|
|
75
|
+
}];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Generate sections
|
|
79
|
+
const sections: Section[] = [{
|
|
80
|
+
chunks: [],
|
|
81
|
+
score: 0,
|
|
82
|
+
tokenCount: 0
|
|
83
|
+
}];
|
|
84
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
85
|
+
const chunk = chunks[i];
|
|
86
|
+
let section = sections[sections.length - 1];
|
|
87
|
+
if (section.tokenCount + chunk.tokenCount > maxTokens) {
|
|
88
|
+
sections.push({
|
|
89
|
+
chunks: [],
|
|
90
|
+
score: 0,
|
|
91
|
+
tokenCount: 0
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
sections[sections.length - 1].chunks.push(chunk);
|
|
95
|
+
sections[sections.length - 1].score += chunk.score;
|
|
96
|
+
sections[sections.length - 1].tokenCount += chunk.tokenCount;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Normalize section scores
|
|
100
|
+
sections.forEach(section => section.score /= section.chunks.length);
|
|
101
|
+
|
|
102
|
+
// Sort sections by score and limit to maxSections
|
|
103
|
+
sections.sort((a, b) => b.score - a.score);
|
|
104
|
+
if (sections.length > maxSections) {
|
|
105
|
+
sections.splice(maxSections, sections.length - maxSections);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Combine adjacent chunks of text
|
|
109
|
+
sections.forEach(section => {
|
|
110
|
+
for (let i = 0; i < section.chunks.length - 1; i++) {
|
|
111
|
+
const chunk = section.chunks[i];
|
|
112
|
+
const nextChunk = section.chunks[i + 1];
|
|
113
|
+
if (chunk.endPos + 1 === nextChunk.startPos) {
|
|
114
|
+
chunk.text += nextChunk.text;
|
|
115
|
+
chunk.endPos = nextChunk.endPos;
|
|
116
|
+
chunk.tokenCount += nextChunk.tokenCount;
|
|
117
|
+
section.chunks.splice(i + 1, 1);
|
|
118
|
+
i--;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Add overlapping chunks of text to each section until the maxTokens is reached
|
|
124
|
+
const connector: SectionChunk = {
|
|
125
|
+
text: '\n\n...\n\n',
|
|
126
|
+
startPos: -1,
|
|
127
|
+
endPos: -1,
|
|
128
|
+
score: 0,
|
|
129
|
+
tokenCount: this._tokenizer.encode('\n\n...\n\n').length
|
|
130
|
+
};
|
|
131
|
+
sections.forEach(section => {
|
|
132
|
+
// Insert connectors between chunks
|
|
133
|
+
if (section.chunks.length > 1) {
|
|
134
|
+
for (let i = 0; i < section.chunks.length - 1; i++) {
|
|
135
|
+
section.chunks.splice(i + 1, 0, connector);
|
|
136
|
+
section.tokenCount += connector.tokenCount;
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Add chunks to beginning and end of the section until maxTokens is reached
|
|
142
|
+
let budget = maxTokens - section.tokenCount;
|
|
143
|
+
if (budget > 40) {
|
|
144
|
+
const sectionStart = section.chunks[0].startPos;
|
|
145
|
+
const sectionEnd = section.chunks[section.chunks.length - 1].endPos;
|
|
146
|
+
if (sectionStart > 0) {
|
|
147
|
+
const beforeTex = text.substring(0, section.chunks[0].startPos);
|
|
148
|
+
const beforeTokens = this._tokenizer.encode(beforeTex);
|
|
149
|
+
const beforeBudget = sectionEnd < text.length - 1 ? Math.min(beforeTokens.length, Math.ceil(budget/2)) : Math.min(beforeTokens.length, budget);
|
|
150
|
+
const chunk: SectionChunk = {
|
|
151
|
+
text: this._tokenizer.decode(beforeTokens.slice(-beforeBudget)),
|
|
152
|
+
startPos: sectionStart - beforeBudget,
|
|
153
|
+
endPos: sectionStart - 1,
|
|
154
|
+
score: 0,
|
|
155
|
+
tokenCount: beforeBudget
|
|
156
|
+
};
|
|
157
|
+
section.chunks.unshift(chunk);
|
|
158
|
+
section.tokenCount += chunk.tokenCount;
|
|
159
|
+
budget -= chunk.tokenCount;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (sectionEnd < text.length - 1) {
|
|
163
|
+
const afterText = text.substring(sectionEnd + 1);
|
|
164
|
+
const afterTokens = this._tokenizer.encode(afterText);
|
|
165
|
+
const afterBudget = Math.min(afterTokens.length, budget);
|
|
166
|
+
const chunk: SectionChunk = {
|
|
167
|
+
text: this._tokenizer.decode(afterTokens.slice(0, afterBudget)),
|
|
168
|
+
startPos: sectionEnd + 1,
|
|
169
|
+
endPos: sectionEnd + afterBudget,
|
|
170
|
+
score: 0,
|
|
171
|
+
tokenCount: afterBudget
|
|
172
|
+
};
|
|
173
|
+
section.chunks.push(chunk);
|
|
174
|
+
section.tokenCount += chunk.tokenCount;
|
|
175
|
+
budget -= chunk.tokenCount;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Return final rendered sections
|
|
181
|
+
return sections.map(section => {
|
|
182
|
+
let text = '';
|
|
183
|
+
section.chunks.forEach(chunk => text += chunk.text);
|
|
184
|
+
return {
|
|
185
|
+
text: text,
|
|
186
|
+
tokenCount: section.tokenCount,
|
|
187
|
+
score: section.score
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface SectionChunk {
|
|
194
|
+
text: string;
|
|
195
|
+
startPos: number;
|
|
196
|
+
endPos: number;
|
|
197
|
+
score: number;
|
|
198
|
+
tokenCount: number;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface Section {
|
|
202
|
+
chunks: SectionChunk[];
|
|
203
|
+
score: number;
|
|
204
|
+
tokenCount: number;
|
|
205
|
+
}
|
|
206
|
+
|
package/src/LocalIndex.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'fs/promises';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { v4 } from 'uuid';
|
|
4
4
|
import { ItemSelector } from './ItemSelector';
|
|
5
|
+
import { IndexItem, IndexStats, MetadataFilter, MetadataTypes, QueryResult } from './types';
|
|
5
6
|
|
|
6
7
|
export interface CreateIndexConfig {
|
|
7
8
|
version: number;
|
|
@@ -11,83 +12,6 @@ export interface CreateIndexConfig {
|
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export interface IndexStats {
|
|
15
|
-
version: number;
|
|
16
|
-
metadata_config: {
|
|
17
|
-
indexed?: string[];
|
|
18
|
-
};
|
|
19
|
-
items: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface IndexItem<TMetadata = Record<string,MetadataTypes>> {
|
|
23
|
-
id: string;
|
|
24
|
-
metadata: TMetadata;
|
|
25
|
-
vector: number[];
|
|
26
|
-
norm: number;
|
|
27
|
-
metadataFile?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface QueryResult<TMetadata = Record<string,MetadataTypes>> {
|
|
31
|
-
item: IndexItem<TMetadata>;
|
|
32
|
-
score: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface MetadataFilter {
|
|
36
|
-
[key: string]: MetadataTypes|MetadataFilter|(number|string)[]|MetadataFilter[];
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Equal to (number, string, boolean)
|
|
40
|
-
*/
|
|
41
|
-
'$eq': number|string|boolean;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Not equal to (number, string, boolean)
|
|
45
|
-
*/
|
|
46
|
-
'$ne': number|string|boolean;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Greater than (number)
|
|
50
|
-
*/
|
|
51
|
-
'$gt': number;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Greater than or equal to (number)
|
|
55
|
-
*/
|
|
56
|
-
'$gte': number;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Less than (number)
|
|
60
|
-
*/
|
|
61
|
-
'$lt': number;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Less than or equal to (number)
|
|
65
|
-
*/
|
|
66
|
-
'$lte': number;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* In array (string or number)
|
|
70
|
-
*/
|
|
71
|
-
'$in': (number|string)[];
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Not in array (string or number)
|
|
75
|
-
*/
|
|
76
|
-
'$nin': (number|string)[];
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* AND (MetadataFilter[])
|
|
80
|
-
*/
|
|
81
|
-
'$and': MetadataFilter[];
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* OR (MetadataFilter[])
|
|
85
|
-
*/
|
|
86
|
-
'$or': MetadataFilter[];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export type MetadataTypes = number|string|boolean;
|
|
90
|
-
|
|
91
15
|
/**
|
|
92
16
|
* Local vector index instance.
|
|
93
17
|
* @remarks
|
|
@@ -107,6 +31,13 @@ export class LocalIndex {
|
|
|
107
31
|
this._folderPath = folderPath;
|
|
108
32
|
}
|
|
109
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Path to the index folder.
|
|
36
|
+
*/
|
|
37
|
+
public get folderPath(): string {
|
|
38
|
+
return this._folderPath;
|
|
39
|
+
}
|
|
40
|
+
|
|
110
41
|
/**
|
|
111
42
|
* Begins an update to the index.
|
|
112
43
|
* @remarks
|
|
@@ -364,7 +295,10 @@ export class LocalIndex {
|
|
|
364
295
|
}
|
|
365
296
|
}
|
|
366
297
|
|
|
367
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Ensures that the index has been loaded into memory.
|
|
300
|
+
*/
|
|
301
|
+
protected async loadIndexData(): Promise<void> {
|
|
368
302
|
if (this._data) {
|
|
369
303
|
return;
|
|
370
304
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios';
|
|
2
|
+
import { EmbeddingsModel, EmbeddingsResponse } from "./types";
|
|
3
|
+
import { CreateEmbeddingRequest, CreateEmbeddingResponse, OpenAICreateEmbeddingRequest } from "./internals";
|
|
4
|
+
|
|
5
|
+
export interface BaseOpenAIEmbeddingsOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Optional. Retry policy to use when calling the OpenAI API.
|
|
8
|
+
* @remarks
|
|
9
|
+
* The default retry policy is `[2000, 5000]` which means that the first retry will be after
|
|
10
|
+
* 2 seconds and the second retry will be after 5 seconds.
|
|
11
|
+
*/
|
|
12
|
+
retryPolicy?: number[];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Optional. Request options to use when calling the OpenAI API.
|
|
16
|
+
*/
|
|
17
|
+
requestConfig?: AxiosRequestConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for configuring an `OpenAIEmbeddings` to generate embeddings using an OpenAI hosted model.
|
|
22
|
+
*/
|
|
23
|
+
export interface OpenAIEmbeddingsOptions extends BaseOpenAIEmbeddingsOptions {
|
|
24
|
+
/**
|
|
25
|
+
* API key to use when calling the OpenAI API.
|
|
26
|
+
* @remarks
|
|
27
|
+
* A new API key can be created at https://platform.openai.com/account/api-keys.
|
|
28
|
+
*/
|
|
29
|
+
apiKey: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Model to use for completion.
|
|
33
|
+
* @remarks
|
|
34
|
+
* For Azure OpenAI this is the name of the deployment to use.
|
|
35
|
+
*/
|
|
36
|
+
model: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional. Organization to use when calling the OpenAI API.
|
|
40
|
+
*/
|
|
41
|
+
organization?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Optional. Endpoint to use when calling the OpenAI API.
|
|
45
|
+
* @remarks
|
|
46
|
+
* For Azure OpenAI this is the deployment endpoint.
|
|
47
|
+
*/
|
|
48
|
+
endpoint?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Options for configuring an `OpenAIEmbeddings` to generate embeddings using an Azure OpenAI hosted model.
|
|
53
|
+
*/
|
|
54
|
+
export interface AzureOpenAIEmbeddingsOptions extends BaseOpenAIEmbeddingsOptions {
|
|
55
|
+
/**
|
|
56
|
+
* API key to use when making requests to Azure OpenAI.
|
|
57
|
+
*/
|
|
58
|
+
azureApiKey: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Deployment endpoint to use.
|
|
62
|
+
*/
|
|
63
|
+
azureEndpoint: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Name of the Azure OpenAI deployment (model) to use.
|
|
67
|
+
*/
|
|
68
|
+
azureDeployment: string;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Optional. Version of the API being called. Defaults to `2023-05-15`.
|
|
72
|
+
*/
|
|
73
|
+
azureApiVersion?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A `PromptCompletionModel` for calling OpenAI and Azure OpenAI hosted models.
|
|
78
|
+
* @remarks
|
|
79
|
+
*/
|
|
80
|
+
export class OpenAIEmbeddings implements EmbeddingsModel {
|
|
81
|
+
private readonly _httpClient: AxiosInstance;
|
|
82
|
+
private readonly _useAzure: boolean;
|
|
83
|
+
|
|
84
|
+
private readonly UserAgent = 'AlphaWave';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Options the client was configured with.
|
|
88
|
+
*/
|
|
89
|
+
public readonly options: OpenAIEmbeddingsOptions|AzureOpenAIEmbeddingsOptions;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Creates a new `OpenAIClient` instance.
|
|
93
|
+
* @param options Options for configuring an `OpenAIClient`.
|
|
94
|
+
*/
|
|
95
|
+
public constructor(options: OpenAIEmbeddingsOptions|AzureOpenAIEmbeddingsOptions) {
|
|
96
|
+
// Check for azure config
|
|
97
|
+
if ((options as AzureOpenAIEmbeddingsOptions).azureApiKey) {
|
|
98
|
+
this._useAzure = true;
|
|
99
|
+
this.options = Object.assign({
|
|
100
|
+
retryPolicy: [2000, 5000],
|
|
101
|
+
azureApiVersion: '2023-05-15',
|
|
102
|
+
}, options) as AzureOpenAIEmbeddingsOptions;
|
|
103
|
+
|
|
104
|
+
// Cleanup and validate endpoint
|
|
105
|
+
let endpoint = this.options.azureEndpoint.trim();
|
|
106
|
+
if (endpoint.endsWith('/')) {
|
|
107
|
+
endpoint = endpoint.substring(0, endpoint.length - 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!endpoint.toLowerCase().startsWith('https://')) {
|
|
111
|
+
throw new Error(`Client created with an invalid endpoint of '${endpoint}'. The endpoint must be a valid HTTPS url.`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.options.azureEndpoint = endpoint;
|
|
115
|
+
} else {
|
|
116
|
+
this._useAzure = false;
|
|
117
|
+
this.options = Object.assign({
|
|
118
|
+
retryPolicy: [2000, 5000]
|
|
119
|
+
}, options) as OpenAIEmbeddingsOptions;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Create client
|
|
123
|
+
this._httpClient = axios.create({
|
|
124
|
+
validateStatus: (status) => status < 400 || status == 429
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates embeddings for the given inputs using the OpenAI API.
|
|
130
|
+
* @param model Name of the model to use (or deployment for Azure).
|
|
131
|
+
* @param inputs Text inputs to create embeddings for.
|
|
132
|
+
* @returns A `EmbeddingsResponse` with a status and the generated embeddings or a message when an error occurs.
|
|
133
|
+
*/
|
|
134
|
+
public async createEmbeddings(inputs: string | string[]): Promise<EmbeddingsResponse> {
|
|
135
|
+
const response = await this.createEmbeddingRequest({
|
|
136
|
+
input: inputs,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Process response
|
|
140
|
+
if (response.status < 300) {
|
|
141
|
+
return { status: 'success', output: response.data.data.sort((a, b) => a.index - b.index).map((item) => item.embedding) };
|
|
142
|
+
} else if (response.status == 429) {
|
|
143
|
+
return { status: 'rate_limited', message: `The embeddings API returned a rate limit error.` }
|
|
144
|
+
} else {
|
|
145
|
+
return { status: 'error', message: `The embeddings API returned an error status of ${response.status}: ${response.statusText}` };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
protected createEmbeddingRequest(request: CreateEmbeddingRequest): Promise<AxiosResponse<CreateEmbeddingResponse>> {
|
|
153
|
+
if (this._useAzure) {
|
|
154
|
+
const options = this.options as AzureOpenAIEmbeddingsOptions;
|
|
155
|
+
const url = `${options.azureEndpoint}/openai/deployments/${options.azureDeployment}/embeddings?api-version=${options.azureApiVersion!}`;
|
|
156
|
+
return this.post(url, request);
|
|
157
|
+
} else {
|
|
158
|
+
const options = this.options as OpenAIEmbeddingsOptions;
|
|
159
|
+
const url = `${options.endpoint ?? 'https://api.openai.com'}/v1/embeddings`;
|
|
160
|
+
(request as OpenAICreateEmbeddingRequest).model = options.model;
|
|
161
|
+
return this.post(url, request);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
protected async post<TData>(url: string, body: object, retryCount = 0): Promise<AxiosResponse<TData>> {
|
|
169
|
+
// Initialize request config
|
|
170
|
+
const requestConfig: AxiosRequestConfig = Object.assign({}, this.options.requestConfig);
|
|
171
|
+
|
|
172
|
+
// Initialize request headers
|
|
173
|
+
if (!requestConfig.headers) {
|
|
174
|
+
requestConfig.headers = {};
|
|
175
|
+
}
|
|
176
|
+
if (!requestConfig.headers['Content-Type']) {
|
|
177
|
+
requestConfig.headers['Content-Type'] = 'application/json';
|
|
178
|
+
}
|
|
179
|
+
if (!requestConfig.headers['User-Agent']) {
|
|
180
|
+
requestConfig.headers['User-Agent'] = this.UserAgent;
|
|
181
|
+
}
|
|
182
|
+
if (this._useAzure) {
|
|
183
|
+
const options = this.options as AzureOpenAIEmbeddingsOptions;
|
|
184
|
+
requestConfig.headers['api-key'] = options.azureApiKey;
|
|
185
|
+
} else {
|
|
186
|
+
const options = this.options as OpenAIEmbeddingsOptions;
|
|
187
|
+
requestConfig.headers['Authorization'] = `Bearer ${options.apiKey}`;
|
|
188
|
+
if (options.organization) {
|
|
189
|
+
requestConfig.headers['OpenAI-Organization'] = options.organization;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Send request
|
|
194
|
+
const response = await this._httpClient.post(url, body, requestConfig);
|
|
195
|
+
|
|
196
|
+
// Check for rate limit error
|
|
197
|
+
if (response.status == 429 && Array.isArray(this.options.retryPolicy) && retryCount < this.options.retryPolicy.length) {
|
|
198
|
+
const delay = this.options.retryPolicy[retryCount];
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
200
|
+
return this.post(url, body, retryCount + 1);
|
|
201
|
+
} else {
|
|
202
|
+
return response;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|