fugu-sdk 1.0.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.
- package/README.md +127 -0
- package/dist/client.d.ts +28 -0
- package/dist/client.js +33 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +47 -0
- package/dist/http.d.ts +37 -0
- package/dist/http.js +84 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +30 -0
- package/dist/resources/credentials.d.ts +13 -0
- package/dist/resources/credentials.js +38 -0
- package/dist/resources/documents.d.ts +23 -0
- package/dist/resources/documents.js +45 -0
- package/dist/resources/query.d.ts +19 -0
- package/dist/resources/query.js +63 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +2 -0
- package/examples/basic-query.ts +52 -0
- package/package.json +21 -0
- package/src/client.ts +44 -0
- package/src/errors.ts +51 -0
- package/src/http.ts +107 -0
- package/src/index.ts +6 -0
- package/src/resources/credentials.ts +39 -0
- package/src/resources/documents.ts +58 -0
- package/src/resources/query.ts +63 -0
- package/src/types.ts +83 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# fugu-sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the FUGU routed RAG API. Class-based, zero runtime
|
|
4
|
+
dependencies (uses the platform `fetch`), works in Node 18+ and browsers.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install fugu-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { FuguClient } from 'fugu-sdk';
|
|
16
|
+
|
|
17
|
+
const client = new FuguClient({ apiKey: 'fugu_sk_...' });
|
|
18
|
+
|
|
19
|
+
const result = await client.query.execute('what does FUGU combine to answer questions');
|
|
20
|
+
console.log(result.answer);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Pass `baseUrl` to point at a non-default deployment:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
new FuguClient({ apiKey: '...', baseUrl: 'https://api.example.com/api' });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Resources
|
|
30
|
+
|
|
31
|
+
The client exposes three resource groups:
|
|
32
|
+
|
|
33
|
+
- `client.query` — run queries (`execute`, `stream`)
|
|
34
|
+
- `client.documents` — manage documents (`list`, `get`, `upload`, `delete`, `retry`)
|
|
35
|
+
- `client.credentials` — manage the org's BYOK LLM credential (`get`, `listModels`, `save`, `remove`)
|
|
36
|
+
|
|
37
|
+
### Queries
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const result = await client.query.execute('my question', {
|
|
41
|
+
strategy: 'hybrid', // 'vector_only' | 'graph_only' | 'hybrid' | 'auto'
|
|
42
|
+
top_k: 10,
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Streaming (async generator of SSE events):
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
for await (const event of client.query.stream('my question')) {
|
|
50
|
+
switch (event.type) {
|
|
51
|
+
case 'meta':
|
|
52
|
+
console.log('sources:', event.results);
|
|
53
|
+
break;
|
|
54
|
+
case 'delta':
|
|
55
|
+
process.stdout.write(event.text);
|
|
56
|
+
break;
|
|
57
|
+
case 'done':
|
|
58
|
+
console.log('\ncitations:', event.citations);
|
|
59
|
+
break;
|
|
60
|
+
case 'error':
|
|
61
|
+
console.error('stream error:', event.message);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Documents
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const { document_id } = await client.documents.upload({
|
|
71
|
+
filename: 'report.pdf',
|
|
72
|
+
data: fs.readFileSync('report.pdf'), // Buffer, Uint8Array, or Blob
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const docs = await client.documents.list({ limit: 20, offset: 0 });
|
|
76
|
+
const doc = await client.documents.get(document_id);
|
|
77
|
+
await client.documents.retry(document_id);
|
|
78
|
+
await client.documents.delete(document_id);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Credentials (BYOK)
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const models = await client.credentials.listModels('openrouter');
|
|
85
|
+
|
|
86
|
+
await client.credentials.save({
|
|
87
|
+
provider: 'openrouter',
|
|
88
|
+
model: models.find((m) => m.free)?.id ?? models[0].id,
|
|
89
|
+
apiKey: 'sk-or-...',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const credential = await client.credentials.get(); // { provider, model, keyLastFour, lastVerifiedAt } | null
|
|
93
|
+
await client.credentials.remove();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Errors
|
|
97
|
+
|
|
98
|
+
All non-2xx responses throw `FuguApiError` (with `status` and `code`).
|
|
99
|
+
Two conditions get typed subclasses so you can branch without string-matching:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { FuguApiError, BYOKRequiredError, QuotaExceededError } from 'fugu-sdk';
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await client.query.execute('...');
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err instanceof BYOKRequiredError) {
|
|
108
|
+
// org has no LLM credential configured (HTTP 409)
|
|
109
|
+
} else if (err instanceof QuotaExceededError) {
|
|
110
|
+
// monthly query quota exceeded (HTTP 429)
|
|
111
|
+
} else if (err instanceof FuguApiError) {
|
|
112
|
+
console.error(err.status, err.code, err.message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Example
|
|
118
|
+
|
|
119
|
+
See [examples/basic-query.ts](examples/basic-query.ts). Run with:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm install
|
|
123
|
+
npm run build
|
|
124
|
+
FUGU_API_KEY=fugu_sk_... node -r ts-node/register examples/basic-query.ts
|
|
125
|
+
# or, after building:
|
|
126
|
+
FUGU_API_KEY=fugu_sk_... node -e "require('./dist/index')" # see examples for a full script
|
|
127
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { QueryResource } from './resources/query';
|
|
2
|
+
import { DocumentsResource } from './resources/documents';
|
|
3
|
+
import { CredentialsResource } from './resources/credentials';
|
|
4
|
+
export interface FuguClientOptions {
|
|
5
|
+
/** API key issued from the FUGU dashboard, of the form `fugu_sk_...`. */
|
|
6
|
+
apiKey: string;
|
|
7
|
+
/** Base API URL. Defaults to `http://localhost:3001/api` for local dev. */
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
/** Custom `fetch` implementation (useful for testing or non-standard runtimes). */
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Entry point for the FUGU SDK.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const client = new FuguClient({ apiKey: 'fugu_sk_...' });
|
|
17
|
+
* const result = await client.query.execute('what does FUGU combine to answer questions');
|
|
18
|
+
* for await (const event of client.query.stream('...')) { ... }
|
|
19
|
+
* await client.documents.upload({ filename: 'doc.pdf', data: buffer });
|
|
20
|
+
* const credential = await client.credentials.get();
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare class FuguClient {
|
|
24
|
+
readonly query: QueryResource;
|
|
25
|
+
readonly documents: DocumentsResource;
|
|
26
|
+
readonly credentials: CredentialsResource;
|
|
27
|
+
constructor(options: FuguClientOptions);
|
|
28
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FuguClient = void 0;
|
|
4
|
+
const http_1 = require("./http");
|
|
5
|
+
const query_1 = require("./resources/query");
|
|
6
|
+
const documents_1 = require("./resources/documents");
|
|
7
|
+
const credentials_1 = require("./resources/credentials");
|
|
8
|
+
/**
|
|
9
|
+
* Entry point for the FUGU SDK.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const client = new FuguClient({ apiKey: 'fugu_sk_...' });
|
|
13
|
+
* const result = await client.query.execute('what does FUGU combine to answer questions');
|
|
14
|
+
* for await (const event of client.query.stream('...')) { ... }
|
|
15
|
+
* await client.documents.upload({ filename: 'doc.pdf', data: buffer });
|
|
16
|
+
* const credential = await client.credentials.get();
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
class FuguClient {
|
|
20
|
+
constructor(options) {
|
|
21
|
+
if (!options.apiKey)
|
|
22
|
+
throw new Error('FuguClient requires an apiKey');
|
|
23
|
+
const http = new http_1.HttpClient({
|
|
24
|
+
apiKey: options.apiKey,
|
|
25
|
+
baseUrl: (options.baseUrl ?? 'http://localhost:3001/api').replace(/\/+$/, ''),
|
|
26
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
27
|
+
});
|
|
28
|
+
this.query = new query_1.QueryResource(http);
|
|
29
|
+
this.documents = new documents_1.DocumentsResource(http);
|
|
30
|
+
this.credentials = new credentials_1.CredentialsResource(http);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.FuguClient = FuguClient;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ApiErrorBody {
|
|
2
|
+
error?: {
|
|
3
|
+
message?: string;
|
|
4
|
+
code?: string;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
/** Base error for any non-2xx response from the FUGU API. */
|
|
8
|
+
export declare class FuguApiError extends Error {
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly code?: string | undefined;
|
|
11
|
+
constructor(message: string, status: number, code?: string | undefined);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Thrown when a query is rejected because the organization has no active
|
|
15
|
+
* LLM credential configured (BYOK). Distinct subclass so callers can branch
|
|
16
|
+
* on `instanceof BYOKRequiredError` instead of string-matching `code`.
|
|
17
|
+
*/
|
|
18
|
+
export declare class BYOKRequiredError extends FuguApiError {
|
|
19
|
+
constructor(message: string, status: number);
|
|
20
|
+
}
|
|
21
|
+
/** Thrown when the organization's monthly query quota has been exceeded. */
|
|
22
|
+
export declare class QuotaExceededError extends FuguApiError {
|
|
23
|
+
constructor(message: string, status: number);
|
|
24
|
+
}
|
|
25
|
+
/** Builds the right error subclass from a parsed error-response body. */
|
|
26
|
+
export declare function errorFromResponse(status: number, body: ApiErrorBody | null): FuguApiError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QuotaExceededError = exports.BYOKRequiredError = exports.FuguApiError = void 0;
|
|
4
|
+
exports.errorFromResponse = errorFromResponse;
|
|
5
|
+
/** Base error for any non-2xx response from the FUGU API. */
|
|
6
|
+
class FuguApiError extends Error {
|
|
7
|
+
constructor(message, status, code) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = 'FuguApiError';
|
|
12
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.FuguApiError = FuguApiError;
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when a query is rejected because the organization has no active
|
|
18
|
+
* LLM credential configured (BYOK). Distinct subclass so callers can branch
|
|
19
|
+
* on `instanceof BYOKRequiredError` instead of string-matching `code`.
|
|
20
|
+
*/
|
|
21
|
+
class BYOKRequiredError extends FuguApiError {
|
|
22
|
+
constructor(message, status) {
|
|
23
|
+
super(message, status, 'BYOK_REQUIRED');
|
|
24
|
+
this.name = 'BYOKRequiredError';
|
|
25
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.BYOKRequiredError = BYOKRequiredError;
|
|
29
|
+
/** Thrown when the organization's monthly query quota has been exceeded. */
|
|
30
|
+
class QuotaExceededError extends FuguApiError {
|
|
31
|
+
constructor(message, status) {
|
|
32
|
+
super(message, status, 'QUOTA_EXCEEDED');
|
|
33
|
+
this.name = 'QuotaExceededError';
|
|
34
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.QuotaExceededError = QuotaExceededError;
|
|
38
|
+
/** Builds the right error subclass from a parsed error-response body. */
|
|
39
|
+
function errorFromResponse(status, body) {
|
|
40
|
+
const message = body?.error?.message ?? `Request failed with status ${status}`;
|
|
41
|
+
const code = body?.error?.code;
|
|
42
|
+
if (code === 'BYOK_REQUIRED')
|
|
43
|
+
return new BYOKRequiredError(message, status);
|
|
44
|
+
if (code === 'QUOTA_EXCEEDED')
|
|
45
|
+
return new QuotaExceededError(message, status);
|
|
46
|
+
return new FuguApiError(message, status, code);
|
|
47
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface HttpClientOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
fetchImpl: typeof fetch;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Thin wrapper around `fetch` shared by all resource sub-clients. Handles
|
|
8
|
+
* auth header injection, base URL joining, JSON (de)serialization, and
|
|
9
|
+
* translating non-2xx responses into typed `FuguApiError`s.
|
|
10
|
+
*/
|
|
11
|
+
export declare class HttpClient {
|
|
12
|
+
private readonly opts;
|
|
13
|
+
constructor(opts: HttpClientOptions);
|
|
14
|
+
private url;
|
|
15
|
+
private authHeaders;
|
|
16
|
+
requestJson<T>(path: string, init: {
|
|
17
|
+
method: string;
|
|
18
|
+
body?: unknown;
|
|
19
|
+
query?: Record<string, string | number | undefined>;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}): Promise<T>;
|
|
22
|
+
requestForm<T>(path: string, init: {
|
|
23
|
+
method: string;
|
|
24
|
+
form: FormData;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}): Promise<T>;
|
|
27
|
+
requestVoid(path: string, init: {
|
|
28
|
+
method: string;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
/** Raw streaming request used by the SSE query stream. Returns the fetch Response. */
|
|
32
|
+
requestStream(path: string, init: {
|
|
33
|
+
method: string;
|
|
34
|
+
body?: unknown;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}): Promise<Response>;
|
|
37
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HttpClient = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
/**
|
|
6
|
+
* Thin wrapper around `fetch` shared by all resource sub-clients. Handles
|
|
7
|
+
* auth header injection, base URL joining, JSON (de)serialization, and
|
|
8
|
+
* translating non-2xx responses into typed `FuguApiError`s.
|
|
9
|
+
*/
|
|
10
|
+
class HttpClient {
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
this.opts = opts;
|
|
13
|
+
}
|
|
14
|
+
url(path) {
|
|
15
|
+
return `${this.opts.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
16
|
+
}
|
|
17
|
+
authHeaders(extra) {
|
|
18
|
+
return {
|
|
19
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
20
|
+
...extra,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async requestJson(path, init) {
|
|
24
|
+
let finalUrl = this.url(path);
|
|
25
|
+
if (init.query) {
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
for (const [k, v] of Object.entries(init.query)) {
|
|
28
|
+
if (v !== undefined)
|
|
29
|
+
params.set(k, String(v));
|
|
30
|
+
}
|
|
31
|
+
const qs = params.toString();
|
|
32
|
+
if (qs)
|
|
33
|
+
finalUrl += `?${qs}`;
|
|
34
|
+
}
|
|
35
|
+
const res = await this.opts.fetchImpl(finalUrl, {
|
|
36
|
+
method: init.method,
|
|
37
|
+
headers: this.authHeaders(init.body !== undefined ? { 'Content-Type': 'application/json' } : undefined),
|
|
38
|
+
body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
|
|
39
|
+
signal: init.signal,
|
|
40
|
+
});
|
|
41
|
+
if (res.status === 204) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const body = await res.json().catch(() => null);
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
throw (0, errors_1.errorFromResponse)(res.status, body);
|
|
47
|
+
}
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
50
|
+
async requestForm(path, init) {
|
|
51
|
+
const res = await this.opts.fetchImpl(this.url(path), {
|
|
52
|
+
method: init.method,
|
|
53
|
+
headers: this.authHeaders(),
|
|
54
|
+
body: init.form,
|
|
55
|
+
signal: init.signal,
|
|
56
|
+
});
|
|
57
|
+
const body = await res.json().catch(() => null);
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
throw (0, errors_1.errorFromResponse)(res.status, body);
|
|
60
|
+
}
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
63
|
+
async requestVoid(path, init) {
|
|
64
|
+
const res = await this.opts.fetchImpl(this.url(path), {
|
|
65
|
+
method: init.method,
|
|
66
|
+
headers: this.authHeaders(),
|
|
67
|
+
signal: init.signal,
|
|
68
|
+
});
|
|
69
|
+
if (res.status === 204 || res.ok)
|
|
70
|
+
return;
|
|
71
|
+
const body = await res.json().catch(() => null);
|
|
72
|
+
throw (0, errors_1.errorFromResponse)(res.status, body);
|
|
73
|
+
}
|
|
74
|
+
/** Raw streaming request used by the SSE query stream. Returns the fetch Response. */
|
|
75
|
+
async requestStream(path, init) {
|
|
76
|
+
return this.opts.fetchImpl(this.url(path), {
|
|
77
|
+
method: init.method,
|
|
78
|
+
headers: this.authHeaders(init.body !== undefined ? { 'Content-Type': 'application/json' } : undefined),
|
|
79
|
+
body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
|
|
80
|
+
signal: init.signal,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.HttpClient = HttpClient;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { FuguClient, type FuguClientOptions } from './client';
|
|
2
|
+
export { QueryResource } from './resources/query';
|
|
3
|
+
export { DocumentsResource, type UploadFileInput } from './resources/documents';
|
|
4
|
+
export { CredentialsResource } from './resources/credentials';
|
|
5
|
+
export { FuguApiError, BYOKRequiredError, QuotaExceededError } from './errors';
|
|
6
|
+
export * from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.QuotaExceededError = exports.BYOKRequiredError = exports.FuguApiError = exports.CredentialsResource = exports.DocumentsResource = exports.QueryResource = exports.FuguClient = void 0;
|
|
18
|
+
var client_1 = require("./client");
|
|
19
|
+
Object.defineProperty(exports, "FuguClient", { enumerable: true, get: function () { return client_1.FuguClient; } });
|
|
20
|
+
var query_1 = require("./resources/query");
|
|
21
|
+
Object.defineProperty(exports, "QueryResource", { enumerable: true, get: function () { return query_1.QueryResource; } });
|
|
22
|
+
var documents_1 = require("./resources/documents");
|
|
23
|
+
Object.defineProperty(exports, "DocumentsResource", { enumerable: true, get: function () { return documents_1.DocumentsResource; } });
|
|
24
|
+
var credentials_1 = require("./resources/credentials");
|
|
25
|
+
Object.defineProperty(exports, "CredentialsResource", { enumerable: true, get: function () { return credentials_1.CredentialsResource; } });
|
|
26
|
+
var errors_1 = require("./errors");
|
|
27
|
+
Object.defineProperty(exports, "FuguApiError", { enumerable: true, get: function () { return errors_1.FuguApiError; } });
|
|
28
|
+
Object.defineProperty(exports, "BYOKRequiredError", { enumerable: true, get: function () { return errors_1.BYOKRequiredError; } });
|
|
29
|
+
Object.defineProperty(exports, "QuotaExceededError", { enumerable: true, get: function () { return errors_1.QuotaExceededError; } });
|
|
30
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HttpClient } from '../http';
|
|
2
|
+
import type { CredentialDisplay, CredentialModel, LLMCredentialProvider, SaveCredentialInput } from '../types';
|
|
3
|
+
export declare class CredentialsResource {
|
|
4
|
+
private readonly http;
|
|
5
|
+
constructor(http: HttpClient);
|
|
6
|
+
/** Returns the organization's configured BYOK credential, or `null` if none is set. */
|
|
7
|
+
get(signal?: AbortSignal): Promise<CredentialDisplay | null>;
|
|
8
|
+
/** Lists available models for a provider, including free-tier availability. */
|
|
9
|
+
listModels(provider: LLMCredentialProvider, signal?: AbortSignal): Promise<CredentialModel[]>;
|
|
10
|
+
/** Saves (creates or replaces) the organization's BYOK credential. Validates the key with a real test call server-side. */
|
|
11
|
+
save(input: SaveCredentialInput, signal?: AbortSignal): Promise<CredentialDisplay>;
|
|
12
|
+
remove(signal?: AbortSignal): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CredentialsResource = void 0;
|
|
4
|
+
class CredentialsResource {
|
|
5
|
+
constructor(http) {
|
|
6
|
+
this.http = http;
|
|
7
|
+
}
|
|
8
|
+
/** Returns the organization's configured BYOK credential, or `null` if none is set. */
|
|
9
|
+
async get(signal) {
|
|
10
|
+
const res = await this.http.requestJson('/organization/llm-credential', {
|
|
11
|
+
method: 'GET',
|
|
12
|
+
signal,
|
|
13
|
+
});
|
|
14
|
+
return res.credential;
|
|
15
|
+
}
|
|
16
|
+
/** Lists available models for a provider, including free-tier availability. */
|
|
17
|
+
async listModels(provider, signal) {
|
|
18
|
+
const res = await this.http.requestJson('/organization/llm-credential/models', {
|
|
19
|
+
method: 'GET',
|
|
20
|
+
query: { provider },
|
|
21
|
+
signal,
|
|
22
|
+
});
|
|
23
|
+
return res.models;
|
|
24
|
+
}
|
|
25
|
+
/** Saves (creates or replaces) the organization's BYOK credential. Validates the key with a real test call server-side. */
|
|
26
|
+
async save(input, signal) {
|
|
27
|
+
const res = await this.http.requestJson('/organization/llm-credential', {
|
|
28
|
+
method: 'PUT',
|
|
29
|
+
body: input,
|
|
30
|
+
signal,
|
|
31
|
+
});
|
|
32
|
+
return res.credential;
|
|
33
|
+
}
|
|
34
|
+
async remove(signal) {
|
|
35
|
+
await this.http.requestVoid('/organization/llm-credential', { method: 'DELETE', signal });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.CredentialsResource = CredentialsResource;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { HttpClient } from '../http';
|
|
2
|
+
import type { DocumentUploadResponse, FuguDocument, ListDocumentsOptions } from '../types';
|
|
3
|
+
export interface UploadFileInput {
|
|
4
|
+
/** File name including extension, used for type detection server-side. */
|
|
5
|
+
filename: string;
|
|
6
|
+
/** File contents. Accepts a Blob/File (browser) or a Buffer/Uint8Array (Node). */
|
|
7
|
+
data: Blob | Buffer | Uint8Array;
|
|
8
|
+
/** Optional MIME type; inferred by the platform's Blob implementation if omitted. */
|
|
9
|
+
contentType?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class DocumentsResource {
|
|
12
|
+
private readonly http;
|
|
13
|
+
constructor(http: HttpClient);
|
|
14
|
+
list(options?: ListDocumentsOptions): Promise<FuguDocument[]>;
|
|
15
|
+
get(id: string, signal?: AbortSignal): Promise<FuguDocument>;
|
|
16
|
+
/** Uploads a document (multipart/form-data, field name `file`). 50MB limit. */
|
|
17
|
+
upload(file: UploadFileInput, signal?: AbortSignal): Promise<DocumentUploadResponse>;
|
|
18
|
+
delete(id: string, signal?: AbortSignal): Promise<void>;
|
|
19
|
+
/** Re-queues a failed or pending document for ingestion. */
|
|
20
|
+
retry(id: string, signal?: AbortSignal): Promise<{
|
|
21
|
+
status: 'pending';
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DocumentsResource = void 0;
|
|
4
|
+
class DocumentsResource {
|
|
5
|
+
constructor(http) {
|
|
6
|
+
this.http = http;
|
|
7
|
+
}
|
|
8
|
+
async list(options = {}) {
|
|
9
|
+
const res = await this.http.requestJson('/documents', {
|
|
10
|
+
method: 'GET',
|
|
11
|
+
query: { limit: options.limit, offset: options.offset },
|
|
12
|
+
signal: options.signal,
|
|
13
|
+
});
|
|
14
|
+
return res.documents;
|
|
15
|
+
}
|
|
16
|
+
async get(id, signal) {
|
|
17
|
+
const res = await this.http.requestJson(`/documents/${encodeURIComponent(id)}`, {
|
|
18
|
+
method: 'GET',
|
|
19
|
+
signal,
|
|
20
|
+
});
|
|
21
|
+
return res.document;
|
|
22
|
+
}
|
|
23
|
+
/** Uploads a document (multipart/form-data, field name `file`). 50MB limit. */
|
|
24
|
+
async upload(file, signal) {
|
|
25
|
+
const form = new FormData();
|
|
26
|
+
const blob = file.data instanceof Blob ? file.data : new Blob([new Uint8Array(file.data)], { type: file.contentType });
|
|
27
|
+
form.append('file', blob, file.filename);
|
|
28
|
+
return this.http.requestForm('/documents', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
form,
|
|
31
|
+
signal,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async delete(id, signal) {
|
|
35
|
+
await this.http.requestVoid(`/documents/${encodeURIComponent(id)}`, { method: 'DELETE', signal });
|
|
36
|
+
}
|
|
37
|
+
/** Re-queues a failed or pending document for ingestion. */
|
|
38
|
+
async retry(id, signal) {
|
|
39
|
+
return this.http.requestJson(`/documents/${encodeURIComponent(id)}/retry`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
signal,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.DocumentsResource = DocumentsResource;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { HttpClient } from '../http';
|
|
2
|
+
import type { QueryOptions, QueryResponse, QueryStreamEvent } from '../types';
|
|
3
|
+
export declare class QueryResource {
|
|
4
|
+
private readonly http;
|
|
5
|
+
constructor(http: HttpClient);
|
|
6
|
+
/** Runs a query and waits for the full (non-streaming) answer. */
|
|
7
|
+
execute(query: string, options?: QueryOptions): Promise<QueryResponse>;
|
|
8
|
+
/**
|
|
9
|
+
* Runs a query and streams the answer token-by-token via Server-Sent
|
|
10
|
+
* Events. Yields `QueryStreamEvent`s as they arrive:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* for await (const event of client.query.stream('...')) {
|
|
14
|
+
* if (event.type === 'delta') process.stdout.write(event.text);
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
stream(query: string, options?: QueryOptions): AsyncGenerator<QueryStreamEvent, void, void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueryResource = void 0;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
class QueryResource {
|
|
6
|
+
constructor(http) {
|
|
7
|
+
this.http = http;
|
|
8
|
+
}
|
|
9
|
+
/** Runs a query and waits for the full (non-streaming) answer. */
|
|
10
|
+
async execute(query, options = {}) {
|
|
11
|
+
return this.http.requestJson('/queries/v1/query', {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
body: { query, strategy: options.strategy, top_k: options.top_k },
|
|
14
|
+
signal: options.signal,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs a query and streams the answer token-by-token via Server-Sent
|
|
19
|
+
* Events. Yields `QueryStreamEvent`s as they arrive:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* for await (const event of client.query.stream('...')) {
|
|
23
|
+
* if (event.type === 'delta') process.stdout.write(event.text);
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
async *stream(query, options = {}) {
|
|
28
|
+
const res = await this.http.requestStream('/queries/stream', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
body: { query, strategy: options.strategy, top_k: options.top_k },
|
|
31
|
+
signal: options.signal,
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok || !res.body) {
|
|
34
|
+
const body = await res.json().catch(() => null);
|
|
35
|
+
throw (0, errors_1.errorFromResponse)(res.status, body);
|
|
36
|
+
}
|
|
37
|
+
const reader = res.body.getReader();
|
|
38
|
+
const decoder = new TextDecoder();
|
|
39
|
+
let buffer = '';
|
|
40
|
+
try {
|
|
41
|
+
for (;;) {
|
|
42
|
+
const { done, value } = await reader.read();
|
|
43
|
+
if (done)
|
|
44
|
+
break;
|
|
45
|
+
buffer += decoder.decode(value, { stream: true });
|
|
46
|
+
let sep;
|
|
47
|
+
while ((sep = buffer.indexOf('\n\n')) !== -1) {
|
|
48
|
+
const frame = buffer.slice(0, sep);
|
|
49
|
+
buffer = buffer.slice(sep + 2);
|
|
50
|
+
const line = frame.split('\n').find((l) => l.startsWith('data:'));
|
|
51
|
+
if (!line)
|
|
52
|
+
continue;
|
|
53
|
+
const event = JSON.parse(line.slice(5).trim());
|
|
54
|
+
yield event;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
reader.releaseLock();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.QueryResource = QueryResource;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type QueryStrategy = 'vector_only' | 'graph_only' | 'hybrid' | 'auto';
|
|
2
|
+
export interface QuerySource {
|
|
3
|
+
content: string;
|
|
4
|
+
source: 'vector' | 'graph';
|
|
5
|
+
score: number;
|
|
6
|
+
document_id?: string;
|
|
7
|
+
metadata?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface QuotaInfo {
|
|
10
|
+
used: number;
|
|
11
|
+
limit: number;
|
|
12
|
+
percent: number;
|
|
13
|
+
warn: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface QueryResponse {
|
|
16
|
+
answer: string;
|
|
17
|
+
citations: string[];
|
|
18
|
+
answer_degraded: boolean;
|
|
19
|
+
results: QuerySource[];
|
|
20
|
+
explain: Record<string, unknown>;
|
|
21
|
+
quota: QuotaInfo;
|
|
22
|
+
}
|
|
23
|
+
export interface QueryOptions {
|
|
24
|
+
strategy?: QueryStrategy;
|
|
25
|
+
top_k?: number;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}
|
|
28
|
+
/** Server-Sent Events emitted by `POST /api/queries/stream`. */
|
|
29
|
+
export type QueryStreamEvent = {
|
|
30
|
+
type: 'meta';
|
|
31
|
+
results: QuerySource[];
|
|
32
|
+
explain: Record<string, unknown>;
|
|
33
|
+
quota: QuotaInfo;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'delta';
|
|
36
|
+
text: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: 'done';
|
|
39
|
+
citations: string[];
|
|
40
|
+
answer_degraded: boolean;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'error';
|
|
43
|
+
message: string;
|
|
44
|
+
};
|
|
45
|
+
export type DocumentStatus = 'pending' | 'processing' | 'ready' | 'failed';
|
|
46
|
+
export interface FuguDocument {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
file_type: string;
|
|
50
|
+
file_size: number;
|
|
51
|
+
status: DocumentStatus;
|
|
52
|
+
created_at: string;
|
|
53
|
+
updated_at?: string;
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
export interface DocumentUploadResponse {
|
|
57
|
+
document_id: string;
|
|
58
|
+
status: 'pending';
|
|
59
|
+
}
|
|
60
|
+
export interface ListDocumentsOptions {
|
|
61
|
+
limit?: number;
|
|
62
|
+
offset?: number;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}
|
|
65
|
+
export type LLMCredentialProvider = 'anthropic' | 'openai' | 'gemini' | 'openrouter';
|
|
66
|
+
export interface CredentialDisplay {
|
|
67
|
+
provider: LLMCredentialProvider;
|
|
68
|
+
model: string;
|
|
69
|
+
keyLastFour: string;
|
|
70
|
+
lastVerifiedAt: string | null;
|
|
71
|
+
}
|
|
72
|
+
export interface CredentialModel {
|
|
73
|
+
id: string;
|
|
74
|
+
label: string;
|
|
75
|
+
free: boolean;
|
|
76
|
+
}
|
|
77
|
+
export interface SaveCredentialInput {
|
|
78
|
+
provider: LLMCredentialProvider;
|
|
79
|
+
model: string;
|
|
80
|
+
apiKey: string;
|
|
81
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FuguClient, BYOKRequiredError, QuotaExceededError } from '../src/index';
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const apiKey = process.env.FUGU_API_KEY;
|
|
5
|
+
if (!apiKey) {
|
|
6
|
+
throw new Error('Set FUGU_API_KEY env var to a valid fugu_sk_... key');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const client = new FuguClient({
|
|
10
|
+
apiKey,
|
|
11
|
+
baseUrl: process.env.FUGU_BASE_URL ?? 'http://localhost:3001/api',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// 1. Non-streaming query
|
|
15
|
+
try {
|
|
16
|
+
const result = await client.query.execute('what does FUGU combine to answer questions');
|
|
17
|
+
console.log('Answer:', result.answer);
|
|
18
|
+
console.log('Strategy used:', result.explain.strategy_final);
|
|
19
|
+
console.log('Sources:', result.results.length);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err instanceof BYOKRequiredError) {
|
|
22
|
+
console.error('No LLM credential configured for this org. Set one via client.credentials.save().');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (err instanceof QuotaExceededError) {
|
|
26
|
+
console.error('Monthly query quota exceeded.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Streaming query
|
|
33
|
+
console.log('\nStreaming:');
|
|
34
|
+
for await (const event of client.query.stream('what does FUGU combine to answer questions')) {
|
|
35
|
+
if (event.type === 'delta') process.stdout.write(event.text);
|
|
36
|
+
if (event.type === 'done') console.log('\n[done]', event.citations);
|
|
37
|
+
if (event.type === 'error') console.error('\n[stream error]', event.message);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 3. Documents
|
|
41
|
+
const docs = await client.documents.list({ limit: 5 });
|
|
42
|
+
console.log('\nDocuments:', docs.length);
|
|
43
|
+
|
|
44
|
+
// 4. Credentials
|
|
45
|
+
const credential = await client.credentials.get();
|
|
46
|
+
console.log('Credential:', credential);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main().catch((err) => {
|
|
50
|
+
console.error(err);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fugu-sdk",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "TypeScript SDK for the FUGU routed RAG API",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dogukandemirci-software-engineer/fugu-ragrouting",
|
|
10
|
+
"directory": "sdk-typescript"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["fugu", "rag", "sdk"],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^5.9.3",
|
|
19
|
+
"@types/node": "^22.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { HttpClient } from './http';
|
|
2
|
+
import { QueryResource } from './resources/query';
|
|
3
|
+
import { DocumentsResource } from './resources/documents';
|
|
4
|
+
import { CredentialsResource } from './resources/credentials';
|
|
5
|
+
|
|
6
|
+
export interface FuguClientOptions {
|
|
7
|
+
/** API key issued from the FUGU dashboard, of the form `fugu_sk_...`. */
|
|
8
|
+
apiKey: string;
|
|
9
|
+
/** Base API URL. Defaults to `http://localhost:3001/api` for local dev. */
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
/** Custom `fetch` implementation (useful for testing or non-standard runtimes). */
|
|
12
|
+
fetchImpl?: typeof fetch;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Entry point for the FUGU SDK.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const client = new FuguClient({ apiKey: 'fugu_sk_...' });
|
|
20
|
+
* const result = await client.query.execute('what does FUGU combine to answer questions');
|
|
21
|
+
* for await (const event of client.query.stream('...')) { ... }
|
|
22
|
+
* await client.documents.upload({ filename: 'doc.pdf', data: buffer });
|
|
23
|
+
* const credential = await client.credentials.get();
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export class FuguClient {
|
|
27
|
+
readonly query: QueryResource;
|
|
28
|
+
readonly documents: DocumentsResource;
|
|
29
|
+
readonly credentials: CredentialsResource;
|
|
30
|
+
|
|
31
|
+
constructor(options: FuguClientOptions) {
|
|
32
|
+
if (!options.apiKey) throw new Error('FuguClient requires an apiKey');
|
|
33
|
+
|
|
34
|
+
const http = new HttpClient({
|
|
35
|
+
apiKey: options.apiKey,
|
|
36
|
+
baseUrl: (options.baseUrl ?? 'http://localhost:3001/api').replace(/\/+$/, ''),
|
|
37
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
this.query = new QueryResource(http);
|
|
41
|
+
this.documents = new DocumentsResource(http);
|
|
42
|
+
this.credentials = new CredentialsResource(http);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface ApiErrorBody {
|
|
2
|
+
error?: {
|
|
3
|
+
message?: string;
|
|
4
|
+
code?: string;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Base error for any non-2xx response from the FUGU API. */
|
|
9
|
+
export class FuguApiError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
message: string,
|
|
12
|
+
public readonly status: number,
|
|
13
|
+
public readonly code?: string
|
|
14
|
+
) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'FuguApiError';
|
|
17
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Thrown when a query is rejected because the organization has no active
|
|
23
|
+
* LLM credential configured (BYOK). Distinct subclass so callers can branch
|
|
24
|
+
* on `instanceof BYOKRequiredError` instead of string-matching `code`.
|
|
25
|
+
*/
|
|
26
|
+
export class BYOKRequiredError extends FuguApiError {
|
|
27
|
+
constructor(message: string, status: number) {
|
|
28
|
+
super(message, status, 'BYOK_REQUIRED');
|
|
29
|
+
this.name = 'BYOKRequiredError';
|
|
30
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Thrown when the organization's monthly query quota has been exceeded. */
|
|
35
|
+
export class QuotaExceededError extends FuguApiError {
|
|
36
|
+
constructor(message: string, status: number) {
|
|
37
|
+
super(message, status, 'QUOTA_EXCEEDED');
|
|
38
|
+
this.name = 'QuotaExceededError';
|
|
39
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Builds the right error subclass from a parsed error-response body. */
|
|
44
|
+
export function errorFromResponse(status: number, body: ApiErrorBody | null): FuguApiError {
|
|
45
|
+
const message = body?.error?.message ?? `Request failed with status ${status}`;
|
|
46
|
+
const code = body?.error?.code;
|
|
47
|
+
|
|
48
|
+
if (code === 'BYOK_REQUIRED') return new BYOKRequiredError(message, status);
|
|
49
|
+
if (code === 'QUOTA_EXCEEDED') return new QuotaExceededError(message, status);
|
|
50
|
+
return new FuguApiError(message, status, code);
|
|
51
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { errorFromResponse, type ApiErrorBody } from './errors';
|
|
2
|
+
|
|
3
|
+
export interface HttpClientOptions {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
fetchImpl: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Thin wrapper around `fetch` shared by all resource sub-clients. Handles
|
|
11
|
+
* auth header injection, base URL joining, JSON (de)serialization, and
|
|
12
|
+
* translating non-2xx responses into typed `FuguApiError`s.
|
|
13
|
+
*/
|
|
14
|
+
export class HttpClient {
|
|
15
|
+
constructor(private readonly opts: HttpClientOptions) {}
|
|
16
|
+
|
|
17
|
+
private url(path: string): string {
|
|
18
|
+
return `${this.opts.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private authHeaders(extra?: Record<string, string>): Record<string, string> {
|
|
22
|
+
return {
|
|
23
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
24
|
+
...extra,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async requestJson<T>(
|
|
29
|
+
path: string,
|
|
30
|
+
init: { method: string; body?: unknown; query?: Record<string, string | number | undefined>; signal?: AbortSignal }
|
|
31
|
+
): Promise<T> {
|
|
32
|
+
let finalUrl = this.url(path);
|
|
33
|
+
if (init.query) {
|
|
34
|
+
const params = new URLSearchParams();
|
|
35
|
+
for (const [k, v] of Object.entries(init.query)) {
|
|
36
|
+
if (v !== undefined) params.set(k, String(v));
|
|
37
|
+
}
|
|
38
|
+
const qs = params.toString();
|
|
39
|
+
if (qs) finalUrl += `?${qs}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const res = await this.opts.fetchImpl(finalUrl, {
|
|
43
|
+
method: init.method,
|
|
44
|
+
headers: this.authHeaders(init.body !== undefined ? { 'Content-Type': 'application/json' } : undefined),
|
|
45
|
+
body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
|
|
46
|
+
signal: init.signal,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (res.status === 204) {
|
|
50
|
+
return undefined as T;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const body = await res.json().catch(() => null);
|
|
54
|
+
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
throw errorFromResponse(res.status, body as ApiErrorBody | null);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return body as T;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async requestForm<T>(
|
|
63
|
+
path: string,
|
|
64
|
+
init: { method: string; form: FormData; signal?: AbortSignal }
|
|
65
|
+
): Promise<T> {
|
|
66
|
+
const res = await this.opts.fetchImpl(this.url(path), {
|
|
67
|
+
method: init.method,
|
|
68
|
+
headers: this.authHeaders(),
|
|
69
|
+
body: init.form,
|
|
70
|
+
signal: init.signal,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const body = await res.json().catch(() => null);
|
|
74
|
+
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
throw errorFromResponse(res.status, body as ApiErrorBody | null);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return body as T;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async requestVoid(path: string, init: { method: string; signal?: AbortSignal }): Promise<void> {
|
|
83
|
+
const res = await this.opts.fetchImpl(this.url(path), {
|
|
84
|
+
method: init.method,
|
|
85
|
+
headers: this.authHeaders(),
|
|
86
|
+
signal: init.signal,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (res.status === 204 || res.ok) return;
|
|
90
|
+
|
|
91
|
+
const body = await res.json().catch(() => null);
|
|
92
|
+
throw errorFromResponse(res.status, body as ApiErrorBody | null);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Raw streaming request used by the SSE query stream. Returns the fetch Response. */
|
|
96
|
+
async requestStream(
|
|
97
|
+
path: string,
|
|
98
|
+
init: { method: string; body?: unknown; signal?: AbortSignal }
|
|
99
|
+
): Promise<Response> {
|
|
100
|
+
return this.opts.fetchImpl(this.url(path), {
|
|
101
|
+
method: init.method,
|
|
102
|
+
headers: this.authHeaders(init.body !== undefined ? { 'Content-Type': 'application/json' } : undefined),
|
|
103
|
+
body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
|
|
104
|
+
signal: init.signal,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { FuguClient, type FuguClientOptions } from './client';
|
|
2
|
+
export { QueryResource } from './resources/query';
|
|
3
|
+
export { DocumentsResource, type UploadFileInput } from './resources/documents';
|
|
4
|
+
export { CredentialsResource } from './resources/credentials';
|
|
5
|
+
export { FuguApiError, BYOKRequiredError, QuotaExceededError } from './errors';
|
|
6
|
+
export * from './types';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { HttpClient } from '../http';
|
|
2
|
+
import type { CredentialDisplay, CredentialModel, LLMCredentialProvider, SaveCredentialInput } from '../types';
|
|
3
|
+
|
|
4
|
+
export class CredentialsResource {
|
|
5
|
+
constructor(private readonly http: HttpClient) {}
|
|
6
|
+
|
|
7
|
+
/** Returns the organization's configured BYOK credential, or `null` if none is set. */
|
|
8
|
+
async get(signal?: AbortSignal): Promise<CredentialDisplay | null> {
|
|
9
|
+
const res = await this.http.requestJson<{ credential: CredentialDisplay | null }>('/organization/llm-credential', {
|
|
10
|
+
method: 'GET',
|
|
11
|
+
signal,
|
|
12
|
+
});
|
|
13
|
+
return res.credential;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Lists available models for a provider, including free-tier availability. */
|
|
17
|
+
async listModels(provider: LLMCredentialProvider, signal?: AbortSignal): Promise<CredentialModel[]> {
|
|
18
|
+
const res = await this.http.requestJson<{ models: CredentialModel[] }>('/organization/llm-credential/models', {
|
|
19
|
+
method: 'GET',
|
|
20
|
+
query: { provider },
|
|
21
|
+
signal,
|
|
22
|
+
});
|
|
23
|
+
return res.models;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Saves (creates or replaces) the organization's BYOK credential. Validates the key with a real test call server-side. */
|
|
27
|
+
async save(input: SaveCredentialInput, signal?: AbortSignal): Promise<CredentialDisplay> {
|
|
28
|
+
const res = await this.http.requestJson<{ credential: CredentialDisplay }>('/organization/llm-credential', {
|
|
29
|
+
method: 'PUT',
|
|
30
|
+
body: input,
|
|
31
|
+
signal,
|
|
32
|
+
});
|
|
33
|
+
return res.credential;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async remove(signal?: AbortSignal): Promise<void> {
|
|
37
|
+
await this.http.requestVoid('/organization/llm-credential', { method: 'DELETE', signal });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { HttpClient } from '../http';
|
|
2
|
+
import type { DocumentUploadResponse, FuguDocument, ListDocumentsOptions } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface UploadFileInput {
|
|
5
|
+
/** File name including extension, used for type detection server-side. */
|
|
6
|
+
filename: string;
|
|
7
|
+
/** File contents. Accepts a Blob/File (browser) or a Buffer/Uint8Array (Node). */
|
|
8
|
+
data: Blob | Buffer | Uint8Array;
|
|
9
|
+
/** Optional MIME type; inferred by the platform's Blob implementation if omitted. */
|
|
10
|
+
contentType?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class DocumentsResource {
|
|
14
|
+
constructor(private readonly http: HttpClient) {}
|
|
15
|
+
|
|
16
|
+
async list(options: ListDocumentsOptions = {}): Promise<FuguDocument[]> {
|
|
17
|
+
const res = await this.http.requestJson<{ documents: FuguDocument[] }>('/documents', {
|
|
18
|
+
method: 'GET',
|
|
19
|
+
query: { limit: options.limit, offset: options.offset },
|
|
20
|
+
signal: options.signal,
|
|
21
|
+
});
|
|
22
|
+
return res.documents;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async get(id: string, signal?: AbortSignal): Promise<FuguDocument> {
|
|
26
|
+
const res = await this.http.requestJson<{ document: FuguDocument }>(`/documents/${encodeURIComponent(id)}`, {
|
|
27
|
+
method: 'GET',
|
|
28
|
+
signal,
|
|
29
|
+
});
|
|
30
|
+
return res.document;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Uploads a document (multipart/form-data, field name `file`). 50MB limit. */
|
|
34
|
+
async upload(file: UploadFileInput, signal?: AbortSignal): Promise<DocumentUploadResponse> {
|
|
35
|
+
const form = new FormData();
|
|
36
|
+
const blob =
|
|
37
|
+
file.data instanceof Blob ? file.data : new Blob([new Uint8Array(file.data)], { type: file.contentType });
|
|
38
|
+
form.append('file', blob, file.filename);
|
|
39
|
+
|
|
40
|
+
return this.http.requestForm<DocumentUploadResponse>('/documents', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
form,
|
|
43
|
+
signal,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async delete(id: string, signal?: AbortSignal): Promise<void> {
|
|
48
|
+
await this.http.requestVoid(`/documents/${encodeURIComponent(id)}`, { method: 'DELETE', signal });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Re-queues a failed or pending document for ingestion. */
|
|
52
|
+
async retry(id: string, signal?: AbortSignal): Promise<{ status: 'pending' }> {
|
|
53
|
+
return this.http.requestJson<{ status: 'pending' }>(`/documents/${encodeURIComponent(id)}/retry`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
signal,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { HttpClient } from '../http';
|
|
2
|
+
import { errorFromResponse, type ApiErrorBody } from '../errors';
|
|
3
|
+
import type { QueryOptions, QueryResponse, QueryStreamEvent } from '../types';
|
|
4
|
+
|
|
5
|
+
export class QueryResource {
|
|
6
|
+
constructor(private readonly http: HttpClient) {}
|
|
7
|
+
|
|
8
|
+
/** Runs a query and waits for the full (non-streaming) answer. */
|
|
9
|
+
async execute(query: string, options: QueryOptions = {}): Promise<QueryResponse> {
|
|
10
|
+
return this.http.requestJson<QueryResponse>('/queries/v1/query', {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
body: { query, strategy: options.strategy, top_k: options.top_k },
|
|
13
|
+
signal: options.signal,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runs a query and streams the answer token-by-token via Server-Sent
|
|
19
|
+
* Events. Yields `QueryStreamEvent`s as they arrive:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* for await (const event of client.query.stream('...')) {
|
|
23
|
+
* if (event.type === 'delta') process.stdout.write(event.text);
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
async *stream(query: string, options: QueryOptions = {}): AsyncGenerator<QueryStreamEvent, void, void> {
|
|
28
|
+
const res = await this.http.requestStream('/queries/stream', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
body: { query, strategy: options.strategy, top_k: options.top_k },
|
|
31
|
+
signal: options.signal,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!res.ok || !res.body) {
|
|
35
|
+
const body = await res.json().catch(() => null);
|
|
36
|
+
throw errorFromResponse(res.status, body as ApiErrorBody | null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const reader = res.body.getReader();
|
|
40
|
+
const decoder = new TextDecoder();
|
|
41
|
+
let buffer = '';
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
for (;;) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done) break;
|
|
47
|
+
buffer += decoder.decode(value, { stream: true });
|
|
48
|
+
|
|
49
|
+
let sep: number;
|
|
50
|
+
while ((sep = buffer.indexOf('\n\n')) !== -1) {
|
|
51
|
+
const frame = buffer.slice(0, sep);
|
|
52
|
+
buffer = buffer.slice(sep + 2);
|
|
53
|
+
const line = frame.split('\n').find((l) => l.startsWith('data:'));
|
|
54
|
+
if (!line) continue;
|
|
55
|
+
const event = JSON.parse(line.slice(5).trim()) as QueryStreamEvent;
|
|
56
|
+
yield event;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
reader.releaseLock();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type QueryStrategy = 'vector_only' | 'graph_only' | 'hybrid' | 'auto';
|
|
2
|
+
|
|
3
|
+
export interface QuerySource {
|
|
4
|
+
content: string;
|
|
5
|
+
source: 'vector' | 'graph';
|
|
6
|
+
score: number;
|
|
7
|
+
document_id?: string;
|
|
8
|
+
metadata?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface QuotaInfo {
|
|
12
|
+
used: number;
|
|
13
|
+
limit: number;
|
|
14
|
+
percent: number;
|
|
15
|
+
warn: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QueryResponse {
|
|
19
|
+
answer: string;
|
|
20
|
+
citations: string[];
|
|
21
|
+
answer_degraded: boolean;
|
|
22
|
+
results: QuerySource[];
|
|
23
|
+
explain: Record<string, unknown>;
|
|
24
|
+
quota: QuotaInfo;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface QueryOptions {
|
|
28
|
+
strategy?: QueryStrategy;
|
|
29
|
+
top_k?: number;
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Server-Sent Events emitted by `POST /api/queries/stream`. */
|
|
34
|
+
export type QueryStreamEvent =
|
|
35
|
+
| { type: 'meta'; results: QuerySource[]; explain: Record<string, unknown>; quota: QuotaInfo }
|
|
36
|
+
| { type: 'delta'; text: string }
|
|
37
|
+
| { type: 'done'; citations: string[]; answer_degraded: boolean }
|
|
38
|
+
| { type: 'error'; message: string };
|
|
39
|
+
|
|
40
|
+
export type DocumentStatus = 'pending' | 'processing' | 'ready' | 'failed';
|
|
41
|
+
|
|
42
|
+
export interface FuguDocument {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
file_type: string;
|
|
46
|
+
file_size: number;
|
|
47
|
+
status: DocumentStatus;
|
|
48
|
+
created_at: string;
|
|
49
|
+
updated_at?: string;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DocumentUploadResponse {
|
|
54
|
+
document_id: string;
|
|
55
|
+
status: 'pending';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ListDocumentsOptions {
|
|
59
|
+
limit?: number;
|
|
60
|
+
offset?: number;
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type LLMCredentialProvider = 'anthropic' | 'openai' | 'gemini' | 'openrouter';
|
|
65
|
+
|
|
66
|
+
export interface CredentialDisplay {
|
|
67
|
+
provider: LLMCredentialProvider;
|
|
68
|
+
model: string;
|
|
69
|
+
keyLastFour: string;
|
|
70
|
+
lastVerifiedAt: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CredentialModel {
|
|
74
|
+
id: string;
|
|
75
|
+
label: string;
|
|
76
|
+
free: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SaveCredentialInput {
|
|
80
|
+
provider: LLMCredentialProvider;
|
|
81
|
+
model: string;
|
|
82
|
+
apiKey: string;
|
|
83
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["ES2020", "DOM"],
|
|
5
|
+
"module": "CommonJS",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|