@bedrockio/ai 0.3.0 → 0.4.2

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 (60) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +58 -17
  4. package/__mocks__/@anthropic-ai/sdk.js +16 -22
  5. package/__mocks__/@google/generative-ai.js +1 -1
  6. package/__mocks__/openai.js +33 -28
  7. package/dist/cjs/BaseClient.js +242 -182
  8. package/dist/cjs/anthropic.js +115 -93
  9. package/dist/cjs/google.js +74 -80
  10. package/dist/cjs/index.js +23 -75
  11. package/dist/cjs/openai.js +114 -72
  12. package/dist/cjs/package.json +1 -0
  13. package/dist/cjs/utils/code.js +11 -0
  14. package/dist/cjs/utils/json.js +53 -0
  15. package/dist/cjs/utils/templates.js +83 -0
  16. package/dist/cjs/xai.js +11 -20
  17. package/dist/esm/BaseClient.js +243 -0
  18. package/dist/esm/anthropic.js +116 -0
  19. package/dist/esm/google.js +75 -0
  20. package/dist/esm/index.js +25 -0
  21. package/dist/esm/openai.js +113 -0
  22. package/dist/esm/utils/code.js +8 -0
  23. package/dist/esm/utils/json.js +50 -0
  24. package/dist/esm/utils/templates.js +76 -0
  25. package/dist/esm/xai.js +10 -0
  26. package/eslint.config.js +2 -0
  27. package/package.json +18 -17
  28. package/src/BaseClient.js +233 -140
  29. package/src/anthropic.js +96 -56
  30. package/src/google.js +3 -6
  31. package/src/index.js +6 -54
  32. package/src/openai.js +96 -33
  33. package/src/utils/code.js +9 -0
  34. package/src/utils/json.js +58 -0
  35. package/src/utils/templates.js +87 -0
  36. package/src/xai.js +2 -9
  37. package/tsconfig.cjs.json +8 -0
  38. package/tsconfig.esm.json +8 -0
  39. package/tsconfig.types.json +9 -0
  40. package/types/BaseClient.d.ts +67 -26
  41. package/types/BaseClient.d.ts.map +1 -1
  42. package/types/anthropic.d.ts +26 -2
  43. package/types/anthropic.d.ts.map +1 -1
  44. package/types/google.d.ts.map +1 -1
  45. package/types/index.d.ts +4 -11
  46. package/types/index.d.ts.map +1 -1
  47. package/types/openai.d.ts +45 -2
  48. package/types/openai.d.ts.map +1 -1
  49. package/types/utils/code.d.ts +2 -0
  50. package/types/utils/code.d.ts.map +1 -0
  51. package/types/utils/json.d.ts +2 -0
  52. package/types/utils/json.d.ts.map +1 -0
  53. package/types/utils/templates.d.ts +3 -0
  54. package/types/utils/templates.d.ts.map +1 -0
  55. package/types/utils.d.ts +4 -0
  56. package/types/utils.d.ts.map +1 -0
  57. package/types/xai.d.ts.map +1 -1
  58. package/vitest.config.js +10 -0
  59. package/dist/cjs/util.js +0 -62
  60. package/src/util.js +0 -60
@@ -1,186 +1,246 @@
1
1
  "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = void 0;
