@brownandroot/api 1.2.1 → 2.0.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/dist/index.js CHANGED
@@ -1,12 +1,69 @@
1
+ import { getLlmLogs, chat as llmChat } from './llm.remote.js';
2
+ import { listDocuments, uploadDocument, deleteDocument, searchDocuments } from './rag.remote.js';
3
+ export class ApiHubError extends Error {
4
+ status;
5
+ path;
6
+ url;
7
+ responseBody;
8
+ retriable;
9
+ service;
10
+ constructor(options) {
11
+ super(options.message);
12
+ this.name = 'ApiHubError';
13
+ this.status = options.status;
14
+ this.path = options.path;
15
+ this.url = options.url;
16
+ this.responseBody = options.responseBody;
17
+ this.retriable = options.retriable;
18
+ this.service = options.service ?? 'apihub';
19
+ }
20
+ }
1
21
  export class ApiHubClient {
2
22
  baseUrl;
3
23
  apiKey;
4
24
  cacheTtl;
25
+ timeoutMs;
26
+ retryCount;
27
+ onError;
5
28
  cache = new Map();
6
29
  constructor(options) {
7
30
  this.baseUrl = options.baseUrl.replace(/\/+$/, '');
8
31
  this.apiKey = options.apiKey;
9
32
  this.cacheTtl = options.cacheTtl ?? 5 * 60 * 1000;
33
+ this.timeoutMs = options.timeoutMs ?? 10_000;
34
+ this.retryCount = options.retryCount ?? 2;
35
+ this.onError = options.onError;
36
+ }
37
+ isRetriableStatus(status) {
38
+ return status === 429 || status === 502 || status === 503 || status === 504;
39
+ }
40
+ createApiHubError(options) {
41
+ const error = new ApiHubError({ ...options, service: 'apihub' });
42
+ this.onError?.(error);
43
+ if (typeof window === 'undefined') {
44
+ console.error('[APIHubError]', {
45
+ status: error.status,
46
+ path: error.path,
47
+ url: error.url,
48
+ retriable: error.retriable,
49
+ responseBody: error.responseBody,
50
+ message: error.message,
51
+ });
52
+ }
53
+ return error;
54
+ }
55
+ async fetchWithTimeout(url, init) {
56
+ const controller = new AbortController();
57
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
58
+ try {
59
+ return await fetch(url, { ...init, signal: controller.signal });
60
+ }
61
+ finally {
62
+ clearTimeout(timeout);
63
+ }
64
+ }
65
+ async delay(ms) {
66
+ await new Promise((resolve) => setTimeout(resolve, ms));
10
67
  }
11
68
  /** Clear all cached responses, or a specific path */
12
69
  clearCache(path) {
@@ -32,22 +89,62 @@ export class ApiHubClient {
32
89
  return data;
33
90
  }
34
91
  async request(path) {
35
- let res;
36
- try {
37
- res = await fetch(`${this.baseUrl}${path}`, {
38
- headers: { 'x-api-key': this.apiKey },
39
- });
40
- }
41
- catch {
42
- throw new Error(`APIHub unavailable: ${this.baseUrl}${path}`);
43
- }
44
- if (!res.ok) {
92
+ const url = `${this.baseUrl}${path}`;
93
+ for (let attempt = 0; attempt <= this.retryCount; attempt++) {
94
+ let res;
95
+ try {
96
+ res = await this.fetchWithTimeout(url, {
97
+ headers: { 'x-api-key': this.apiKey },
98
+ });
99
+ }
100
+ catch (error) {
101
+ const retriable = attempt < this.retryCount;
102
+ if (retriable) {
103
+ const jitter = Math.floor(Math.random() * 100);
104
+ await this.delay(150 * Math.pow(2, attempt) + jitter);
105
+ continue;
106
+ }
107
+ const message = error instanceof Error && error.name === 'AbortError'
108
+ ? `APIHub request timed out after ${this.timeoutMs}ms`
109
+ : 'APIHub unavailable';
110
+ throw this.createApiHubError({
111
+ message,
112
+ status: null,
113
+ path,
114
+ url,
115
+ responseBody: null,
116
+ retriable: false,
117
+ });
118
+ }
119
+ if (res.ok) {
120
+ return res.json();
121
+ }
45
122
  const body = await res.json().catch(() => null);
123
+ const retriable = this.isRetriableStatus(res.status);
124
+ if (retriable && attempt < this.retryCount) {
125
+ const jitter = Math.floor(Math.random() * 100);
126
+ await this.delay(150 * Math.pow(2, attempt) + jitter);
127
+ continue;
128
+ }
46
129
  const message = (body && typeof body === 'object' && 'error' in body && typeof body.error === 'string' ? body.error : null) ??
47
130
  `Request failed: ${res.status} ${res.statusText}`;
48
- throw new Error(message);
131
+ throw this.createApiHubError({
132
+ message,
133
+ status: res.status,
134
+ path,
135
+ url,
136
+ responseBody: body,
137
+ retriable,
138
+ });
49
139
  }
50
- return res.json();
140
+ throw this.createApiHubError({
141
+ message: 'APIHub request failed after retries',
142
+ status: null,
143
+ path,
144
+ url,
145
+ responseBody: null,
146
+ retriable: false,
147
+ });
51
148
  }
52
149
  /** Get all employees */
53
150
  async getEmployees() {
@@ -125,6 +222,11 @@ export class ApiHubClient {
125
222
  }
126
223
  /** Send a chat completion request to the LLM */
127
224
  async chat(request) {
225
+ const { enableRag, ...rest } = request;
226
+ const payload = { ...rest };
227
+ if (enableRag !== undefined) {
228
+ payload.enableRag = enableRag;
229
+ }
128
230
  let res;
129
231
  try {
130
232
  res = await fetch(`${this.baseUrl}/llm/chat`, {
@@ -133,7 +235,7 @@ export class ApiHubClient {
133
235
  'x-api-key': this.apiKey,
134
236
  'Content-Type': 'application/json',
135
237
  },
136
- body: JSON.stringify(request),
238
+ body: JSON.stringify(payload),
137
239
  });
138
240
  }
139
241
  catch {
@@ -152,6 +254,12 @@ export class ApiHubClient {
152
254
  // -----------------------------------------------------------------------
153
255
  /** Start a streaming chat session — returns the raw SSE Response for proxying */
154
256
  async chatStream(request) {
257
+ const { enableRag, ...rest } = request;
258
+ const payload = { ...rest };
259
+ if (enableRag !== undefined) {
260
+ // Streaming endpoint currently expects useRag in payload.
261
+ payload.useRag = enableRag;
262
+ }
155
263
  let res;
156
264
  try {
157
265
  res = await fetch(`${this.baseUrl}/ai/chat/stream`, {
@@ -160,7 +268,7 @@ export class ApiHubClient {
160
268
  'x-api-key': this.apiKey,
161
269
  'Content-Type': 'application/json',
162
270
  },
163
- body: JSON.stringify(request),
271
+ body: JSON.stringify(payload),
164
272
  });
165
273
  }
166
274
  catch {
@@ -333,3 +441,14 @@ export class ApiHubClient {
333
441
  return this.request(`/jobtypejobsteps/${encodeURIComponent(id)}`);
334
442
  }
335
443
  }
444
+ export { employees, workorders, costcodes, paytypes, businessUnits, jobtypejobsteps, clearCache } from './cache.js';
445
+ export const llm = {
446
+ getLogs: getLlmLogs,
447
+ chat: llmChat,
448
+ };
449
+ export const rag = {
450
+ list: listDocuments,
451
+ upload: uploadDocument,
452
+ delete: deleteDocument,
453
+ search: searchDocuments,
454
+ };
@@ -1,3 +1,3 @@
1
1
  export declare const getJobtypejobsteps: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").Jobtypejobstep[]>;
2
2
  export declare const getJobtypejobstepsDropdown: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").DropdownOption[]>;
3
- export declare const getJobtypejobstep: import("@sveltejs/kit").RemoteQueryFunction<string, import("./index.js").Jobtypejobstep>;
3
+ export declare const getJobtypejobstep: import("@sveltejs/kit").RemoteQueryFunction<string, import("./index.js").Jobtypejobstep | null>;
@@ -1,6 +1,21 @@
1
1
  import { query } from '$app/server';
2
2
  import { z } from 'zod';
3
3
  import { getClient } from './client.js';
4
+ function isNotFoundError(error) {
5
+ if (!(error instanceof Error))
6
+ return false;
7
+ return /not found/i.test(error.message);
8
+ }
9
+ async function asNullable(fetcher) {
10
+ try {
11
+ return await fetcher();
12
+ }
13
+ catch (error) {
14
+ if (isNotFoundError(error))
15
+ return null;
16
+ throw error;
17
+ }
18
+ }
4
19
  export const getJobtypejobsteps = query(async () => getClient().getJobtypejobsteps());
5
20
  export const getJobtypejobstepsDropdown = query(async () => getClient().getJobtypejobstepsDropdown());
6
- export const getJobtypejobstep = query(z.string(), async (id) => getClient().getJobtypejobstep(id));
21
+ export const getJobtypejobstep = query(z.string(), async (id) => asNullable(() => getClient().getJobtypejobstep(id)));
@@ -9,4 +9,5 @@ export declare const chat: import("@sveltejs/kit").RemoteCommand<{
9
9
  function?: string | undefined;
10
10
  temperature?: number | undefined;
11
11
  maxTokens?: number | undefined;
12
+ enableRag?: boolean | undefined;
12
13
  }, Promise<import("./index.js").ChatResponse>>;
@@ -12,6 +12,7 @@ const chatRequestSchema = z.object({
12
12
  function: z.string().optional(),
13
13
  temperature: z.number().min(0).max(2).optional(),
14
14
  maxTokens: z.number().int().positive().optional(),
15
+ enableRag: z.boolean().optional(),
15
16
  });
16
17
  export const getLlmLogs = query(async () => getClient().getLlmLogs());
17
18
  export const chat = command(chatRequestSchema, async (request) => getClient().chat(request));
@@ -1,3 +1,3 @@
1
1
  export declare const getPaytypes: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").Paytype[]>;
2
2
  export declare const getPaytypesDropdown: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").PaytypeDropdownOption[]>;
3
- export declare const getPaytype: import("@sveltejs/kit").RemoteQueryFunction<string, import("./index.js").Paytype>;
3
+ export declare const getPaytype: import("@sveltejs/kit").RemoteQueryFunction<string, import("./index.js").Paytype | null>;
@@ -1,6 +1,21 @@
1
1
  import { query } from '$app/server';
2
2
  import { z } from 'zod';
3
3
  import { getClient } from './client.js';
4
+ function isNotFoundError(error) {
5
+ if (!(error instanceof Error))
6
+ return false;
7
+ return /not found/i.test(error.message);
8
+ }
9
+ async function asNullable(fetcher) {
10
+ try {
11
+ return await fetcher();
12
+ }
13
+ catch (error) {
14
+ if (isNotFoundError(error))
15
+ return null;
16
+ throw error;
17
+ }
18
+ }
4
19
  export const getPaytypes = query(async () => getClient().getPaytypes());
5
20
  export const getPaytypesDropdown = query(async () => getClient().getPaytypesDropdown());
6
- export const getPaytype = query(z.string(), async (id) => getClient().getPaytype(id));
21
+ export const getPaytype = query(z.string(), async (id) => asNullable(() => getClient().getPaytype(id)));
@@ -1,3 +1,3 @@
1
1
  export declare const getWorkorders: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").Workorder[]>;
2
2
  export declare const getWorkordersDropdown: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").DropdownOption[]>;
3
- export declare const getWorkorder: import("@sveltejs/kit").RemoteQueryFunction<string, import("./index.js").Workorder>;
3
+ export declare const getWorkorder: import("@sveltejs/kit").RemoteQueryFunction<string, import("./index.js").Workorder | null>;
@@ -1,6 +1,21 @@
1
1
  import { query } from '$app/server';
2
2
  import { z } from 'zod';
3
3
  import { getClient } from './client.js';
4
+ function isNotFoundError(error) {
5
+ if (!(error instanceof Error))
6
+ return false;
7
+ return /not found/i.test(error.message);
8
+ }
9
+ async function asNullable(fetcher) {
10
+ try {
11
+ return await fetcher();
12
+ }
13
+ catch (error) {
14
+ if (isNotFoundError(error))
15
+ return null;
16
+ throw error;
17
+ }
18
+ }
4
19
  export const getWorkorders = query(async () => getClient().getWorkorders());
5
20
  export const getWorkordersDropdown = query(async () => getClient().getWorkordersDropdown());
6
- export const getWorkorder = query(z.string(), async (id) => getClient().getWorkorder(id));
21
+ export const getWorkorder = query(z.string(), async (id) => asNullable(() => getClient().getWorkorder(id)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brownandroot/api",
3
- "version": "1.2.1",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,42 +13,6 @@
13
13
  ".": {
14
14
  "types": "./dist/index.d.ts",
15
15
  "default": "./dist/index.js"
16
- },
17
- "./cache": {
18
- "types": "./dist/cache.d.ts",
19
- "default": "./dist/cache.js"
20
- },
21
- "./employees": {
22
- "types": "./dist/employees.remote.d.ts",
23
- "default": "./dist/employees.remote.js"
24
- },
25
- "./businessUnits": {
26
- "types": "./dist/businessUnits.remote.d.ts",
27
- "default": "./dist/businessUnits.remote.js"
28
- },
29
- "./costcodes": {
30
- "types": "./dist/costcodes.remote.d.ts",
31
- "default": "./dist/costcodes.remote.js"
32
- },
33
- "./paytypes": {
34
- "types": "./dist/paytypes.remote.d.ts",
35
- "default": "./dist/paytypes.remote.js"
36
- },
37
- "./workorders": {
38
- "types": "./dist/workorders.remote.d.ts",
39
- "default": "./dist/workorders.remote.js"
40
- },
41
- "./jobtypejobsteps": {
42
- "types": "./dist/jobtypejobsteps.remote.d.ts",
43
- "default": "./dist/jobtypejobsteps.remote.js"
44
- },
45
- "./llm": {
46
- "types": "./dist/llm.remote.d.ts",
47
- "default": "./dist/llm.remote.js"
48
- },
49
- "./rag": {
50
- "types": "./dist/rag.remote.d.ts",
51
- "default": "./dist/rag.remote.js"
52
16
  }
53
17
  },
54
18
  "files": [