@cobbl-ai/sdk 0.1.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 +516 -0
- package/dist/index.d.mts +348 -0
- package/dist/index.d.ts +348 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +233 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +73 -0
- package/src/__tests__/client.test.ts +650 -0
- package/src/__tests__/errors.test.ts +100 -0
- package/src/__tests__/integration.test.ts +346 -0
- package/src/client.ts +257 -0
- package/src/errors.ts +70 -0
- package/src/index.ts +43 -0
- package/src/types.ts +94 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CobblError } from '../errors'
|
|
2
|
+
|
|
3
|
+
describe('CobblError', () => {
|
|
4
|
+
describe('constructor', () => {
|
|
5
|
+
it('should create an error with message and code', () => {
|
|
6
|
+
const error = new CobblError('Test error', 'INVALID_REQUEST')
|
|
7
|
+
|
|
8
|
+
expect(error).toBeInstanceOf(Error)
|
|
9
|
+
expect(error).toBeInstanceOf(CobblError)
|
|
10
|
+
expect(error.message).toBe('Test error')
|
|
11
|
+
expect(error.code).toBe('INVALID_REQUEST')
|
|
12
|
+
expect(error.name).toBe('CobblError')
|
|
13
|
+
expect(error.details).toBeUndefined()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should create an error with details', () => {
|
|
17
|
+
const details = { field: 'apiKey', reason: 'missing' }
|
|
18
|
+
const error = new CobblError('Invalid config', 'INVALID_CONFIG', details)
|
|
19
|
+
|
|
20
|
+
expect(error.message).toBe('Invalid config')
|
|
21
|
+
expect(error.code).toBe('INVALID_CONFIG')
|
|
22
|
+
expect(error.details).toEqual(details)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should maintain proper stack trace', () => {
|
|
26
|
+
const error = new CobblError('Test error', 'API_ERROR')
|
|
27
|
+
|
|
28
|
+
expect(error.stack).toBeDefined()
|
|
29
|
+
expect(error.stack).toContain('CobblError')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('isCobblError', () => {
|
|
34
|
+
it('should return true for CobblError instances', () => {
|
|
35
|
+
const error = new CobblError('Test', 'NETWORK_ERROR')
|
|
36
|
+
|
|
37
|
+
expect(CobblError.isCobblError(error)).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should return false for standard Error instances', () => {
|
|
41
|
+
const error = new Error('Test')
|
|
42
|
+
|
|
43
|
+
expect(CobblError.isCobblError(error)).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should return false for non-error values', () => {
|
|
47
|
+
expect(CobblError.isCobblError(null)).toBe(false)
|
|
48
|
+
expect(CobblError.isCobblError(undefined)).toBe(false)
|
|
49
|
+
expect(CobblError.isCobblError('error')).toBe(false)
|
|
50
|
+
expect(CobblError.isCobblError({ message: 'error' })).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('toJSON', () => {
|
|
55
|
+
it('should serialize error to JSON', () => {
|
|
56
|
+
const error = new CobblError('Test error', 'SERVER_ERROR', {
|
|
57
|
+
status: 500,
|
|
58
|
+
})
|
|
59
|
+
const json = error.toJSON()
|
|
60
|
+
|
|
61
|
+
expect(json).toEqual({
|
|
62
|
+
name: 'CobblError',
|
|
63
|
+
message: 'Test error',
|
|
64
|
+
code: 'SERVER_ERROR',
|
|
65
|
+
details: { status: 500 },
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should serialize error without details', () => {
|
|
70
|
+
const error = new CobblError('Test error', 'NOT_FOUND')
|
|
71
|
+
const json = error.toJSON()
|
|
72
|
+
|
|
73
|
+
expect(json).toEqual({
|
|
74
|
+
name: 'CobblError',
|
|
75
|
+
message: 'Test error',
|
|
76
|
+
code: 'NOT_FOUND',
|
|
77
|
+
details: undefined,
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('error codes', () => {
|
|
83
|
+
const errorCodes = [
|
|
84
|
+
'INVALID_CONFIG',
|
|
85
|
+
'INVALID_REQUEST',
|
|
86
|
+
'UNAUTHORIZED',
|
|
87
|
+
'NOT_FOUND',
|
|
88
|
+
'RATE_LIMIT_EXCEEDED',
|
|
89
|
+
'SERVER_ERROR',
|
|
90
|
+
'NETWORK_ERROR',
|
|
91
|
+
'API_ERROR',
|
|
92
|
+
] as const
|
|
93
|
+
|
|
94
|
+
it.each(errorCodes)('should support %s error code', (code) => {
|
|
95
|
+
const error = new CobblError('Test', code)
|
|
96
|
+
|
|
97
|
+
expect(error.code).toBe(code)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { CobblClient, CobblError } from '../index'
|
|
2
|
+
|
|
3
|
+
// Mock fetch globally
|
|
4
|
+
const mockFetch = jest.fn()
|
|
5
|
+
global.fetch = mockFetch as any
|
|
6
|
+
|
|
7
|
+
describe('CobblClient Integration Tests', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockFetch.mockClear()
|
|
10
|
+
delete process.env.COBBL_API_URL
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('complete workflow', () => {
|
|
14
|
+
it('should run prompt and submit feedback successfully', async () => {
|
|
15
|
+
const client = new CobblClient({ apiKey: 'test-key' })
|
|
16
|
+
|
|
17
|
+
// Mock runPrompt response
|
|
18
|
+
mockFetch.mockResolvedValueOnce({
|
|
19
|
+
ok: true,
|
|
20
|
+
json: async () => ({
|
|
21
|
+
runId: 'run-123',
|
|
22
|
+
output: 'AI generated summary of Q4 results in a friendly tone.',
|
|
23
|
+
tokenUsage: {
|
|
24
|
+
promptTokens: 50,
|
|
25
|
+
completionTokens: 100,
|
|
26
|
+
totalTokens: 150,
|
|
27
|
+
},
|
|
28
|
+
renderedPrompt:
|
|
29
|
+
'Summarize Q4 Results in a friendly tone for investors',
|
|
30
|
+
promptVersion: {
|
|
31
|
+
id: 'version-1',
|
|
32
|
+
promptId: 'sales-summary',
|
|
33
|
+
version: 1,
|
|
34
|
+
provider: 'openai',
|
|
35
|
+
model: 'gpt-4',
|
|
36
|
+
template: 'Summarize {{topic}} in a {{tone}} tone for {{audience}}',
|
|
37
|
+
variables: ['topic', 'tone', 'audience'],
|
|
38
|
+
isActive: true,
|
|
39
|
+
createdAt: new Date('2024-01-01'),
|
|
40
|
+
metadata: {},
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const runResult = await client.runPrompt('sales_summary', {
|
|
46
|
+
topic: 'Q4 Results',
|
|
47
|
+
tone: 'friendly',
|
|
48
|
+
audience: 'investors',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect(runResult.runId).toBe('run-123')
|
|
52
|
+
expect(runResult.output).toContain('Q4 results')
|
|
53
|
+
expect(runResult.tokenUsage.totalTokens).toBe(150)
|
|
54
|
+
|
|
55
|
+
// Mock submitFeedback response
|
|
56
|
+
mockFetch.mockResolvedValueOnce({
|
|
57
|
+
ok: true,
|
|
58
|
+
json: async () => ({
|
|
59
|
+
feedbackId: 'feedback-123',
|
|
60
|
+
message: 'Feedback submitted successfully',
|
|
61
|
+
}),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const feedbackResult = await client.submitFeedback({
|
|
65
|
+
runId: runResult.runId,
|
|
66
|
+
helpful: 'helpful',
|
|
67
|
+
userFeedback: 'Great summary! Very clear and concise.',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(feedbackResult.feedbackId).toBe('feedback-123')
|
|
71
|
+
expect(mockFetch).toHaveBeenCalledTimes(2)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should handle error in runPrompt and not proceed to feedback', async () => {
|
|
75
|
+
const client = new CobblClient({ apiKey: 'test-key' })
|
|
76
|
+
|
|
77
|
+
mockFetch.mockResolvedValueOnce({
|
|
78
|
+
ok: false,
|
|
79
|
+
status: 404,
|
|
80
|
+
statusText: 'Not Found',
|
|
81
|
+
json: async () => ({ error: 'Prompt not found' }),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
await expect(client.runPrompt('nonexistent-prompt', {})).rejects.toThrow(
|
|
85
|
+
CobblError
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// Should not call submitFeedback after error
|
|
89
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('error handling scenarios', () => {
|
|
94
|
+
it('should handle multiple API errors gracefully', async () => {
|
|
95
|
+
const client = new CobblClient({ apiKey: 'test-key' })
|
|
96
|
+
|
|
97
|
+
// First call fails with 429
|
|
98
|
+
mockFetch.mockResolvedValueOnce({
|
|
99
|
+
ok: false,
|
|
100
|
+
status: 429,
|
|
101
|
+
statusText: 'Too Many Requests',
|
|
102
|
+
json: async () => ({
|
|
103
|
+
error: 'Rate limit exceeded. Please try again later.',
|
|
104
|
+
}),
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await client.runPrompt('test', {})
|
|
109
|
+
fail('Should have thrown')
|
|
110
|
+
} catch (error) {
|
|
111
|
+
expect(error).toBeInstanceOf(CobblError)
|
|
112
|
+
expect((error as CobblError).code).toBe('RATE_LIMIT_EXCEEDED')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Second call fails with 500
|
|
116
|
+
mockFetch.mockResolvedValueOnce({
|
|
117
|
+
ok: false,
|
|
118
|
+
status: 500,
|
|
119
|
+
statusText: 'Internal Server Error',
|
|
120
|
+
json: async () => ({ error: 'Internal server error' }),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await client.runPrompt('test', {})
|
|
125
|
+
fail('Should have thrown')
|
|
126
|
+
} catch (error) {
|
|
127
|
+
expect(error).toBeInstanceOf(CobblError)
|
|
128
|
+
expect((error as CobblError).code).toBe('SERVER_ERROR')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
expect(mockFetch).toHaveBeenCalledTimes(2)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should maintain state across multiple successful calls', async () => {
|
|
135
|
+
const client = new CobblClient({ apiKey: 'test-key' })
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < 3; i++) {
|
|
138
|
+
mockFetch.mockResolvedValueOnce({
|
|
139
|
+
ok: true,
|
|
140
|
+
json: async () => ({
|
|
141
|
+
runId: `run-${i}`,
|
|
142
|
+
output: `Output ${i}`,
|
|
143
|
+
tokenUsage: {
|
|
144
|
+
promptTokens: 10,
|
|
145
|
+
completionTokens: 20,
|
|
146
|
+
totalTokens: 30,
|
|
147
|
+
},
|
|
148
|
+
renderedPrompt: 'test',
|
|
149
|
+
promptVersion: {
|
|
150
|
+
id: 'v1',
|
|
151
|
+
promptId: 'p1',
|
|
152
|
+
version: 1,
|
|
153
|
+
provider: 'openai',
|
|
154
|
+
model: 'gpt-4',
|
|
155
|
+
template: 'test',
|
|
156
|
+
variables: [],
|
|
157
|
+
isActive: true,
|
|
158
|
+
createdAt: new Date(),
|
|
159
|
+
metadata: {},
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const result = await client.runPrompt('test', {})
|
|
165
|
+
expect(result.runId).toBe(`run-${i}`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
expect(mockFetch).toHaveBeenCalledTimes(3)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('exports', () => {
|
|
173
|
+
it('should export CobblClient', () => {
|
|
174
|
+
expect(CobblClient).toBeDefined()
|
|
175
|
+
expect(typeof CobblClient).toBe('function')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should export CobblError', () => {
|
|
179
|
+
expect(CobblError).toBeDefined()
|
|
180
|
+
expect(typeof CobblError).toBe('function')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should be able to create instances of exported classes', () => {
|
|
184
|
+
const client = new CobblClient({ apiKey: 'test' })
|
|
185
|
+
const error = new CobblError('test', 'API_ERROR')
|
|
186
|
+
|
|
187
|
+
expect(client).toBeInstanceOf(CobblClient)
|
|
188
|
+
expect(error).toBeInstanceOf(CobblError)
|
|
189
|
+
expect(error).toBeInstanceOf(Error)
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('API URL configuration', () => {
|
|
194
|
+
it('should use default URL when not configured', async () => {
|
|
195
|
+
const client = new CobblClient({ apiKey: 'test-key' })
|
|
196
|
+
|
|
197
|
+
mockFetch.mockResolvedValueOnce({
|
|
198
|
+
ok: true,
|
|
199
|
+
json: async () => ({
|
|
200
|
+
runId: 'run-123',
|
|
201
|
+
output: 'test',
|
|
202
|
+
tokenUsage: {
|
|
203
|
+
promptTokens: 10,
|
|
204
|
+
completionTokens: 20,
|
|
205
|
+
totalTokens: 30,
|
|
206
|
+
},
|
|
207
|
+
renderedPrompt: 'test',
|
|
208
|
+
promptVersion: {
|
|
209
|
+
id: 'v1',
|
|
210
|
+
promptId: 'p1',
|
|
211
|
+
version: 1,
|
|
212
|
+
provider: 'openai',
|
|
213
|
+
model: 'gpt-4',
|
|
214
|
+
template: 'test',
|
|
215
|
+
variables: [],
|
|
216
|
+
isActive: true,
|
|
217
|
+
createdAt: new Date(),
|
|
218
|
+
metadata: {},
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
await client.runPrompt('test', {})
|
|
224
|
+
|
|
225
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
226
|
+
'https://api.cobbl.ai/prompt/run',
|
|
227
|
+
expect.any(Object)
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should use custom URL from environment', async () => {
|
|
232
|
+
process.env.COBBL_API_URL = 'https://staging.cobbl.ai'
|
|
233
|
+
const client = new CobblClient({ apiKey: 'test-key' })
|
|
234
|
+
|
|
235
|
+
mockFetch.mockResolvedValueOnce({
|
|
236
|
+
ok: true,
|
|
237
|
+
json: async () => ({
|
|
238
|
+
runId: 'run-123',
|
|
239
|
+
output: 'test',
|
|
240
|
+
tokenUsage: {
|
|
241
|
+
promptTokens: 10,
|
|
242
|
+
completionTokens: 20,
|
|
243
|
+
totalTokens: 30,
|
|
244
|
+
},
|
|
245
|
+
renderedPrompt: 'test',
|
|
246
|
+
promptVersion: {
|
|
247
|
+
id: 'v1',
|
|
248
|
+
promptId: 'p1',
|
|
249
|
+
version: 1,
|
|
250
|
+
provider: 'openai',
|
|
251
|
+
model: 'gpt-4',
|
|
252
|
+
template: 'test',
|
|
253
|
+
variables: [],
|
|
254
|
+
isActive: true,
|
|
255
|
+
createdAt: new Date(),
|
|
256
|
+
metadata: {},
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
await client.runPrompt('test', {})
|
|
262
|
+
|
|
263
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
264
|
+
'https://staging.cobbl.ai/prompt/run',
|
|
265
|
+
expect.any(Object)
|
|
266
|
+
)
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe('type safety', () => {
|
|
271
|
+
it('should enforce correct types for runPrompt input', async () => {
|
|
272
|
+
const client = new CobblClient({ apiKey: 'test-key' })
|
|
273
|
+
|
|
274
|
+
mockFetch.mockResolvedValueOnce({
|
|
275
|
+
ok: true,
|
|
276
|
+
json: async () => ({
|
|
277
|
+
runId: 'run-123',
|
|
278
|
+
output: 'test',
|
|
279
|
+
tokenUsage: {
|
|
280
|
+
promptTokens: 10,
|
|
281
|
+
completionTokens: 20,
|
|
282
|
+
totalTokens: 30,
|
|
283
|
+
},
|
|
284
|
+
renderedPrompt: 'test',
|
|
285
|
+
promptVersion: {
|
|
286
|
+
id: 'v1',
|
|
287
|
+
promptId: 'p1',
|
|
288
|
+
version: 1,
|
|
289
|
+
provider: 'openai',
|
|
290
|
+
model: 'gpt-4',
|
|
291
|
+
template: 'test',
|
|
292
|
+
variables: [],
|
|
293
|
+
isActive: true,
|
|
294
|
+
createdAt: new Date(),
|
|
295
|
+
metadata: {},
|
|
296
|
+
},
|
|
297
|
+
}),
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// Should accept various input types
|
|
301
|
+
await client.runPrompt('test', {
|
|
302
|
+
string: 'value',
|
|
303
|
+
number: 123,
|
|
304
|
+
boolean: true,
|
|
305
|
+
nested: { key: 'value' },
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
expect(mockFetch).toHaveBeenCalled()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should enforce correct types for feedback submission', async () => {
|
|
312
|
+
const client = new CobblClient({ apiKey: 'test-key' })
|
|
313
|
+
|
|
314
|
+
mockFetch.mockResolvedValueOnce({
|
|
315
|
+
ok: true,
|
|
316
|
+
json: async () => ({
|
|
317
|
+
feedbackId: 'fb-123',
|
|
318
|
+
message: 'Success',
|
|
319
|
+
}),
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// Should accept 'helpful' or 'not_helpful'
|
|
323
|
+
await client.submitFeedback({
|
|
324
|
+
runId: 'run-123',
|
|
325
|
+
helpful: 'helpful',
|
|
326
|
+
userFeedback: 'Great!',
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
mockFetch.mockResolvedValueOnce({
|
|
330
|
+
ok: true,
|
|
331
|
+
json: async () => ({
|
|
332
|
+
feedbackId: 'fb-124',
|
|
333
|
+
message: 'Success',
|
|
334
|
+
}),
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
await client.submitFeedback({
|
|
338
|
+
runId: 'run-124',
|
|
339
|
+
helpful: 'not_helpful',
|
|
340
|
+
userFeedback: 'Needs improvement',
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
expect(mockFetch).toHaveBeenCalledTimes(2)
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
})
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CobblClient - SDK for the Cobbl platform
|
|
3
|
+
*
|
|
4
|
+
* Provides methods to run prompts and submit feedback to the Cobbl API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PromptInput, PromptVersionClient } from '@prompti/shared'
|
|
8
|
+
import type {
|
|
9
|
+
RunPromptResponse,
|
|
10
|
+
SubmitFeedbackResponse,
|
|
11
|
+
CobblConfig,
|
|
12
|
+
TokenUsage,
|
|
13
|
+
FeedbackSubmission,
|
|
14
|
+
} from './types'
|
|
15
|
+
import { CobblError } from './errors'
|
|
16
|
+
|
|
17
|
+
const REQUEST_TIMEOUT_MS = 30_000
|
|
18
|
+
|
|
19
|
+
export class CobblClient {
|
|
20
|
+
private readonly apiKey: string
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the Cobbl SDK client
|
|
24
|
+
*
|
|
25
|
+
* @param config - Configuration object
|
|
26
|
+
* @param config.apiKey - Your Cobbl API key
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const client = new CobblClient({
|
|
31
|
+
* apiKey: process.env.COBBL_API_KEY
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
constructor(config: CobblConfig) {
|
|
36
|
+
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
37
|
+
throw new CobblError('API key is required', 'INVALID_CONFIG')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.apiKey = config.apiKey
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Execute a prompt with the given input variables
|
|
45
|
+
*
|
|
46
|
+
* @param promptSlug - The unique slug identifier for the prompt
|
|
47
|
+
* @param input - Input variables to populate the prompt template
|
|
48
|
+
* @returns Promise containing the prompt execution results
|
|
49
|
+
*
|
|
50
|
+
* @throws {CobblError} When the request fails or API returns an error
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const result = await client.runPrompt('sales_summary', {
|
|
55
|
+
* topic: 'Q4 Results',
|
|
56
|
+
* tone: 'friendly',
|
|
57
|
+
* audience: 'investors'
|
|
58
|
+
* })
|
|
59
|
+
*
|
|
60
|
+
* console.log(result.output) // AI-generated response
|
|
61
|
+
* console.log(result.runId) // Save this to link feedback later
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
async runPrompt(
|
|
65
|
+
promptSlug: string,
|
|
66
|
+
input: PromptInput
|
|
67
|
+
): Promise<RunPromptResponse> {
|
|
68
|
+
if (!promptSlug || promptSlug.trim().length === 0) {
|
|
69
|
+
throw new CobblError('promptSlug is required', 'INVALID_REQUEST')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const response = await this.makeRequest('/prompt/run', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
promptSlug: promptSlug.trim(),
|
|
77
|
+
input,
|
|
78
|
+
}),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
await this.handleErrorResponse(response)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const data = (await response.json()) as {
|
|
86
|
+
runId: string
|
|
87
|
+
output: string
|
|
88
|
+
tokenUsage: TokenUsage
|
|
89
|
+
renderedPrompt: string
|
|
90
|
+
promptVersion: PromptVersionClient
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
runId: data.runId,
|
|
95
|
+
output: data.output,
|
|
96
|
+
tokenUsage: data.tokenUsage,
|
|
97
|
+
renderedPrompt: data.renderedPrompt,
|
|
98
|
+
promptVersion: data.promptVersion,
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error instanceof CobblError) {
|
|
102
|
+
throw error
|
|
103
|
+
}
|
|
104
|
+
throw new CobblError(
|
|
105
|
+
`Failed to run prompt: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
106
|
+
'NETWORK_ERROR'
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Submit user feedback for a prompt run
|
|
113
|
+
*
|
|
114
|
+
* @param feedback - Feedback submission data
|
|
115
|
+
* @param feedback.runId - The run ID from a previous runPrompt call
|
|
116
|
+
* @param feedback.helpful - Whether the output was helpful ('helpful' or 'not_helpful')
|
|
117
|
+
* @param feedback.userFeedback - Detailed feedback message from the user
|
|
118
|
+
* @returns Promise containing the created feedback ID
|
|
119
|
+
*
|
|
120
|
+
* @throws {CobblError} When the request fails or API returns an error
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* await client.submitFeedback({
|
|
125
|
+
* runId: result.runId,
|
|
126
|
+
* helpful: 'not_helpful',
|
|
127
|
+
* userFeedback: 'The response was too formal and lengthy'
|
|
128
|
+
* })
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
async submitFeedback(
|
|
132
|
+
feedback: FeedbackSubmission
|
|
133
|
+
): Promise<SubmitFeedbackResponse> {
|
|
134
|
+
if (!feedback.runId || feedback.runId.trim().length === 0) {
|
|
135
|
+
throw new CobblError('runId is required', 'INVALID_REQUEST')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!feedback.userFeedback || feedback.userFeedback.trim().length === 0) {
|
|
139
|
+
throw new CobblError('userFeedback is required', 'INVALID_REQUEST')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
!feedback.helpful ||
|
|
144
|
+
!['helpful', 'not_helpful'].includes(feedback.helpful)
|
|
145
|
+
) {
|
|
146
|
+
throw new CobblError(
|
|
147
|
+
'helpful must be either "helpful" or "not_helpful"',
|
|
148
|
+
'INVALID_REQUEST'
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const response = await this.makeRequest('/feedback', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
runId: feedback.runId.trim(),
|
|
157
|
+
helpful: feedback.helpful,
|
|
158
|
+
userFeedback: feedback.userFeedback.trim(),
|
|
159
|
+
}),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
await this.handleErrorResponse(response)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const data = (await response.json()) as {
|
|
167
|
+
feedbackId: string
|
|
168
|
+
message: string
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
feedbackId: data.feedbackId,
|
|
173
|
+
message: data.message,
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error instanceof CobblError) {
|
|
177
|
+
throw error
|
|
178
|
+
}
|
|
179
|
+
throw new CobblError(
|
|
180
|
+
`Failed to submit feedback: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
181
|
+
'NETWORK_ERROR'
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Make an HTTP request to the Cobbl API
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
private async makeRequest(
|
|
191
|
+
path: string,
|
|
192
|
+
options: RequestInit
|
|
193
|
+
): Promise<Response> {
|
|
194
|
+
const baseUrl = process.env.COBBL_API_URL || 'https://api.cobbl.ai'
|
|
195
|
+
const url = `${baseUrl}${path}`
|
|
196
|
+
|
|
197
|
+
const controller = new AbortController()
|
|
198
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const response = await fetch(url, {
|
|
202
|
+
...options,
|
|
203
|
+
headers: {
|
|
204
|
+
'Content-Type': 'application/json',
|
|
205
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
206
|
+
...options.headers,
|
|
207
|
+
},
|
|
208
|
+
signal: controller.signal,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
return response
|
|
212
|
+
} finally {
|
|
213
|
+
clearTimeout(timeoutId)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Handle error responses from the API
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
private async handleErrorResponse(response: Response): Promise<never> {
|
|
222
|
+
let errorData: any
|
|
223
|
+
try {
|
|
224
|
+
errorData = await response.json()
|
|
225
|
+
} catch {
|
|
226
|
+
throw new CobblError(
|
|
227
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
228
|
+
'API_ERROR',
|
|
229
|
+
{ status: response.status }
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const message = errorData.error || errorData.message || 'Unknown error'
|
|
234
|
+
const details = errorData.details
|
|
235
|
+
|
|
236
|
+
switch (response.status) {
|
|
237
|
+
case 400:
|
|
238
|
+
throw new CobblError(message, 'INVALID_REQUEST', details)
|
|
239
|
+
case 401:
|
|
240
|
+
throw new CobblError(message, 'UNAUTHORIZED', details)
|
|
241
|
+
case 404:
|
|
242
|
+
throw new CobblError(message, 'NOT_FOUND', details)
|
|
243
|
+
case 429:
|
|
244
|
+
throw new CobblError(message, 'RATE_LIMIT_EXCEEDED', details)
|
|
245
|
+
case 500:
|
|
246
|
+
case 502:
|
|
247
|
+
case 503:
|
|
248
|
+
case 504:
|
|
249
|
+
throw new CobblError(message, 'SERVER_ERROR', details)
|
|
250
|
+
default:
|
|
251
|
+
throw new CobblError(message, 'API_ERROR', {
|
|
252
|
+
status: response.status,
|
|
253
|
+
...details,
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|