@plasius/ai 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -1
- package/README.md +15 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/chatWithAI.d.ts +41 -1
- package/dist/lib/chatWithAI.d.ts.map +1 -1
- package/dist/lib/chatWithAI.js +170 -1
- package/dist-cjs/index.d.ts +1 -0
- package/dist-cjs/index.d.ts.map +1 -1
- package/dist-cjs/index.js +1 -0
- package/dist-cjs/lib/chatWithAI.d.ts +41 -0
- package/dist-cjs/lib/chatWithAI.d.ts.map +1 -1
- package/dist-cjs/lib/chatWithAI.js +175 -0
- package/docs/adrs/index.md +5 -0
- package/package.json +18 -17
- package/src/index.ts +1 -0
- package/src/lib/chatWithAI.ts +260 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,8 +12,41 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
12
12
|
- (placeholder)
|
|
13
13
|
|
|
14
14
|
- **Changed**
|
|
15
|
+
- Hardened GitHub CD publish flow to publish only after successful install, test, and build, then push tags/releases post-publish.
|
|
16
|
+
- Standardized npm publish path on workflow-dispatched `.github/workflows/cd.yml` using provenance and production environment secrets.
|
|
17
|
+
- Replaced `audit:deps` from `depcheck` to `npm ls --all --omit=optional --omit=peer > /dev/null 2>&1 || true` to avoid deprecated dependency-chain risk.
|
|
18
|
+
|
|
19
|
+
- **Fixed**
|
|
15
20
|
- (placeholder)
|
|
16
21
|
|
|
22
|
+
- **Security**
|
|
23
|
+
- Removed `depcheck` (and its `multimatch`/`minimatch` chain) from devDependencies to resolve reported high-severity audit findings.
|
|
24
|
+
|
|
25
|
+
## [1.0.4] - 2026-02-21
|
|
26
|
+
|
|
27
|
+
- **Added**
|
|
28
|
+
- Added `npm run demo:run` for one-command local package/demo verification.
|
|
29
|
+
|
|
30
|
+
- **Changed**
|
|
31
|
+
- Aligned OpenAI requirement to `^5.23.2` to match current `plasius-ltd-site` resolved baseline.
|
|
32
|
+
- Updated React Router and toolchain dependency minimums to current `plasius-ltd-site` requirements.
|
|
33
|
+
|
|
34
|
+
- **Fixed**
|
|
35
|
+
- Updated demo docs to run via the package script instead of manual multi-step commands.
|
|
36
|
+
|
|
37
|
+
- **Security**
|
|
38
|
+
- (placeholder)
|
|
39
|
+
|
|
40
|
+
## [1.0.4] - 2026-02-21
|
|
41
|
+
|
|
42
|
+
- **Added**
|
|
43
|
+
- Add a typed chatbot API client (`chatWithAI`, `getChatbotUsage`) for `/ai/chatbot`.
|
|
44
|
+
- Add `ChatbotApiError` with HTTP status/code/usage metadata for auth and quota handling.
|
|
45
|
+
- Add CSRF bootstrap support (GET-first cookie hydration, then `x-csrf-token` on POST).
|
|
46
|
+
|
|
47
|
+
- **Changed**
|
|
48
|
+
- Export chatbot client utilities from the package root.
|
|
49
|
+
|
|
17
50
|
- **Fixed**
|
|
18
51
|
- (placeholder)
|
|
19
52
|
|
|
@@ -54,7 +87,8 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
54
87
|
|
|
55
88
|
---
|
|
56
89
|
|
|
57
|
-
[Unreleased]: https://github.com/Plasius-LTD/ai/compare/v1.0.
|
|
90
|
+
[Unreleased]: https://github.com/Plasius-LTD/ai/compare/v1.0.4...HEAD
|
|
91
|
+
[1.0.4]: https://github.com/Plasius-LTD/ai/releases/tag/v1.0.4
|
|
58
92
|
|
|
59
93
|
## [1.0.0] - 2026-02-11
|
|
60
94
|
|
|
@@ -70,3 +104,4 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
70
104
|
- **Security**
|
|
71
105
|
- (placeholder)
|
|
72
106
|
[1.0.3]: https://github.com/Plasius-LTD/ai/releases/tag/v1.0.3
|
|
107
|
+
[1.0.4]: https://github.com/Plasius-LTD/ai/releases/tag/v1.0.4
|
package/README.md
CHANGED
|
@@ -119,8 +119,23 @@ npm install
|
|
|
119
119
|
npm run build
|
|
120
120
|
npm test
|
|
121
121
|
npm run test:coverage
|
|
122
|
+
npm run demo:run
|
|
122
123
|
```
|
|
123
124
|
|
|
125
|
+
## Demo Sanity Check
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm run demo:run
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Publishing
|
|
132
|
+
|
|
133
|
+
This package is published via GitHub CD only.
|
|
134
|
+
|
|
135
|
+
1. Configure repository environment `production` with secret `NPM_TOKEN`.
|
|
136
|
+
2. Run `.github/workflows/cd.yml` via **Actions -> CD (Publish to npm) -> Run workflow**.
|
|
137
|
+
3. Select the version bump (`patch`, `minor`, `major`, or `none`) and optional pre-release id.
|
|
138
|
+
|
|
124
139
|
## Build Outputs
|
|
125
140
|
|
|
126
141
|
- ESM: `dist/`
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/lib/chatWithAI.d.ts
CHANGED
|
@@ -1,2 +1,42 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type ChatRole = "system" | "user" | "assistant";
|
|
2
|
+
export interface ChatMessage {
|
|
3
|
+
role: ChatRole;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ChatWithAIRequest {
|
|
7
|
+
message: string;
|
|
8
|
+
history?: ChatMessage[];
|
|
9
|
+
systemPrompt?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ChatbotUsage {
|
|
12
|
+
limit: number;
|
|
13
|
+
used: number;
|
|
14
|
+
remaining: number;
|
|
15
|
+
exhausted: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ChatbotResponse {
|
|
18
|
+
reply: string;
|
|
19
|
+
model: string;
|
|
20
|
+
usage: ChatbotUsage;
|
|
21
|
+
}
|
|
22
|
+
export interface ChatbotUsageResponse {
|
|
23
|
+
usage: ChatbotUsage;
|
|
24
|
+
}
|
|
25
|
+
export interface ChatWithAIClientOptions {
|
|
26
|
+
endpoint?: string;
|
|
27
|
+
credentials?: RequestCredentials;
|
|
28
|
+
fetchFn?: typeof fetch;
|
|
29
|
+
authHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
30
|
+
csrfCookieName?: string;
|
|
31
|
+
csrfHeaderName?: string;
|
|
32
|
+
bootstrapCsrf?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare class ChatbotApiError extends Error {
|
|
35
|
+
status: number;
|
|
36
|
+
code?: string;
|
|
37
|
+
usage?: ChatbotUsage;
|
|
38
|
+
constructor(status: number, message: string, code?: string, usage?: ChatbotUsage);
|
|
39
|
+
}
|
|
40
|
+
export declare function getChatbotUsage(options?: ChatWithAIClientOptions): Promise<ChatbotUsageResponse>;
|
|
41
|
+
export declare function chatWithAI(request: ChatWithAIRequest, options?: ChatWithAIClientOptions): Promise<ChatbotResponse>;
|
|
2
42
|
//# sourceMappingURL=chatWithAI.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatWithAI.d.ts","sourceRoot":"","sources":["../../src/lib/chatWithAI.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"chatWithAI.d.ts","sourceRoot":"","sources":["../../src/lib/chatWithAI.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,WAAW,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAYD,qBAAa,eAAgB,SAAQ,KAAK;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,YAAY,CAAC;gBAET,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY;CAOjF;AAuID,wBAAsB,eAAe,CACnC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,oBAAoB,CAAC,CAmB/B;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,EAC1B,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,eAAe,CAAC,CAoC1B"}
|
package/dist/lib/chatWithAI.js
CHANGED
|
@@ -1 +1,170 @@
|
|
|
1
|
-
|
|
1
|
+
const DEFAULT_ENDPOINT = "/ai/chatbot";
|
|
2
|
+
const DEFAULT_CSRF_COOKIE_NAME = "csrf-token";
|
|
3
|
+
const DEFAULT_CSRF_HEADER_NAME = "x-csrf-token";
|
|
4
|
+
export class ChatbotApiError extends Error {
|
|
5
|
+
constructor(status, message, code, usage) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ChatbotApiError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.usage = usage;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function resolveFetch(fetchFn) {
|
|
14
|
+
const resolved = fetchFn ?? (typeof fetch !== "undefined" ? fetch : undefined);
|
|
15
|
+
if (!resolved) {
|
|
16
|
+
throw new Error("No fetch implementation available for chatWithAI.");
|
|
17
|
+
}
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
async function resolveAuthHeaders(authHeaders) {
|
|
21
|
+
if (!authHeaders)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (typeof authHeaders === "function") {
|
|
24
|
+
return await authHeaders();
|
|
25
|
+
}
|
|
26
|
+
return authHeaders;
|
|
27
|
+
}
|
|
28
|
+
function readCookie(cookieName) {
|
|
29
|
+
if (typeof document === "undefined" || typeof document.cookie !== "string") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const entries = document.cookie.split(";").map((part) => part.trim());
|
|
33
|
+
const match = entries.find((entry) => entry.startsWith(`${cookieName}=`));
|
|
34
|
+
if (!match)
|
|
35
|
+
return undefined;
|
|
36
|
+
const [, value = ""] = match.split("=");
|
|
37
|
+
try {
|
|
38
|
+
return decodeURIComponent(value);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function parseResponseBody(response) {
|
|
45
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
46
|
+
if (contentType.includes("application/json")) {
|
|
47
|
+
return await response.json();
|
|
48
|
+
}
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
if (!text)
|
|
51
|
+
return undefined;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function ensureCsrfToken(fetcher, endpoint, options, headers) {
|
|
60
|
+
const csrfCookieName = options.csrfCookieName ?? DEFAULT_CSRF_COOKIE_NAME;
|
|
61
|
+
const existingToken = readCookie(csrfCookieName);
|
|
62
|
+
if (existingToken || options.bootstrapCsrf === false) {
|
|
63
|
+
return existingToken;
|
|
64
|
+
}
|
|
65
|
+
await fetcher(endpoint, {
|
|
66
|
+
method: "GET",
|
|
67
|
+
credentials: options.credentials ?? "include",
|
|
68
|
+
headers,
|
|
69
|
+
});
|
|
70
|
+
return readCookie(csrfCookieName);
|
|
71
|
+
}
|
|
72
|
+
function normalizeError(status, body) {
|
|
73
|
+
const payload = body && typeof body === "object" ? body : undefined;
|
|
74
|
+
const message = payload?.message ||
|
|
75
|
+
(status === 401
|
|
76
|
+
? "You must be signed in to use chatbot."
|
|
77
|
+
: "Chatbot request failed.");
|
|
78
|
+
return new ChatbotApiError(status, message, payload?.error, payload?.usage);
|
|
79
|
+
}
|
|
80
|
+
function buildUsage(payload) {
|
|
81
|
+
if (!payload || typeof payload !== "object")
|
|
82
|
+
return undefined;
|
|
83
|
+
const maybe = payload;
|
|
84
|
+
const limit = maybe.limit;
|
|
85
|
+
const used = maybe.used;
|
|
86
|
+
const remaining = maybe.remaining;
|
|
87
|
+
const exhausted = maybe.exhausted;
|
|
88
|
+
if (typeof limit !== "number" ||
|
|
89
|
+
typeof used !== "number" ||
|
|
90
|
+
typeof remaining !== "number" ||
|
|
91
|
+
typeof exhausted !== "boolean") {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return { limit, used, remaining, exhausted };
|
|
95
|
+
}
|
|
96
|
+
function mapToChatbotResponse(body) {
|
|
97
|
+
if (!body || typeof body !== "object") {
|
|
98
|
+
throw new Error("Invalid chatbot response payload.");
|
|
99
|
+
}
|
|
100
|
+
const payload = body;
|
|
101
|
+
const reply = payload.reply;
|
|
102
|
+
const model = payload.model;
|
|
103
|
+
const usage = buildUsage(payload.usage);
|
|
104
|
+
if (typeof reply !== "string" || typeof model !== "string" || !usage) {
|
|
105
|
+
throw new Error("Invalid chatbot response payload.");
|
|
106
|
+
}
|
|
107
|
+
return { reply, model, usage };
|
|
108
|
+
}
|
|
109
|
+
function mapToChatbotUsageResponse(body) {
|
|
110
|
+
if (!body || typeof body !== "object") {
|
|
111
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
112
|
+
}
|
|
113
|
+
const payload = body;
|
|
114
|
+
const usage = buildUsage(payload.usage);
|
|
115
|
+
if (!usage) {
|
|
116
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
117
|
+
}
|
|
118
|
+
return { usage };
|
|
119
|
+
}
|
|
120
|
+
export async function getChatbotUsage(options = {}) {
|
|
121
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
122
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
123
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
124
|
+
const response = await fetcher(endpoint, {
|
|
125
|
+
method: "GET",
|
|
126
|
+
credentials: options.credentials ?? "include",
|
|
127
|
+
headers: {
|
|
128
|
+
Accept: "application/json",
|
|
129
|
+
...(authHeaders ?? {}),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
const body = await parseResponseBody(response);
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw normalizeError(response.status, body);
|
|
135
|
+
}
|
|
136
|
+
return mapToChatbotUsageResponse(body);
|
|
137
|
+
}
|
|
138
|
+
export async function chatWithAI(request, options = {}) {
|
|
139
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
140
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
141
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
142
|
+
const baseHeaders = {
|
|
143
|
+
Accept: "application/json",
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
...(authHeaders ?? {}),
|
|
146
|
+
};
|
|
147
|
+
const csrfToken = await ensureCsrfToken(fetcher, endpoint, options, baseHeaders);
|
|
148
|
+
const csrfHeaderName = options.csrfHeaderName ?? DEFAULT_CSRF_HEADER_NAME;
|
|
149
|
+
const headers = csrfToken
|
|
150
|
+
? {
|
|
151
|
+
...baseHeaders,
|
|
152
|
+
[csrfHeaderName]: csrfToken,
|
|
153
|
+
}
|
|
154
|
+
: baseHeaders;
|
|
155
|
+
const response = await fetcher(endpoint, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
credentials: options.credentials ?? "include",
|
|
158
|
+
headers,
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
message: request.message,
|
|
161
|
+
history: request.history ?? [],
|
|
162
|
+
systemPrompt: request.systemPrompt,
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
const body = await parseResponseBody(response);
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw normalizeError(response.status, body);
|
|
168
|
+
}
|
|
169
|
+
return mapToChatbotResponse(body);
|
|
170
|
+
}
|
package/dist-cjs/index.d.ts
CHANGED
package/dist-cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC"}
|
package/dist-cjs/index.js
CHANGED
|
@@ -1 +1,42 @@
|
|
|
1
|
+
export type ChatRole = "system" | "user" | "assistant";
|
|
2
|
+
export interface ChatMessage {
|
|
3
|
+
role: ChatRole;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ChatWithAIRequest {
|
|
7
|
+
message: string;
|
|
8
|
+
history?: ChatMessage[];
|
|
9
|
+
systemPrompt?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ChatbotUsage {
|
|
12
|
+
limit: number;
|
|
13
|
+
used: number;
|
|
14
|
+
remaining: number;
|
|
15
|
+
exhausted: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ChatbotResponse {
|
|
18
|
+
reply: string;
|
|
19
|
+
model: string;
|
|
20
|
+
usage: ChatbotUsage;
|
|
21
|
+
}
|
|
22
|
+
export interface ChatbotUsageResponse {
|
|
23
|
+
usage: ChatbotUsage;
|
|
24
|
+
}
|
|
25
|
+
export interface ChatWithAIClientOptions {
|
|
26
|
+
endpoint?: string;
|
|
27
|
+
credentials?: RequestCredentials;
|
|
28
|
+
fetchFn?: typeof fetch;
|
|
29
|
+
authHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
30
|
+
csrfCookieName?: string;
|
|
31
|
+
csrfHeaderName?: string;
|
|
32
|
+
bootstrapCsrf?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare class ChatbotApiError extends Error {
|
|
35
|
+
status: number;
|
|
36
|
+
code?: string;
|
|
37
|
+
usage?: ChatbotUsage;
|
|
38
|
+
constructor(status: number, message: string, code?: string, usage?: ChatbotUsage);
|
|
39
|
+
}
|
|
40
|
+
export declare function getChatbotUsage(options?: ChatWithAIClientOptions): Promise<ChatbotUsageResponse>;
|
|
41
|
+
export declare function chatWithAI(request: ChatWithAIRequest, options?: ChatWithAIClientOptions): Promise<ChatbotResponse>;
|
|
1
42
|
//# sourceMappingURL=chatWithAI.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatWithAI.d.ts","sourceRoot":"","sources":["../../src/lib/chatWithAI.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"chatWithAI.d.ts","sourceRoot":"","sources":["../../src/lib/chatWithAI.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,WAAW,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAYD,qBAAa,eAAgB,SAAQ,KAAK;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,YAAY,CAAC;gBAET,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY;CAOjF;AAuID,wBAAsB,eAAe,CACnC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,oBAAoB,CAAC,CAmB/B;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,EAC1B,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,eAAe,CAAC,CAoC1B"}
|
|
@@ -1 +1,176 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChatbotApiError = void 0;
|
|
4
|
+
exports.getChatbotUsage = getChatbotUsage;
|
|
5
|
+
exports.chatWithAI = chatWithAI;
|
|
6
|
+
const DEFAULT_ENDPOINT = "/ai/chatbot";
|
|
7
|
+
const DEFAULT_CSRF_COOKIE_NAME = "csrf-token";
|
|
8
|
+
const DEFAULT_CSRF_HEADER_NAME = "x-csrf-token";
|
|
9
|
+
class ChatbotApiError extends Error {
|
|
10
|
+
constructor(status, message, code, usage) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "ChatbotApiError";
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.usage = usage;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.ChatbotApiError = ChatbotApiError;
|
|
19
|
+
function resolveFetch(fetchFn) {
|
|
20
|
+
const resolved = fetchFn ?? (typeof fetch !== "undefined" ? fetch : undefined);
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
throw new Error("No fetch implementation available for chatWithAI.");
|
|
23
|
+
}
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
26
|
+
async function resolveAuthHeaders(authHeaders) {
|
|
27
|
+
if (!authHeaders)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (typeof authHeaders === "function") {
|
|
30
|
+
return await authHeaders();
|
|
31
|
+
}
|
|
32
|
+
return authHeaders;
|
|
33
|
+
}
|
|
34
|
+
function readCookie(cookieName) {
|
|
35
|
+
if (typeof document === "undefined" || typeof document.cookie !== "string") {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const entries = document.cookie.split(";").map((part) => part.trim());
|
|
39
|
+
const match = entries.find((entry) => entry.startsWith(`${cookieName}=`));
|
|
40
|
+
if (!match)
|
|
41
|
+
return undefined;
|
|
42
|
+
const [, value = ""] = match.split("=");
|
|
43
|
+
try {
|
|
44
|
+
return decodeURIComponent(value);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function parseResponseBody(response) {
|
|
51
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
52
|
+
if (contentType.includes("application/json")) {
|
|
53
|
+
return await response.json();
|
|
54
|
+
}
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
if (!text)
|
|
57
|
+
return undefined;
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function ensureCsrfToken(fetcher, endpoint, options, headers) {
|
|
66
|
+
const csrfCookieName = options.csrfCookieName ?? DEFAULT_CSRF_COOKIE_NAME;
|
|
67
|
+
const existingToken = readCookie(csrfCookieName);
|
|
68
|
+
if (existingToken || options.bootstrapCsrf === false) {
|
|
69
|
+
return existingToken;
|
|
70
|
+
}
|
|
71
|
+
await fetcher(endpoint, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
credentials: options.credentials ?? "include",
|
|
74
|
+
headers,
|
|
75
|
+
});
|
|
76
|
+
return readCookie(csrfCookieName);
|
|
77
|
+
}
|
|
78
|
+
function normalizeError(status, body) {
|
|
79
|
+
const payload = body && typeof body === "object" ? body : undefined;
|
|
80
|
+
const message = payload?.message ||
|
|
81
|
+
(status === 401
|
|
82
|
+
? "You must be signed in to use chatbot."
|
|
83
|
+
: "Chatbot request failed.");
|
|
84
|
+
return new ChatbotApiError(status, message, payload?.error, payload?.usage);
|
|
85
|
+
}
|
|
86
|
+
function buildUsage(payload) {
|
|
87
|
+
if (!payload || typeof payload !== "object")
|
|
88
|
+
return undefined;
|
|
89
|
+
const maybe = payload;
|
|
90
|
+
const limit = maybe.limit;
|
|
91
|
+
const used = maybe.used;
|
|
92
|
+
const remaining = maybe.remaining;
|
|
93
|
+
const exhausted = maybe.exhausted;
|
|
94
|
+
if (typeof limit !== "number" ||
|
|
95
|
+
typeof used !== "number" ||
|
|
96
|
+
typeof remaining !== "number" ||
|
|
97
|
+
typeof exhausted !== "boolean") {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return { limit, used, remaining, exhausted };
|
|
101
|
+
}
|
|
102
|
+
function mapToChatbotResponse(body) {
|
|
103
|
+
if (!body || typeof body !== "object") {
|
|
104
|
+
throw new Error("Invalid chatbot response payload.");
|
|
105
|
+
}
|
|
106
|
+
const payload = body;
|
|
107
|
+
const reply = payload.reply;
|
|
108
|
+
const model = payload.model;
|
|
109
|
+
const usage = buildUsage(payload.usage);
|
|
110
|
+
if (typeof reply !== "string" || typeof model !== "string" || !usage) {
|
|
111
|
+
throw new Error("Invalid chatbot response payload.");
|
|
112
|
+
}
|
|
113
|
+
return { reply, model, usage };
|
|
114
|
+
}
|
|
115
|
+
function mapToChatbotUsageResponse(body) {
|
|
116
|
+
if (!body || typeof body !== "object") {
|
|
117
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
118
|
+
}
|
|
119
|
+
const payload = body;
|
|
120
|
+
const usage = buildUsage(payload.usage);
|
|
121
|
+
if (!usage) {
|
|
122
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
123
|
+
}
|
|
124
|
+
return { usage };
|
|
125
|
+
}
|
|
126
|
+
async function getChatbotUsage(options = {}) {
|
|
127
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
128
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
129
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
130
|
+
const response = await fetcher(endpoint, {
|
|
131
|
+
method: "GET",
|
|
132
|
+
credentials: options.credentials ?? "include",
|
|
133
|
+
headers: {
|
|
134
|
+
Accept: "application/json",
|
|
135
|
+
...(authHeaders ?? {}),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const body = await parseResponseBody(response);
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw normalizeError(response.status, body);
|
|
141
|
+
}
|
|
142
|
+
return mapToChatbotUsageResponse(body);
|
|
143
|
+
}
|
|
144
|
+
async function chatWithAI(request, options = {}) {
|
|
145
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
146
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
147
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
148
|
+
const baseHeaders = {
|
|
149
|
+
Accept: "application/json",
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
...(authHeaders ?? {}),
|
|
152
|
+
};
|
|
153
|
+
const csrfToken = await ensureCsrfToken(fetcher, endpoint, options, baseHeaders);
|
|
154
|
+
const csrfHeaderName = options.csrfHeaderName ?? DEFAULT_CSRF_HEADER_NAME;
|
|
155
|
+
const headers = csrfToken
|
|
156
|
+
? {
|
|
157
|
+
...baseHeaders,
|
|
158
|
+
[csrfHeaderName]: csrfToken,
|
|
159
|
+
}
|
|
160
|
+
: baseHeaders;
|
|
161
|
+
const response = await fetcher(endpoint, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
credentials: options.credentials ?? "include",
|
|
164
|
+
headers,
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
message: request.message,
|
|
167
|
+
history: request.history ?? [],
|
|
168
|
+
systemPrompt: request.systemPrompt,
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
const body = await parseResponseBody(response);
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw normalizeError(response.status, body);
|
|
174
|
+
}
|
|
175
|
+
return mapToChatbotResponse(body);
|
|
176
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# ADR Index
|
|
2
|
+
|
|
3
|
+
- [ADR-0001: Standalone @plasius/ai Package Scope](./adr-0001-ai-package-scope.md)
|
|
4
|
+
- [ADR-0002: Public Repository Governance Baseline](./adr-0002-public-repo-governance.md)
|
|
5
|
+
- [ADR-0003: Contracts-First Documentation Baseline](./adr-0003-contracts-first-documentation.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plasius/ai",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Plasius AI functions providing chatbot, text-to-speech, speech-to-text, and AI-generated images and videos",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chatbot",
|
|
@@ -24,13 +24,14 @@
|
|
|
24
24
|
"reset:clean": "rm -rf node_modules package-lock.json && npm run clean",
|
|
25
25
|
"audit:ts": "tsc --noEmit --pretty",
|
|
26
26
|
"audit:eslint": "eslint \"{src,apps,packages}/**/*.{ts,tsx}\" --max-warnings=0 --ext .ts,.tsx",
|
|
27
|
-
"audit:deps": "
|
|
27
|
+
"audit:deps": "npm ls --all --omit=optional --omit=peer > /dev/null 2>&1 || true",
|
|
28
28
|
"audit:npm": "npm audit --audit-level=moderate || true",
|
|
29
29
|
"audit:test": "vitest run --coverage",
|
|
30
30
|
"audit:all": "npm-run-all -l audit:ts audit:eslint audit:deps audit:npm audit:test",
|
|
31
31
|
"build:cjs": "tsc -p tsconfig.json --module commonjs --moduleResolution node --outDir dist-cjs --tsBuildInfoFile dist-cjs/tsconfig.tsbuildinfo",
|
|
32
32
|
"lint": "eslint .",
|
|
33
|
-
"prepare": "npm run build"
|
|
33
|
+
"prepare": "npm run build",
|
|
34
|
+
"demo:run": "npm run build && node demo/example.mjs"
|
|
34
35
|
},
|
|
35
36
|
"author": "Plasius LTD <development@plasius.co.uk>",
|
|
36
37
|
"license": "MIT",
|
|
@@ -41,29 +42,28 @@
|
|
|
41
42
|
"@plasius/schema": "^1.0.0"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
|
-
"openai": "^5.
|
|
45
|
+
"openai": "^5.23.2",
|
|
45
46
|
"react": "^19.1.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@testing-library/react": "^16.3.0",
|
|
49
50
|
"@types/node": "^24.3.1",
|
|
50
|
-
"@types/react": "^19.
|
|
51
|
+
"@types/react": "^19.2.14",
|
|
51
52
|
"@types/uuid": "^10.0.0",
|
|
52
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
53
|
-
"@typescript-eslint/parser": "^8.
|
|
53
|
+
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
|
54
|
+
"@typescript-eslint/parser": "^8.55.0",
|
|
54
55
|
"@vitest/coverage-v8": "^3.2.4",
|
|
55
56
|
"ajv": "^6.12.6",
|
|
56
|
-
"
|
|
57
|
-
"eslint": "^9.33.0",
|
|
57
|
+
"eslint": "^9.39.2",
|
|
58
58
|
"npm-run-all": "^4.1.5",
|
|
59
|
-
"openai": "^5.
|
|
60
|
-
"react": "19.
|
|
61
|
-
"react-dom": "19.
|
|
62
|
-
"react-router-dom": "^7.
|
|
63
|
-
"tsx": "^4.
|
|
64
|
-
"typescript": "^5.
|
|
59
|
+
"openai": "^5.23.2",
|
|
60
|
+
"react": "19.2.4",
|
|
61
|
+
"react-dom": "19.2.4",
|
|
62
|
+
"react-router-dom": "^7.13.0",
|
|
63
|
+
"tsx": "^4.21.0",
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
65
|
"vitest": "^3.2.4",
|
|
66
|
-
"zod": "^4.1.
|
|
66
|
+
"zod": "^4.1.12"
|
|
67
67
|
},
|
|
68
68
|
"sideEffects": [
|
|
69
69
|
"*.css"
|
|
@@ -113,5 +113,6 @@
|
|
|
113
113
|
],
|
|
114
114
|
"engines": {
|
|
115
115
|
"node": ">=22.12"
|
|
116
|
-
}
|
|
116
|
+
},
|
|
117
|
+
"packageManager": "npm@11.4.2"
|
|
117
118
|
}
|
package/src/index.ts
CHANGED
package/src/lib/chatWithAI.ts
CHANGED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
export type ChatRole = "system" | "user" | "assistant";
|
|
2
|
+
|
|
3
|
+
export interface ChatMessage {
|
|
4
|
+
role: ChatRole;
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ChatWithAIRequest {
|
|
9
|
+
message: string;
|
|
10
|
+
history?: ChatMessage[];
|
|
11
|
+
systemPrompt?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChatbotUsage {
|
|
15
|
+
limit: number;
|
|
16
|
+
used: number;
|
|
17
|
+
remaining: number;
|
|
18
|
+
exhausted: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ChatbotResponse {
|
|
22
|
+
reply: string;
|
|
23
|
+
model: string;
|
|
24
|
+
usage: ChatbotUsage;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ChatbotUsageResponse {
|
|
28
|
+
usage: ChatbotUsage;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ChatWithAIClientOptions {
|
|
32
|
+
endpoint?: string;
|
|
33
|
+
credentials?: RequestCredentials;
|
|
34
|
+
fetchFn?: typeof fetch;
|
|
35
|
+
authHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
36
|
+
csrfCookieName?: string;
|
|
37
|
+
csrfHeaderName?: string;
|
|
38
|
+
bootstrapCsrf?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ErrorBody {
|
|
42
|
+
error?: string;
|
|
43
|
+
message?: string;
|
|
44
|
+
usage?: ChatbotUsage;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DEFAULT_ENDPOINT = "/ai/chatbot";
|
|
48
|
+
const DEFAULT_CSRF_COOKIE_NAME = "csrf-token";
|
|
49
|
+
const DEFAULT_CSRF_HEADER_NAME = "x-csrf-token";
|
|
50
|
+
|
|
51
|
+
export class ChatbotApiError extends Error {
|
|
52
|
+
status: number;
|
|
53
|
+
code?: string;
|
|
54
|
+
usage?: ChatbotUsage;
|
|
55
|
+
|
|
56
|
+
constructor(status: number, message: string, code?: string, usage?: ChatbotUsage) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "ChatbotApiError";
|
|
59
|
+
this.status = status;
|
|
60
|
+
this.code = code;
|
|
61
|
+
this.usage = usage;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveFetch(fetchFn?: typeof fetch): typeof fetch {
|
|
66
|
+
const resolved = fetchFn ?? (typeof fetch !== "undefined" ? fetch : undefined);
|
|
67
|
+
if (!resolved) {
|
|
68
|
+
throw new Error("No fetch implementation available for chatWithAI.");
|
|
69
|
+
}
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveAuthHeaders(
|
|
74
|
+
authHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)
|
|
75
|
+
): Promise<HeadersInit | undefined> {
|
|
76
|
+
if (!authHeaders) return undefined;
|
|
77
|
+
if (typeof authHeaders === "function") {
|
|
78
|
+
return await authHeaders();
|
|
79
|
+
}
|
|
80
|
+
return authHeaders;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readCookie(cookieName: string): string | undefined {
|
|
84
|
+
if (typeof document === "undefined" || typeof document.cookie !== "string") {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const entries = document.cookie.split(";").map((part) => part.trim());
|
|
89
|
+
const match = entries.find((entry) => entry.startsWith(`${cookieName}=`));
|
|
90
|
+
if (!match) return undefined;
|
|
91
|
+
|
|
92
|
+
const [, value = ""] = match.split("=");
|
|
93
|
+
try {
|
|
94
|
+
return decodeURIComponent(value);
|
|
95
|
+
} catch {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function parseResponseBody(response: Response): Promise<unknown> {
|
|
101
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
102
|
+
if (contentType.includes("application/json")) {
|
|
103
|
+
return await response.json();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const text = await response.text();
|
|
107
|
+
if (!text) return undefined;
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(text) as unknown;
|
|
110
|
+
} catch {
|
|
111
|
+
return text;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function ensureCsrfToken(
|
|
116
|
+
fetcher: typeof fetch,
|
|
117
|
+
endpoint: string,
|
|
118
|
+
options: ChatWithAIClientOptions,
|
|
119
|
+
headers: HeadersInit
|
|
120
|
+
): Promise<string | undefined> {
|
|
121
|
+
const csrfCookieName = options.csrfCookieName ?? DEFAULT_CSRF_COOKIE_NAME;
|
|
122
|
+
const existingToken = readCookie(csrfCookieName);
|
|
123
|
+
if (existingToken || options.bootstrapCsrf === false) {
|
|
124
|
+
return existingToken;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await fetcher(endpoint, {
|
|
128
|
+
method: "GET",
|
|
129
|
+
credentials: options.credentials ?? "include",
|
|
130
|
+
headers,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return readCookie(csrfCookieName);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeError(status: number, body: unknown): ChatbotApiError {
|
|
137
|
+
const payload = body && typeof body === "object" ? (body as ErrorBody) : undefined;
|
|
138
|
+
const message =
|
|
139
|
+
payload?.message ||
|
|
140
|
+
(status === 401
|
|
141
|
+
? "You must be signed in to use chatbot."
|
|
142
|
+
: "Chatbot request failed.");
|
|
143
|
+
|
|
144
|
+
return new ChatbotApiError(status, message, payload?.error, payload?.usage);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildUsage(payload: unknown): ChatbotUsage | undefined {
|
|
148
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
149
|
+
const maybe = payload as Record<string, unknown>;
|
|
150
|
+
const limit = maybe.limit;
|
|
151
|
+
const used = maybe.used;
|
|
152
|
+
const remaining = maybe.remaining;
|
|
153
|
+
const exhausted = maybe.exhausted;
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
typeof limit !== "number" ||
|
|
157
|
+
typeof used !== "number" ||
|
|
158
|
+
typeof remaining !== "number" ||
|
|
159
|
+
typeof exhausted !== "boolean"
|
|
160
|
+
) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { limit, used, remaining, exhausted };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function mapToChatbotResponse(body: unknown): ChatbotResponse {
|
|
168
|
+
if (!body || typeof body !== "object") {
|
|
169
|
+
throw new Error("Invalid chatbot response payload.");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const payload = body as Record<string, unknown>;
|
|
173
|
+
const reply = payload.reply;
|
|
174
|
+
const model = payload.model;
|
|
175
|
+
const usage = buildUsage(payload.usage);
|
|
176
|
+
|
|
177
|
+
if (typeof reply !== "string" || typeof model !== "string" || !usage) {
|
|
178
|
+
throw new Error("Invalid chatbot response payload.");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { reply, model, usage };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function mapToChatbotUsageResponse(body: unknown): ChatbotUsageResponse {
|
|
185
|
+
if (!body || typeof body !== "object") {
|
|
186
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const payload = body as Record<string, unknown>;
|
|
190
|
+
const usage = buildUsage(payload.usage);
|
|
191
|
+
if (!usage) {
|
|
192
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { usage };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function getChatbotUsage(
|
|
199
|
+
options: ChatWithAIClientOptions = {}
|
|
200
|
+
): Promise<ChatbotUsageResponse> {
|
|
201
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
202
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
203
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
204
|
+
const response = await fetcher(endpoint, {
|
|
205
|
+
method: "GET",
|
|
206
|
+
credentials: options.credentials ?? "include",
|
|
207
|
+
headers: {
|
|
208
|
+
Accept: "application/json",
|
|
209
|
+
...(authHeaders ?? {}),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const body = await parseResponseBody(response);
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
throw normalizeError(response.status, body);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return mapToChatbotUsageResponse(body);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function chatWithAI(
|
|
222
|
+
request: ChatWithAIRequest,
|
|
223
|
+
options: ChatWithAIClientOptions = {}
|
|
224
|
+
): Promise<ChatbotResponse> {
|
|
225
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
226
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
227
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
228
|
+
const baseHeaders: HeadersInit = {
|
|
229
|
+
Accept: "application/json",
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
...(authHeaders ?? {}),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const csrfToken = await ensureCsrfToken(fetcher, endpoint, options, baseHeaders);
|
|
235
|
+
const csrfHeaderName = options.csrfHeaderName ?? DEFAULT_CSRF_HEADER_NAME;
|
|
236
|
+
const headers = csrfToken
|
|
237
|
+
? {
|
|
238
|
+
...baseHeaders,
|
|
239
|
+
[csrfHeaderName]: csrfToken,
|
|
240
|
+
}
|
|
241
|
+
: baseHeaders;
|
|
242
|
+
|
|
243
|
+
const response = await fetcher(endpoint, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
credentials: options.credentials ?? "include",
|
|
246
|
+
headers,
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
message: request.message,
|
|
249
|
+
history: request.history ?? [],
|
|
250
|
+
systemPrompt: request.systemPrompt,
|
|
251
|
+
}),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const body = await parseResponseBody(response);
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw normalizeError(response.status, body);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return mapToChatbotResponse(body);
|
|
260
|
+
}
|