sideagent 0.0.0-stage → 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 +131 -2
- package/dist/index.d.ts +97 -0
- package/dist/index.js +742 -0
- package/dist/protocol.d.ts +87 -0
- package/dist/protocol.js +0 -0
- package/dist/server/browser.d.ts +2 -0
- package/dist/server/browser.js +4 -0
- package/dist/server/index.d.ts +247 -0
- package/dist/server/index.js +1113 -0
- package/dist/testing/mock-llm.d.ts +7 -0
- package/dist/testing/mock-llm.js +84 -0
- package/package.json +66 -4
package/README.md
CHANGED
|
@@ -1,3 +1,132 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sideagent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Embed a chat agent in your app. You give it an OpenAPI spec and an OpenAI-compatible host and key. It gives you a secure agent endpoint and a React chat UI.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm i sideagent
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Server (`sideagent/server`)
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createAgent, honoHandler } from "sideagent/server";
|
|
13
|
+
|
|
14
|
+
const agent = await createAgent({
|
|
15
|
+
spec: "https://api.example.com/openapi.json", // or an object / JSON string
|
|
16
|
+
baseUrl: "https://api.example.com", // pinned; the spec's `servers` is ignored
|
|
17
|
+
llm: { baseUrl: "https://api.openai.com/v1", apiKey: process.env.LLM_API_KEY!, model: "gpt-4o-mini" },
|
|
18
|
+
allow: ["listInvoices", "createInvoice", "tag:reports"], // default-deny allowlist
|
|
19
|
+
allowedOrigins: ["https://app.example.com"],
|
|
20
|
+
approvalSecret: process.env.APPROVAL_SECRET, // set on serverless / multi-instance
|
|
21
|
+
getAuth: (req) => ({ userId: "…", headers: { authorization: req.headers.get("authorization")! } }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.on(["POST", "OPTIONS"], "/agent", honoHandler(agent)); // also: nextRoute, expressHandler
|
|
25
|
+
console.log(await agent.doctor()); // what the agent can do + LLM check
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Secure by default: allowlisted operations only, the end user's own credentials forwarded, non-GET calls need
|
|
29
|
+
server-verified approval bound to the exact arguments, pinned `baseUrl` with SSRF guards, size/step/time limits,
|
|
30
|
+
origin checks, threads bound to the user, redacted audit events.
|
|
31
|
+
|
|
32
|
+
Per-operation hints go in the spec as `x-agent`: `{ approval, description, invalidates: ["tag"], responseFields: ["id"] }`.
|
|
33
|
+
|
|
34
|
+
## Client (`sideagent`)
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { AgentChat } from "sideagent";
|
|
38
|
+
|
|
39
|
+
<AgentChat
|
|
40
|
+
endpoint="/agent"
|
|
41
|
+
mode="floating" // "floating" | "sidebar"; the user can switch
|
|
42
|
+
hooks={{ onInvalidate: (tag) => tag === "invoices" && refetchInvoices() }}
|
|
43
|
+
/>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Docked sidebar
|
|
47
|
+
|
|
48
|
+
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.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { AgentProvider } from "sideagent";
|
|
52
|
+
|
|
53
|
+
<AgentProvider endpoint="/agent" mode="sidebar" defaultOpen>
|
|
54
|
+
<App />
|
|
55
|
+
</AgentProvider>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`AgentProvider` fills the viewport height (`100dvh`); override with the `className` or `style` props.
|
|
59
|
+
|
|
60
|
+
Anything under `AgentProvider` can call `useAgent()` to get `{ open, setOpen }`, e.g. an "Ask assistant" button in your toolbar.
|
|
61
|
+
|
|
62
|
+
## Try the demo (no API key needed)
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
pnpm install
|
|
66
|
+
pnpm demo # fake invoices API + agent + mock LLM on :8787, UI on http://localhost:5173
|
|
67
|
+
```
|
|
68
|
+
The demo has a dark mode toggle and uses `AgentProvider`. Run `pnpm demo:api` alone for just the API.
|
|
69
|
+
Set `LLM_API_KEY` (and optionally `LLM_BASE_URL`, `LLM_MODEL`) to use a real model.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
pnpm build # tsup -> dist/
|
|
75
|
+
pnpm test # vitest
|
|
76
|
+
pnpm typecheck
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Theming
|
|
80
|
+
|
|
81
|
+
Neutral by default (grayscale, host font, 10px radius). `theme="auto"` follows the OS and a host `.dark` / `data-theme="dark"` / `data-bs-theme="dark"`; pass `"light"` or `"dark"` to force one. Match your design system with CSS variables on `:root`:
|
|
82
|
+
|
|
83
|
+
```css
|
|
84
|
+
:root {
|
|
85
|
+
--sideagent-accent: var(--primary); /* buttons */
|
|
86
|
+
--sideagent-accent-fg: var(--primary-foreground);
|
|
87
|
+
--sideagent-bg: var(--background);
|
|
88
|
+
--sideagent-fg: var(--foreground);
|
|
89
|
+
--sideagent-border: var(--border);
|
|
90
|
+
--sideagent-surface: var(--muted); /* user bubble, hovers */
|
|
91
|
+
--sideagent-muted: var(--muted-foreground);
|
|
92
|
+
--sideagent-radius: 10px;
|
|
93
|
+
--sideagent-font: inherit;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Results and icons
|
|
98
|
+
|
|
99
|
+
Tool results render as readable views: arrays of objects become tables (up to 50 rows and 8 columns, numbers right-aligned, dates and booleans formatted), objects become labelled fields, errors and rejections get their own state. Raw JSON stays under "Details". Everything is plain text, never HTML. Override per operation:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<AgentChat resultRenderers={{ listInvoices: ({ result }) => <MyInvoiceTable rows={result as any[]} /> }} />
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Icons come from [Radix Icons](https://github.com/radix-ui/icons) (MIT).
|
|
106
|
+
|
|
107
|
+
## Chat history
|
|
108
|
+
|
|
109
|
+
`AgentChat` has a History button (list, open, delete), reopens the last conversation after a refresh, and restores a pending approval. Messages live in your LangGraph `checkpointer`; the list lives in a `threadStore` (in-memory by default). Both must be persistent in production:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
createAgent({ …, checkpointer: new PostgresSaver(…), threadStore: myThreadStore }); // ThreadStore: list / has / touch / delete
|
|
113
|
+
```
|
|
114
|
+
History is only ever shown from the user's own index, and tool results are rebuilt from the already-filtered copies.
|
|
115
|
+
|
|
116
|
+
## Knowledge (markdown and URLs)
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
createAgent({
|
|
120
|
+
…,
|
|
121
|
+
knowledge: {
|
|
122
|
+
embeddings: { model: "text-embedding-3-small" }, // OpenAI-compatible /embeddings; baseUrl/apiKey default to llm's
|
|
123
|
+
sources: [{ id: "guide", title: "Invoicing guide", content: markdownString }], // pass strings: no filesystem on the edge
|
|
124
|
+
urls: ["https://docs.example.com/payment-terms", { url: "https://…", title: "Shipping" }], // fetched by the server only
|
|
125
|
+
},
|
|
126
|
+
vectorStore, // optional: your own VectorStore (pgvector, Vectorize…). Default is in-memory.
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The agent gets a `search_knowledge` tool; answers show a "Sources" card (links only for your configured URLs). Passages reach the model as labelled untrusted data and never reach the browser. URLs come only from config, must be public http(s) (private ranges need `allowPrivateNetworks`, metadata addresses never), follow only same-host redirects, and are capped by `maxSourceBytes`. URL sources refresh in the background after `ttlMs` (default 1h) or on `agent.knowledge.refresh()`. A source that fails to load is skipped with a warning and shown in `doctor()`.
|
|
131
|
+
|
|
132
|
+
With the default in-memory store, every start re-embeds all sources (embedding cost). Use a persistent `vectorStore` for large knowledge.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { AgentEvent, ChatRequest } from './protocol.js';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { ReactNode, CSSProperties } from 'react';
|
|
4
|
+
|
|
5
|
+
interface OperationEvent {
|
|
6
|
+
operationId: string;
|
|
7
|
+
status: number;
|
|
8
|
+
args: unknown;
|
|
9
|
+
result: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* UI hooks. They run your code after the server confirms something happened, e.g. refetch a
|
|
13
|
+
* list after `createInvoice`. The model can never trigger them or any other client action.
|
|
14
|
+
*/
|
|
15
|
+
interface AgentHooks {
|
|
16
|
+
onTurnStart?: () => void;
|
|
17
|
+
onTurnEnd?: () => void;
|
|
18
|
+
onApprovalRequested?: (e: Extract<AgentEvent, {
|
|
19
|
+
type: "approval.request";
|
|
20
|
+
}>) => void;
|
|
21
|
+
onError?: (e: {
|
|
22
|
+
code: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}) => void;
|
|
25
|
+
/** Fires for every finished operation, successful or not. */
|
|
26
|
+
onAnyOperation?: (e: OperationEvent) => void;
|
|
27
|
+
/** Fires for one operationId. */
|
|
28
|
+
onOperationComplete?: Record<string, (e: OperationEvent) => void>;
|
|
29
|
+
/** Fires per `x-agent: { invalidates: [...] }` tag after a successful operation. */
|
|
30
|
+
onInvalidate?: (tag: string, e: OperationEvent) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type AgentChatMode = "floating" | "sidebar";
|
|
34
|
+
interface AgentChatProps {
|
|
35
|
+
/** URL of your sideagent endpoint, e.g. "/api/agent". */
|
|
36
|
+
endpoint: string;
|
|
37
|
+
/** Starting layout. The user can switch; their choice is remembered. */
|
|
38
|
+
mode?: AgentChatMode;
|
|
39
|
+
title?: string;
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
hooks?: AgentHooks;
|
|
42
|
+
/**
|
|
43
|
+
* Extra info about the page (route, selected record). The model sees it as untrusted data;
|
|
44
|
+
* it never grants permissions.
|
|
45
|
+
*/
|
|
46
|
+
context?: unknown | (() => unknown);
|
|
47
|
+
/** Use "include" when the endpoint is on another origin and relies on cookies. */
|
|
48
|
+
credentials?: RequestCredentials;
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
defaultOpen?: boolean;
|
|
51
|
+
/** Custom result views per operationId. Anything not listed gets the built-in table / fields view. */
|
|
52
|
+
resultRenderers?: Record<string, (e: {
|
|
53
|
+
operationId: string;
|
|
54
|
+
args: unknown;
|
|
55
|
+
result: unknown;
|
|
56
|
+
}) => ReactNode>;
|
|
57
|
+
/** "auto" follows the OS and host `.dark` / `data-theme=dark`. Colors are overridable via --sideagent-* CSS variables. */
|
|
58
|
+
theme?: "auto" | "light" | "dark";
|
|
59
|
+
/** Set false to hide the layout switcher. */
|
|
60
|
+
allowModeSwitch?: boolean;
|
|
61
|
+
}
|
|
62
|
+
interface AgentControls {
|
|
63
|
+
open: boolean;
|
|
64
|
+
setOpen: (open: boolean) => void;
|
|
65
|
+
}
|
|
66
|
+
/** Open or close the assistant from anywhere under `AgentProvider`. */
|
|
67
|
+
declare function useAgent(): AgentControls;
|
|
68
|
+
/** Lays the app and a docked sidebar chat side by side, and shares open state via `useAgent`. Takes the same props as AgentChat. */
|
|
69
|
+
declare function AgentProvider({ children, className, style, ...chat }: AgentChatProps & {
|
|
70
|
+
children: ReactNode;
|
|
71
|
+
className?: string;
|
|
72
|
+
style?: CSSProperties;
|
|
73
|
+
}): react.JSX.Element;
|
|
74
|
+
declare function AgentChat(props: AgentChatProps): react.JSX.Element;
|
|
75
|
+
|
|
76
|
+
interface TransportOptions {
|
|
77
|
+
endpoint: string;
|
|
78
|
+
credentials?: RequestCredentials;
|
|
79
|
+
headers?: Record<string, string>;
|
|
80
|
+
signal?: AbortSignal;
|
|
81
|
+
}
|
|
82
|
+
/** POSTs a chat request and yields the server's SSE events. */
|
|
83
|
+
declare function streamChat(body: ChatRequest, opts: TransportOptions): AsyncGenerator<AgentEvent>;
|
|
84
|
+
/** POSTs a non-streaming request (history actions) and returns the JSON body. */
|
|
85
|
+
declare function callJson<T>(body: ChatRequest, opts: Omit<TransportOptions, "signal">): Promise<{
|
|
86
|
+
status: number;
|
|
87
|
+
data: T | null;
|
|
88
|
+
}>;
|
|
89
|
+
|
|
90
|
+
/** "createdAt" / "created_at" -> "Created at" */
|
|
91
|
+
declare function humanize(key: string): string;
|
|
92
|
+
/** Turns an API result into a readable view. Plain text only: nothing here renders HTML. */
|
|
93
|
+
declare function ResultView({ result }: {
|
|
94
|
+
result: unknown;
|
|
95
|
+
}): ReactNode;
|
|
96
|
+
|
|
97
|
+
export { AgentChat, type AgentChatMode, type AgentChatProps, type AgentControls, AgentEvent, type AgentHooks, AgentProvider, ChatRequest, type OperationEvent, ResultView, callJson, humanize, streamChat, useAgent };
|