@reqvet-sdk/sdk 2.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.
- package/CHANGELOG.md +23 -0
- package/README.md +179 -0
- package/SDK_REFERENCE.md +516 -0
- package/SECURITY.md +119 -0
- package/package.json +42 -0
- package/src/index.d.ts +261 -0
- package/src/index.js +444 -0
- package/src/webhooks.d.ts +14 -0
- package/src/webhooks.js +52 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// @reqvet/sdk — Type definitions
|
|
2
|
+
|
|
3
|
+
export interface ReqVetOptions {
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
pollInterval?: number;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UploadResult {
|
|
10
|
+
/** Canonical path to the uploaded audio in ReqVet storage */
|
|
11
|
+
audio_file: string;
|
|
12
|
+
/** Alias for audio_file (for DX / backward compatibility) */
|
|
13
|
+
path: string;
|
|
14
|
+
size_bytes: number;
|
|
15
|
+
content_type: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface JobResult {
|
|
19
|
+
job_id: string;
|
|
20
|
+
status: 'pending' | 'transcribing' | 'generating' | 'completed' | 'failed' | 'amending';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Field Extraction Types ─────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** Supported field types for structured extraction */
|
|
26
|
+
export type FieldType = 'string' | 'text' | 'number' | 'boolean' | 'array' | 'html';
|
|
27
|
+
|
|
28
|
+
/** A single field definition in an organization's field_schema */
|
|
29
|
+
export interface FieldDefinition {
|
|
30
|
+
/** Unique key in snake_case (e.g. "espece", "poids", "traitements") */
|
|
31
|
+
key: string;
|
|
32
|
+
/** Human-readable label (e.g. "Espèce", "Poids (kg)") */
|
|
33
|
+
label: string;
|
|
34
|
+
/** Data type for this field */
|
|
35
|
+
type: FieldType;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extracted fields from a generation.
|
|
40
|
+
* Keys match the field_schema definitions on the organization.
|
|
41
|
+
* Values are typed according to the field type, or null if not mentioned.
|
|
42
|
+
*/
|
|
43
|
+
export type ExtractedFields = Record<string, string | number | boolean | string[] | null>;
|
|
44
|
+
|
|
45
|
+
// ─── Report & Job Types ─────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export interface ReqVetReport {
|
|
48
|
+
jobId: string;
|
|
49
|
+
html: string;
|
|
50
|
+
/** Structured fields extracted from the transcription (null if org has no field_schema) */
|
|
51
|
+
fields: ExtractedFields | null;
|
|
52
|
+
transcription: string;
|
|
53
|
+
animalName: string;
|
|
54
|
+
cost: {
|
|
55
|
+
transcription_usd: number;
|
|
56
|
+
generation_usd: number;
|
|
57
|
+
total_usd: number;
|
|
58
|
+
};
|
|
59
|
+
metadata: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface Template {
|
|
63
|
+
id: string;
|
|
64
|
+
org_id: string | null;
|
|
65
|
+
name: string;
|
|
66
|
+
description: string;
|
|
67
|
+
content: string;
|
|
68
|
+
is_default: boolean;
|
|
69
|
+
created_at: string;
|
|
70
|
+
updated_at: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Params ──────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export interface GenerateReportParams {
|
|
76
|
+
audio: Blob | Buffer | File;
|
|
77
|
+
animalName: string;
|
|
78
|
+
templateId: string;
|
|
79
|
+
fileName?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Optional webhook URL.
|
|
82
|
+
* If provided, ReqVet will POST the final result to this endpoint when ready.
|
|
83
|
+
*/
|
|
84
|
+
callbackUrl?: string;
|
|
85
|
+
metadata?: Record<string, unknown>;
|
|
86
|
+
/** Optional: extra instructions injected into the generation prompt (kept separate from metadata) */
|
|
87
|
+
extraInstructions?: string;
|
|
88
|
+
/**
|
|
89
|
+
* When true, generateReport() will poll until completion and return the final report.
|
|
90
|
+
* When false (default), it returns immediately after creating the job.
|
|
91
|
+
*/
|
|
92
|
+
waitForResult?: boolean;
|
|
93
|
+
/** Called on each poll with current status (only when waitForResult=true). */
|
|
94
|
+
onStatus?: (status: string) => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CreateJobParams {
|
|
98
|
+
audioFile: string;
|
|
99
|
+
animalName: string;
|
|
100
|
+
templateId: string;
|
|
101
|
+
/** Optional webhook URL (falls back to the org default webhook_url server-side if omitted). */
|
|
102
|
+
callbackUrl?: string;
|
|
103
|
+
metadata?: Record<string, unknown>;
|
|
104
|
+
extraInstructions?: string; // 👈 add
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface CreateTemplateParams {
|
|
108
|
+
name: string;
|
|
109
|
+
content: string;
|
|
110
|
+
description?: string;
|
|
111
|
+
is_default?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ListJobsOptions {
|
|
115
|
+
limit?: number;
|
|
116
|
+
offset?: number;
|
|
117
|
+
status?: 'pending' | 'transcribing' | 'generating' | 'completed' | 'failed' | 'amending';
|
|
118
|
+
sort?: 'created_at' | 'updated_at';
|
|
119
|
+
order?: 'asc' | 'desc';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface JobSummary {
|
|
123
|
+
id: string;
|
|
124
|
+
status: 'pending' | 'transcribing' | 'generating' | 'completed' | 'failed' | 'amending';
|
|
125
|
+
animal_name: string;
|
|
126
|
+
template_id: string;
|
|
127
|
+
metadata: Record<string, unknown>;
|
|
128
|
+
created_at: string;
|
|
129
|
+
updated_at: string;
|
|
130
|
+
error_message?: string | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface ListJobsResult {
|
|
134
|
+
jobs: JobSummary[];
|
|
135
|
+
pagination: {
|
|
136
|
+
total: number;
|
|
137
|
+
limit: number;
|
|
138
|
+
offset: number;
|
|
139
|
+
has_more: boolean;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface RegenerateOptions {
|
|
144
|
+
extraInstructions?: string;
|
|
145
|
+
templateId?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface RegenerateResult {
|
|
149
|
+
job_id: string;
|
|
150
|
+
status: string;
|
|
151
|
+
result: {
|
|
152
|
+
html: string;
|
|
153
|
+
fields?: ExtractedFields;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type ReformulationPurpose =
|
|
158
|
+
| 'owner'
|
|
159
|
+
| 'referral'
|
|
160
|
+
| 'summary'
|
|
161
|
+
| 'custom'
|
|
162
|
+
| 'diagnostic_hypothesis';
|
|
163
|
+
|
|
164
|
+
export interface ReformulateParams {
|
|
165
|
+
purpose: ReformulationPurpose;
|
|
166
|
+
customInstructions?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface ReqVetReformulation {
|
|
170
|
+
id: string;
|
|
171
|
+
job_id: string;
|
|
172
|
+
purpose: ReformulationPurpose;
|
|
173
|
+
html: string;
|
|
174
|
+
custom_instructions?: string;
|
|
175
|
+
cost: {
|
|
176
|
+
model: string;
|
|
177
|
+
prompt_tokens: number;
|
|
178
|
+
completion_tokens: number;
|
|
179
|
+
cost_usd: number;
|
|
180
|
+
};
|
|
181
|
+
created_at: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Client ──────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
export declare class ReqVetError extends Error {
|
|
187
|
+
status: number;
|
|
188
|
+
body: unknown;
|
|
189
|
+
constructor(message: string, status: number, body: unknown);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export declare class ReqVet {
|
|
193
|
+
constructor(apiKey: string, options?: ReqVetOptions);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Convenience helper.
|
|
197
|
+
* - Default: upload → create job → return {job_id, status}
|
|
198
|
+
* - If waitForResult=true: upload → create job → poll → return final report
|
|
199
|
+
*/
|
|
200
|
+
generateReport(params: GenerateReportParams & { waitForResult: true }): Promise<ReqVetReport>;
|
|
201
|
+
generateReport(params: GenerateReportParams & { waitForResult?: false }): Promise<JobResult>;
|
|
202
|
+
generateReport(params: GenerateReportParams): Promise<JobResult | ReqVetReport>;
|
|
203
|
+
|
|
204
|
+
/** Upload an audio file */
|
|
205
|
+
uploadAudio(audio: Blob | Buffer | File, fileName?: string): Promise<UploadResult>;
|
|
206
|
+
|
|
207
|
+
/** Create a generation job */
|
|
208
|
+
createJob(params: CreateJobParams): Promise<JobResult>;
|
|
209
|
+
|
|
210
|
+
/** List jobs with optional pagination and filtering */
|
|
211
|
+
listJobs(options?: ListJobsOptions): Promise<ListJobsResult>;
|
|
212
|
+
|
|
213
|
+
/** Get job status */
|
|
214
|
+
getJob(jobId: string): Promise<Record<string, unknown>>;
|
|
215
|
+
|
|
216
|
+
/** Wait for a job to finish */
|
|
217
|
+
waitForJob(jobId: string, onStatus?: (status: string) => void): Promise<ReqVetReport>;
|
|
218
|
+
|
|
219
|
+
/** Regenerate a completed report */
|
|
220
|
+
regenerateJob(jobId: string, options?: RegenerateOptions): Promise<RegenerateResult>;
|
|
221
|
+
|
|
222
|
+
/** Add an audio complement to a completed job */
|
|
223
|
+
amendJob(
|
|
224
|
+
jobId: string,
|
|
225
|
+
params: { audioFile: string; templateId?: string },
|
|
226
|
+
): Promise<{
|
|
227
|
+
job_id: string;
|
|
228
|
+
status: 'amending';
|
|
229
|
+
amendment_number: number;
|
|
230
|
+
message: string;
|
|
231
|
+
}>;
|
|
232
|
+
|
|
233
|
+
/** Reformulate a completed report for a specific audience */
|
|
234
|
+
reformulateReport(jobId: string, params: ReformulateParams): Promise<ReqVetReformulation>;
|
|
235
|
+
|
|
236
|
+
/** List all reformulations for a job */
|
|
237
|
+
listReformulations(jobId: string): Promise<{ reformulations: ReqVetReformulation[] }>;
|
|
238
|
+
|
|
239
|
+
/** List templates */
|
|
240
|
+
listTemplates(): Promise<{ custom: Template[]; system: Template[] }>;
|
|
241
|
+
|
|
242
|
+
/** Get a template */
|
|
243
|
+
getTemplate(templateId: string): Promise<Template>;
|
|
244
|
+
|
|
245
|
+
/** Create a template */
|
|
246
|
+
createTemplate(params: CreateTemplateParams): Promise<Template>;
|
|
247
|
+
|
|
248
|
+
/** Update a template */
|
|
249
|
+
updateTemplate(
|
|
250
|
+
templateId: string,
|
|
251
|
+
updates: Partial<Pick<CreateTemplateParams, 'name' | 'content' | 'description' | 'is_default'>>,
|
|
252
|
+
): Promise<Template>;
|
|
253
|
+
|
|
254
|
+
/** Delete a template */
|
|
255
|
+
deleteTemplate(templateId: string): Promise<{ success: boolean }>;
|
|
256
|
+
|
|
257
|
+
/** Health check */
|
|
258
|
+
health(): Promise<{ status: string; services: Record<string, string> }>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export default ReqVet;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// @reqvet/sdk — Official JavaScript SDK
|
|
2
|
+
// Zero dependencies. Works in Node.js 18+ and modern browsers.
|
|
3
|
+
|
|
4
|
+
class ReqVetError extends Error {
|
|
5
|
+
constructor(message, status, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ReqVetError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function mimeFromFileName(fileName) {
|
|
14
|
+
const name = (fileName || '').toLowerCase();
|
|
15
|
+
const ext = name.split('.').pop();
|
|
16
|
+
switch (ext) {
|
|
17
|
+
case 'mp3':
|
|
18
|
+
return 'audio/mpeg';
|
|
19
|
+
case 'wav':
|
|
20
|
+
return 'audio/wav';
|
|
21
|
+
case 'webm':
|
|
22
|
+
return 'audio/webm';
|
|
23
|
+
case 'ogg':
|
|
24
|
+
return 'audio/ogg';
|
|
25
|
+
case 'm4a':
|
|
26
|
+
return 'audio/mp4';
|
|
27
|
+
case 'aac':
|
|
28
|
+
return 'audio/aac';
|
|
29
|
+
case 'flac':
|
|
30
|
+
return 'audio/flac';
|
|
31
|
+
default:
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class ReqVet {
|
|
37
|
+
/**
|
|
38
|
+
* Create a ReqVet client.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} apiKey - Your API key (rqv_live_...)
|
|
41
|
+
* @param {Object} [options]
|
|
42
|
+
* @param {string} [options.baseUrl] - API base URL (default: https://api.reqvet.com)
|
|
43
|
+
* @param {number} [options.pollInterval] - Polling interval in ms (default: 5000)
|
|
44
|
+
* @param {number} [options.timeout] - Max wait time in ms (default: 300000 = 5 min)
|
|
45
|
+
*/
|
|
46
|
+
constructor(apiKey, options = {}) {
|
|
47
|
+
if (!apiKey?.startsWith('rqv_')) {
|
|
48
|
+
throw new Error('Invalid API key. Must start with "rqv_".');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.apiKey = apiKey;
|
|
52
|
+
this.baseUrl = (options.baseUrl || 'https://api.reqvet.com').replace(/\/$/, '');
|
|
53
|
+
this.pollInterval = options.pollInterval || 5000;
|
|
54
|
+
this.timeout = options.timeout || 300_000;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Core: Generate a report (upload + job + poll) ─────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate a veterinary report from an audio file.
|
|
61
|
+
* Handles the full flow: upload → create job → wait for result.
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} params
|
|
64
|
+
* @param {Blob|Buffer|File} params.audio - Audio file
|
|
65
|
+
* @param {string} params.animalName - Name of the animal
|
|
66
|
+
* @param {string} params.templateId - Template UUID
|
|
67
|
+
* @param {string} [params.fileName] - File name (default: audio.webm)
|
|
68
|
+
* @param {string} [params.callbackUrl] - Webhook URL for result
|
|
69
|
+
* @param {Object} [params.metadata] - Custom data passed through
|
|
70
|
+
* @param {Function} [params.onStatus] - Called on each poll with current status
|
|
71
|
+
* @returns {Promise<ReqVetReport>}
|
|
72
|
+
*/
|
|
73
|
+
async generateReport({
|
|
74
|
+
audio,
|
|
75
|
+
animalName,
|
|
76
|
+
templateId,
|
|
77
|
+
fileName,
|
|
78
|
+
callbackUrl,
|
|
79
|
+
metadata,
|
|
80
|
+
extraInstructions,
|
|
81
|
+
onStatus,
|
|
82
|
+
waitForResult = false,
|
|
83
|
+
}) {
|
|
84
|
+
// Convenience helper.
|
|
85
|
+
// - Default: webhook-first (returns immediately after job creation). If callbackUrl is provided,
|
|
86
|
+
// ReqVet will POST the final result to that URL when ready.
|
|
87
|
+
// - Optional: polling mode (waitForResult=true) which returns the final report.
|
|
88
|
+
|
|
89
|
+
// Step 1: Upload
|
|
90
|
+
const upload = await this.uploadAudio(audio, fileName);
|
|
91
|
+
|
|
92
|
+
// Step 2: Create job (pipeline starts server-side)
|
|
93
|
+
const job = await this.createJob({
|
|
94
|
+
audioFile: upload.audio_file,
|
|
95
|
+
animalName,
|
|
96
|
+
templateId,
|
|
97
|
+
callbackUrl,
|
|
98
|
+
metadata, // 👈 ne touche pas metadata
|
|
99
|
+
extraInstructions, // 👈 passe le param séparé
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Step 3: Either return immediately, or poll until completion.
|
|
103
|
+
if (waitForResult) {
|
|
104
|
+
return this.waitForJob(job.job_id, onStatus);
|
|
105
|
+
}
|
|
106
|
+
return job;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Upload ────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Upload an audio file to ReqVet storage.
|
|
113
|
+
*
|
|
114
|
+
* @param {Blob|Buffer|File} audio - The audio file
|
|
115
|
+
* @param {string} [fileName] - File name
|
|
116
|
+
* @returns {Promise<{audio_file: string, size_bytes: number}>}
|
|
117
|
+
*/
|
|
118
|
+
async uploadAudio(audio, fileName) {
|
|
119
|
+
const form = new FormData();
|
|
120
|
+
|
|
121
|
+
if (typeof Blob !== 'undefined' && audio instanceof Blob) {
|
|
122
|
+
form.append('file', audio, fileName || 'audio.webm');
|
|
123
|
+
} else if (Buffer.isBuffer(audio)) {
|
|
124
|
+
// Node.js Buffer → Blob
|
|
125
|
+
const finalName = fileName || 'audio.webm';
|
|
126
|
+
const mime = mimeFromFileName(finalName);
|
|
127
|
+
const blob = mime ? new Blob([audio], { type: mime }) : new Blob([audio]);
|
|
128
|
+
form.append('file', blob, finalName);
|
|
129
|
+
} else {
|
|
130
|
+
throw new ReqVetError('audio must be a Blob, File, or Buffer', 0, null);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const res = await this._fetch('POST', '/api/v1/upload', null, form);
|
|
134
|
+
// DX: provide a stable alias `path` for consumers.
|
|
135
|
+
if (res && typeof res === 'object' && res.audio_file && !res.path) {
|
|
136
|
+
return { ...res, path: res.audio_file };
|
|
137
|
+
}
|
|
138
|
+
return res;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Jobs ──────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a new CR generation job.
|
|
145
|
+
*
|
|
146
|
+
* @param {Object} params
|
|
147
|
+
* @param {string} params.audioFile - Path from upload
|
|
148
|
+
* @param {string} params.animalName
|
|
149
|
+
* @param {string} params.templateId
|
|
150
|
+
* @param {string} [params.callbackUrl]
|
|
151
|
+
* @param {Object} [params.metadata]
|
|
152
|
+
* @returns {Promise<{job_id: string, status: string}>}
|
|
153
|
+
*/
|
|
154
|
+
async createJob({ audioFile, animalName, templateId, callbackUrl, metadata, extraInstructions }) {
|
|
155
|
+
return this._fetch('POST', '/api/v1/jobs', {
|
|
156
|
+
audio_file: audioFile,
|
|
157
|
+
animal_name: animalName,
|
|
158
|
+
template_id: templateId,
|
|
159
|
+
callback_url: callbackUrl,
|
|
160
|
+
metadata,
|
|
161
|
+
extra_instructions: extraInstructions,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get the status and result of a job.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} jobId
|
|
169
|
+
* @returns {Promise<ReqVetJob>}
|
|
170
|
+
*/
|
|
171
|
+
async getJob(jobId) {
|
|
172
|
+
return this._fetch('GET', `/api/v1/jobs/${jobId}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* List jobs with optional pagination and filtering.
|
|
177
|
+
*
|
|
178
|
+
* @param {Object} [options]
|
|
179
|
+
* @param {number} [options.limit] - Number of jobs to return (1-100, default 20)
|
|
180
|
+
* @param {number} [options.offset] - Pagination offset (default 0)
|
|
181
|
+
* @param {string} [options.status] - Filter by status (pending|transcribing|generating|completed|failed|amending)
|
|
182
|
+
* @param {string} [options.sort] - Sort field: 'created_at' (default) or 'updated_at'
|
|
183
|
+
* @param {string} [options.order] - Sort direction: 'desc' (default) or 'asc'
|
|
184
|
+
* @returns {Promise<{jobs: Object[], pagination: {total: number, limit: number, offset: number, has_more: boolean}}>}
|
|
185
|
+
*/
|
|
186
|
+
async listJobs({ limit, offset, status, sort, order } = {}) {
|
|
187
|
+
const params = new URLSearchParams();
|
|
188
|
+
if (limit != null) params.set('limit', String(limit));
|
|
189
|
+
if (offset != null) params.set('offset', String(offset));
|
|
190
|
+
if (status != null) params.set('status', status);
|
|
191
|
+
if (sort != null) params.set('sort', sort);
|
|
192
|
+
if (order != null) params.set('order', order);
|
|
193
|
+
const qs = params.toString();
|
|
194
|
+
return this._fetch('GET', `/api/v1/jobs${qs ? `?${qs}` : ''}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Wait for a job to complete by polling.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} jobId
|
|
201
|
+
* @param {Function} [onStatus] - Called on each poll
|
|
202
|
+
* @returns {Promise<ReqVetReport>}
|
|
203
|
+
*/
|
|
204
|
+
async waitForJob(jobId, onStatus) {
|
|
205
|
+
const deadline = Date.now() + this.timeout;
|
|
206
|
+
|
|
207
|
+
while (Date.now() < deadline) {
|
|
208
|
+
await this._sleep(this.pollInterval);
|
|
209
|
+
|
|
210
|
+
const job = await this.getJob(jobId);
|
|
211
|
+
|
|
212
|
+
if (onStatus) onStatus(job.status);
|
|
213
|
+
|
|
214
|
+
if (job.status === 'completed') {
|
|
215
|
+
return {
|
|
216
|
+
jobId: job.job_id,
|
|
217
|
+
html: job.result?.html || '',
|
|
218
|
+
fields: job.result?.fields || null,
|
|
219
|
+
transcription: job.transcription || '',
|
|
220
|
+
animalName: job.animal_name,
|
|
221
|
+
cost: job.cost || {},
|
|
222
|
+
metadata: job.metadata || {},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (job.status === 'failed') {
|
|
227
|
+
throw new ReqVetError(`Job failed: ${job.error || 'Unknown error'}`, 0, job);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
throw new ReqVetError('Timeout waiting for job to complete', 0, { jobId });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Regenerate a completed report with new instructions.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} jobId
|
|
238
|
+
* @param {Object} [options]
|
|
239
|
+
* @param {string} [options.extraInstructions]
|
|
240
|
+
* @param {string} [options.templateId]
|
|
241
|
+
* @returns {Promise<{job_id: string, html: string}>}
|
|
242
|
+
*/
|
|
243
|
+
async regenerateJob(jobId, options = {}) {
|
|
244
|
+
return this._fetch('POST', `/api/v1/jobs/${jobId}/regenerate`, {
|
|
245
|
+
extra_instructions: options.extraInstructions,
|
|
246
|
+
template_id: options.templateId,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ─── Amendments (audio complements) ────────────────────────
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Add an audio complement to a completed job.
|
|
254
|
+
*
|
|
255
|
+
* The new audio is transcribed and merged with the existing
|
|
256
|
+
* transcription(s). The CR is then regenerated with the full context.
|
|
257
|
+
* Supports multiple amendments — each one appends.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} jobId - The completed job ID
|
|
260
|
+
* @param {Object} params
|
|
261
|
+
* @param {string} params.audioFile - Path from signed upload
|
|
262
|
+
* @param {string} [params.templateId] - Optional: switch to a different template
|
|
263
|
+
* @returns {Promise<{job_id: string, status: string, amendment_number: number}>}
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* // Upload the complement audio first
|
|
267
|
+
* const { audio_file } = await reqvet.uploadAudio(newAudioBlob, 'complement.webm');
|
|
268
|
+
*
|
|
269
|
+
* // Submit the amendment
|
|
270
|
+
* const amend = await reqvet.amendJob(jobId, { audioFile: audio_file });
|
|
271
|
+
* // amend.status === 'amending'
|
|
272
|
+
*
|
|
273
|
+
* // Poll for completion (same as initial job)
|
|
274
|
+
* const updated = await reqvet.waitForJob(jobId);
|
|
275
|
+
* // updated.html now includes the complement info
|
|
276
|
+
*/
|
|
277
|
+
async amendJob(jobId, { audioFile, templateId } = {}) {
|
|
278
|
+
if (!audioFile) throw new Error('audioFile is required for amendJob');
|
|
279
|
+
return this._fetch('POST', `/api/v1/jobs/${jobId}/amend`, {
|
|
280
|
+
audio_file: audioFile,
|
|
281
|
+
template_id: templateId,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ─── Reformulations ─────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Generate a reformulation of a completed report.
|
|
289
|
+
*
|
|
290
|
+
* Produces an alternative version of the CR adapted to a specific audience.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} jobId - The completed job ID
|
|
293
|
+
* @param {Object} params
|
|
294
|
+
* @param {string} params.purpose - 'owner' | 'referral' | 'summary' | 'custom'
|
|
295
|
+
* @param {string} [params.customInstructions] - Required when purpose is 'custom'
|
|
296
|
+
* @returns {Promise<ReqVetReformulation>}
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* // Simplified version for the pet owner
|
|
300
|
+
* const ownerVersion = await reqvet.reformulateReport(jobId, { purpose: 'owner' });
|
|
301
|
+
*
|
|
302
|
+
* // Clinical summary for specialist referral
|
|
303
|
+
* const referral = await reqvet.reformulateReport(jobId, { purpose: 'referral' });
|
|
304
|
+
*
|
|
305
|
+
* // Short internal summary
|
|
306
|
+
* const summary = await reqvet.reformulateReport(jobId, { purpose: 'summary' });
|
|
307
|
+
*
|
|
308
|
+
* // Custom reformulation
|
|
309
|
+
* const custom = await reqvet.reformulateReport(jobId, {
|
|
310
|
+
* purpose: 'custom',
|
|
311
|
+
* customInstructions: 'Reformule en insistant sur le pronostic et le suivi nutritionnel',
|
|
312
|
+
* });
|
|
313
|
+
*/
|
|
314
|
+
async reformulateReport(jobId, { purpose, customInstructions } = {}) {
|
|
315
|
+
if (!purpose) {
|
|
316
|
+
throw new ReqVetError(
|
|
317
|
+
'purpose is required. Must be one of: owner, referral, summary, custom, diagnostic_hypothesis',
|
|
318
|
+
0,
|
|
319
|
+
null,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return this._fetch('POST', `/api/v1/jobs/${jobId}/reformulate`, {
|
|
323
|
+
purpose,
|
|
324
|
+
custom_instructions: customInstructions,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* List all reformulations for a completed job.
|
|
330
|
+
*
|
|
331
|
+
* @param {string} jobId
|
|
332
|
+
* @returns {Promise<{reformulations: ReqVetReformulation[]}>}
|
|
333
|
+
*/
|
|
334
|
+
async listReformulations(jobId) {
|
|
335
|
+
return this._fetch('GET', `/api/v1/jobs/${jobId}/reformulate`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── Templates ─────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* List all accessible templates.
|
|
342
|
+
* @returns {Promise<{custom: Template[], system: Template[]}>}
|
|
343
|
+
*/
|
|
344
|
+
async listTemplates() {
|
|
345
|
+
return this._fetch('GET', '/api/v1/templates');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get a template by ID.
|
|
350
|
+
* @param {string} templateId
|
|
351
|
+
*/
|
|
352
|
+
async getTemplate(templateId) {
|
|
353
|
+
return this._fetch('GET', `/api/v1/templates/${templateId}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Create a new template.
|
|
358
|
+
* @param {Object} params
|
|
359
|
+
* @param {string} params.name
|
|
360
|
+
* @param {string} params.content - The prompt instructions
|
|
361
|
+
* @param {string} [params.description]
|
|
362
|
+
*/
|
|
363
|
+
async createTemplate({ name, content, description }) {
|
|
364
|
+
return this._fetch('POST', '/api/v1/templates', {
|
|
365
|
+
name,
|
|
366
|
+
content,
|
|
367
|
+
description,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Update a template.
|
|
373
|
+
* @param {string} templateId
|
|
374
|
+
* @param {Object} updates
|
|
375
|
+
* @param {string} [updates.name]
|
|
376
|
+
* @param {string} [updates.content]
|
|
377
|
+
* @param {string} [updates.description]
|
|
378
|
+
*/
|
|
379
|
+
async updateTemplate(templateId, updates) {
|
|
380
|
+
return this._fetch('PUT', `/api/v1/templates/${templateId}`, updates);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Delete a template.
|
|
385
|
+
* @param {string} templateId
|
|
386
|
+
*/
|
|
387
|
+
async deleteTemplate(templateId) {
|
|
388
|
+
return this._fetch('DELETE', `/api/v1/templates/${templateId}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── Health ────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Check API health.
|
|
395
|
+
* @returns {Promise<{status: string, services: Object}>}
|
|
396
|
+
*/
|
|
397
|
+
async health() {
|
|
398
|
+
return this._fetch('GET', '/api/v1/health');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ─── Internal ──────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
async _fetch(method, path, body, formData) {
|
|
404
|
+
const url = `${this.baseUrl}${path}`;
|
|
405
|
+
const headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
406
|
+
const opts = { method, headers };
|
|
407
|
+
|
|
408
|
+
const isRealFormData =
|
|
409
|
+
typeof FormData !== 'undefined' && formData && formData instanceof FormData;
|
|
410
|
+
|
|
411
|
+
if (isRealFormData) {
|
|
412
|
+
opts.body = formData;
|
|
413
|
+
} else if (body) {
|
|
414
|
+
headers['Content-Type'] = 'application/json';
|
|
415
|
+
// Strip null/undefined values to keep payloads clean
|
|
416
|
+
const cleaned = Object.fromEntries(
|
|
417
|
+
Object.entries(body).filter(([, v]) => v !== null && v !== undefined),
|
|
418
|
+
);
|
|
419
|
+
opts.body = JSON.stringify(cleaned);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const response = await fetch(url, opts);
|
|
423
|
+
|
|
424
|
+
let data;
|
|
425
|
+
try {
|
|
426
|
+
data = await response.json();
|
|
427
|
+
} catch {
|
|
428
|
+
throw new ReqVetError(`Non-JSON response from ${path}`, response.status, null);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!response.ok) {
|
|
432
|
+
throw new ReqVetError(data?.error || `HTTP ${response.status}`, response.status, data);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return data;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
_sleep(ms) {
|
|
439
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export { ReqVet, ReqVetError };
|
|
444
|
+
export default ReqVet;
|