sideagent 0.4.0 → 0.5.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 +12 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +45 -9
- package/dist/server/index.d.ts +46 -22
- package/dist/server/index.js +163 -75
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -55,6 +55,18 @@ With an agent hosted on sideagent Cloud, pass its publishable key instead of `en
|
|
|
55
55
|
|
|
56
56
|
Pass `endpoint` as well to send the requests somewhere else, such as a proxy in front of sideagent Cloud. A malformed key, or neither prop, throws during render. `parsePublishableKey` and `publishableEndpoint` are exported from `sideagent/keys`.
|
|
57
57
|
|
|
58
|
+
Signed-in users: pass `getHeaders`, which the chat calls before every request, so a login token that expires is fetched fresh each time. Pass `storageKey` (the user's ID) so the last conversation the chat remembers in `localStorage` belongs to that user; on a shared browser another user starts from their own.
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<AgentChat
|
|
62
|
+
publishableKey="pk_live_…"
|
|
63
|
+
getHeaders={async () => ({ Authorization: `Bearer ${await getToken()}` })}
|
|
64
|
+
storageKey={user.id}
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`getHeaders` works with `endpoint` too. Its headers are merged under `headers`, which wins on a clash, so it cannot replace the publishable key. If it throws, the request is not sent and the chat shows an error.
|
|
69
|
+
|
|
58
70
|
### Docked sidebar
|
|
59
71
|
|
|
60
72
|
Wrap your app in `AgentProvider` to dock the chat on the right, side by side with the app instead of over it. It takes the same props as `AgentChat`. The user drags the panel's left edge (or uses the arrow keys on it) to resize; the width is remembered. Outside `AgentProvider`, sidebar mode overlays the page.
|
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,17 @@ interface AgentChatProps {
|
|
|
49
49
|
/** Use "include" when the endpoint is on another origin and relies on cookies. */
|
|
50
50
|
credentials?: RequestCredentials;
|
|
51
51
|
headers?: Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Called before every request; its headers are added to `headers` (which wins on a clash). Use it for a
|
|
54
|
+
* login token that expires, e.g. `Authorization: Bearer <token>` from your login SDK. If it throws, the
|
|
55
|
+
* request is not sent and the chat shows an error.
|
|
56
|
+
*/
|
|
57
|
+
getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
58
|
+
/**
|
|
59
|
+
* Scopes what the chat remembers in localStorage (the last conversation) to one user, e.g. their user ID.
|
|
60
|
+
* Changing it starts over with that user's last conversation.
|
|
61
|
+
*/
|
|
62
|
+
storageKey?: string;
|
|
52
63
|
defaultOpen?: boolean;
|
|
53
64
|
/** Custom result views per operationId. Anything not listed gets the built-in table / fields view. */
|
|
54
65
|
resultRenderers?: Record<string, (e: {
|
|
@@ -79,10 +90,13 @@ interface AgentControls {
|
|
|
79
90
|
/** Open or close the assistant from anywhere under `AgentProvider`. */
|
|
80
91
|
declare function useAgent(): AgentControls;
|
|
81
92
|
|
|
93
|
+
type GetHeaders = () => Record<string, string> | Promise<Record<string, string>>;
|
|
82
94
|
interface TransportOptions {
|
|
83
95
|
endpoint: string;
|
|
84
96
|
credentials?: RequestCredentials;
|
|
85
97
|
headers?: Record<string, string>;
|
|
98
|
+
/** Called before every request, e.g. for a fresh login token. `headers` wins on a clash. */
|
|
99
|
+
getHeaders?: GetHeaders;
|
|
86
100
|
signal?: AbortSignal;
|
|
87
101
|
}
|
|
88
102
|
/** POSTs a chat request and yields the server's SSE events. */
|
package/dist/index.js
CHANGED
|
@@ -343,11 +343,22 @@ function toInitialMessages(messages) {
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
// src/client/transport.ts
|
|
346
|
+
async function requestHeaders(opts) {
|
|
347
|
+
const dynamic = opts.getHeaders ? await opts.getHeaders() : {};
|
|
348
|
+
return { "content-type": "application/json", ...dynamic, ...opts.headers };
|
|
349
|
+
}
|
|
346
350
|
async function* streamChat(body, opts) {
|
|
351
|
+
let headers;
|
|
352
|
+
try {
|
|
353
|
+
headers = await requestHeaders(opts);
|
|
354
|
+
} catch {
|
|
355
|
+
yield { type: "error", code: "headers_failed", message: "Could not get credentials. Sign in again." };
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
347
358
|
const res = await fetch(opts.endpoint, {
|
|
348
359
|
method: "POST",
|
|
349
360
|
credentials: opts.credentials ?? "same-origin",
|
|
350
|
-
headers
|
|
361
|
+
headers,
|
|
351
362
|
body: JSON.stringify(body),
|
|
352
363
|
signal: opts.signal
|
|
353
364
|
});
|
|
@@ -377,10 +388,16 @@ async function* streamChat(body, opts) {
|
|
|
377
388
|
}
|
|
378
389
|
}
|
|
379
390
|
async function callJson(body, opts) {
|
|
391
|
+
let headers;
|
|
392
|
+
try {
|
|
393
|
+
headers = await requestHeaders(opts);
|
|
394
|
+
} catch {
|
|
395
|
+
return { status: 0, data: null };
|
|
396
|
+
}
|
|
380
397
|
const res = await fetch(opts.endpoint, {
|
|
381
398
|
method: "POST",
|
|
382
399
|
credentials: opts.credentials ?? "same-origin",
|
|
383
|
-
headers
|
|
400
|
+
headers,
|
|
384
401
|
body: JSON.stringify(body)
|
|
385
402
|
});
|
|
386
403
|
let data = null;
|
|
@@ -394,7 +411,7 @@ async function callJson(body, opts) {
|
|
|
394
411
|
// src/client/Chat.tsx
|
|
395
412
|
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
396
413
|
function Chat(props) {
|
|
397
|
-
const { endpoint, hooks, credentials, headers } = props;
|
|
414
|
+
const { endpoint, hooks, credentials, headers, getHeaders } = props;
|
|
398
415
|
const latest = useRef(props);
|
|
399
416
|
latest.current = props;
|
|
400
417
|
const threadId = useRef(props.session.threadId);
|
|
@@ -417,6 +434,7 @@ function Chat(props) {
|
|
|
417
434
|
endpoint: p.endpoint,
|
|
418
435
|
credentials: p.credentials,
|
|
419
436
|
headers: p.headers,
|
|
437
|
+
getHeaders: p.getHeaders,
|
|
420
438
|
signal: abortSignal
|
|
421
439
|
})) {
|
|
422
440
|
parts = applyEvent(parts, ev);
|
|
@@ -459,7 +477,7 @@ function Chat(props) {
|
|
|
459
477
|
setLoadedPending(null);
|
|
460
478
|
for await (const ev of streamChat(
|
|
461
479
|
{ threadId: threadId.current, approval: { token: req.token, decision } },
|
|
462
|
-
{ endpoint, credentials, headers }
|
|
480
|
+
{ endpoint, credentials, headers, getHeaders }
|
|
463
481
|
)) {
|
|
464
482
|
if (ev.type === "tool.complete") fireComplete(hooks, ev);
|
|
465
483
|
if (ev.type === "error") safely(() => hooks?.onError?.({ code: ev.code, message: ev.message }));
|
|
@@ -499,7 +517,12 @@ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
|
499
517
|
function HistoryList(props) {
|
|
500
518
|
const [threads, setThreads] = useState2(null);
|
|
501
519
|
const [error, setError] = useState2(false);
|
|
502
|
-
const opts = {
|
|
520
|
+
const opts = {
|
|
521
|
+
endpoint: props.endpoint,
|
|
522
|
+
credentials: props.credentials,
|
|
523
|
+
headers: props.headers,
|
|
524
|
+
getHeaders: props.getHeaders
|
|
525
|
+
};
|
|
503
526
|
useEffect(() => {
|
|
504
527
|
callJson({ action: "threads.list" }, opts).then((r) => r.data?.threads ? setThreads(r.data.threads) : setError(true)).catch(() => setError(true));
|
|
505
528
|
}, []);
|
|
@@ -538,7 +561,7 @@ function HistoryList(props) {
|
|
|
538
561
|
// src/client/storage.ts
|
|
539
562
|
var MODE_KEY = "sideagent:mode";
|
|
540
563
|
var WIDTH_KEY = "sideagent:width";
|
|
541
|
-
var threadKey = (endpoint) => `sideagent:thread:${endpoint}`;
|
|
564
|
+
var threadKey = (endpoint, storageKey) => `sideagent:thread:${endpoint}${storageKey ? `:${storageKey}` : ""}`;
|
|
542
565
|
var MODES = ["floating", "sidebar"];
|
|
543
566
|
function storeGet(k) {
|
|
544
567
|
try {
|
|
@@ -739,8 +762,13 @@ function AgentChat(props) {
|
|
|
739
762
|
const [width, setWidth] = useState3(() => clampWidth(Number(storeGet(WIDTH_KEY)) || DEFAULT_WIDTH));
|
|
740
763
|
const [resizing, setResizing] = useState3(false);
|
|
741
764
|
const booted = useRef2(false);
|
|
742
|
-
const key = threadKey(target.endpoint);
|
|
743
|
-
const opts = {
|
|
765
|
+
const key = threadKey(target.endpoint, props.storageKey);
|
|
766
|
+
const opts = {
|
|
767
|
+
endpoint: target.endpoint,
|
|
768
|
+
credentials: props.credentials,
|
|
769
|
+
headers: target.headers,
|
|
770
|
+
getHeaders: props.getHeaders
|
|
771
|
+
};
|
|
744
772
|
const setMode = useCallback((m) => {
|
|
745
773
|
setModeState(m);
|
|
746
774
|
storeSet(MODE_KEY, m);
|
|
@@ -765,7 +793,7 @@ function AgentChat(props) {
|
|
|
765
793
|
}
|
|
766
794
|
},
|
|
767
795
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
768
|
-
[target, props.credentials]
|
|
796
|
+
[target, props.credentials, props.getHeaders]
|
|
769
797
|
);
|
|
770
798
|
const resizeTo = useCallback((w) => {
|
|
771
799
|
const next = clampWidth(w);
|
|
@@ -796,6 +824,14 @@ function AgentChat(props) {
|
|
|
796
824
|
setView("chat");
|
|
797
825
|
};
|
|
798
826
|
const visible = open;
|
|
827
|
+
const shownKey = useRef2(key);
|
|
828
|
+
useEffect2(() => {
|
|
829
|
+
if (shownKey.current === key) return;
|
|
830
|
+
shownKey.current = key;
|
|
831
|
+
booted.current = false;
|
|
832
|
+
setView("chat");
|
|
833
|
+
setSession((s) => ({ key: s.key + 1 }));
|
|
834
|
+
}, [key]);
|
|
799
835
|
useEffect2(() => {
|
|
800
836
|
if (!visible || booted.current) return;
|
|
801
837
|
booted.current = true;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -2,7 +2,11 @@ import { ThreadMeta } from '../protocol.js';
|
|
|
2
2
|
export { AgentEvent, ChatRequest } from '../protocol.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
5
|
-
import { BaseCheckpointSaver } from '@langchain/langgraph';
|
|
5
|
+
import { BaseStore, BaseCheckpointSaver } from '@langchain/langgraph';
|
|
6
|
+
import { Document } from '@langchain/core/documents';
|
|
7
|
+
import { EmbeddingsInterface } from '@langchain/core/embeddings';
|
|
8
|
+
import { VectorStore, VectorStoreInterface } from '@langchain/core/vectorstores';
|
|
9
|
+
export { VectorStoreInterface as VectorStore } from '@langchain/core/vectorstores';
|
|
6
10
|
|
|
7
11
|
declare const configSchema: z.ZodObject<{
|
|
8
12
|
spec: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
|
|
@@ -71,31 +75,35 @@ interface ChunkMeta {
|
|
|
71
75
|
/** Only set for sources that came from a URL listed in config. */
|
|
72
76
|
url?: string;
|
|
73
77
|
}
|
|
74
|
-
interface StoredChunk {
|
|
75
|
-
text: string;
|
|
76
|
-
vector: number[];
|
|
77
|
-
meta: ChunkMeta;
|
|
78
|
-
}
|
|
79
78
|
interface Hit {
|
|
80
79
|
text: string;
|
|
81
80
|
score: number;
|
|
82
81
|
meta: ChunkMeta;
|
|
83
82
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
|
|
84
|
+
/** Default store: cosine similarity over everything in process memory. */
|
|
85
|
+
declare class MemoryVectorStore extends VectorStore {
|
|
86
|
+
FilterType: object;
|
|
87
|
+
lc_namespace: string[];
|
|
88
|
+
private rows;
|
|
89
|
+
constructor(embeddings: EmbeddingsInterface);
|
|
90
|
+
_vectorstoreType(): string;
|
|
91
|
+
addVectors(vectors: number[][], documents: Document[], options?: {
|
|
92
|
+
ids?: string[];
|
|
93
|
+
}): Promise<string[]>;
|
|
94
|
+
addDocuments(documents: Document[], options?: {
|
|
95
|
+
ids?: string[];
|
|
96
|
+
}): Promise<string[]>;
|
|
97
|
+
delete(params?: {
|
|
98
|
+
ids?: string[];
|
|
99
|
+
}): Promise<void>;
|
|
100
|
+
similaritySearchVectorWithScore(query: number[], k: number): Promise<[Document, number][]>;
|
|
93
101
|
}
|
|
94
102
|
|
|
95
|
-
/** Splits markdown/plain text into chunks of
|
|
96
|
-
declare function chunkText(content: string, size: number): string[]
|
|
103
|
+
/** Splits markdown/plain text into chunks of at most about `size` chars, each prefixed with its nearest heading. */
|
|
104
|
+
declare function chunkText(content: string, size: number): Promise<string[]>;
|
|
97
105
|
|
|
98
|
-
/**
|
|
106
|
+
/** HTML to text/markdown-ish conversion on a real parser. Plain text out; never HTML. */
|
|
99
107
|
declare function htmlToText(html: string): {
|
|
100
108
|
title?: string;
|
|
101
109
|
text: string;
|
|
@@ -130,7 +138,7 @@ interface SourceStatus {
|
|
|
130
138
|
interface KnowledgeDeps {
|
|
131
139
|
embedDocuments(texts: string[]): Promise<number[][]>;
|
|
132
140
|
embedQuery(text: string): Promise<number[]>;
|
|
133
|
-
store?:
|
|
141
|
+
store?: VectorStoreInterface;
|
|
134
142
|
allowPrivateNetworks?: boolean;
|
|
135
143
|
fetch?: typeof fetch;
|
|
136
144
|
now?: () => number;
|
|
@@ -138,8 +146,10 @@ interface KnowledgeDeps {
|
|
|
138
146
|
declare class KnowledgeBase {
|
|
139
147
|
private opts;
|
|
140
148
|
private deps;
|
|
141
|
-
readonly store:
|
|
149
|
+
readonly store: VectorStoreInterface;
|
|
142
150
|
private statuses;
|
|
151
|
+
/** Chunk ids currently stored per source, so a refresh can drop the ones that disappeared. */
|
|
152
|
+
private chunkIds;
|
|
143
153
|
private refreshing;
|
|
144
154
|
private lastRefresh;
|
|
145
155
|
private totalChunks;
|
|
@@ -208,6 +218,20 @@ declare class MemoryThreadStore implements ThreadStore {
|
|
|
208
218
|
touch(userId: string, threadId: string, title: string): Promise<void>;
|
|
209
219
|
delete(userId: string, threadId: string): Promise<void>;
|
|
210
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Thread index on any LangGraph `BaseStore` (Postgres, Redis, …), so one store can serve the checkpointer
|
|
223
|
+
* ecosystem and the thread list. Keeps at most 200 threads per user, like `MemoryThreadStore`.
|
|
224
|
+
*/
|
|
225
|
+
declare class LangGraphThreadStore implements ThreadStore {
|
|
226
|
+
private store;
|
|
227
|
+
constructor(store: BaseStore);
|
|
228
|
+
private ns;
|
|
229
|
+
private all;
|
|
230
|
+
list(userId: string): Promise<ThreadMeta[]>;
|
|
231
|
+
has(userId: string, threadId: string): Promise<boolean>;
|
|
232
|
+
touch(userId: string, threadId: string, title: string): Promise<void>;
|
|
233
|
+
delete(userId: string, threadId: string): Promise<void>;
|
|
234
|
+
}
|
|
211
235
|
|
|
212
236
|
interface AuthResult {
|
|
213
237
|
/** Stable identity of the signed-in user. Threads are bound to it. */
|
|
@@ -238,7 +262,7 @@ interface CreateAgentOptions extends AgentConfigInput {
|
|
|
238
262
|
/** Index of each user's conversations (history list). Defaults to in-memory. */
|
|
239
263
|
threadStore?: ThreadStore;
|
|
240
264
|
/** Where knowledge embeddings live. Defaults to in-memory (re-embedded on every start). */
|
|
241
|
-
vectorStore?:
|
|
265
|
+
vectorStore?: VectorStoreInterface;
|
|
242
266
|
}
|
|
243
267
|
declare function createAgent(options: CreateAgentOptions): Promise<{
|
|
244
268
|
handler: (request: Request) => Promise<Response>;
|
|
@@ -262,4 +286,4 @@ declare function honoHandler(agent: Agent): (c: {
|
|
|
262
286
|
/** Express (use express.json() before it): `app.post("/agent", expressHandler(agent))` and `app.options("/agent", expressHandler(agent))` */
|
|
263
287
|
declare function expressHandler(agent: Agent): (req: any, res: any) => Promise<any>;
|
|
264
288
|
|
|
265
|
-
export { type Agent, type AgentConfig, type AgentConfigInput, type AuditEvent, type AuthResult, type CreateAgentOptions, type DoctorReport, type EmbeddingsLike, KnowledgeBase, type KnowledgeSourceInput, MemoryThreadStore, MemoryVectorStore, type ThreadStore,
|
|
289
|
+
export { type Agent, type AgentConfig, type AgentConfigInput, type AuditEvent, type AuthResult, type CreateAgentOptions, type DoctorReport, type EmbeddingsLike, KnowledgeBase, type KnowledgeSourceInput, LangGraphThreadStore, MemoryThreadStore, MemoryVectorStore, type ThreadStore, assertAllowedUrl, chunkText, configSchema, createAgent, expressHandler, honoHandler, htmlToText, nextRoute, parseConfig };
|
package/dist/server/index.js
CHANGED
|
@@ -232,34 +232,39 @@ import {
|
|
|
232
232
|
} from "@langchain/langgraph";
|
|
233
233
|
|
|
234
234
|
// src/server/knowledge/chunk.ts
|
|
235
|
-
|
|
235
|
+
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
|
|
236
|
+
var HEADING = /^#{1,6}\s.*$/;
|
|
237
|
+
async function chunkText(content, size) {
|
|
236
238
|
const out = [];
|
|
237
239
|
let heading = "";
|
|
238
|
-
let
|
|
239
|
-
const flush = () => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
let body = [];
|
|
241
|
+
const flush = async () => {
|
|
242
|
+
const text = body.join("\n").trim();
|
|
243
|
+
body = [];
|
|
244
|
+
if (!text) return;
|
|
245
|
+
const room = Math.max(size - heading.length - 2, Math.floor(size / 2));
|
|
246
|
+
const splitter = RecursiveCharacterTextSplitter.fromLanguage("markdown", {
|
|
247
|
+
chunkSize: room,
|
|
248
|
+
chunkOverlap: Math.floor(room / 10)
|
|
249
|
+
});
|
|
250
|
+
for (const piece of await splitter.splitText(text)) {
|
|
251
|
+
const t = piece.trim();
|
|
252
|
+
if (t) out.push(heading ? `${heading}
|
|
244
253
|
|
|
245
|
-
${t}` : t;
|
|
246
|
-
for (const raw of content.replace(/\r\n/g, "\n").split(/\n{2,}/)) {
|
|
247
|
-
const block = raw.trim();
|
|
248
|
-
if (!block) continue;
|
|
249
|
-
if (/^#{1,6}\s/.test(block)) {
|
|
250
|
-
flush();
|
|
251
|
-
heading = block.split("\n")[0];
|
|
254
|
+
${t}` : t);
|
|
252
255
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
256
|
+
};
|
|
257
|
+
let fence = false;
|
|
258
|
+
for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
259
|
+
if (/^\s*(```|~~~)/.test(line)) fence = !fence;
|
|
260
|
+
if (!fence && HEADING.test(line)) {
|
|
261
|
+
await flush();
|
|
262
|
+
heading = line.trim();
|
|
263
|
+
} else {
|
|
264
|
+
body.push(line);
|
|
260
265
|
}
|
|
261
266
|
}
|
|
262
|
-
flush();
|
|
267
|
+
await flush();
|
|
263
268
|
return out;
|
|
264
269
|
}
|
|
265
270
|
|
|
@@ -357,25 +362,44 @@ async function executeOperation(op, args, ctx, signal) {
|
|
|
357
362
|
}
|
|
358
363
|
|
|
359
364
|
// src/server/knowledge/html.ts
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
">": ">",
|
|
364
|
-
""": '"',
|
|
365
|
-
"'": "'",
|
|
366
|
-
" ": " "
|
|
367
|
-
};
|
|
365
|
+
import { Parser } from "htmlparser2";
|
|
366
|
+
var SKIP = /* @__PURE__ */ new Set(["script", "style", "noscript", "svg", "nav", "footer", "header", "form", "iframe", "template"]);
|
|
367
|
+
var BLOCK_END = /* @__PURE__ */ new Set(["p", "div", "section", "article", "tr", "ul", "ol", "table", "pre", "blockquote"]);
|
|
368
368
|
function htmlToText(html) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
369
|
+
let out = "";
|
|
370
|
+
let title = "";
|
|
371
|
+
let skip = 0;
|
|
372
|
+
let inTitle = false;
|
|
373
|
+
const parser = new Parser(
|
|
374
|
+
{
|
|
375
|
+
onopentag(name) {
|
|
376
|
+
if (SKIP.has(name)) skip++;
|
|
377
|
+
else if (name === "title") inTitle = true;
|
|
378
|
+
else if (skip) return;
|
|
379
|
+
else if (/^h[1-6]$/.test(name)) out += `
|
|
375
380
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
381
|
+
${"#".repeat(Number(name[1]))} `;
|
|
382
|
+
else if (name === "li") out += "\n- ";
|
|
383
|
+
else if (name === "br") out += "\n";
|
|
384
|
+
},
|
|
385
|
+
onclosetag(name) {
|
|
386
|
+
if (SKIP.has(name)) skip = Math.max(0, skip - 1);
|
|
387
|
+
else if (name === "title") inTitle = false;
|
|
388
|
+
else if (skip) return;
|
|
389
|
+
else if (/^h[1-6]$/.test(name) || BLOCK_END.has(name)) out += "\n\n";
|
|
390
|
+
},
|
|
391
|
+
ontext(text2) {
|
|
392
|
+
if (inTitle) title += text2;
|
|
393
|
+
else if (!skip) out += text2;
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
{ decodeEntities: true }
|
|
397
|
+
);
|
|
398
|
+
parser.write(html);
|
|
399
|
+
parser.end();
|
|
400
|
+
const text = out.replace(/[ \t ]+/g, " ").replace(/ ?\n ?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
401
|
+
const clean = title.replace(/\s+/g, " ").trim();
|
|
402
|
+
return { title: clean || void 0, text };
|
|
379
403
|
}
|
|
380
404
|
|
|
381
405
|
// src/server/knowledge/fetch-page.ts
|
|
@@ -415,32 +439,43 @@ async function fetchPage(url2, opts) {
|
|
|
415
439
|
throw new Error("too many redirects");
|
|
416
440
|
}
|
|
417
441
|
|
|
442
|
+
// src/server/knowledge/index.ts
|
|
443
|
+
import { Document } from "@langchain/core/documents";
|
|
444
|
+
|
|
418
445
|
// src/server/knowledge/vector-store.ts
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
446
|
+
import { VectorStore } from "@langchain/core/vectorstores";
|
|
447
|
+
import { cosineSimilarity } from "@langchain/core/utils/math";
|
|
448
|
+
var MemoryVectorStore = class extends VectorStore {
|
|
449
|
+
lc_namespace = ["sideagent", "vectorstores", "memory"];
|
|
450
|
+
rows = /* @__PURE__ */ new Map();
|
|
451
|
+
constructor(embeddings) {
|
|
452
|
+
super(embeddings, {});
|
|
453
|
+
}
|
|
454
|
+
_vectorstoreType() {
|
|
455
|
+
return "sideagent_memory";
|
|
456
|
+
}
|
|
457
|
+
async addVectors(vectors, documents, options) {
|
|
458
|
+
const ids = documents.map((_, i) => options?.ids?.[i] ?? crypto.randomUUID());
|
|
459
|
+
documents.forEach((doc, i) => this.rows.set(ids[i], { vector: vectors[i], doc }));
|
|
460
|
+
return ids;
|
|
461
|
+
}
|
|
462
|
+
async addDocuments(documents, options) {
|
|
463
|
+
const vectors = await this.embeddings.embedDocuments(documents.map((d) => d.pageContent));
|
|
464
|
+
return this.addVectors(vectors, documents, options);
|
|
465
|
+
}
|
|
466
|
+
async delete(params) {
|
|
467
|
+
for (const id of params?.ids ?? []) this.rows.delete(id);
|
|
468
|
+
}
|
|
469
|
+
async similaritySearchVectorWithScore(query, k) {
|
|
470
|
+
const rows = [...this.rows.values()].filter((r) => r.vector.length === query.length);
|
|
471
|
+
if (!rows.length) return [];
|
|
472
|
+
const scores = cosineSimilarity(
|
|
473
|
+
[query],
|
|
474
|
+
rows.map((r) => r.vector)
|
|
475
|
+
)[0];
|
|
476
|
+
return rows.map((r, i) => [r.doc, scores[i]]).sort((a, b) => b[1] - a[1]).slice(0, k);
|
|
430
477
|
}
|
|
431
478
|
};
|
|
432
|
-
function cosine(a, b) {
|
|
433
|
-
let dot = 0;
|
|
434
|
-
let na = 0;
|
|
435
|
-
let nb = 0;
|
|
436
|
-
const n = Math.min(a.length, b.length);
|
|
437
|
-
for (let i = 0; i < n; i++) {
|
|
438
|
-
dot += a[i] * b[i];
|
|
439
|
-
na += a[i] * a[i];
|
|
440
|
-
nb += b[i] * b[i];
|
|
441
|
-
}
|
|
442
|
-
return na && nb ? dot / Math.sqrt(na * nb) : 0;
|
|
443
|
-
}
|
|
444
479
|
|
|
445
480
|
// src/server/knowledge/format.ts
|
|
446
481
|
function wrapKnowledge(hits, maxBytes) {
|
|
@@ -478,12 +513,14 @@ var KnowledgeBase = class {
|
|
|
478
513
|
constructor(opts, deps) {
|
|
479
514
|
this.opts = opts;
|
|
480
515
|
this.deps = deps;
|
|
481
|
-
this.store = deps.store ?? new MemoryVectorStore();
|
|
516
|
+
this.store = deps.store ?? new MemoryVectorStore({ embedDocuments: (t) => deps.embedDocuments(t), embedQuery: (t) => deps.embedQuery(t) });
|
|
482
517
|
}
|
|
483
518
|
opts;
|
|
484
519
|
deps;
|
|
485
520
|
store;
|
|
486
521
|
statuses = /* @__PURE__ */ new Map();
|
|
522
|
+
/** Chunk ids currently stored per source, so a refresh can drop the ones that disappeared. */
|
|
523
|
+
chunkIds = /* @__PURE__ */ new Map();
|
|
487
524
|
refreshing = null;
|
|
488
525
|
lastRefresh = 0;
|
|
489
526
|
totalChunks = 0;
|
|
@@ -525,16 +562,24 @@ var KnowledgeBase = class {
|
|
|
525
562
|
if (new TextEncoder().encode(content).byteLength > this.opts.maxSourceBytes) {
|
|
526
563
|
throw new Error(`source larger than maxSourceBytes (${this.opts.maxSourceBytes})`);
|
|
527
564
|
}
|
|
528
|
-
const texts = chunkText(content, this.opts.chunkSize);
|
|
565
|
+
const texts = await chunkText(content, this.opts.chunkSize);
|
|
529
566
|
const others = this.totalChunks - (this.statuses.get(id)?.chunks ?? 0);
|
|
530
567
|
if (others + texts.length > this.opts.maxChunks)
|
|
531
568
|
throw new Error(`would exceed maxChunks (${this.opts.maxChunks})`);
|
|
532
569
|
const vectors = texts.length ? await this.deps.embedDocuments(texts) : [];
|
|
533
570
|
const meta = { sourceId: id, title, ...url2 ? { url: url2 } : {} };
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
571
|
+
const ids = texts.map((_, i) => `${id}#${i}`);
|
|
572
|
+
if (texts.length) {
|
|
573
|
+
await this.store.addVectors(
|
|
574
|
+
vectors,
|
|
575
|
+
texts.map((text) => new Document({ pageContent: text, metadata: meta })),
|
|
576
|
+
{ ids }
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const keep = new Set(ids);
|
|
580
|
+
const stale = (this.chunkIds.get(id) ?? []).filter((old) => !keep.has(old));
|
|
581
|
+
if (stale.length) await this.store.delete({ ids: stale });
|
|
582
|
+
this.chunkIds.set(id, ids);
|
|
538
583
|
this.totalChunks = others + texts.length;
|
|
539
584
|
this.statuses.set(id, { id, title, ...url2 ? { url: url2 } : {}, chunks: texts.length, loadedAt: this.now() });
|
|
540
585
|
} catch (e) {
|
|
@@ -557,7 +602,8 @@ var KnowledgeBase = class {
|
|
|
557
602
|
if (this.opts.urls.length && this.now() - this.lastRefresh > this.opts.ttlMs) void this.refresh().catch(() => {
|
|
558
603
|
});
|
|
559
604
|
const vector = await this.deps.embedQuery(query);
|
|
560
|
-
|
|
605
|
+
const found = await this.store.similaritySearchVectorWithScore(vector, this.opts.topK);
|
|
606
|
+
return found.map(([doc, score]) => ({ text: doc.pageContent, score, meta: doc.metadata }));
|
|
561
607
|
}
|
|
562
608
|
};
|
|
563
609
|
|
|
@@ -647,6 +693,7 @@ async function verifyApproval(secret, token, claims) {
|
|
|
647
693
|
}
|
|
648
694
|
|
|
649
695
|
// src/server/openapi.ts
|
|
696
|
+
import { Validator } from "@langchain/core/utils/json_schema";
|
|
650
697
|
import { dereference } from "@scalar/openapi-parser";
|
|
651
698
|
var METHODS = ["get", "post", "put", "patch", "delete"];
|
|
652
699
|
async function loadSpec(spec) {
|
|
@@ -743,6 +790,7 @@ async function loadOperations(spec, allow, maxTools) {
|
|
|
743
790
|
}
|
|
744
791
|
return ops;
|
|
745
792
|
}
|
|
793
|
+
var validators = /* @__PURE__ */ new WeakMap();
|
|
746
794
|
function validateArgs(op, args) {
|
|
747
795
|
if (!args || typeof args !== "object" || Array.isArray(args)) throw new Error("arguments must be an object");
|
|
748
796
|
const a = args;
|
|
@@ -751,6 +799,13 @@ function validateArgs(op, args) {
|
|
|
751
799
|
for (const r of op.inputSchema.required ?? []) {
|
|
752
800
|
if (a[r] === void 0) throw new Error(`missing required argument "${r}"`);
|
|
753
801
|
}
|
|
802
|
+
let validator = validators.get(op);
|
|
803
|
+
if (!validator) validators.set(op, validator = new Validator(op.inputSchema, "2019-09", false));
|
|
804
|
+
const result = validator.validate(a);
|
|
805
|
+
if (!result.valid) {
|
|
806
|
+
const detail = result.errors.slice(0, 3).map((e) => `${e.instanceLocation.replace(/^#\/?/, "") || "arguments"}: ${e.error}`).join("; ");
|
|
807
|
+
throw new Error(detail);
|
|
808
|
+
}
|
|
754
809
|
return a;
|
|
755
810
|
}
|
|
756
811
|
|
|
@@ -982,6 +1037,34 @@ var MemoryThreadStore = class {
|
|
|
982
1037
|
this.of(userId).delete(threadId);
|
|
983
1038
|
}
|
|
984
1039
|
};
|
|
1040
|
+
var LangGraphThreadStore = class {
|
|
1041
|
+
constructor(store) {
|
|
1042
|
+
this.store = store;
|
|
1043
|
+
}
|
|
1044
|
+
store;
|
|
1045
|
+
// Namespace labels cannot contain "." and user ids are often emails.
|
|
1046
|
+
ns = (userId) => ["sideagent", "threads", encodeURIComponent(userId).replace(/\./g, "%2E")];
|
|
1047
|
+
async all(userId) {
|
|
1048
|
+
const items = await this.store.search(this.ns(userId), { limit: MAX_THREADS_PER_USER + 1 });
|
|
1049
|
+
return items.map((i) => i.value).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1050
|
+
}
|
|
1051
|
+
async list(userId) {
|
|
1052
|
+
return (await this.all(userId)).slice(0, MAX_THREADS_PER_USER);
|
|
1053
|
+
}
|
|
1054
|
+
async has(userId, threadId) {
|
|
1055
|
+
return await this.store.get(this.ns(userId), threadId) != null;
|
|
1056
|
+
}
|
|
1057
|
+
async touch(userId, threadId, title) {
|
|
1058
|
+
const existing = (await this.store.get(this.ns(userId), threadId))?.value;
|
|
1059
|
+
const meta = { id: threadId, title: existing?.title ?? title, updatedAt: Date.now() };
|
|
1060
|
+
await this.store.put(this.ns(userId), threadId, meta);
|
|
1061
|
+
const threads = await this.all(userId);
|
|
1062
|
+
if (threads.length > MAX_THREADS_PER_USER) await this.delete(userId, threads[threads.length - 1].id);
|
|
1063
|
+
}
|
|
1064
|
+
async delete(userId, threadId) {
|
|
1065
|
+
await this.store.delete(this.ns(userId), threadId);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
985
1068
|
var titleFrom = (message) => message.replace(/\s+/g, " ").trim().slice(0, 60) || "New chat";
|
|
986
1069
|
var threadPrefix = (userId) => `${encodeURIComponent(userId)}:`;
|
|
987
1070
|
async function ownsThread(store, userId, threadId) {
|
|
@@ -1048,7 +1131,7 @@ function toHistory(messages) {
|
|
|
1048
1131
|
return last;
|
|
1049
1132
|
};
|
|
1050
1133
|
for (const m of messages) {
|
|
1051
|
-
const type = m.
|
|
1134
|
+
const type = m.getType();
|
|
1052
1135
|
if (type === "human") {
|
|
1053
1136
|
out.push({ role: "user", parts: [{ type: "text", text: messageText(m.content) }] });
|
|
1054
1137
|
} else if (type === "ai") {
|
|
@@ -1092,7 +1175,7 @@ async function handleThreadAction(body, { userId, threads, graph }, cors) {
|
|
|
1092
1175
|
|
|
1093
1176
|
// src/server/http/turn.ts
|
|
1094
1177
|
import { HumanMessage } from "@langchain/core/messages";
|
|
1095
|
-
import { Command } from "@langchain/langgraph";
|
|
1178
|
+
import { Command, GraphRecursionError } from "@langchain/langgraph";
|
|
1096
1179
|
async function runTurn(request, body, auth, deps, cors) {
|
|
1097
1180
|
const { config, graph, threads, secret, active } = deps;
|
|
1098
1181
|
const { userId } = auth;
|
|
@@ -1122,7 +1205,11 @@ async function runTurn(request, body, auth, deps, cors) {
|
|
|
1122
1205
|
await threads.touch(userId, threadId, titleFrom("message" in body ? body.message : ""));
|
|
1123
1206
|
active.set(userId, (active.get(userId) ?? 0) + 1);
|
|
1124
1207
|
const timeout = new AbortController();
|
|
1125
|
-
|
|
1208
|
+
let timedOut = false;
|
|
1209
|
+
const timer = setTimeout(() => {
|
|
1210
|
+
timedOut = true;
|
|
1211
|
+
timeout.abort(new Error("turn timed out"));
|
|
1212
|
+
}, config.limits.turnTimeoutMs);
|
|
1126
1213
|
request.signal.addEventListener("abort", () => timeout.abort(), { once: true });
|
|
1127
1214
|
const encoder = new TextEncoder();
|
|
1128
1215
|
const stream = new ReadableStream({
|
|
@@ -1155,9 +1242,9 @@ async function runTurn(request, body, auth, deps, cors) {
|
|
|
1155
1242
|
const waiting = pendingApprovalOf(await graph.getState(runCfg));
|
|
1156
1243
|
if (waiting) send({ type: "approval.request", ...waiting });
|
|
1157
1244
|
} catch (e) {
|
|
1158
|
-
const
|
|
1159
|
-
const
|
|
1160
|
-
send({ type: "error", code, message:
|
|
1245
|
+
const code = e instanceof GraphRecursionError ? "step_limit" : timedOut ? "timeout" : "error";
|
|
1246
|
+
const message = code === "step_limit" ? e.message : timedOut ? "turn timed out" : void 0;
|
|
1247
|
+
send({ type: "error", code, message: message ?? "The agent failed. Try again." });
|
|
1161
1248
|
} finally {
|
|
1162
1249
|
clearTimeout(timer);
|
|
1163
1250
|
active.set(userId, Math.max(0, (active.get(userId) ?? 1) - 1));
|
|
@@ -1341,6 +1428,7 @@ function expressHandler(agent) {
|
|
|
1341
1428
|
}
|
|
1342
1429
|
export {
|
|
1343
1430
|
KnowledgeBase,
|
|
1431
|
+
LangGraphThreadStore,
|
|
1344
1432
|
MemoryThreadStore,
|
|
1345
1433
|
MemoryVectorStore,
|
|
1346
1434
|
assertAllowedUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sideagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -54,8 +54,10 @@
|
|
|
54
54
|
"@langchain/core": "^1.0.0",
|
|
55
55
|
"@langchain/langgraph": "^1.5.2",
|
|
56
56
|
"@langchain/openai": "^1.7.0",
|
|
57
|
+
"@langchain/textsplitters": "^1.0.2",
|
|
57
58
|
"@radix-ui/react-icons": "^1.3.2",
|
|
58
59
|
"@scalar/openapi-parser": "^0.29.16",
|
|
60
|
+
"htmlparser2": "^12.0.0",
|
|
59
61
|
"react-markdown": "^10.1.0",
|
|
60
62
|
"remark-gfm": "^4.0.1",
|
|
61
63
|
"zod": "^4.0.0"
|