7
- var _mustache = _interopRequireDefault(require("mustache"));
8
- var _util = require("./util.js");
9
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
- const MESSAGES_REG = /(?:^|\n)-{3,}\s*(\w+)\s*-{3,}(.*?)(?=\n-{3,}|$)/gs;
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const code_js_1 = require("./utils/code.js");
4
+ const json_js_1 = require("./utils/json.js");
5
+ const templates_js_1 = require("./utils/templates.js");
11
6
  class BaseClient {
12
- constructor(options) {
13
- this.options = options;
14
- this.templates = null;
15
- }
16
-
17
- /**
18
- * Interpolates vars into the provided template and
19
- * runs the chat completion. The "output" option may
20
- * be omitted and will default to `"text"`.
21
- * {@link https://github.com/bedrockio/ai?tab=readme-ov-file#bedrockioai Documentation}
22
- *
23
- * @param {object} options
24
- * @param {string} options.model - The model to use.
25
- * @param {"raw" | "text" | "json" | "messages"} [options.output] - The output to use.
26
- * @param {Object.<string, any>} [options.other] - Additional props
27
- * will be interpolated in the template.
28
- */
29
- async prompt(options) {
30
- options = {
31
- ...this.options,
32
- ...options
33
- };
34
- const messages = await this.getMessages(options);
35
- return await this.getCompletion({
36
- ...options,
37
- messages
38
- });
39
- }
40
-
41
- /**
42
- * Streams the prompt response.
43
- * @returns {AsyncIterator}
44
- */
45
- async *stream(options) {
46
- const stream = await this.getStream(options);
47
- let started = false;
48
-
49
- // @ts-ignore
50
- for await (const chunk of stream) {
51
- const resolved = this.getStreamedChunk(chunk, started);
52
- started = true;
53
-
54
- // @ts-ignore
55
- if (resolved) {
56
- yield resolved;
57
- }
58
- }
59
- }
60
- async getMessages(options) {
61
- const {
62
- text
63
- } = options;
64
- const template = await this.resolveTemplate(options);
65
- if (template) {
66
- const raw = render(template, options);
67
- const messages = [];
68
- for (let match of raw.matchAll(MESSAGES_REG)) {
69
- const [, role, content] = match;
70
- messages.push({
71
- role: role.toLowerCase(),
72
- content: content.trim()
73
- });
74
- }
75
- if (!messages.length) {
76
- messages.push({
77
- role: 'user',
78
- content: raw.trim()
79
- });
80
- }
81
- return messages;
82
- } else if (text) {
83
- return [{
84
- role: 'user',
85
- content: text
86
- }];
87
- } else {
88
- throw new Error('No input provided.');
89
- }
90
- }
91
- async buildTemplate(options) {
92
- const template = await this.resolveTemplate(options);
93
- return render(template, options);
94
- }
95
- async loadTemplates() {
96
- const {
97
- templates
98
- } = this.options;
99
- this.templates ||= await (0, _util.loadTemplates)(templates);
100
- }
101
- async resolveTemplate(options) {
102
- const {
103
- template,
104
- file
105
- } = options;
106
- if (template) {
107
- return template;
108
- } else if (file?.endsWith('.md')) {
109
- return await (0, _util.loadTemplate)(file);
110
- } else if (file) {
111
- await this.loadTemplates();
112
- return this.templates[file];
113
- }
114
- }
115
- async getStream(options) {
116
- return await this.prompt({
117
- ...options,
118
- output: 'raw',
119
- stream: true
120
- });
121
- }
122
- getCompletion(options) {
123
- void options;
124
- new Error('Method not implemented.');
125
- }
126
- getStreamedChunk(chunk, started) {
127
- void chunk;
128
- void started;
129
- new Error('Method not implemented.');
130
- }
7
+ constructor(options) {
8
+ this.options = {
9
+ // @ts-ignore
10
+ model: this.constructor.DEFAULT_MODEL,
11
+ ...options,
12
+ };
13
+ this.templates = null;
14
+ }
15
+ // Public
16
+ /**
17
+ * Interpolates vars into the provided template as instructions and runs the
18
+ * prompt.
19
+ *
20
+ * @param {PromptOptions} options
21
+ */
22
+ async prompt(options) {
23
+ options = await this.normalizeOptions(options);
24
+ const { input, output, stream, schema } = options;
25
+ const response = await this.runPrompt(options);
26
+ if (!stream) {
27
+ this.debug('Response:', response);
28
+ }
29
+ if (output === 'raw') {
30
+ return response;
31
+ }
32
+ let result;
33
+ if (schema) {
34
+ result = this.getStructuredResponse(response);
35
+ // @ts-ignore
36
+ if (options.hasWrappedSchema) {
37
+ result = result.items;
38
+ }
39
+ }
40
+ else if (output === 'json') {
41
+ result = JSON.parse((0, code_js_1.parseCode)(this.getTextResponse(response)));
42
+ }
43
+ else {
44
+ result = (0, code_js_1.parseCode)(this.getTextResponse(response));
45
+ }
46
+ if (output === 'messages') {
47
+ return {
48
+ result,
49
+ ...this.getMessagesResponse(input, response),
50
+ };
51
+ }
52
+ else {
53
+ return result;
54
+ }
55
+ }
56
+ /**
57
+ * Streams the prompt response.
58
+ *
59
+ * @param {PromptOptions & StreamOptions} options
60
+ * @returns {AsyncIterator}
61
+ */
62
+ async *stream(options) {
63
+ options = await this.normalizeOptions(options);
64
+ const extractor = this.getMessageExtractor(options);
65
+ try {
66
+ const stream = await this.runStream(options);
67
+ // @ts-ignore
68
+ for await (let event of stream) {
69
+ this.debug('Event:', event);
70
+ event = this.normalizeStreamEvent(event);
71
+ if (event) {
72
+ yield event;
73
+ }
74
+ const extractedMessages = extractor?.(event) || [];
75
+ for (let message of extractedMessages) {
76
+ const { key, delta, text, done } = message;
77
+ let extractEvent;
78
+ if (done) {
79
+ extractEvent = {
80
+ type: 'extract:done',
81
+ text,
82
+ key,
83
+ };
84
+ }
85
+ else {
86
+ extractEvent = {
87
+ type: 'extract:delta',
88
+ delta,
89
+ key,
90
+ };
91
+ }
92
+ this.debug('Extract:', extractEvent);
93
+ yield extractEvent;
94
+ }
95
+ }
96
+ }
97
+ catch (error) {
98
+ const { message, code } = error;
99
+ yield {
100
+ type: 'error',
101
+ code,
102
+ message,
103
+ };
104
+ }
105
+ }
106
+ async buildTemplate(options) {
107
+ const template = await this.resolveTemplate(options);
108
+ return (0, templates_js_1.renderTemplate)(template, options);
109
+ }
110
+ // Protected
111
+ runPrompt(options) {
112
+ void options;
113
+ throw new Error('Method not implemented.');
114
+ }
115
+ runStream(options) {
116
+ void options;
117
+ throw new Error('Method not implemented.');
118
+ }
119
+ getTextResponse(response) {
120
+ void response;
121
+ throw new Error('Method not implemented.');
122
+ }
123
+ /**
124
+ * @returns {Object}
125
+ */
126
+ getStructuredResponse(response) {
127
+ void response;
128
+ throw new Error('Method not implemented.');
129
+ }
130
+ /**
131
+ * @returns {Object}
132
+ */
133
+ getMessagesResponse(input, response) {
134
+ void response;
135
+ throw new Error('Method not implemented.');
136
+ }
137
+ /**
138
+ * @returns {Object}
139
+ */
140
+ normalizeStreamEvent(event) {
141
+ void event;
142
+ throw new Error('Method not implemented.');
143
+ }
144
+ // Private
145
+ async normalizeOptions(options) {
146
+ options = {
147
+ input: '',
148
+ output: 'text',
149
+ ...this.options,
150
+ ...options,
151
+ };
152
+ options.input = this.normalizeInput(options);
153
+ options.schema = this.normalizeSchema(options);
154
+ options.instructions ||= await this.resolveInstructions(options);
155
+ return options;
156
+ }
157
+ normalizeInput(options) {
158
+ let { input = '', output } = options;
159
+ if (typeof input === 'string') {
160
+ if (output === 'json') {
161
+ input += '\nOutput only valid JSON.';
162
+ }
163
+ input = [
164
+ {
165
+ role: 'user',
166
+ content: input,
167
+ },
168
+ ];
169
+ }
170
+ return input;
171
+ }
172
+ normalizeSchema(options) {
173
+ let { schema } = options;
174
+ if (!schema) {
175
+ return;
176
+ }
177
+ // Convert to JSON schema.
178
+ schema = schema.toJSON?.() || schema;
179
+ if (schema?.type === 'array') {
180
+ schema = {
181
+ type: 'object',
182
+ properties: {
183
+ items: schema,
184
+ },
185
+ required: ['items'],
186
+ additionalProperties: false,
187
+ };
188
+ options.hasWrappedSchema = true;
189
+ }
190
+ return schema;
191
+ }
192
+ getMessageExtractor(options) {
193
+ const { extractMessages } = options;
194
+ if (!extractMessages) {
195
+ return;
196
+ }
197
+ const messageExtractor = (0, json_js_1.createMessageExtractor)([extractMessages]);
198
+ return (event) => {
199
+ if (event?.type === 'delta') {
200
+ return messageExtractor(event.delta);
201
+ }
202
+ };
203
+ }
204
+ debug(message, arg) {
205
+ if (this.options.debug) {
206
+ // TODO: replace with logger when opentelemetry is removed
207
+ // eslint-disable-next-line
208
+ console.debug(`${message}\n${JSON.stringify(arg, null, 2)}\n`);
209
+ }
210
+ }
211
+ async resolveInstructions(options) {
212
+ if (options.template) {
213
+ const template = await this.resolveTemplate(options);
214
+ return (0, templates_js_1.renderTemplate)(template, options);
215
+ }
216
+ }
217
+ async resolveTemplate(options) {
218
+ const { template } = options;
219
+ await this.loadTemplates();
220
+ return this.templates[template] || template;
221
+ }
222
+ async loadTemplates() {
223
+ const { templates } = this.options;
224
+ this.templates ||= await (0, templates_js_1.loadTemplates)(templates);
225
+ }
131
226
  }
