@promptbook/cli 0.103.0-54 → 0.103.0-55
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/apps/agents-server/config.ts +0 -2
- package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +79 -6
- package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +171 -69
- package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +203 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +3 -1
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -1
- package/apps/agents-server/src/app/agents/[agentName]/api/openai/chat/completions/route.ts +3 -169
- package/apps/agents-server/src/app/agents/[agentName]/api/openrouter/chat/completions/route.ts +10 -0
- package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +218 -0
- package/apps/agents-server/src/app/api/auth/change-password/route.ts +75 -0
- package/apps/agents-server/src/app/api/chat-feedback/export/route.ts +55 -0
- package/apps/agents-server/src/app/api/chat-history/export/route.ts +55 -0
- package/apps/agents-server/src/components/ChangePasswordDialog/ChangePasswordDialog.tsx +41 -0
- package/apps/agents-server/src/components/ChangePasswordForm/ChangePasswordForm.tsx +159 -0
- package/apps/agents-server/src/components/Header/Header.tsx +94 -38
- package/apps/agents-server/src/middleware.ts +1 -1
- package/apps/agents-server/src/utils/convertToCsv.ts +31 -0
- package/apps/agents-server/src/utils/handleChatCompletion.ts +183 -0
- package/apps/agents-server/src/utils/resolveInheritedAgentSource.ts +93 -0
- package/esm/index.es.js +770 -181
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/_packages/core.index.d.ts +8 -6
- package/esm/typings/src/_packages/types.index.d.ts +1 -1
- package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +4 -0
- package/esm/typings/src/commitments/CLOSED/CLOSED.d.ts +35 -0
- package/esm/typings/src/commitments/COMPONENT/COMPONENT.d.ts +28 -0
- package/esm/typings/src/commitments/FROM/FROM.d.ts +34 -0
- package/esm/typings/src/commitments/IMPORTANT/IMPORTANT.d.ts +26 -0
- package/esm/typings/src/commitments/LANGUAGE/LANGUAGE.d.ts +35 -0
- package/esm/typings/src/commitments/OPEN/OPEN.d.ts +35 -0
- package/esm/typings/src/commitments/index.d.ts +1 -82
- package/esm/typings/src/commitments/registry.d.ts +68 -0
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +2 -2
- package/umd/index.umd.js +770 -181
- package/umd/index.umd.js.map +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Loader2, Lock } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
type ChangePasswordFormProps = {
|
|
7
|
+
onSuccess?: () => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function ChangePasswordForm(props: ChangePasswordFormProps) {
|
|
12
|
+
const { onSuccess, className } = props;
|
|
13
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
14
|
+
const [error, setError] = useState<string | null>(null);
|
|
15
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
setIsLoading(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
setSuccessMessage(null);
|
|
22
|
+
|
|
23
|
+
const formData = new FormData(event.currentTarget);
|
|
24
|
+
const currentPassword = formData.get('currentPassword') as string;
|
|
25
|
+
const newPassword = formData.get('newPassword') as string;
|
|
26
|
+
const confirmNewPassword = formData.get('confirmNewPassword') as string;
|
|
27
|
+
|
|
28
|
+
if (newPassword !== confirmNewPassword) {
|
|
29
|
+
setError('New passwords do not match');
|
|
30
|
+
setIsLoading(false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch('/api/auth/change-password', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ currentPassword, newPassword }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const result = await response.json();
|
|
44
|
+
|
|
45
|
+
if (response.ok) {
|
|
46
|
+
setSuccessMessage('Password changed successfully');
|
|
47
|
+
// Reset form
|
|
48
|
+
event.currentTarget.reset();
|
|
49
|
+
if (onSuccess) {
|
|
50
|
+
setTimeout(onSuccess, 1500);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
setError(result.error || 'An error occurred');
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
setError('An unexpected error occurred');
|
|
57
|
+
console.error(error);
|
|
58
|
+
} finally {
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<form onSubmit={handleSubmit} className={`space-y-4 ${className || ''}`}>
|
|
65
|
+
<div className="space-y-2">
|
|
66
|
+
<label
|
|
67
|
+
htmlFor="currentPassword"
|
|
68
|
+
className="text-sm font-medium text-gray-700 block"
|
|
69
|
+
>
|
|
70
|
+
Current Password
|
|
71
|
+
</label>
|
|
72
|
+
<div className="relative">
|
|
73
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
|
74
|
+
<Lock className="w-4 h-4" />
|
|
75
|
+
</div>
|
|
76
|
+
<input
|
|
77
|
+
id="currentPassword"
|
|
78
|
+
name="currentPassword"
|
|
79
|
+
type="password"
|
|
80
|
+
required
|
|
81
|
+
className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
|
|
82
|
+
placeholder="Enter current password"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div className="space-y-2">
|
|
88
|
+
<label
|
|
89
|
+
htmlFor="newPassword"
|
|
90
|
+
className="text-sm font-medium text-gray-700 block"
|
|
91
|
+
>
|
|
92
|
+
New Password
|
|
93
|
+
</label>
|
|
94
|
+
<div className="relative">
|
|
95
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
|
96
|
+
<Lock className="w-4 h-4" />
|
|
97
|
+
</div>
|
|
98
|
+
<input
|
|
99
|
+
id="newPassword"
|
|
100
|
+
name="newPassword"
|
|
101
|
+
type="password"
|
|
102
|
+
required
|
|
103
|
+
className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
|
|
104
|
+
placeholder="Enter new password"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div className="space-y-2">
|
|
110
|
+
<label
|
|
111
|
+
htmlFor="confirmNewPassword"
|
|
112
|
+
className="text-sm font-medium text-gray-700 block"
|
|
113
|
+
>
|
|
114
|
+
Confirm New Password
|
|
115
|
+
</label>
|
|
116
|
+
<div className="relative">
|
|
117
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
|
118
|
+
<Lock className="w-4 h-4" />
|
|
119
|
+
</div>
|
|
120
|
+
<input
|
|
121
|
+
id="confirmNewPassword"
|
|
122
|
+
name="confirmNewPassword"
|
|
123
|
+
type="password"
|
|
124
|
+
required
|
|
125
|
+
className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
|
|
126
|
+
placeholder="Confirm new password"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{error && (
|
|
132
|
+
<div className="p-3 text-sm text-red-500 bg-red-50 border border-red-200 rounded-md">
|
|
133
|
+
{error}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{successMessage && (
|
|
138
|
+
<div className="p-3 text-sm text-green-500 bg-green-50 border border-green-200 rounded-md">
|
|
139
|
+
{successMessage}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
<button
|
|
144
|
+
type="submit"
|
|
145
|
+
disabled={isLoading}
|
|
146
|
+
className="w-full inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-promptbook-blue-dark text-white hover:bg-promptbook-blue-dark/90 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none transition-colors"
|
|
147
|
+
>
|
|
148
|
+
{isLoading ? (
|
|
149
|
+
<>
|
|
150
|
+
<Loader2 className="mr-2 w-4 h-4 animate-spin" />
|
|
151
|
+
Changing Password...
|
|
152
|
+
</>
|
|
153
|
+
) : (
|
|
154
|
+
'Change Password'
|
|
155
|
+
)}
|
|
156
|
+
</button>
|
|
157
|
+
</form>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import promptbookLogoBlueTransparent from '@/public/logo-blue-white-256.png';
|
|
4
4
|
import { $createAgentAction, logoutAction } from '@/src/app/actions';
|
|
5
|
-
import { ArrowRight, ChevronDown, LogIn, LogOut } from 'lucide-react';
|
|
5
|
+
import { ArrowRight, ChevronDown, Lock, LogIn, LogOut, User } from 'lucide-react';
|
|
6
6
|
import Image from 'next/image';
|
|
7
7
|
import Link from 'next/link';
|
|
8
8
|
import { useRouter } from 'next/navigation';
|
|
@@ -12,6 +12,7 @@ import { HamburgerMenu } from '../../../../../src/book-components/_common/Hambur
|
|
|
12
12
|
import { just } from '../../../../../src/utils/organization/just';
|
|
13
13
|
import type { UserInfo } from '../../utils/getCurrentUser';
|
|
14
14
|
import { getVisibleCommitmentDefinitions } from '../../utils/getVisibleCommitmentDefinitions';
|
|
15
|
+
import { ChangePasswordDialog } from '../ChangePasswordDialog/ChangePasswordDialog';
|
|
15
16
|
import { LoginDialog } from '../LoginDialog/LoginDialog';
|
|
16
17
|
import { useUsersAdmin } from '../UsersList/useUsersAdmin';
|
|
17
18
|
|
|
@@ -73,12 +74,16 @@ export function Header(props: HeaderProps) {
|
|
|
73
74
|
|
|
74
75
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
75
76
|
const [isLoginOpen, setIsLoginOpen] = useState(false);
|
|
77
|
+
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
|
|
76
78
|
const [isAgentsOpen, setIsAgentsOpen] = useState(false);
|
|
77
79
|
const [isDocsOpen, setIsDocsOpen] = useState(false);
|
|
78
80
|
const [isUsersOpen, setIsUsersOpen] = useState(false);
|
|
81
|
+
const [isSystemOpen, setIsSystemOpen] = useState(false);
|
|
82
|
+
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
|
79
83
|
const [isMobileAgentsOpen, setIsMobileAgentsOpen] = useState(false);
|
|
80
84
|
const [isMobileDocsOpen, setIsMobileDocsOpen] = useState(false);
|
|
81
85
|
const [isMobileUsersOpen, setIsMobileUsersOpen] = useState(false);
|
|
86
|
+
const [isMobileSystemOpen, setIsMobileSystemOpen] = useState(false);
|
|
82
87
|
const [isCreatingAgent, setIsCreatingAgent] = useState(false);
|
|
83
88
|
const router = useRouter();
|
|
84
89
|
|
|
@@ -131,7 +136,9 @@ export function Header(props: HeaderProps) {
|
|
|
131
136
|
label: (
|
|
132
137
|
<>
|
|
133
138
|
{primary.type}
|
|
134
|
-
{aliases.length > 0 &&
|
|
139
|
+
{aliases.length > 0 && (
|
|
140
|
+
<span className="text-gray-400 font-normal"> / {aliases.join(' / ')}</span>
|
|
141
|
+
)}
|
|
135
142
|
</>
|
|
136
143
|
),
|
|
137
144
|
href: `/docs/${primary.type}`,
|
|
@@ -210,24 +217,30 @@ export function Header(props: HeaderProps) {
|
|
|
210
217
|
],
|
|
211
218
|
},
|
|
212
219
|
{
|
|
213
|
-
type: '
|
|
214
|
-
label: '
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
220
|
+
type: 'dropdown' as const,
|
|
221
|
+
label: 'System',
|
|
222
|
+
isOpen: isSystemOpen,
|
|
223
|
+
setIsOpen: setIsSystemOpen,
|
|
224
|
+
isMobileOpen: isMobileSystemOpen,
|
|
225
|
+
setIsMobileOpen: setIsMobileSystemOpen,
|
|
226
|
+
items: [
|
|
227
|
+
{
|
|
228
|
+
label: 'API Tokens',
|
|
229
|
+
href: '/admin/api-tokens',
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
label: 'Metadata',
|
|
233
|
+
href: '/admin/metadata',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
label: 'Chat history',
|
|
237
|
+
href: '/admin/chat-history',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
label: 'Chat feedback',
|
|
241
|
+
href: '/admin/chat-feedback',
|
|
242
|
+
},
|
|
243
|
+
],
|
|
231
244
|
},
|
|
232
245
|
{
|
|
233
246
|
type: 'link' as const,
|
|
@@ -241,6 +254,7 @@ export function Header(props: HeaderProps) {
|
|
|
241
254
|
return (
|
|
242
255
|
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200 h-16">
|
|
243
256
|
<LoginDialog isOpen={isLoginOpen} onClose={() => setIsLoginOpen(false)} />
|
|
257
|
+
<ChangePasswordDialog isOpen={isChangePasswordOpen} onClose={() => setIsChangePasswordOpen(false)} />
|
|
244
258
|
<div className="container mx-auto px-4 h-full">
|
|
245
259
|
<div className="flex items-center justify-between h-full">
|
|
246
260
|
{/* Logo */}
|
|
@@ -370,24 +384,56 @@ export function Header(props: HeaderProps) {
|
|
|
370
384
|
|
|
371
385
|
{(currentUser || isAdmin) && (
|
|
372
386
|
<div className="hidden lg:flex items-center gap-3">
|
|
373
|
-
<
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
387
|
+
<div className="relative">
|
|
388
|
+
<button
|
|
389
|
+
onClick={() => setIsProfileOpen(!isProfileOpen)}
|
|
390
|
+
onBlur={() => setTimeout(() => setIsProfileOpen(false), 200)}
|
|
391
|
+
className="flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors px-3 py-2 rounded-md hover:bg-gray-50"
|
|
392
|
+
>
|
|
393
|
+
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">
|
|
394
|
+
<User className="w-4 h-4" />
|
|
395
|
+
</div>
|
|
396
|
+
<div className="flex flex-col items-start">
|
|
397
|
+
<span className="leading-none">{currentUser?.username || 'Admin'}</span>
|
|
398
|
+
{(currentUser?.isAdmin || isAdmin) && (
|
|
399
|
+
<span className="text-xs text-blue-600">Admin</span>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
<ChevronDown className="w-4 h-4 ml-1 opacity-50" />
|
|
403
|
+
</button>
|
|
404
|
+
|
|
405
|
+
{isProfileOpen && (
|
|
406
|
+
<div className="absolute top-full right-0 mt-2 w-56 bg-white rounded-md shadow-lg border border-gray-100 py-1 z-50 animate-in fade-in zoom-in-95 duration-200">
|
|
407
|
+
<div className="px-4 py-3 border-b border-gray-100">
|
|
408
|
+
<p className="text-sm font-medium text-gray-900">
|
|
409
|
+
{currentUser?.username || 'Admin'}
|
|
410
|
+
</p>
|
|
411
|
+
{(currentUser?.isAdmin || isAdmin) && (
|
|
412
|
+
<p className="text-xs text-blue-600 mt-1">Administrator</p>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<button
|
|
417
|
+
onClick={() => setIsChangePasswordOpen(true)}
|
|
418
|
+
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 flex items-center gap-2"
|
|
419
|
+
>
|
|
420
|
+
<Lock className="w-4 h-4" />
|
|
421
|
+
Change Password
|
|
422
|
+
</button>
|
|
423
|
+
|
|
424
|
+
<button
|
|
425
|
+
onClick={() => {
|
|
426
|
+
handleLogout();
|
|
427
|
+
setIsMenuOpen(false);
|
|
428
|
+
}}
|
|
429
|
+
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 hover:text-red-700 flex items-center gap-2"
|
|
430
|
+
>
|
|
431
|
+
<LogOut className="w-4 h-4" />
|
|
432
|
+
Log out
|
|
433
|
+
</button>
|
|
434
|
+
</div>
|
|
379
435
|
)}
|
|
380
|
-
</
|
|
381
|
-
<button
|
|
382
|
-
onClick={() => {
|
|
383
|
-
handleLogout();
|
|
384
|
-
setIsMenuOpen(false);
|
|
385
|
-
}}
|
|
386
|
-
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors"
|
|
387
|
-
>
|
|
388
|
-
Log out
|
|
389
|
-
<LogOut className="ml-2 w-4 h-4" />
|
|
390
|
-
</button>
|
|
436
|
+
</div>
|
|
391
437
|
</div>
|
|
392
438
|
)}
|
|
393
439
|
|
|
@@ -403,7 +449,7 @@ export function Header(props: HeaderProps) {
|
|
|
403
449
|
{/* Mobile Navigation */}
|
|
404
450
|
{isMenuOpen && (
|
|
405
451
|
<div
|
|
406
|
-
className="lg:hidden absolute top-16 left-0 right-0 z-50 bg-white
|
|
452
|
+
className="lg:hidden absolute top-16 left-0 right-0 z-50 bg-white shadow-xl py-4 border-t border-gray-100 animate-in slide-in-from-top-2 h-[calc(100vh-4rem)] overflow-y-auto"
|
|
407
453
|
style={{
|
|
408
454
|
backdropFilter: 'blur(20px)',
|
|
409
455
|
WebkitBackdropFilter: 'blur(20px)',
|
|
@@ -435,6 +481,16 @@ export function Header(props: HeaderProps) {
|
|
|
435
481
|
</span>
|
|
436
482
|
)}
|
|
437
483
|
</div>
|
|
484
|
+
<button
|
|
485
|
+
onClick={() => {
|
|
486
|
+
setIsChangePasswordOpen(true);
|
|
487
|
+
setIsMenuOpen(false);
|
|
488
|
+
}}
|
|
489
|
+
className="flex items-center gap-2 text-base font-medium text-gray-600 hover:text-gray-900 w-full"
|
|
490
|
+
>
|
|
491
|
+
<Lock className="w-4 h-4" />
|
|
492
|
+
Change Password
|
|
493
|
+
</button>
|
|
438
494
|
<button
|
|
439
495
|
onClick={() => {
|
|
440
496
|
handleLogout();
|
|
@@ -134,7 +134,7 @@ export async function middleware(req: NextRequest) {
|
|
|
134
134
|
headers: {
|
|
135
135
|
'Access-Control-Allow-Origin': '*',
|
|
136
136
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
137
|
-
'Access-Control-Allow-Headers': 'Content-Type',
|
|
137
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
138
138
|
},
|
|
139
139
|
});
|
|
140
140
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an array of objects to a CSV string
|
|
3
|
+
*/
|
|
4
|
+
export function convertToCsv(data: Array<Record<string, unknown>>): string {
|
|
5
|
+
if (data.length === 0) {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const headers = Object.keys(data[0]);
|
|
10
|
+
const csvRows = [headers.join(',')];
|
|
11
|
+
|
|
12
|
+
for (const row of data) {
|
|
13
|
+
const values = headers.map((header) => {
|
|
14
|
+
let value = row[header];
|
|
15
|
+
|
|
16
|
+
if (value === null || value === undefined) {
|
|
17
|
+
value = '';
|
|
18
|
+
} else if (typeof value === 'object') {
|
|
19
|
+
value = JSON.stringify(value);
|
|
20
|
+
} else {
|
|
21
|
+
value = String(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const escaped = (value as string).replace(/"/g, '""');
|
|
25
|
+
return `"${escaped}"`;
|
|
26
|
+
});
|
|
27
|
+
csvRows.push(values.join(','));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return csvRows.join('\n');
|
|
31
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
|
|
2
|
+
import { $provideExecutionToolsForServer } from '@/src/tools/$provideExecutionToolsForServer';
|
|
3
|
+
import { Agent } from '@promptbook-local/core';
|
|
4
|
+
import { ChatMessage, ChatPromptResult, Prompt, TODO_any } from '@promptbook-local/types';
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
|
|
7
|
+
export async function handleChatCompletion(
|
|
8
|
+
request: NextRequest,
|
|
9
|
+
params: { agentName: string },
|
|
10
|
+
title: string = 'API Chat Completion'
|
|
11
|
+
) {
|
|
12
|
+
const { agentName } = params;
|
|
13
|
+
|
|
14
|
+
// Note: Authentication is handled by middleware
|
|
15
|
+
// If we are here, the request is either authenticated or public access is allowed (but middleware blocks it if not)
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const body = await request.json();
|
|
19
|
+
const { messages, stream, model } = body;
|
|
20
|
+
|
|
21
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{
|
|
24
|
+
error: {
|
|
25
|
+
message: 'Messages array is required and cannot be empty.',
|
|
26
|
+
type: 'invalid_request_error',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{ status: 400 },
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const collection = await $provideAgentCollectionForServer();
|
|
34
|
+
let agentSource;
|
|
35
|
+
try {
|
|
36
|
+
agentSource = await collection.getAgentSource(agentName);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return NextResponse.json(
|
|
39
|
+
{ error: { message: `Agent '${agentName}' not found.`, type: 'invalid_request_error' } },
|
|
40
|
+
{ status: 404 },
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!agentSource) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: { message: `Agent '${agentName}' not found.`, type: 'invalid_request_error' } },
|
|
47
|
+
{ status: 404 },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const executionTools = await $provideExecutionToolsForServer();
|
|
52
|
+
const agent = new Agent({
|
|
53
|
+
agentSource,
|
|
54
|
+
executionTools,
|
|
55
|
+
isVerbose: true, // or false
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Prepare thread and content
|
|
59
|
+
const lastMessage = messages[messages.length - 1];
|
|
60
|
+
const previousMessages = messages.slice(0, -1);
|
|
61
|
+
|
|
62
|
+
const thread: ChatMessage[] = previousMessages.map((msg: TODO_any, index: number) => ({
|
|
63
|
+
id: `msg-${index}`, // Placeholder ID
|
|
64
|
+
from: msg.role === 'assistant' ? 'agent' : 'user', // Mapping standard OpenAI roles
|
|
65
|
+
content: msg.content,
|
|
66
|
+
isComplete: true,
|
|
67
|
+
date: new Date(), // We don't have the real date, using current
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
const prompt: Prompt = {
|
|
71
|
+
title,
|
|
72
|
+
content: lastMessage.content,
|
|
73
|
+
modelRequirements: {
|
|
74
|
+
modelVariant: 'CHAT',
|
|
75
|
+
// We could pass 'model' from body if we wanted to enforce it, but Agent usually has its own config
|
|
76
|
+
},
|
|
77
|
+
parameters: {},
|
|
78
|
+
thread,
|
|
79
|
+
} as Prompt;
|
|
80
|
+
// Note: Casting as Prompt because the type definition might require properties we don't strictly use or that are optional but TS complains
|
|
81
|
+
|
|
82
|
+
if (stream) {
|
|
83
|
+
const encoder = new TextEncoder();
|
|
84
|
+
const readableStream = new ReadableStream({
|
|
85
|
+
async start(controller) {
|
|
86
|
+
const runId = `chatcmpl-${Math.random().toString(36).substring(2, 15)}`;
|
|
87
|
+
const created = Math.floor(Date.now() / 1000);
|
|
88
|
+
|
|
89
|
+
let previousContent = '';
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await agent.callChatModelStream(prompt, (chunk: ChatPromptResult) => {
|
|
93
|
+
const fullContent = chunk.content;
|
|
94
|
+
const deltaContent = fullContent.substring(previousContent.length);
|
|
95
|
+
previousContent = fullContent;
|
|
96
|
+
|
|
97
|
+
if (deltaContent) {
|
|
98
|
+
const chunkData = {
|
|
99
|
+
id: runId,
|
|
100
|
+
object: 'chat.completion.chunk',
|
|
101
|
+
created,
|
|
102
|
+
model: model || 'promptbook-agent',
|
|
103
|
+
choices: [
|
|
104
|
+
{
|
|
105
|
+
index: 0,
|
|
106
|
+
delta: {
|
|
107
|
+
content: deltaContent,
|
|
108
|
+
},
|
|
109
|
+
finish_reason: null,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunkData)}\n\n`));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const doneChunkData = {
|
|
118
|
+
id: runId,
|
|
119
|
+
object: 'chat.completion.chunk',
|
|
120
|
+
created,
|
|
121
|
+
model: model || 'promptbook-agent',
|
|
122
|
+
choices: [
|
|
123
|
+
{
|
|
124
|
+
index: 0,
|
|
125
|
+
delta: {},
|
|
126
|
+
finish_reason: 'stop',
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(doneChunkData)}\n\n`));
|
|
131
|
+
controller.enqueue(encoder.encode('[DONE]'));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Error during streaming:', error);
|
|
134
|
+
// OpenAI stream doesn't usually send error JSON in stream, just closes or sends error text?
|
|
135
|
+
// But we should try to close gracefully or error.
|
|
136
|
+
controller.error(error);
|
|
137
|
+
}
|
|
138
|
+
controller.close();
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return new Response(readableStream, {
|
|
143
|
+
headers: {
|
|
144
|
+
'Content-Type': 'text/event-stream',
|
|
145
|
+
'Cache-Control': 'no-cache',
|
|
146
|
+
Connection: 'keep-alive',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
const result = await agent.callChatModel(prompt);
|
|
151
|
+
|
|
152
|
+
return NextResponse.json({
|
|
153
|
+
id: `chatcmpl-${Math.random().toString(36).substring(2, 15)}`,
|
|
154
|
+
object: 'chat.completion',
|
|
155
|
+
created: Math.floor(Date.now() / 1000),
|
|
156
|
+
model: model || 'promptbook-agent',
|
|
157
|
+
choices: [
|
|
158
|
+
{
|
|
159
|
+
index: 0,
|
|
160
|
+
message: {
|
|
161
|
+
role: 'assistant',
|
|
162
|
+
content: result.content,
|
|
163
|
+
},
|
|
164
|
+
finish_reason: 'stop',
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
usage: {
|
|
168
|
+
prompt_tokens: result.usage?.input?.tokensCount?.value || 0,
|
|
169
|
+
completion_tokens: result.usage?.output?.tokensCount?.value || 0,
|
|
170
|
+
total_tokens:
|
|
171
|
+
(result.usage?.input?.tokensCount?.value || 0) +
|
|
172
|
+
(result.usage?.output?.tokensCount?.value || 0),
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error(`Error in ${title} handler:`, error);
|
|
178
|
+
return NextResponse.json(
|
|
179
|
+
{ error: { message: (error as Error).message || 'Internal Server Error', type: 'server_error' } },
|
|
180
|
+
{ status: 500 },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|