@extrachill/chat 0.2.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 +24 -0
- package/README.md +154 -0
- package/css/chat.css +552 -0
- package/dist/Chat.d.ts +73 -0
- package/dist/Chat.d.ts.map +1 -0
- package/dist/Chat.js +50 -0
- package/dist/api.d.ts +68 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +93 -0
- package/dist/components/AvailabilityGate.d.ts +19 -0
- package/dist/components/AvailabilityGate.d.ts.map +1 -0
- package/dist/components/AvailabilityGate.js +32 -0
- package/dist/components/ChatInput.d.ts +21 -0
- package/dist/components/ChatInput.d.ts.map +1 -0
- package/dist/components/ChatInput.js +52 -0
- package/dist/components/ChatMessage.d.ts +23 -0
- package/dist/components/ChatMessage.d.ts.map +1 -0
- package/dist/components/ChatMessage.js +34 -0
- package/dist/components/ChatMessages.d.ts +28 -0
- package/dist/components/ChatMessages.d.ts.map +1 -0
- package/dist/components/ChatMessages.js +121 -0
- package/dist/components/ErrorBoundary.d.ts +27 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +34 -0
- package/dist/components/SessionSwitcher.d.ts +25 -0
- package/dist/components/SessionSwitcher.d.ts.map +1 -0
- package/dist/components/SessionSwitcher.js +44 -0
- package/dist/components/ToolMessage.d.ts +34 -0
- package/dist/components/ToolMessage.d.ts.map +1 -0
- package/dist/components/ToolMessage.js +39 -0
- package/dist/components/TypingIndicator.d.ts +16 -0
- package/dist/components/TypingIndicator.d.ts.map +1 -0
- package/dist/components/TypingIndicator.js +14 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +8 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useChat.d.ts +102 -0
- package/dist/hooks/useChat.d.ts.map +1 -0
- package/dist/hooks/useChat.js +192 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/normalizer.d.ts +24 -0
- package/dist/normalizer.d.ts.map +1 -0
- package/dist/normalizer.js +96 -0
- package/dist/types/adapter.d.ts +151 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +11 -0
- package/dist/types/api.d.ts +137 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +8 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/message.d.ts +62 -0
- package/dist/types/message.d.ts.map +1 -0
- package/dist/types/message.js +7 -0
- package/dist/types/session.d.ts +59 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +7 -0
- package/package.json +61 -0
- package/src/Chat.tsx +157 -0
- package/src/api.ts +173 -0
- package/src/components/AvailabilityGate.tsx +85 -0
- package/src/components/ChatInput.tsx +114 -0
- package/src/components/ChatMessage.tsx +85 -0
- package/src/components/ChatMessages.tsx +193 -0
- package/src/components/ErrorBoundary.tsx +66 -0
- package/src/components/SessionSwitcher.tsx +129 -0
- package/src/components/ToolMessage.tsx +112 -0
- package/src/components/TypingIndicator.tsx +36 -0
- package/src/components/index.ts +8 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useChat.ts +310 -0
- package/src/index.ts +79 -0
- package/src/normalizer.ts +112 -0
- package/src/types/api.ts +146 -0
- package/src/types/index.ts +26 -0
- package/src/types/message.ts +66 -0
- package/src/types/session.ts +50 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import type { ChatMessage } from '../types/message.ts';
|
|
3
|
+
import type { ChatSession } from '../types/session.ts';
|
|
4
|
+
import type { ChatAvailability } from '../types/session.ts';
|
|
5
|
+
import type { FetchFn, ChatApiConfig } from '../api.ts';
|
|
6
|
+
import {
|
|
7
|
+
sendMessage as apiSendMessage,
|
|
8
|
+
continueResponse as apiContinueResponse,
|
|
9
|
+
listSessions as apiListSessions,
|
|
10
|
+
loadSession as apiLoadSession,
|
|
11
|
+
deleteSession as apiDeleteSession,
|
|
12
|
+
} from '../api.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configuration for the useChat hook.
|
|
16
|
+
*/
|
|
17
|
+
export interface UseChatOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Base path for the chat REST endpoints.
|
|
20
|
+
* e.g. '/datamachine/v1/chat'
|
|
21
|
+
*/
|
|
22
|
+
basePath: string;
|
|
23
|
+
/**
|
|
24
|
+
* Fetch function for API calls. Must accept { path, method?, data? }
|
|
25
|
+
* and return parsed JSON. @wordpress/api-fetch works directly.
|
|
26
|
+
*/
|
|
27
|
+
fetchFn: FetchFn;
|
|
28
|
+
/**
|
|
29
|
+
* Agent ID to scope the chat to.
|
|
30
|
+
*/
|
|
31
|
+
agentId?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Initial messages to hydrate state with (e.g. server-rendered).
|
|
34
|
+
*/
|
|
35
|
+
initialMessages?: ChatMessage[];
|
|
36
|
+
/**
|
|
37
|
+
* Initial session ID (e.g. from server-rendered state).
|
|
38
|
+
*/
|
|
39
|
+
initialSessionId?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Maximum number of continuation turns before stopping.
|
|
42
|
+
* Defaults to 20.
|
|
43
|
+
*/
|
|
44
|
+
maxContinueTurns?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Called when a new message is added to the conversation.
|
|
47
|
+
*/
|
|
48
|
+
onMessage?: (message: ChatMessage) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Called when an error occurs.
|
|
51
|
+
*/
|
|
52
|
+
onError?: (error: Error) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return value of the useChat hook.
|
|
57
|
+
*/
|
|
58
|
+
export interface UseChatReturn {
|
|
59
|
+
/** All messages in the current conversation. */
|
|
60
|
+
messages: ChatMessage[];
|
|
61
|
+
/** Whether a message is being sent/processed. */
|
|
62
|
+
isLoading: boolean;
|
|
63
|
+
/** Current continuation turn count (0 when not processing). */
|
|
64
|
+
turnCount: number;
|
|
65
|
+
/** Current availability state. */
|
|
66
|
+
availability: ChatAvailability;
|
|
67
|
+
/** Active session ID. */
|
|
68
|
+
sessionId: string | null;
|
|
69
|
+
/** List of sessions. */
|
|
70
|
+
sessions: ChatSession[];
|
|
71
|
+
/** Whether sessions are loading. */
|
|
72
|
+
sessionsLoading: boolean;
|
|
73
|
+
/** Send a user message. */
|
|
74
|
+
sendMessage: (content: string) => void;
|
|
75
|
+
/** Switch to a different session. */
|
|
76
|
+
switchSession: (sessionId: string) => void;
|
|
77
|
+
/** Create a new session. */
|
|
78
|
+
newSession: () => void;
|
|
79
|
+
/** Delete a session. */
|
|
80
|
+
deleteSession: (sessionId: string) => void;
|
|
81
|
+
/** Clear the current session's messages locally. */
|
|
82
|
+
clearSession: () => void;
|
|
83
|
+
/** Refresh the session list. */
|
|
84
|
+
refreshSessions: () => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let messageIdCounter = 0;
|
|
88
|
+
function generateMessageId(): string {
|
|
89
|
+
return `msg_${Date.now()}_${++messageIdCounter}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Core state orchestrator for the chat UI.
|
|
94
|
+
*
|
|
95
|
+
* Manages messages, sessions, continuation loops, and availability
|
|
96
|
+
* by calling the standard chat REST endpoints directly.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```tsx
|
|
100
|
+
* import apiFetch from '@wordpress/api-fetch';
|
|
101
|
+
*
|
|
102
|
+
* const chat = useChat({
|
|
103
|
+
* basePath: '/datamachine/v1/chat',
|
|
104
|
+
* fetchFn: apiFetch,
|
|
105
|
+
* agentId: 5,
|
|
106
|
+
* });
|
|
107
|
+
*
|
|
108
|
+
* return (
|
|
109
|
+
* <>
|
|
110
|
+
* <ChatMessages messages={chat.messages} />
|
|
111
|
+
* <TypingIndicator visible={chat.isLoading} />
|
|
112
|
+
* <ChatInput onSend={chat.sendMessage} disabled={chat.isLoading} />
|
|
113
|
+
* </>
|
|
114
|
+
* );
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export function useChat({
|
|
118
|
+
basePath,
|
|
119
|
+
fetchFn,
|
|
120
|
+
agentId,
|
|
121
|
+
initialMessages,
|
|
122
|
+
initialSessionId,
|
|
123
|
+
maxContinueTurns = 20,
|
|
124
|
+
onMessage,
|
|
125
|
+
onError,
|
|
126
|
+
}: UseChatOptions): UseChatReturn {
|
|
127
|
+
const [messages, setMessages] = useState<ChatMessage[]>(initialMessages ?? []);
|
|
128
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
129
|
+
const [turnCount, setTurnCount] = useState(0);
|
|
130
|
+
const [availability, setAvailability] = useState<ChatAvailability>({ status: 'ready' });
|
|
131
|
+
const [sessionId, setSessionId] = useState<string | null>(initialSessionId ?? null);
|
|
132
|
+
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
|
133
|
+
const [sessionsLoading, setSessionsLoading] = useState(false);
|
|
134
|
+
|
|
135
|
+
// Build API config from props
|
|
136
|
+
const configRef = useRef<ChatApiConfig>({ basePath, fetchFn, agentId });
|
|
137
|
+
configRef.current = { basePath, fetchFn, agentId };
|
|
138
|
+
|
|
139
|
+
const sessionIdRef = useRef(sessionId);
|
|
140
|
+
sessionIdRef.current = sessionId;
|
|
141
|
+
|
|
142
|
+
// Load sessions on mount
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const loadSessions = async () => {
|
|
145
|
+
setSessionsLoading(true);
|
|
146
|
+
try {
|
|
147
|
+
const list = await apiListSessions(configRef.current);
|
|
148
|
+
setSessions(list);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// Sessions not available — degrade gracefully
|
|
151
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
152
|
+
} finally {
|
|
153
|
+
setSessionsLoading(false);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
loadSessions();
|
|
158
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
const sendMessage = useCallback(async (content: string) => {
|
|
162
|
+
if (isLoading) return;
|
|
163
|
+
|
|
164
|
+
// Optimistically add user message
|
|
165
|
+
const userMessage: ChatMessage = {
|
|
166
|
+
id: generateMessageId(),
|
|
167
|
+
role: 'user',
|
|
168
|
+
content,
|
|
169
|
+
timestamp: new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
173
|
+
onMessage?.(userMessage);
|
|
174
|
+
setIsLoading(true);
|
|
175
|
+
setTurnCount(0);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const result = await apiSendMessage(
|
|
179
|
+
configRef.current,
|
|
180
|
+
content,
|
|
181
|
+
sessionIdRef.current ?? undefined,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Update session ID (may be newly created)
|
|
185
|
+
setSessionId(result.sessionId);
|
|
186
|
+
sessionIdRef.current = result.sessionId;
|
|
187
|
+
|
|
188
|
+
// Replace all messages with the full normalized conversation
|
|
189
|
+
setMessages(result.messages);
|
|
190
|
+
|
|
191
|
+
// Handle multi-turn continuation
|
|
192
|
+
if (!result.completed && !result.maxTurnsReached) {
|
|
193
|
+
let completed = false;
|
|
194
|
+
let turns = 0;
|
|
195
|
+
|
|
196
|
+
while (!completed && turns < maxContinueTurns) {
|
|
197
|
+
turns++;
|
|
198
|
+
setTurnCount(turns);
|
|
199
|
+
|
|
200
|
+
const continuation = await apiContinueResponse(
|
|
201
|
+
configRef.current,
|
|
202
|
+
result.sessionId,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
setMessages((prev) => [...prev, ...continuation.messages]);
|
|
206
|
+
for (const msg of continuation.messages) {
|
|
207
|
+
onMessage?.(msg);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
completed = continuation.completed || continuation.maxTurnsReached;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Refresh sessions list after a message
|
|
215
|
+
apiListSessions(configRef.current)
|
|
216
|
+
.then(setSessions)
|
|
217
|
+
.catch(() => { /* ignore */ });
|
|
218
|
+
|
|
219
|
+
} catch (err) {
|
|
220
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
221
|
+
onError?.(error);
|
|
222
|
+
|
|
223
|
+
// Check if it's an auth error
|
|
224
|
+
if (error.message.includes('403') || error.message.includes('rest_forbidden')) {
|
|
225
|
+
setAvailability({ status: 'login-required' });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Add error as assistant message so it's visible in the UI
|
|
229
|
+
const errorMessage: ChatMessage = {
|
|
230
|
+
id: generateMessageId(),
|
|
231
|
+
role: 'assistant',
|
|
232
|
+
content: `Sorry, something went wrong: ${error.message}`,
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
};
|
|
235
|
+
setMessages((prev) => [...prev, errorMessage]);
|
|
236
|
+
} finally {
|
|
237
|
+
setIsLoading(false);
|
|
238
|
+
setTurnCount(0);
|
|
239
|
+
}
|
|
240
|
+
}, [isLoading, maxContinueTurns, onMessage, onError]);
|
|
241
|
+
|
|
242
|
+
const switchSession = useCallback(async (newSessionId: string) => {
|
|
243
|
+
setSessionId(newSessionId);
|
|
244
|
+
sessionIdRef.current = newSessionId;
|
|
245
|
+
setIsLoading(true);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const loaded = await apiLoadSession(configRef.current, newSessionId);
|
|
249
|
+
setMessages(loaded);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
252
|
+
setMessages([]);
|
|
253
|
+
} finally {
|
|
254
|
+
setIsLoading(false);
|
|
255
|
+
}
|
|
256
|
+
}, [onError]);
|
|
257
|
+
|
|
258
|
+
const newSession = useCallback(() => {
|
|
259
|
+
setSessionId(null);
|
|
260
|
+
sessionIdRef.current = null;
|
|
261
|
+
setMessages([]);
|
|
262
|
+
}, []);
|
|
263
|
+
|
|
264
|
+
const deleteSessionHandler = useCallback(async (targetSessionId: string) => {
|
|
265
|
+
try {
|
|
266
|
+
await apiDeleteSession(configRef.current, targetSessionId);
|
|
267
|
+
setSessions((prev) => prev.filter((s) => s.id !== targetSessionId));
|
|
268
|
+
|
|
269
|
+
if (sessionIdRef.current === targetSessionId) {
|
|
270
|
+
setSessionId(null);
|
|
271
|
+
sessionIdRef.current = null;
|
|
272
|
+
setMessages([]);
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
276
|
+
}
|
|
277
|
+
}, [onError]);
|
|
278
|
+
|
|
279
|
+
const clearSession = useCallback(() => {
|
|
280
|
+
setMessages([]);
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
const refreshSessions = useCallback(async () => {
|
|
284
|
+
setSessionsLoading(true);
|
|
285
|
+
try {
|
|
286
|
+
const list = await apiListSessions(configRef.current);
|
|
287
|
+
setSessions(list);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
290
|
+
} finally {
|
|
291
|
+
setSessionsLoading(false);
|
|
292
|
+
}
|
|
293
|
+
}, [onError]);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
messages,
|
|
297
|
+
isLoading,
|
|
298
|
+
turnCount,
|
|
299
|
+
availability,
|
|
300
|
+
sessionId,
|
|
301
|
+
sessions,
|
|
302
|
+
sessionsLoading,
|
|
303
|
+
sendMessage,
|
|
304
|
+
switchSession,
|
|
305
|
+
newSession,
|
|
306
|
+
deleteSession: deleteSessionHandler,
|
|
307
|
+
clearSession,
|
|
308
|
+
refreshSessions,
|
|
309
|
+
};
|
|
310
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
MessageRole,
|
|
4
|
+
ToolCall,
|
|
5
|
+
ToolResultMeta,
|
|
6
|
+
ChatMessage,
|
|
7
|
+
ContentFormat,
|
|
8
|
+
ChatSession,
|
|
9
|
+
ChatAvailability,
|
|
10
|
+
ChatInitialState,
|
|
11
|
+
RawMessage,
|
|
12
|
+
RawSession,
|
|
13
|
+
SessionMetadata,
|
|
14
|
+
} from './types/index.ts';
|
|
15
|
+
|
|
16
|
+
// API
|
|
17
|
+
export type { FetchFn, FetchOptions, ChatApiConfig, SendResult, ContinueResult } from './api.ts';
|
|
18
|
+
export {
|
|
19
|
+
sendMessage,
|
|
20
|
+
continueResponse,
|
|
21
|
+
listSessions,
|
|
22
|
+
loadSession,
|
|
23
|
+
deleteSession,
|
|
24
|
+
} from './api.ts';
|
|
25
|
+
|
|
26
|
+
// Normalizer
|
|
27
|
+
export { normalizeMessage, normalizeConversation, normalizeSession } from './normalizer.ts';
|
|
28
|
+
|
|
29
|
+
// Components
|
|
30
|
+
export {
|
|
31
|
+
ChatMessage as ChatMessageComponent,
|
|
32
|
+
type ChatMessageProps,
|
|
33
|
+
} from './components/ChatMessage.tsx';
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
ChatMessages,
|
|
37
|
+
type ChatMessagesProps,
|
|
38
|
+
} from './components/ChatMessages.tsx';
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
ChatInput,
|
|
42
|
+
type ChatInputProps,
|
|
43
|
+
} from './components/ChatInput.tsx';
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
ToolMessage,
|
|
47
|
+
type ToolMessageProps,
|
|
48
|
+
type ToolGroup,
|
|
49
|
+
} from './components/ToolMessage.tsx';
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
TypingIndicator,
|
|
53
|
+
type TypingIndicatorProps,
|
|
54
|
+
} from './components/TypingIndicator.tsx';
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
SessionSwitcher,
|
|
58
|
+
type SessionSwitcherProps,
|
|
59
|
+
} from './components/SessionSwitcher.tsx';
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
ErrorBoundary,
|
|
63
|
+
type ErrorBoundaryProps,
|
|
64
|
+
} from './components/ErrorBoundary.tsx';
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
AvailabilityGate,
|
|
68
|
+
type AvailabilityGateProps,
|
|
69
|
+
} from './components/AvailabilityGate.tsx';
|
|
70
|
+
|
|
71
|
+
// Hook
|
|
72
|
+
export {
|
|
73
|
+
useChat,
|
|
74
|
+
type UseChatOptions,
|
|
75
|
+
type UseChatReturn,
|
|
76
|
+
} from './hooks/useChat.ts';
|
|
77
|
+
|
|
78
|
+
// Composed
|
|
79
|
+
export { Chat, type ChatProps } from './Chat.tsx';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize raw backend messages into the ChatMessage format
|
|
3
|
+
* used by the UI components.
|
|
4
|
+
*
|
|
5
|
+
* The backend stores tool calls and results as regular messages
|
|
6
|
+
* with metadata.type = 'tool_call' | 'tool_result'. This normalizer
|
|
7
|
+
* maps them into the package's role-based message model.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ChatMessage, ToolCall } from './types/message.ts';
|
|
11
|
+
import type { ChatSession } from './types/session.ts';
|
|
12
|
+
import type { RawMessage, RawSession } from './types/api.ts';
|
|
13
|
+
|
|
14
|
+
let idCounter = 0;
|
|
15
|
+
function generateId(): string {
|
|
16
|
+
return `msg_${Date.now()}_${++idCounter}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalize a raw backend message into a ChatMessage.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeMessage(raw: RawMessage, index: number): ChatMessage {
|
|
23
|
+
const type = raw.metadata?.type ?? 'text';
|
|
24
|
+
const timestamp = raw.metadata?.timestamp ?? new Date().toISOString();
|
|
25
|
+
|
|
26
|
+
if (type === 'tool_call') {
|
|
27
|
+
const toolCalls: ToolCall[] = [];
|
|
28
|
+
|
|
29
|
+
// Extract from metadata (single tool call)
|
|
30
|
+
if (raw.metadata?.tool_name) {
|
|
31
|
+
toolCalls.push({
|
|
32
|
+
id: `tc_${index}_${raw.metadata.tool_name}`,
|
|
33
|
+
name: raw.metadata.tool_name,
|
|
34
|
+
parameters: raw.metadata.parameters ?? {},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Also check top-level tool_calls array
|
|
39
|
+
if (raw.tool_calls?.length) {
|
|
40
|
+
for (const tc of raw.tool_calls) {
|
|
41
|
+
// Avoid duplicates
|
|
42
|
+
if (!toolCalls.some((t) => t.name === tc.tool_name)) {
|
|
43
|
+
toolCalls.push({
|
|
44
|
+
id: `tc_${index}_${tc.tool_name}`,
|
|
45
|
+
name: tc.tool_name,
|
|
46
|
+
parameters: tc.parameters ?? {},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: generateId(),
|
|
54
|
+
role: 'tool_call',
|
|
55
|
+
content: raw.content,
|
|
56
|
+
timestamp,
|
|
57
|
+
toolCalls,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (type === 'tool_result') {
|
|
62
|
+
return {
|
|
63
|
+
id: generateId(),
|
|
64
|
+
role: 'tool_result',
|
|
65
|
+
content: raw.content,
|
|
66
|
+
timestamp,
|
|
67
|
+
toolResult: {
|
|
68
|
+
toolName: raw.metadata?.tool_name ?? 'unknown',
|
|
69
|
+
success: raw.metadata?.success ?? false,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Regular text message (user or assistant)
|
|
75
|
+
const message: ChatMessage = {
|
|
76
|
+
id: generateId(),
|
|
77
|
+
role: raw.role === 'user' ? 'user' : 'assistant',
|
|
78
|
+
content: raw.content,
|
|
79
|
+
timestamp,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Assistant messages may carry tool_calls at the top level
|
|
83
|
+
if (raw.role === 'assistant' && raw.tool_calls?.length) {
|
|
84
|
+
message.toolCalls = raw.tool_calls.map((tc, i) => ({
|
|
85
|
+
id: `tc_${index}_${i}_${tc.tool_name}`,
|
|
86
|
+
name: tc.tool_name,
|
|
87
|
+
parameters: tc.parameters ?? {},
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return message;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Normalize a full conversation array.
|
|
96
|
+
*/
|
|
97
|
+
export function normalizeConversation(raw: RawMessage[]): ChatMessage[] {
|
|
98
|
+
return raw.map(normalizeMessage);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Normalize a raw session into a ChatSession.
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeSession(raw: RawSession): ChatSession {
|
|
105
|
+
return {
|
|
106
|
+
id: raw.session_id,
|
|
107
|
+
title: raw.title ?? raw.first_message ?? undefined,
|
|
108
|
+
createdAt: raw.created_at,
|
|
109
|
+
updatedAt: raw.updated_at,
|
|
110
|
+
messageCount: raw.message_count,
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/types/api.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API contract types.
|
|
3
|
+
*
|
|
4
|
+
* These describe the expected shape of the chat REST endpoints.
|
|
5
|
+
* Any backend that wants to work with this package implements
|
|
6
|
+
* these same endpoints and response shapes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A raw message as stored/returned by the backend.
|
|
11
|
+
* The package normalizes these into ChatMessage before rendering.
|
|
12
|
+
*/
|
|
13
|
+
export interface RawMessage {
|
|
14
|
+
role: 'user' | 'assistant';
|
|
15
|
+
content: string;
|
|
16
|
+
metadata?: {
|
|
17
|
+
timestamp?: string;
|
|
18
|
+
type?: 'text' | 'tool_call' | 'tool_result';
|
|
19
|
+
tool_name?: string;
|
|
20
|
+
parameters?: Record<string, unknown>;
|
|
21
|
+
success?: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
turn?: number;
|
|
24
|
+
tool_data?: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
tool_calls?: Array<{
|
|
27
|
+
tool_name: string;
|
|
28
|
+
parameters: Record<string, unknown>;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* POST /chat — Send a message.
|
|
34
|
+
*/
|
|
35
|
+
export interface SendRequest {
|
|
36
|
+
message: string;
|
|
37
|
+
session_id?: string;
|
|
38
|
+
agent_id?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SendResponse {
|
|
42
|
+
success: boolean;
|
|
43
|
+
data: {
|
|
44
|
+
session_id: string;
|
|
45
|
+
response: string;
|
|
46
|
+
tool_calls: Array<{
|
|
47
|
+
tool_name: string;
|
|
48
|
+
parameters: Record<string, unknown>;
|
|
49
|
+
}>;
|
|
50
|
+
conversation: RawMessage[];
|
|
51
|
+
metadata: SessionMetadata;
|
|
52
|
+
completed: boolean;
|
|
53
|
+
max_turns: number;
|
|
54
|
+
turn_number: number;
|
|
55
|
+
max_turns_reached: boolean;
|
|
56
|
+
warning?: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* POST /chat/continue — Continue a multi-turn response.
|
|
62
|
+
*/
|
|
63
|
+
export interface ContinueRequest {
|
|
64
|
+
session_id: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ContinueResponse {
|
|
68
|
+
success: boolean;
|
|
69
|
+
data: {
|
|
70
|
+
session_id: string;
|
|
71
|
+
new_messages: RawMessage[];
|
|
72
|
+
final_content: string;
|
|
73
|
+
tool_calls: Array<{
|
|
74
|
+
tool_name: string;
|
|
75
|
+
parameters: Record<string, unknown>;
|
|
76
|
+
}>;
|
|
77
|
+
completed: boolean;
|
|
78
|
+
turn_number: number;
|
|
79
|
+
max_turns: number;
|
|
80
|
+
max_turns_reached: boolean;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* GET /chat/sessions — List sessions.
|
|
86
|
+
*/
|
|
87
|
+
export interface ListSessionsResponse {
|
|
88
|
+
success: boolean;
|
|
89
|
+
data: {
|
|
90
|
+
sessions: RawSession[];
|
|
91
|
+
total: number;
|
|
92
|
+
limit: number;
|
|
93
|
+
offset: number;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface RawSession {
|
|
98
|
+
session_id: string;
|
|
99
|
+
title: string | null;
|
|
100
|
+
context: string;
|
|
101
|
+
first_message: string | null;
|
|
102
|
+
message_count: number;
|
|
103
|
+
created_at: string;
|
|
104
|
+
updated_at: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* GET /chat/{session_id} — Get a single session.
|
|
109
|
+
*/
|
|
110
|
+
export interface GetSessionResponse {
|
|
111
|
+
success: boolean;
|
|
112
|
+
data: {
|
|
113
|
+
session_id: string;
|
|
114
|
+
conversation: RawMessage[];
|
|
115
|
+
metadata: SessionMetadata;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* DELETE /chat/{session_id} — Delete a session.
|
|
121
|
+
*/
|
|
122
|
+
export interface DeleteSessionResponse {
|
|
123
|
+
success: boolean;
|
|
124
|
+
data: {
|
|
125
|
+
session_id: string;
|
|
126
|
+
deleted: boolean;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Session metadata shape from the backend.
|
|
132
|
+
*/
|
|
133
|
+
export interface SessionMetadata {
|
|
134
|
+
status?: 'pending' | 'processing' | 'completed' | 'error';
|
|
135
|
+
started_at?: string;
|
|
136
|
+
last_activity?: string;
|
|
137
|
+
message_count?: number;
|
|
138
|
+
current_turn?: number;
|
|
139
|
+
has_pending_tools?: boolean;
|
|
140
|
+
token_usage?: {
|
|
141
|
+
prompt_tokens: number;
|
|
142
|
+
completion_tokens: number;
|
|
143
|
+
total_tokens: number;
|
|
144
|
+
};
|
|
145
|
+
error_message?: string;
|
|
146
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
MessageRole,
|
|
3
|
+
ToolCall,
|
|
4
|
+
ToolResultMeta,
|
|
5
|
+
ChatMessage,
|
|
6
|
+
ContentFormat,
|
|
7
|
+
} from './message.ts';
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
ChatSession,
|
|
11
|
+
ChatAvailability,
|
|
12
|
+
ChatInitialState,
|
|
13
|
+
} from './session.ts';
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
RawMessage,
|
|
17
|
+
RawSession,
|
|
18
|
+
SendRequest,
|
|
19
|
+
SendResponse,
|
|
20
|
+
ContinueRequest,
|
|
21
|
+
ContinueResponse,
|
|
22
|
+
ListSessionsResponse,
|
|
23
|
+
GetSessionResponse,
|
|
24
|
+
DeleteSessionResponse,
|
|
25
|
+
SessionMetadata,
|
|
26
|
+
} from './api.ts';
|