132
227
  exports.default = BaseClient;
133
- function render(template, options) {
134
- let params = {
135
- ...options,
136
- ...options.params
137
- };
138
- params = mapObjects(params);
139
- params = wrapProxy(params);
140
- return _mustache.default.render(template, params);
141
- }
142
-
143
- // Transform arrays and object to versions
144
- // that are more understandable in the context
145
- // of a template that may have meaningful whitespace.
146
- function mapObjects(params) {
147
- const result = {};
148
- for (let [key, value] of Object.entries(params)) {
149
- if (Array.isArray(value)) {
150
- value = mapArray(value);
151
- } else if (typeof value === 'object') {
152
- value = JSON.stringify(value, null, 2);
153
- }
154
- result[key] = value;
155
- }
156
- return result;
157
- }
158
- function mapArray(arr) {
159
- // Only map simple arrays of primitives.
160
- if (typeof arr[0] === 'string') {
161
- arr = arr.map(el => {
162
- return `- ${el}`;
163
- }).join('\n');
164
- }
165
- return arr;
166
- }
167
-
168
- // Wrap params with a proxy object that reports
169
- // as having all properties. If one is accessed
170
- // that does not exist then return the original
171
- // token. This way templates can be partially
172
- // interpolated and re-interpolated later.
173
- function wrapProxy(params) {
174
- return new Proxy(params, {
175
- has() {
176
- return true;
177
- },
178
- get(target, prop) {
179
- if (prop in target) {
180
- return target[prop];
181
- } else {
182
- return `{{{${prop.toString()}}}}`;
183
- }
184
- }
185
- });
186
- }
228
+ /**
229
+ * @typedef {Object} PromptOptions
230
+ * @property {string|PromptMessage[]} input - Input to use.
231
+ * @property {string} [model] - The model to use.
232
+ * @property {boolean} stream - Stream response.
233
+ * @property {Object} [schema] - A JSON schema compatible object that defines the output shape.
234
+ * @property {"raw" | "text" | "json" | "messages"} [output] - The return value type.
235
+ * @property {Object} [params] - Params to be interpolated into the template.
236
+ * May also be passed as additional props to options.
237
+ */
238
+ /**
239
+ * @typedef {Object} StreamOptions
240
+ * @property {string} [extractMessages] - Key in JSON response to extract a message stream from.
241
+ */
242
+ /**
243
+ * @typedef {Object} PromptMessage
244
+ * @property {"system" | "user" | "assistant"} role
245
+ * @property {string} content
246
+ */
@@ -1,101 +1,123 @@
1
1
  "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AnthropicClient = void 0;
