@laintern/chat-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -0
- package/dist/chunk-WMKNNVMS.js +266 -0
- package/dist/chunk-WMKNNVMS.js.map +1 -0
- package/dist/conversation-7VzVDT95.d.cts +284 -0
- package/dist/conversation-7VzVDT95.d.ts +284 -0
- package/dist/index.cjs +538 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +268 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +25 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/index.js +30 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
type ContentType = "forum_topic" | "article";
|
|
2
|
+
interface SearchResult {
|
|
3
|
+
content_id: string;
|
|
4
|
+
content_type: ChatContentType;
|
|
5
|
+
title: string;
|
|
6
|
+
url: string;
|
|
7
|
+
imageUrl?: string | null;
|
|
8
|
+
similarity: number;
|
|
9
|
+
snippet?: string;
|
|
10
|
+
reply_count?: number;
|
|
11
|
+
author_first_name?: string;
|
|
12
|
+
/** ISO 8601 — forum: topic created_at; article: updated_at_source or created_at (for display/sort) */
|
|
13
|
+
posted_at?: string;
|
|
14
|
+
/** Kennisbank articles only */
|
|
15
|
+
read_time_minutes?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Where inside the source this result sits — e.g. a `kb_chunk`'s heading
|
|
18
|
+
* path ("Traject › Aanmelden"). Lets a citation name the passage, not just
|
|
19
|
+
* the document. Absent for whole-document sources.
|
|
20
|
+
*/
|
|
21
|
+
section?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What a chat citation can point at. `kb_chunk` is a passage from the chunked
|
|
25
|
+
* knowledge base: it has no public URL, so consumers must render it as a
|
|
26
|
+
* non-navigable source (document title + `section`) rather than a link.
|
|
27
|
+
*/
|
|
28
|
+
type ChatContentType = ContentType | "medical_web" | "kb_chunk";
|
|
29
|
+
interface CitationSource {
|
|
30
|
+
source_id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
url: string;
|
|
33
|
+
content_type: ChatContentType;
|
|
34
|
+
/** Passage location within the source (see {@link SearchResult.section}). */
|
|
35
|
+
section?: string;
|
|
36
|
+
}
|
|
37
|
+
type StepEvent = {
|
|
38
|
+
status: "started";
|
|
39
|
+
tool: string;
|
|
40
|
+
} | {
|
|
41
|
+
status: "completed";
|
|
42
|
+
tool: string;
|
|
43
|
+
count?: number;
|
|
44
|
+
};
|
|
45
|
+
type ExternalSourceType = ContentType;
|
|
46
|
+
interface ExternalSourcesEvent {
|
|
47
|
+
type: "external_sources";
|
|
48
|
+
source_type: ExternalSourceType;
|
|
49
|
+
items: SearchResult[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Where the medical input guardrail wants to point the user.
|
|
53
|
+
* - `112` / `113` are HARD blocks: the agent does not respond.
|
|
54
|
+
* - `huisarts` / `huisartsenpost` / `null` are SOFT advisories: the agent
|
|
55
|
+
* responds normally and the widget shows a less-invasive notice.
|
|
56
|
+
*/
|
|
57
|
+
type GuardrailRedirect = "112" | "113" | "huisarts" | "huisartsenpost" | null;
|
|
58
|
+
type ChatStreamEvent = {
|
|
59
|
+
type: "session";
|
|
60
|
+
session_id: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: "mode";
|
|
63
|
+
mode: "search" | "medical";
|
|
64
|
+
} | {
|
|
65
|
+
type: "step";
|
|
66
|
+
step: StepEvent;
|
|
67
|
+
} | {
|
|
68
|
+
type: "delta";
|
|
69
|
+
content: string;
|
|
70
|
+
} | {
|
|
71
|
+
type: "citation";
|
|
72
|
+
index: number;
|
|
73
|
+
source: CitationSource;
|
|
74
|
+
} | {
|
|
75
|
+
type: "sources";
|
|
76
|
+
items: SearchResult[];
|
|
77
|
+
} | ExternalSourcesEvent
|
|
78
|
+
/**
|
|
79
|
+
* Soft guardrail notice emitted alongside a normal agent response. The
|
|
80
|
+
* widget renders this as an unobtrusive info bar; the assistant message
|
|
81
|
+
* itself still streams as usual. Hard blocks stay on the `error` event
|
|
82
|
+
* with code `GUARDRAIL_TRIGGERED`.
|
|
83
|
+
*/
|
|
84
|
+
| {
|
|
85
|
+
type: "advisory";
|
|
86
|
+
redirect: Exclude<GuardrailRedirect, "112" | "113">;
|
|
87
|
+
message: string;
|
|
88
|
+
} | {
|
|
89
|
+
type: "done";
|
|
90
|
+
usage?: {
|
|
91
|
+
prompt_tokens: number;
|
|
92
|
+
completion_tokens: number;
|
|
93
|
+
};
|
|
94
|
+
message_id?: string;
|
|
95
|
+
} | {
|
|
96
|
+
type: "error";
|
|
97
|
+
code: string;
|
|
98
|
+
message: string;
|
|
99
|
+
guardrail_slug?: string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Refresh this long before a token expires. It mirrors the margin the API
|
|
104
|
+
* applies when it clamps a chat token to a credential inside it: the server
|
|
105
|
+
* guarantees the token dies *after* the credential's own refresh point, and
|
|
106
|
+
* this buffer is what makes the client come back in time to use that.
|
|
107
|
+
*/
|
|
108
|
+
declare const DEFAULT_REFRESH_BUFFER_MS = 60000;
|
|
109
|
+
interface TokenResponse {
|
|
110
|
+
token: string;
|
|
111
|
+
/** ISO-8601 or epoch ms, if the endpoint reports one. */
|
|
112
|
+
expiresAt?: string | number | null;
|
|
113
|
+
}
|
|
114
|
+
/** Minimal storage surface — `sessionStorage`, `localStorage` or your own. */
|
|
115
|
+
interface TokenStorage {
|
|
116
|
+
getItem(key: string): string | null;
|
|
117
|
+
setItem(key: string, value: string): void;
|
|
118
|
+
removeItem(key: string): void;
|
|
119
|
+
}
|
|
120
|
+
interface TokenManagerOptions {
|
|
121
|
+
/**
|
|
122
|
+
* Fetches a fresh token from YOUR backend. This is the one integration
|
|
123
|
+
* point that cannot be generic: only your server may hold the API key that
|
|
124
|
+
* mints Laintern tokens, and only it knows who the current user is.
|
|
125
|
+
*/
|
|
126
|
+
fetchToken: () => Promise<TokenResponse | string>;
|
|
127
|
+
storage?: TokenStorage | null;
|
|
128
|
+
storageKey?: string;
|
|
129
|
+
refreshBufferMs?: number;
|
|
130
|
+
onWarning?: (message: string, detail?: Record<string, unknown>) => void;
|
|
131
|
+
}
|
|
132
|
+
interface TokenManager {
|
|
133
|
+
/** Cached token when still valid, otherwise a freshly fetched one. */
|
|
134
|
+
get(): Promise<string>;
|
|
135
|
+
/** Drop the cached token — the next `get()` fetches. */
|
|
136
|
+
clear(): void;
|
|
137
|
+
/** Expiry of the cached token, in epoch ms. Null when nothing is cached. */
|
|
138
|
+
expiresAt(): number | null;
|
|
139
|
+
}
|
|
140
|
+
/** Read a JWT's `exp` claim (epoch ms) without verifying the signature. */
|
|
141
|
+
declare function decodeJwtExpiryMs(token: string): number | null;
|
|
142
|
+
/**
|
|
143
|
+
* Caches the chat token and refreshes it before it dies.
|
|
144
|
+
*
|
|
145
|
+
* Concurrent callers share one in-flight fetch: a page that opens a stream
|
|
146
|
+
* while another request is already refreshing must not mint two sessions.
|
|
147
|
+
*/
|
|
148
|
+
declare function createTokenManager(options: TokenManagerOptions): TokenManager;
|
|
149
|
+
|
|
150
|
+
interface LainternClientOptions {
|
|
151
|
+
/** Base URL of the Laintern API, e.g. "https://api.laintern.com". */
|
|
152
|
+
apiBase: string;
|
|
153
|
+
/**
|
|
154
|
+
* How to get a chat token. Either your own function, or the URL of an
|
|
155
|
+
* endpoint on your backend that returns `{ token, expires_at }`.
|
|
156
|
+
*
|
|
157
|
+
* Never put a Laintern API key in the browser: the key mints tokens for any
|
|
158
|
+
* user, so it belongs on your server, behind your own session check.
|
|
159
|
+
*/
|
|
160
|
+
token: TokenManagerOptions["fetchToken"] | {
|
|
161
|
+
endpoint: string;
|
|
162
|
+
init?: RequestInit;
|
|
163
|
+
};
|
|
164
|
+
storage?: TokenManagerOptions["storage"];
|
|
165
|
+
refreshBufferMs?: number;
|
|
166
|
+
onWarning?: TokenManagerOptions["onWarning"];
|
|
167
|
+
/** Injectable for tests and non-browser runtimes. */
|
|
168
|
+
fetch?: typeof globalThis.fetch;
|
|
169
|
+
}
|
|
170
|
+
interface SendMessageOptions {
|
|
171
|
+
/** Continues an existing conversation. Omit to start a new one. */
|
|
172
|
+
sessionId?: string | null;
|
|
173
|
+
/** Base64 data URLs, for agents with image input enabled. */
|
|
174
|
+
images?: string[];
|
|
175
|
+
/** Force a specific tool on this turn, when the agent allows it. */
|
|
176
|
+
forceTool?: string;
|
|
177
|
+
signal?: AbortSignal;
|
|
178
|
+
}
|
|
179
|
+
interface AgentConfig {
|
|
180
|
+
/** Transparency notice to show before the conversation starts (EU AI Act). */
|
|
181
|
+
disclaimer: {
|
|
182
|
+
enabled: boolean;
|
|
183
|
+
text: string | null;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
interface LainternClient {
|
|
187
|
+
/**
|
|
188
|
+
* Send one message and iterate the response as it streams.
|
|
189
|
+
*
|
|
190
|
+
* Yields typed events (`delta`, `citation`, `sources`, `step`, `done`, …).
|
|
191
|
+
* Prefer {@link createConversation} unless you want to assemble the turn
|
|
192
|
+
* yourself.
|
|
193
|
+
*/
|
|
194
|
+
sendMessage(message: string, options?: SendMessageOptions): AsyncGenerator<ChatStreamEvent>;
|
|
195
|
+
/** Per-agent presentation config. Best-effort: throws on transport errors. */
|
|
196
|
+
getConfig(): Promise<AgentConfig>;
|
|
197
|
+
submitFeedback(messageId: string, feedback: "positive" | "negative"): Promise<void>;
|
|
198
|
+
/** The token cache, exposed so a host can clear it on logout. */
|
|
199
|
+
tokens: TokenManager;
|
|
200
|
+
}
|
|
201
|
+
declare function createLainternClient(options: LainternClientOptions): LainternClient;
|
|
202
|
+
|
|
203
|
+
/** Soft advisory alongside an answer — the agent flagged the topic, but answered. */
|
|
204
|
+
interface TurnAdvisory {
|
|
205
|
+
redirect: string | null;
|
|
206
|
+
message: string;
|
|
207
|
+
}
|
|
208
|
+
/** A guardrail refused the turn. The agent produced no answer. */
|
|
209
|
+
interface TurnBlock {
|
|
210
|
+
/** Which guardrail fired, when the server names one. */
|
|
211
|
+
slug: string | null;
|
|
212
|
+
/** Copy meant for the end user, written by the guardrail's configuration. */
|
|
213
|
+
message: string;
|
|
214
|
+
}
|
|
215
|
+
interface TurnError {
|
|
216
|
+
code: string;
|
|
217
|
+
message: string;
|
|
218
|
+
rateLimitScope?: "user" | "agent";
|
|
219
|
+
rateLimitWindow?: "minute" | "hour" | "day";
|
|
220
|
+
}
|
|
221
|
+
interface Turn {
|
|
222
|
+
id: string;
|
|
223
|
+
userMessage: string;
|
|
224
|
+
userImages?: string[];
|
|
225
|
+
assistantContent: string;
|
|
226
|
+
/** Citation number (the `[1]` in the text) → the source it points at. */
|
|
227
|
+
citations: Map<number, CitationSource>;
|
|
228
|
+
/** Everything the agent cited or consulted, in the order the server ranked it. */
|
|
229
|
+
sources: SearchResult[];
|
|
230
|
+
/** Sidebar suggestions per content type — not necessarily cited. */
|
|
231
|
+
externalSources: Partial<Record<ExternalSourceType, SearchResult[]>>;
|
|
232
|
+
mode: string | null;
|
|
233
|
+
steps: StepEvent[];
|
|
234
|
+
isStreaming: boolean;
|
|
235
|
+
error?: TurnError;
|
|
236
|
+
blocked?: TurnBlock;
|
|
237
|
+
advisory?: TurnAdvisory;
|
|
238
|
+
messageId?: string;
|
|
239
|
+
feedback?: "positive" | "negative" | null;
|
|
240
|
+
}
|
|
241
|
+
interface ConversationState {
|
|
242
|
+
turns: Turn[];
|
|
243
|
+
sessionId: string | null;
|
|
244
|
+
isStreaming: boolean;
|
|
245
|
+
}
|
|
246
|
+
interface SendOptions {
|
|
247
|
+
images?: string[];
|
|
248
|
+
forceTool?: string;
|
|
249
|
+
}
|
|
250
|
+
interface Conversation {
|
|
251
|
+
getState(): ConversationState;
|
|
252
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
253
|
+
subscribe(listener: (state: ConversationState) => void): () => void;
|
|
254
|
+
/** Send a message and stream the answer into state. Never throws. */
|
|
255
|
+
send(message: string, options?: SendOptions): Promise<void>;
|
|
256
|
+
/** Stop the current stream. The partial answer stays in state. */
|
|
257
|
+
abort(): void;
|
|
258
|
+
/** Drop all turns and start a new server-side session on the next send. */
|
|
259
|
+
reset(): void;
|
|
260
|
+
setFeedback(turnId: string, feedback: "positive" | "negative"): Promise<void>;
|
|
261
|
+
}
|
|
262
|
+
interface ConversationOptions {
|
|
263
|
+
/** Resume an existing server-side session. */
|
|
264
|
+
sessionId?: string | null;
|
|
265
|
+
/** Restore turns from your own storage. */
|
|
266
|
+
initialTurns?: Turn[];
|
|
267
|
+
/** Called whenever the session id changes, so you can persist it. */
|
|
268
|
+
onSessionId?: (sessionId: string) => void;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Stateful conversation on top of {@link LainternClient}.
|
|
272
|
+
*
|
|
273
|
+
* Holds the turn list, applies each stream event to the turn in flight, and
|
|
274
|
+
* notifies subscribers. Framework-agnostic on purpose: the React binding in
|
|
275
|
+
* `@laintern/chat-sdk/react` is a thin `useSyncExternalStore` wrapper, and any
|
|
276
|
+
* other UI layer can subscribe the same way.
|
|
277
|
+
*
|
|
278
|
+
* `send()` never throws. A failed turn is part of the conversation — it lands
|
|
279
|
+
* on `turn.error` (or `turn.blocked` for a guardrail) so the UI can render it
|
|
280
|
+
* in place, which is what you want in a chat transcript.
|
|
281
|
+
*/
|
|
282
|
+
declare function createConversation(client: LainternClient, options?: ConversationOptions): Conversation;
|
|
283
|
+
|
|
284
|
+
export { type AgentConfig as A, type ChatStreamEvent as C, DEFAULT_REFRESH_BUFFER_MS as D, type ExternalSourceType as E, type LainternClient as L, type SearchResult as S, type TokenManager as T, type ChatContentType as a, type CitationSource as b, type ContentType as c, type Conversation as d, type ConversationOptions as e, type ConversationState as f, type LainternClientOptions as g, type SendMessageOptions as h, type SendOptions as i, type StepEvent as j, type TokenManagerOptions as k, type TokenResponse as l, type TokenStorage as m, type Turn as n, type TurnAdvisory as o, type TurnBlock as p, type TurnError as q, createConversation as r, createLainternClient as s, createTokenManager as t, decodeJwtExpiryMs as u };
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
type ContentType = "forum_topic" | "article";
|
|
2
|
+
interface SearchResult {
|
|
3
|
+
content_id: string;
|
|
4
|
+
content_type: ChatContentType;
|
|
5
|
+
title: string;
|
|
6
|
+
url: string;
|
|
7
|
+
imageUrl?: string | null;
|
|
8
|
+
similarity: number;
|
|
9
|
+
snippet?: string;
|
|
10
|
+
reply_count?: number;
|
|
11
|
+
author_first_name?: string;
|
|
12
|
+
/** ISO 8601 — forum: topic created_at; article: updated_at_source or created_at (for display/sort) */
|
|
13
|
+
posted_at?: string;
|
|
14
|
+
/** Kennisbank articles only */
|
|
15
|
+
read_time_minutes?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Where inside the source this result sits — e.g. a `kb_chunk`'s heading
|
|
18
|
+
* path ("Traject › Aanmelden"). Lets a citation name the passage, not just
|
|
19
|
+
* the document. Absent for whole-document sources.
|
|
20
|
+
*/
|
|
21
|
+
section?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What a chat citation can point at. `kb_chunk` is a passage from the chunked
|
|
25
|
+
* knowledge base: it has no public URL, so consumers must render it as a
|
|
26
|
+
* non-navigable source (document title + `section`) rather than a link.
|
|
27
|
+
*/
|
|
28
|
+
type ChatContentType = ContentType | "medical_web" | "kb_chunk";
|
|
29
|
+
interface CitationSource {
|
|
30
|
+
source_id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
url: string;
|
|
33
|
+
content_type: ChatContentType;
|
|
34
|
+
/** Passage location within the source (see {@link SearchResult.section}). */
|
|
35
|
+
section?: string;
|
|
36
|
+
}
|
|
37
|
+
type StepEvent = {
|
|
38
|
+
status: "started";
|
|
39
|
+
tool: string;
|
|
40
|
+
} | {
|
|
41
|
+
status: "completed";
|
|
42
|
+
tool: string;
|
|
43
|
+
count?: number;
|
|
44
|
+
};
|
|
45
|
+
type ExternalSourceType = ContentType;
|
|
46
|
+
interface ExternalSourcesEvent {
|
|
47
|
+
type: "external_sources";
|
|
48
|
+
source_type: ExternalSourceType;
|
|
49
|
+
items: SearchResult[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Where the medical input guardrail wants to point the user.
|
|
53
|
+
* - `112` / `113` are HARD blocks: the agent does not respond.
|
|
54
|
+
* - `huisarts` / `huisartsenpost` / `null` are SOFT advisories: the agent
|
|
55
|
+
* responds normally and the widget shows a less-invasive notice.
|
|
56
|
+
*/
|
|
57
|
+
type GuardrailRedirect = "112" | "113" | "huisarts" | "huisartsenpost" | null;
|
|
58
|
+
type ChatStreamEvent = {
|
|
59
|
+
type: "session";
|
|
60
|
+
session_id: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: "mode";
|
|
63
|
+
mode: "search" | "medical";
|
|
64
|
+
} | {
|
|
65
|
+
type: "step";
|
|
66
|
+
step: StepEvent;
|
|
67
|
+
} | {
|
|
68
|
+
type: "delta";
|
|
69
|
+
content: string;
|
|
70
|
+
} | {
|
|
71
|
+
type: "citation";
|
|
72
|
+
index: number;
|
|
73
|
+
source: CitationSource;
|
|
74
|
+
} | {
|
|
75
|
+
type: "sources";
|
|
76
|
+
items: SearchResult[];
|
|
77
|
+
} | ExternalSourcesEvent
|
|
78
|
+
/**
|
|
79
|
+
* Soft guardrail notice emitted alongside a normal agent response. The
|
|
80
|
+
* widget renders this as an unobtrusive info bar; the assistant message
|
|
81
|
+
* itself still streams as usual. Hard blocks stay on the `error` event
|
|
82
|
+
* with code `GUARDRAIL_TRIGGERED`.
|
|
83
|
+
*/
|
|
84
|
+
| {
|
|
85
|
+
type: "advisory";
|
|
86
|
+
redirect: Exclude<GuardrailRedirect, "112" | "113">;
|
|
87
|
+
message: string;
|
|
88
|
+
} | {
|
|
89
|
+
type: "done";
|
|
90
|
+
usage?: {
|
|
91
|
+
prompt_tokens: number;
|
|
92
|
+
completion_tokens: number;
|
|
93
|
+
};
|
|
94
|
+
message_id?: string;
|
|
95
|
+
} | {
|
|
96
|
+
type: "error";
|
|
97
|
+
code: string;
|
|
98
|
+
message: string;
|
|
99
|
+
guardrail_slug?: string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Refresh this long before a token expires. It mirrors the margin the API
|
|
104
|
+
* applies when it clamps a chat token to a credential inside it: the server
|
|
105
|
+
* guarantees the token dies *after* the credential's own refresh point, and
|
|
106
|
+
* this buffer is what makes the client come back in time to use that.
|
|
107
|
+
*/
|
|
108
|
+
declare const DEFAULT_REFRESH_BUFFER_MS = 60000;
|
|
109
|
+
interface TokenResponse {
|
|
110
|
+
token: string;
|
|
111
|
+
/** ISO-8601 or epoch ms, if the endpoint reports one. */
|
|
112
|
+
expiresAt?: string | number | null;
|
|
113
|
+
}
|
|
114
|
+
/** Minimal storage surface — `sessionStorage`, `localStorage` or your own. */
|
|
115
|
+
interface TokenStorage {
|
|
116
|
+
getItem(key: string): string | null;
|
|
117
|
+
setItem(key: string, value: string): void;
|
|
118
|
+
removeItem(key: string): void;
|
|
119
|
+
}
|
|
120
|
+
interface TokenManagerOptions {
|
|
121
|
+
/**
|
|
122
|
+
* Fetches a fresh token from YOUR backend. This is the one integration
|
|
123
|
+
* point that cannot be generic: only your server may hold the API key that
|
|
124
|
+
* mints Laintern tokens, and only it knows who the current user is.
|
|
125
|
+
*/
|
|
126
|
+
fetchToken: () => Promise<TokenResponse | string>;
|
|
127
|
+
storage?: TokenStorage | null;
|
|
128
|
+
storageKey?: string;
|
|
129
|
+
refreshBufferMs?: number;
|
|
130
|
+
onWarning?: (message: string, detail?: Record<string, unknown>) => void;
|
|
131
|
+
}
|
|
132
|
+
interface TokenManager {
|
|
133
|
+
/** Cached token when still valid, otherwise a freshly fetched one. */
|
|
134
|
+
get(): Promise<string>;
|
|
135
|
+
/** Drop the cached token — the next `get()` fetches. */
|
|
136
|
+
clear(): void;
|
|
137
|
+
/** Expiry of the cached token, in epoch ms. Null when nothing is cached. */
|
|
138
|
+
expiresAt(): number | null;
|
|
139
|
+
}
|
|
140
|
+
/** Read a JWT's `exp` claim (epoch ms) without verifying the signature. */
|
|
141
|
+
declare function decodeJwtExpiryMs(token: string): number | null;
|
|
142
|
+
/**
|
|
143
|
+
* Caches the chat token and refreshes it before it dies.
|
|
144
|
+
*
|
|
145
|
+
* Concurrent callers share one in-flight fetch: a page that opens a stream
|
|
146
|
+
* while another request is already refreshing must not mint two sessions.
|
|
147
|
+
*/
|
|
148
|
+
declare function createTokenManager(options: TokenManagerOptions): TokenManager;
|
|
149
|
+
|
|
150
|
+
interface LainternClientOptions {
|
|
151
|
+
/** Base URL of the Laintern API, e.g. "https://api.laintern.com". */
|
|
152
|
+
apiBase: string;
|
|
153
|
+
/**
|
|
154
|
+
* How to get a chat token. Either your own function, or the URL of an
|
|
155
|
+
* endpoint on your backend that returns `{ token, expires_at }`.
|
|
156
|
+
*
|
|
157
|
+
* Never put a Laintern API key in the browser: the key mints tokens for any
|
|
158
|
+
* user, so it belongs on your server, behind your own session check.
|
|
159
|
+
*/
|
|
160
|
+
token: TokenManagerOptions["fetchToken"] | {
|
|
161
|
+
endpoint: string;
|
|
162
|
+
init?: RequestInit;
|
|
163
|
+
};
|
|
164
|
+
storage?: TokenManagerOptions["storage"];
|
|
165
|
+
refreshBufferMs?: number;
|
|
166
|
+
onWarning?: TokenManagerOptions["onWarning"];
|
|
167
|
+
/** Injectable for tests and non-browser runtimes. */
|
|
168
|
+
fetch?: typeof globalThis.fetch;
|
|
169
|
+
}
|
|
170
|
+
interface SendMessageOptions {
|
|
171
|
+
/** Continues an existing conversation. Omit to start a new one. */
|
|
172
|
+
sessionId?: string | null;
|
|
173
|
+
/** Base64 data URLs, for agents with image input enabled. */
|
|
174
|
+
images?: string[];
|
|
175
|
+
/** Force a specific tool on this turn, when the agent allows it. */
|
|
176
|
+
forceTool?: string;
|
|
177
|
+
signal?: AbortSignal;
|
|
178
|
+
}
|
|
179
|
+
interface AgentConfig {
|
|
180
|
+
/** Transparency notice to show before the conversation starts (EU AI Act). */
|
|
181
|
+
disclaimer: {
|
|
182
|
+
enabled: boolean;
|
|
183
|
+
text: string | null;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
interface LainternClient {
|
|
187
|
+
/**
|
|
188
|
+
* Send one message and iterate the response as it streams.
|
|
189
|
+
*
|
|
190
|
+
* Yields typed events (`delta`, `citation`, `sources`, `step`, `done`, …).
|
|
191
|
+
* Prefer {@link createConversation} unless you want to assemble the turn
|
|
192
|
+
* yourself.
|
|
193
|
+
*/
|
|
194
|
+
sendMessage(message: string, options?: SendMessageOptions): AsyncGenerator<ChatStreamEvent>;
|
|
195
|
+
/** Per-agent presentation config. Best-effort: throws on transport errors. */
|
|
196
|
+
getConfig(): Promise<AgentConfig>;
|
|
197
|
+
submitFeedback(messageId: string, feedback: "positive" | "negative"): Promise<void>;
|
|
198
|
+
/** The token cache, exposed so a host can clear it on logout. */
|
|
199
|
+
tokens: TokenManager;
|
|
200
|
+
}
|
|
201
|
+
declare function createLainternClient(options: LainternClientOptions): LainternClient;
|
|
202
|
+
|
|
203
|
+
/** Soft advisory alongside an answer — the agent flagged the topic, but answered. */
|
|
204
|
+
interface TurnAdvisory {
|
|
205
|
+
redirect: string | null;
|
|
206
|
+
message: string;
|
|
207
|
+
}
|
|
208
|
+
/** A guardrail refused the turn. The agent produced no answer. */
|
|
209
|
+
interface TurnBlock {
|
|
210
|
+
/** Which guardrail fired, when the server names one. */
|
|
211
|
+
slug: string | null;
|
|
212
|
+
/** Copy meant for the end user, written by the guardrail's configuration. */
|
|
213
|
+
message: string;
|
|
214
|
+
}
|
|
215
|
+
interface TurnError {
|
|
216
|
+
code: string;
|
|
217
|
+
message: string;
|
|
218
|
+
rateLimitScope?: "user" | "agent";
|
|
219
|
+
rateLimitWindow?: "minute" | "hour" | "day";
|
|
220
|
+
}
|
|
221
|
+
interface Turn {
|
|
222
|
+
id: string;
|
|
223
|
+
userMessage: string;
|
|
224
|
+
userImages?: string[];
|
|
225
|
+
assistantContent: string;
|
|
226
|
+
/** Citation number (the `[1]` in the text) → the source it points at. */
|
|
227
|
+
citations: Map<number, CitationSource>;
|
|
228
|
+
/** Everything the agent cited or consulted, in the order the server ranked it. */
|
|
229
|
+
sources: SearchResult[];
|
|
230
|
+
/** Sidebar suggestions per content type — not necessarily cited. */
|
|
231
|
+
externalSources: Partial<Record<ExternalSourceType, SearchResult[]>>;
|
|
232
|
+
mode: string | null;
|
|
233
|
+
steps: StepEvent[];
|
|
234
|
+
isStreaming: boolean;
|
|
235
|
+
error?: TurnError;
|
|
236
|
+
blocked?: TurnBlock;
|
|
237
|
+
advisory?: TurnAdvisory;
|
|
238
|
+
messageId?: string;
|
|
239
|
+
feedback?: "positive" | "negative" | null;
|
|
240
|
+
}
|
|
241
|
+
interface ConversationState {
|
|
242
|
+
turns: Turn[];
|
|
243
|
+
sessionId: string | null;
|
|
244
|
+
isStreaming: boolean;
|
|
245
|
+
}
|
|
246
|
+
interface SendOptions {
|
|
247
|
+
images?: string[];
|
|
248
|
+
forceTool?: string;
|
|
249
|
+
}
|
|
250
|
+
interface Conversation {
|
|
251
|
+
getState(): ConversationState;
|
|
252
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
253
|
+
subscribe(listener: (state: ConversationState) => void): () => void;
|
|
254
|
+
/** Send a message and stream the answer into state. Never throws. */
|
|
255
|
+
send(message: string, options?: SendOptions): Promise<void>;
|
|
256
|
+
/** Stop the current stream. The partial answer stays in state. */
|
|
257
|
+
abort(): void;
|
|
258
|
+
/** Drop all turns and start a new server-side session on the next send. */
|
|
259
|
+
reset(): void;
|
|
260
|
+
setFeedback(turnId: string, feedback: "positive" | "negative"): Promise<void>;
|
|
261
|
+
}
|
|
262
|
+
interface ConversationOptions {
|
|
263
|
+
/** Resume an existing server-side session. */
|
|
264
|
+
sessionId?: string | null;
|
|
265
|
+
/** Restore turns from your own storage. */
|
|
266
|
+
initialTurns?: Turn[];
|
|
267
|
+
/** Called whenever the session id changes, so you can persist it. */
|
|
268
|
+
onSessionId?: (sessionId: string) => void;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Stateful conversation on top of {@link LainternClient}.
|
|
272
|
+
*
|
|
273
|
+
* Holds the turn list, applies each stream event to the turn in flight, and
|
|
274
|
+
* notifies subscribers. Framework-agnostic on purpose: the React binding in
|
|
275
|
+
* `@laintern/chat-sdk/react` is a thin `useSyncExternalStore` wrapper, and any
|
|
276
|
+
* other UI layer can subscribe the same way.
|
|
277
|
+
*
|
|
278
|
+
* `send()` never throws. A failed turn is part of the conversation — it lands
|
|
279
|
+
* on `turn.error` (or `turn.blocked` for a guardrail) so the UI can render it
|
|
280
|
+
* in place, which is what you want in a chat transcript.
|
|
281
|
+
*/
|
|
282
|
+
declare function createConversation(client: LainternClient, options?: ConversationOptions): Conversation;
|
|
283
|
+
|
|
284
|
+
export { type AgentConfig as A, type ChatStreamEvent as C, DEFAULT_REFRESH_BUFFER_MS as D, type ExternalSourceType as E, type LainternClient as L, type SearchResult as S, type TokenManager as T, type ChatContentType as a, type CitationSource as b, type ContentType as c, type Conversation as d, type ConversationOptions as e, type ConversationState as f, type LainternClientOptions as g, type SendMessageOptions as h, type SendOptions as i, type StepEvent as j, type TokenManagerOptions as k, type TokenResponse as l, type TokenStorage as m, type Turn as n, type TurnAdvisory as o, type TurnBlock as p, type TurnError as q, createConversation as r, createLainternClient as s, createTokenManager as t, decodeJwtExpiryMs as u };
|