@agentuity/react 0.0.30 → 0.0.31
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/package.json +3 -2
- package/src/context.tsx +20 -0
- package/src/env.ts +6 -0
- package/src/index.ts +4 -0
- package/src/run.ts +96 -0
- package/src/types.ts +39 -0
- package/src/url.ts +25 -0
- package/src/websocket.ts +205 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentuity/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"AGENTS.md",
|
|
15
15
|
"README.md",
|
|
16
|
-
"dist"
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
19
20
|
"clean": "rm -rf dist",
|
package/src/context.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createContext, type Context, type ReactElement } from 'react';
|
|
3
|
+
import { defaultBaseUrl } from './url';
|
|
4
|
+
|
|
5
|
+
export interface ContextProviderArgs {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const AgentuityContext: Context<ContextProviderArgs> = createContext<ContextProviderArgs>({
|
|
11
|
+
baseUrl: '',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const AgentuityProvider = ({ baseUrl, children }: ContextProviderArgs): ReactElement => {
|
|
15
|
+
return (
|
|
16
|
+
<AgentuityContext.Provider value={{ baseUrl: baseUrl || defaultBaseUrl }}>
|
|
17
|
+
{children}
|
|
18
|
+
</AgentuityContext.Provider>
|
|
19
|
+
);
|
|
20
|
+
};
|
package/src/env.ts
ADDED
package/src/index.ts
ADDED
package/src/run.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useContext, useState } from 'react';
|
|
2
|
+
import type { InferInput, InferOutput } from '@agentuity/core';
|
|
3
|
+
import { buildUrl } from './url';
|
|
4
|
+
import { AgentuityContext } from './context';
|
|
5
|
+
import type { AgentName, AgentRegistry } from './types';
|
|
6
|
+
|
|
7
|
+
interface RunArgs {
|
|
8
|
+
/**
|
|
9
|
+
* Optional query parameters to append to the URL
|
|
10
|
+
*/
|
|
11
|
+
query?: URLSearchParams;
|
|
12
|
+
/**
|
|
13
|
+
* Optional headers to send with the request
|
|
14
|
+
*/
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
/**
|
|
17
|
+
* Optional subpath to append to the agent path (such as /agent/:agent_name/:subpath)
|
|
18
|
+
*/
|
|
19
|
+
subpath?: string;
|
|
20
|
+
/**
|
|
21
|
+
* HTTP method to use (default: POST)
|
|
22
|
+
*/
|
|
23
|
+
method?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Optional AbortSignal to cancel the request
|
|
26
|
+
*/
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface UseAgentResponse<TInput, TOutput> {
|
|
31
|
+
data?: TOutput;
|
|
32
|
+
run: (input: TInput, options?: RunArgs) => Promise<TOutput>;
|
|
33
|
+
running: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const useAgent = <
|
|
37
|
+
TName extends AgentName,
|
|
38
|
+
TInput = TName extends keyof AgentRegistry
|
|
39
|
+
? InferInput<AgentRegistry[TName]['inputSchema']>
|
|
40
|
+
: never,
|
|
41
|
+
TOutput = TName extends keyof AgentRegistry
|
|
42
|
+
? InferOutput<AgentRegistry[TName]['outputSchema']>
|
|
43
|
+
: never,
|
|
44
|
+
>(
|
|
45
|
+
name: TName
|
|
46
|
+
): UseAgentResponse<TInput, TOutput> => {
|
|
47
|
+
const context = useContext(AgentuityContext);
|
|
48
|
+
const [data, setData] = useState<TOutput>();
|
|
49
|
+
const [running, setRunning] = useState(false);
|
|
50
|
+
|
|
51
|
+
if (!context) {
|
|
52
|
+
throw new Error('useAgent must be used within a AgentuityProvider');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const run = async (input: TInput, options?: RunArgs): Promise<TOutput> => {
|
|
56
|
+
setRunning(true);
|
|
57
|
+
try {
|
|
58
|
+
const url = buildUrl(context.baseUrl!, `/agent/${name}`, options?.subpath, options?.query);
|
|
59
|
+
const signal = options?.signal ?? new AbortController().signal;
|
|
60
|
+
const response = await fetch(url, {
|
|
61
|
+
method: options?.method ?? 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
...(options?.headers ?? ''),
|
|
65
|
+
},
|
|
66
|
+
signal,
|
|
67
|
+
body:
|
|
68
|
+
input && typeof input === 'object' && options?.method !== 'GET'
|
|
69
|
+
? JSON.stringify(input)
|
|
70
|
+
: undefined,
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`Error invoking agent ${name}: ${response.statusText}`);
|
|
74
|
+
}
|
|
75
|
+
// TODO: handle streams
|
|
76
|
+
const ct = response.headers.get('Content-Type') || '';
|
|
77
|
+
if (ct.includes('text/')) {
|
|
78
|
+
const text = await response.text();
|
|
79
|
+
const _data = text as TOutput;
|
|
80
|
+
setData(_data);
|
|
81
|
+
return _data;
|
|
82
|
+
}
|
|
83
|
+
if (ct.includes('/json')) {
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
const _data = data as TOutput;
|
|
86
|
+
setData(_data);
|
|
87
|
+
return _data;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Unsupported content type: ${ct}`);
|
|
90
|
+
} finally {
|
|
91
|
+
setRunning(false);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return { data, run, running };
|
|
96
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@agentuity/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent definition interface
|
|
5
|
+
*/
|
|
6
|
+
export interface Agent<
|
|
7
|
+
TInput extends StandardSchemaV1 = StandardSchemaV1,
|
|
8
|
+
TOutput extends StandardSchemaV1 = StandardSchemaV1,
|
|
9
|
+
> {
|
|
10
|
+
inputSchema: TInput;
|
|
11
|
+
outputSchema: TOutput;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Registry of all agents in the project.
|
|
16
|
+
* This interface is designed to be augmented by generated code in the user's project.
|
|
17
|
+
*
|
|
18
|
+
* Example usage in generated code (.agentuity/types.d.ts):
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import type { Agent } from '@agentuity/react';
|
|
21
|
+
* import type { MyInputSchema, MyOutputSchema } from './schemas';
|
|
22
|
+
*
|
|
23
|
+
* declare module '@agentuity/react' {
|
|
24
|
+
* interface AgentRegistry {
|
|
25
|
+
* 'my-agent': Agent<MyInputSchema, MyOutputSchema>;
|
|
26
|
+
* 'another-agent': Agent<AnotherInput, AnotherOutput>;
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
32
|
+
export interface AgentRegistry {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Union type of all registered agent names.
|
|
36
|
+
* Falls back to `string` when AgentRegistry is empty (before augmentation).
|
|
37
|
+
* After augmentation, this becomes a strict union of agent names for full type safety.
|
|
38
|
+
*/
|
|
39
|
+
export type AgentName = keyof AgentRegistry extends never ? string : keyof AgentRegistry;
|
package/src/url.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getProcessEnv } from './env';
|
|
2
|
+
|
|
3
|
+
export const buildUrl = (
|
|
4
|
+
base: string,
|
|
5
|
+
path: string,
|
|
6
|
+
subpath?: string,
|
|
7
|
+
query?: URLSearchParams
|
|
8
|
+
): string => {
|
|
9
|
+
path = path.startsWith('/') ? path : `/${path}`;
|
|
10
|
+
let url = base.replace(/\/$/, '') + path;
|
|
11
|
+
if (subpath) {
|
|
12
|
+
subpath = subpath.startsWith('/') ? subpath : `/${subpath}`;
|
|
13
|
+
url += `/${subpath}`;
|
|
14
|
+
}
|
|
15
|
+
if (query) {
|
|
16
|
+
url += `?${query.toString()}`;
|
|
17
|
+
}
|
|
18
|
+
return url;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const defaultBaseUrl: string =
|
|
22
|
+
getProcessEnv('NEXT_PUBLIC__AGENTUITY_URL') ||
|
|
23
|
+
getProcessEnv('VITE_AGENTUITY_URL') ||
|
|
24
|
+
getProcessEnv('AGENTUITY_URL') ||
|
|
25
|
+
'http://localhost:3000';
|
package/src/websocket.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { InferInput, InferOutput } from '@agentuity/core';
|
|
3
|
+
import { AgentuityContext } from './context';
|
|
4
|
+
import { buildUrl } from './url';
|
|
5
|
+
import type { AgentName, AgentRegistry } from './types';
|
|
6
|
+
|
|
7
|
+
type onMessageHandler<T = unknown> = (data: T) => void;
|
|
8
|
+
|
|
9
|
+
interface WebsocketArgs {
|
|
10
|
+
/**
|
|
11
|
+
* Optional query parameters to append to the websocket URL
|
|
12
|
+
*/
|
|
13
|
+
query?: URLSearchParams;
|
|
14
|
+
/**
|
|
15
|
+
* Optional subpath to append to the agent path (such as /agent/:agent_name/:subpath)
|
|
16
|
+
*/
|
|
17
|
+
subpath?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Optional AbortSignal to cancel the websocket connection
|
|
20
|
+
*/
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const serializeWSData = (
|
|
25
|
+
data: unknown
|
|
26
|
+
): string | ArrayBufferLike | Blob | ArrayBufferView<ArrayBufferLike> => {
|
|
27
|
+
if (typeof data === 'string') {
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
if (typeof data === 'object') {
|
|
31
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || data instanceof Blob) {
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(data);
|
|
35
|
+
}
|
|
36
|
+
throw new Error('unsupported data type for websocket: ' + typeof data);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const deserializeData = <T>(data: string): T => {
|
|
40
|
+
if (data) {
|
|
41
|
+
if (data.startsWith('{') || data.startsWith('[')) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(data) as T;
|
|
44
|
+
} catch (ex) {
|
|
45
|
+
console.error('error parsing websocket data as JSON', ex, data);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return data as T;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
interface WebsocketResponse<TInput, TOutput> {
|
|
53
|
+
connected: boolean;
|
|
54
|
+
data?: TOutput;
|
|
55
|
+
send: (data: TInput) => void;
|
|
56
|
+
setHandler: (handler: onMessageHandler<TOutput>) => void;
|
|
57
|
+
readyState: WebSocket['readyState'];
|
|
58
|
+
close: () => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const useWebsocket = <TInput, TOutput>(
|
|
62
|
+
path: string,
|
|
63
|
+
options?: WebsocketArgs
|
|
64
|
+
): WebsocketResponse<TInput, TOutput> => {
|
|
65
|
+
const context = useContext(AgentuityContext);
|
|
66
|
+
|
|
67
|
+
if (!context) {
|
|
68
|
+
throw new Error('useWebsocket must be used within a AgentuityProvider');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const manualClose = useRef(false);
|
|
72
|
+
const wsRef = useRef<WebSocket | undefined>(undefined);
|
|
73
|
+
const pending = useRef<TOutput[]>([]);
|
|
74
|
+
const queued = useRef<TInput[]>([]);
|
|
75
|
+
const [data, setData] = useState<TOutput>();
|
|
76
|
+
const handler = useRef<onMessageHandler<TOutput> | undefined>(undefined);
|
|
77
|
+
const [connected, setConnected] = useState(false);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (options?.signal) {
|
|
81
|
+
const listener = () => {
|
|
82
|
+
wsRef.current?.close();
|
|
83
|
+
wsRef.current = undefined;
|
|
84
|
+
manualClose.current = true;
|
|
85
|
+
};
|
|
86
|
+
options.signal.addEventListener('abort', listener);
|
|
87
|
+
return () => {
|
|
88
|
+
options.signal?.removeEventListener('abort', listener);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
if (!wsRef.current) {
|
|
94
|
+
const wsUrl = buildUrl(
|
|
95
|
+
context.baseUrl!.replace(/^https?:/, 'ws:'),
|
|
96
|
+
path,
|
|
97
|
+
options?.subpath,
|
|
98
|
+
options?.query
|
|
99
|
+
);
|
|
100
|
+
const connect = () => {
|
|
101
|
+
wsRef.current = new WebSocket(wsUrl);
|
|
102
|
+
wsRef.current.onopen = () => {
|
|
103
|
+
setConnected(true);
|
|
104
|
+
if (queued.current.length > 0) {
|
|
105
|
+
queued.current.forEach((msg: unknown) => wsRef.current!.send(serializeWSData(msg)));
|
|
106
|
+
queued.current = [];
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
wsRef.current.onclose = () => {
|
|
110
|
+
wsRef.current = undefined;
|
|
111
|
+
queued.current = [];
|
|
112
|
+
setConnected(false);
|
|
113
|
+
if (manualClose.current) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
setTimeout(
|
|
117
|
+
() => {
|
|
118
|
+
connect();
|
|
119
|
+
},
|
|
120
|
+
Math.random() * 500 + 500
|
|
121
|
+
); // jitter reconnect
|
|
122
|
+
};
|
|
123
|
+
wsRef.current.onmessage = (event: { data: string }) => {
|
|
124
|
+
const payload = deserializeData<TOutput>(event.data);
|
|
125
|
+
setData(payload);
|
|
126
|
+
if (handler.current) {
|
|
127
|
+
handler.current(payload);
|
|
128
|
+
} else {
|
|
129
|
+
pending.current.push(payload);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
connect();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const send = (data: TInput) => {
|
|
137
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
138
|
+
wsRef.current.send(serializeWSData(data));
|
|
139
|
+
} else {
|
|
140
|
+
queued.current.push(data);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const setHandler = (h: onMessageHandler<TOutput>) => {
|
|
145
|
+
handler.current = h;
|
|
146
|
+
pending.current.forEach(h);
|
|
147
|
+
pending.current = [];
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const close = () => {
|
|
151
|
+
manualClose.current = true;
|
|
152
|
+
if (wsRef.current) {
|
|
153
|
+
wsRef.current.close();
|
|
154
|
+
wsRef.current = undefined;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
connected,
|
|
160
|
+
close,
|
|
161
|
+
data,
|
|
162
|
+
send,
|
|
163
|
+
setHandler,
|
|
164
|
+
readyState: wsRef.current?.readyState ?? WebSocket.CLOSED,
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
interface UseAgentWebsocketResponse<TInput, TOutput>
|
|
169
|
+
extends Omit<WebsocketResponse<TInput, TOutput>, 'setHandler'> {
|
|
170
|
+
/**
|
|
171
|
+
* Data received from the agent via WebSocket
|
|
172
|
+
*/
|
|
173
|
+
data?: TOutput;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const useAgentWebsocket = <
|
|
177
|
+
TName extends AgentName,
|
|
178
|
+
TInput = TName extends keyof AgentRegistry
|
|
179
|
+
? InferInput<AgentRegistry[TName]['inputSchema']>
|
|
180
|
+
: never,
|
|
181
|
+
TOutput = TName extends keyof AgentRegistry
|
|
182
|
+
? InferOutput<AgentRegistry[TName]['outputSchema']>
|
|
183
|
+
: never,
|
|
184
|
+
>(
|
|
185
|
+
agent: AgentName,
|
|
186
|
+
options?: WebsocketArgs
|
|
187
|
+
): UseAgentWebsocketResponse<TInput, TOutput> => {
|
|
188
|
+
const [data, setData] = useState<TOutput>();
|
|
189
|
+
const { connected, close, send, setHandler, readyState } = useWebsocket<TInput, TOutput>(
|
|
190
|
+
`/agent/${agent}`,
|
|
191
|
+
options
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
setHandler(setData);
|
|
196
|
+
}, []);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
connected,
|
|
200
|
+
close,
|
|
201
|
+
data,
|
|
202
|
+
send,
|
|
203
|
+
readyState,
|
|
204
|
+
};
|
|
205
|
+
};
|