7
- var _sdk = _interopRequireDefault(require("@anthropic-ai/sdk"));
8
- var _BaseClient = _interopRequireDefault(require("./BaseClient.js"));
9
- var _util = require("./util.js");
10
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
- const MODELS_URL = 'https://docs.anthropic.com/en/docs/about-claude/models';
12
- const DEFAULT_MODEL = 'claude-3-5-sonnet-latest';
13
- class AnthropicClient extends _BaseClient.default {
14
- constructor(options) {
15
- super(options);
16
- this.client = new _sdk.default({
17
- ...options
18
- });
19
- }
20
-
21
- /**
22
- * Lists available models.
23
- * {@link https://docs.anthropic.com/en/docs/about-claude/models Documentation}
24
- */
25
- async models() {
26
- const {
27
- data
28
- } = await this.client.models.list();
29
- return data.map(o => o.id);
30
- }
31
- async getCompletion(options) {
32
- const {
33
- model = DEFAULT_MODEL,
34
- max_tokens = 2048,
35
- output = 'text',
36
- stream = false,
37
- messages
38
- } = options;
39
- const {
40
- client
41
- } = this;
42
- const {
43
- system,
44
- user
45
- } = splitMessages(messages);
46
- if (!model) {
47
- throw new Error(`No model specified. Available models are here: ${MODELS_URL}.`);
7
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
8
+ const BaseClient_js_1 = __importDefault(require("./BaseClient.js"));
9
+ const DEFAULT_TOKENS = 4096;
10
+ class AnthropicClient extends BaseClient_js_1.default {
11
+ static DEFAULT_MODEL = 'claude-sonnet-4-5';
12
+ constructor(options) {
13
+ super(options);
14
+ this.client = new sdk_1.default(options);
48
15
  }
49
- const response = await client.messages.create({
50
- max_tokens,
51
- messages: user,
52
- system,
53
- model,
54
- stream
55
- });
56
- if (output === 'raw') {
57
- return response;
16
+ /**
17
+ * Lists available models.
18
+ * {@link https://docs.anthropic.com/en/docs/about-claude/models Documentation}
19
+ */
20
+ async models() {
21
+ const { data } = await this.client.models.list();
22
+ return data.map((o) => o.id);
58
23
  }
59
-
60
- // @ts-ignore
61
- const message = response.content[0];
62
- return (0, _util.transformResponse)({
63
- ...options,
64
- messages,
65
- message
66
- });
67
- }
68
- getStreamedChunk(chunk) {
69
- // @ts-ignore
70
- let type;
71
- if (chunk.type === 'content_block_start') {
72
- type = 'start';
73
- } else if (chunk.type === 'content_block_delta') {
74
- type = 'chunk';
75
- } else if (chunk.type === 'message_stop') {
76
- type = 'stop';
24
+ async runPrompt(options) {
25
+ const { input, model, temperature, instructions, stream = false, tokens = DEFAULT_TOKENS, } = options;
26
+ // @ts-ignore
27
+ return await this.client.messages.create({
28
+ model,
29
+ stream,
30
+ temperature,
31
+ max_tokens: tokens,
32
+ system: instructions,
33
+ ...this.getSchemaOptions(options),
34
+ messages: input,
35
+ });
77
36
  }
78
- if (type) {
79
- return {
80
- type,
81
- text: chunk.delta?.text || ''
82
- };
37
+ async runStream(options) {
38
+ return await this.runPrompt({
39
+ ...options,
40
+ output: 'raw',
41
+ stream: true,
42
+ });
43
+ }
44
+ getTextResponse(response) {
45
+ const textBlock = response.content.find((block) => {
46
+ return block.type === 'text';
47
+ });
48
+ return textBlock?.text || null;
49
+ }
50
+ getStructuredResponse(response) {
51
+ const toolBlock = response.content.find((block) => {
52
+ return block.type === 'tool_use';
53
+ });
54
+ return toolBlock?.input || null;
55
+ }
56
+ getMessagesResponse(input, response) {
57
+ return {
58
+ messages: [
59
+ ...input,
60
+ ...response.content
61
+ .filter((item) => {
62
+ return item.type === 'text';
63
+ })
64
+ .map((item) => {
65
+ return {
66
+ role: 'assistant',
67
+ content: item.text,
68
+ };
69
+ }),
70
+ ],
71
+ };
72
+ }
73
+ normalizeStreamEvent(event) {
74
+ let { type } = event;
75
+ if (type === 'content_block_start') {
76
+ return {
77
+ type: 'start',
78
+ };
79
+ }
80
+ else if (type === 'content_block_stop') {
81
+ return {
82
+ type: 'stop',
83
+ };
84
+ }
85
+ else if (type === 'content_block_delta') {
86
+ return {
87
+ type: 'delta',
88
+ text: event.delta.text,
89
+ };
90
+ }
91
+ }
92
+ // Private
93
+ getSchemaOptions(options) {
94
+ const { output } = options;
95
+ if (output?.type) {
96
+ let schema = output;
97
+ if (schema.type === 'array') {
98
+ schema = {
99
+ type: 'object',
100
+ properties: {
101
+ items: schema,
102
+ },
103
+ required: ['items'],
104
+ additionalProperties: false,
105
+ };
106
+ }
107
+ return {
108
+ tools: [
109
+ {
110
+ name: 'schema',
111
+ description: 'Follow the schema for JSON output.',
112
+ input_schema: schema,
113
+ },
114
+ ],
115
+ tool_choice: {
116
+ type: 'tool',
117
+ name: 'schema',
118
+ },
119
+ };
120
+ }
83
121
  }
84
- }
85
122
  }
86
123
  exports.AnthropicClient = AnthropicClient;
87
- function splitMessages(messages) {
88
- const system = [];
89
- const user = [];
90
- for (let message of messages) {
91
- if (message.role === 'system') {
92
- system.push(message);
93
- } else {
94
- user.push(message);
95
- }
96
- }
97
- return {
98
- system: system.join('\n'),
99
- user
100
- };
101
- }