@promptbook/cli 0.103.0-52 → 0.103.0-54

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.
Files changed (142) hide show
  1. package/apps/agents-server/README.md +1 -1
  2. package/apps/agents-server/config.ts +3 -3
  3. package/apps/agents-server/next.config.ts +1 -1
  4. package/apps/agents-server/public/sw.js +16 -0
  5. package/apps/agents-server/src/app/AddAgentButton.tsx +24 -4
  6. package/apps/agents-server/src/app/actions.ts +15 -13
  7. package/apps/agents-server/src/app/admin/api-tokens/ApiTokensClient.tsx +186 -0
  8. package/apps/agents-server/src/app/admin/api-tokens/page.tsx +13 -0
  9. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +541 -0
  10. package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +22 -0
  11. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +532 -0
  12. package/apps/agents-server/src/app/admin/chat-history/page.tsx +21 -0
  13. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +241 -27
  14. package/apps/agents-server/src/app/admin/models/page.tsx +22 -0
  15. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +131 -0
  16. package/apps/agents-server/src/app/admin/users/[userId]/page.tsx +21 -0
  17. package/apps/agents-server/src/app/admin/users/page.tsx +18 -0
  18. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +10 -2
  19. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatFeedbackButton.tsx +63 -0
  20. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatHistoryButton.tsx +63 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +41 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +74 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/ServiceWorkerRegister.tsx +24 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +19 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +67 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/api/openai/chat/completions/route.ts +176 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +3 -0
  28. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +177 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +3 -0
  30. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +53 -1
  31. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +11 -11
  32. package/apps/agents-server/src/app/agents/[agentName]/history/RestoreVersionButton.tsx +46 -0
  33. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +12 -0
  34. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +62 -0
  35. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +80 -0
  36. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +92 -0
  37. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +92 -0
  38. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +61 -0
  39. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +102 -0
  40. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +64 -24
  41. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +47 -0
  42. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +19 -0
  43. package/apps/agents-server/src/app/api/agents/route.ts +22 -13
  44. package/apps/agents-server/src/app/api/api-tokens/route.ts +76 -0
  45. package/apps/agents-server/src/app/api/auth/login/route.ts +6 -44
  46. package/apps/agents-server/src/app/api/chat-feedback/[id]/route.ts +38 -0
  47. package/apps/agents-server/src/app/api/chat-feedback/route.ts +157 -0
  48. package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +37 -0
  49. package/apps/agents-server/src/app/api/chat-history/route.ts +147 -0
  50. package/apps/agents-server/src/app/api/federated-agents/route.ts +17 -0
  51. package/apps/agents-server/src/app/api/upload/route.ts +9 -1
  52. package/apps/agents-server/src/app/docs/[docId]/page.tsx +63 -0
  53. package/apps/agents-server/src/app/docs/page.tsx +34 -0
  54. package/apps/agents-server/src/app/layout.tsx +29 -3
  55. package/apps/agents-server/src/app/manifest.ts +109 -0
  56. package/apps/agents-server/src/app/page.tsx +8 -45
  57. package/apps/agents-server/src/app/recycle-bin/RestoreAgentButton.tsx +40 -0
  58. package/apps/agents-server/src/app/recycle-bin/actions.ts +27 -0
  59. package/apps/agents-server/src/app/recycle-bin/page.tsx +58 -0
  60. package/apps/agents-server/src/app/restricted/page.tsx +33 -0
  61. package/apps/agents-server/src/app/test/og-image/README.md +1 -0
  62. package/apps/agents-server/src/app/test/og-image/opengraph-image.tsx +37 -0
  63. package/apps/agents-server/src/app/test/og-image/page.tsx +22 -0
  64. package/apps/agents-server/src/components/Footer/Footer.tsx +175 -0
  65. package/apps/agents-server/src/components/Header/Header.tsx +450 -79
  66. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +46 -14
  67. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +58 -0
  68. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  69. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +21 -0
  70. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +183 -0
  71. package/apps/agents-server/src/components/Homepage/ModelsSection.tsx +75 -0
  72. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +29 -3
  73. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +18 -17
  74. package/apps/agents-server/src/components/Portal/Portal.tsx +38 -0
  75. package/apps/agents-server/src/components/UsersList/UsersList.tsx +82 -131
  76. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +139 -0
  77. package/apps/agents-server/src/database/metadataDefaults.ts +38 -6
  78. package/apps/agents-server/src/database/migrations/2025-12-0010-llm-cache.sql +12 -0
  79. package/apps/agents-server/src/database/migrations/2025-12-0060-api-tokens.sql +13 -0
  80. package/apps/agents-server/src/database/schema.ts +51 -0
  81. package/apps/agents-server/src/middleware.ts +193 -92
  82. package/apps/agents-server/src/tools/$provideCdnForServer.ts +3 -7
  83. package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +10 -1
  84. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  85. package/apps/agents-server/src/utils/authenticateUser.ts +42 -0
  86. package/apps/agents-server/src/utils/cache/SupabaseCacheStorage.ts +55 -0
  87. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +63 -0
  88. package/apps/agents-server/src/utils/chatFeedbackAdmin.ts +96 -0
  89. package/apps/agents-server/src/utils/chatHistoryAdmin.ts +96 -0
  90. package/apps/agents-server/src/utils/getEffectiveFederatedServers.ts +22 -0
  91. package/apps/agents-server/src/utils/getFederatedAgents.ts +31 -8
  92. package/apps/agents-server/src/utils/getFederatedServersFromMetadata.ts +10 -0
  93. package/apps/agents-server/src/utils/getVisibleCommitmentDefinitions.ts +12 -0
  94. package/apps/agents-server/src/utils/isUserAdmin.ts +2 -2
  95. package/apps/agents-server/vercel.json +7 -0
  96. package/esm/index.es.js +279 -2
  97. package/esm/index.es.js.map +1 -1
  98. package/esm/typings/servers.d.ts +8 -1
  99. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  100. package/esm/typings/src/_packages/core.index.d.ts +6 -0
  101. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  102. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  103. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +7 -0
  104. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +4 -0
  105. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +12 -0
  106. package/esm/typings/src/book-components/icons/MicIcon.d.ts +8 -0
  107. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +17 -0
  108. package/esm/typings/src/commitments/ACTION/ACTION.d.ts +4 -0
  109. package/esm/typings/src/commitments/DELETE/DELETE.d.ts +4 -0
  110. package/esm/typings/src/commitments/FORMAT/FORMAT.d.ts +4 -0
  111. package/esm/typings/src/commitments/GOAL/GOAL.d.ts +4 -0
  112. package/esm/typings/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +4 -0
  113. package/esm/typings/src/commitments/MEMORY/MEMORY.d.ts +4 -0
  114. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +32 -0
  115. package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +4 -0
  116. package/esm/typings/src/commitments/MESSAGE/MESSAGE.d.ts +4 -0
  117. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +32 -0
  118. package/esm/typings/src/commitments/META/META.d.ts +4 -0
  119. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +4 -0
  120. package/esm/typings/src/commitments/META_IMAGE/META_IMAGE.d.ts +4 -0
  121. package/esm/typings/src/commitments/META_LINK/META_LINK.d.ts +4 -0
  122. package/esm/typings/src/commitments/MODEL/MODEL.d.ts +4 -0
  123. package/esm/typings/src/commitments/NOTE/NOTE.d.ts +4 -0
  124. package/esm/typings/src/commitments/PERSONA/PERSONA.d.ts +4 -0
  125. package/esm/typings/src/commitments/RULE/RULE.d.ts +4 -0
  126. package/esm/typings/src/commitments/SAMPLE/SAMPLE.d.ts +4 -0
  127. package/esm/typings/src/commitments/SCENARIO/SCENARIO.d.ts +4 -0
  128. package/esm/typings/src/commitments/STYLE/STYLE.d.ts +4 -0
  129. package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +5 -0
  130. package/esm/typings/src/commitments/_base/CommitmentDefinition.d.ts +5 -0
  131. package/esm/typings/src/commitments/_base/NotYetImplementedCommitmentDefinition.d.ts +4 -0
  132. package/esm/typings/src/commitments/index.d.ts +20 -1
  133. package/esm/typings/src/execution/LlmExecutionTools.d.ts +9 -0
  134. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +2 -1
  135. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +10 -1
  136. package/esm/typings/src/utils/normalization/normalizeMessageText.d.ts +9 -0
  137. package/esm/typings/src/utils/normalization/normalizeMessageText.test.d.ts +1 -0
  138. package/esm/typings/src/version.d.ts +1 -1
  139. package/package.json +2 -2
  140. package/umd/index.umd.js +279 -2
  141. package/umd/index.umd.js.map +1 -1
  142. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +0 -119
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { $clearAgentChatHistory } from '../../../utils/chatHistoryAdmin';
5
+
6
+ type ClearAgentChatHistoryButtonProps = {
7
+ /**
8
+ * Agent name for which the chat history should be cleared.
9
+ */
10
+ agentName: string;
11
+
12
+ /**
13
+ * Optional callback invoked after successful clearing.
14
+ */
15
+ onCleared?: () => void;
16
+ };
17
+
18
+ /**
19
+ * Admin-only button to clear chat history for a specific agent.
20
+ *
21
+ * This is intentionally small and self-contained so it can be reused
22
+ * from different admin-oriented surfaces without duplicating logic.
23
+ */
24
+ export function ClearAgentChatHistoryButton({ agentName, onCleared }: ClearAgentChatHistoryButtonProps) {
25
+ const [loading, setLoading] = useState(false);
26
+ const [error, setError] = useState<string | null>(null);
27
+
28
+ const handleClick = async () => {
29
+ if (!agentName) return;
30
+
31
+ const confirmed = window.confirm(
32
+ `Are you sure you want to permanently delete all chat history for agent "${agentName}"?`,
33
+ );
34
+ if (!confirmed) return;
35
+
36
+ try {
37
+ setLoading(true);
38
+ setError(null);
39
+ await $clearAgentChatHistory(agentName);
40
+ if (onCleared) {
41
+ onCleared();
42
+ }
43
+ } catch (err) {
44
+ setError(err instanceof Error ? err.message : 'Failed to clear chat history');
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <div className="flex flex-col gap-2">
52
+ <button
53
+ type="button"
54
+ onClick={handleClick}
55
+ disabled={loading}
56
+ className="inline-flex items-center justify-center whitespace-nowrap rounded-md border border-red-300 bg-white px-3 py-1.5 text-xs font-semibold text-red-700 shadow-sm hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-60"
57
+ >
58
+ {loading ? 'Clearing history…' : 'Clear chat history'}
59
+ </button>
60
+ {error && <div className="text-xs text-red-600">{error}</div>}
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { CopyIcon } from 'lucide-react';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ type CloneAgentButtonProps = {
7
+ agentName: string;
8
+ };
9
+
10
+ export function CloneAgentButton({ agentName }: CloneAgentButtonProps) {
11
+ const router = useRouter();
12
+
13
+ const handleClone = async () => {
14
+ if (!window.confirm(`Clone agent "${agentName}"?`)) return;
15
+
16
+ try {
17
+ const response = await fetch(`/api/agents/${encodeURIComponent(agentName)}/clone`, { method: 'POST' });
18
+
19
+ if (!response.ok) {
20
+ throw new Error('Failed to clone agent');
21
+ }
22
+
23
+ const newAgent = await response.json();
24
+ router.push(`/${newAgent.agentName}`);
25
+ router.refresh();
26
+ } catch (error) {
27
+ alert('Failed to clone agent');
28
+ console.error(error);
29
+ }
30
+ };
31
+
32
+ return (
33
+ <button
34
+ onClick={handleClone}
35
+ className="flex-1 inline-flex items-center justify-center whitespace-nowrap bg-white hover:bg-gray-100 text-gray-800 px-4 py-2 rounded shadow font-semibold transition border border-gray-200"
36
+ >
37
+ <CopyIcon className="ml-2 w-4 h-4 mr-2" />
38
+ Clone
39
+ </button>
40
+ );
41
+ }
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+
3
+ import { TODO_any } from '@promptbook-local/types';
4
+ import { ShoppingBagIcon } from 'lucide-react';
5
+ import { useCallback, useEffect, useState } from 'react';
6
+
7
+ type BeforeInstallPromptEvent = Event & {
8
+ prompt: () => Promise<void>;
9
+ userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
10
+ };
11
+
12
+ export function InstallPwaButton() {
13
+ const [installPromptEvent, setInstallPromptEvent] = useState<BeforeInstallPromptEvent | null>(null);
14
+ const [isInstalled, setIsInstalled] = useState(false);
15
+ const [hasPrompted, setHasPrompted] = useState(false);
16
+
17
+ useEffect(() => {
18
+ function handleBeforeInstallPrompt(e: Event) {
19
+ // Some browsers (Chrome) fire this event when PWA is installable
20
+ e.preventDefault();
21
+ setInstallPromptEvent(e as BeforeInstallPromptEvent);
22
+ }
23
+
24
+ function updateInstalledStatus() {
25
+ const mediaMatch = window.matchMedia('(display-mode: standalone)');
26
+ const standalone = mediaMatch.matches || (window.navigator as TODO_any).standalone === true;
27
+ setIsInstalled(standalone);
28
+ }
29
+
30
+ window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
31
+ updateInstalledStatus();
32
+ window.matchMedia('(display-mode: standalone)').addEventListener('change', updateInstalledStatus);
33
+
34
+ return () => {
35
+ window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
36
+ window.matchMedia('(display-mode: standalone)').removeEventListener('change', updateInstalledStatus);
37
+ };
38
+ }, []);
39
+
40
+ const onInstall = useCallback(async () => {
41
+ if (!installPromptEvent) return;
42
+ try {
43
+ installPromptEvent.prompt();
44
+ setHasPrompted(true);
45
+ const choice = await installPromptEvent.userChoice.catch(() => null);
46
+ if (choice?.outcome === 'accepted') {
47
+ setIsInstalled(true);
48
+ }
49
+ } finally {
50
+ // Clear stored event so button hides if dismissed
51
+ setInstallPromptEvent(null);
52
+ }
53
+ }, [installPromptEvent]);
54
+
55
+ if (isInstalled || (!installPromptEvent && hasPrompted)) return null;
56
+
57
+ return (
58
+ <button
59
+ type="button"
60
+ onClick={onInstall}
61
+ className="flex-1 inline-flex items-center justify-center whitespace-nowrap bg-white hover:bg-gray-100 text-gray-800 px-4 py-2 rounded shadow font-semibold transition border border-gray-200"
62
+ style={{
63
+ opacity: installPromptEvent ? 1 : 0.5,
64
+ cursor: installPromptEvent ? 'pointer' : 'wait',
65
+ }}
66
+ aria-label="Install App"
67
+ disabled={!installPromptEvent}
68
+ >
69
+ {/* Simple icon substitute: download arrow */}
70
+ <ShoppingBagIcon className="ml-2 w-4 h-4 mr-2" />
71
+ Install
72
+ </button>
73
+ );
74
+ }
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+
5
+ type ServiceWorkerRegisterProps = {
6
+ scope: string;
7
+ };
8
+
9
+ export function ServiceWorkerRegister({ scope }: ServiceWorkerRegisterProps) {
10
+ useEffect(() => {
11
+ if ('serviceWorker' in navigator) {
12
+ navigator.serviceWorker
13
+ .register('/sw.js', { scope })
14
+ .then((registration) => {
15
+ console.log('Service Worker registered with scope:', registration.scope);
16
+ })
17
+ .catch((error) => {
18
+ console.error('Service Worker registration failed:', error);
19
+ });
20
+ }
21
+ }, [scope]);
22
+
23
+ return null;
24
+ }
@@ -0,0 +1,19 @@
1
+ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
2
+ import { parseAgentSource } from '@promptbook-local/core';
3
+
4
+ export const AGENT_ACTIONS = ['Emails', 'Web chat', 'Read documents', 'Browser', 'WhatsApp', '<Coding/>'];
5
+
6
+ export async function getAgentName(params: Promise<{ agentName: string }>) {
7
+ const { agentName } = await params;
8
+ return decodeURIComponent(agentName);
9
+ }
10
+
11
+ export async function getAgentProfile(agentName: string) {
12
+ const collection = await $provideAgentCollectionForServer();
13
+ const agentSource = await collection.getAgentSource(agentName);
14
+ return parseAgentSource(agentSource);
15
+ }
16
+
17
+ /**
18
+ * TODO: Split to multiple files
19
+ */
@@ -0,0 +1,67 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ export const dynamic = 'force-dynamic';
4
+
5
+ export async function GET(
6
+ request: NextRequest,
7
+ { params }: { params: Promise<{ agentName: string }> }
8
+ ) {
9
+ try {
10
+ const { agentName } = await params;
11
+ // agentName is likely the federated server URL (e.g., "https://s6.ptbk.io")
12
+ // It comes decoded from the URL params if it was encoded in the request path
13
+
14
+ let serverUrl = agentName;
15
+
16
+ // If the serverUrl doesn't look like a URL, it might be just a hostname or something else
17
+ // But the requirement says we look for /agents/[federated-server]/api/agents
18
+ // The client will likely pass the full URL or hostname.
19
+ // We'll assume if it doesn't start with http, we might need to prepend it, or it's invalid.
20
+ // However, the current federated servers list contains full URLs.
21
+
22
+ // If it was somehow double encoded or something, we might need to handle it, but standard Next.js behavior is single decode.
23
+
24
+ if (!serverUrl.startsWith('http')) {
25
+ // Maybe it is just a hostname?
26
+ // Let's try to assume https if missing
27
+ if (serverUrl.includes('.')) {
28
+ serverUrl = `https://${serverUrl}`;
29
+ } else {
30
+ return NextResponse.json(
31
+ { error: 'Invalid federated server URL' },
32
+ { status: 400 }
33
+ );
34
+ }
35
+ }
36
+
37
+ // Normalize URL (remove trailing slash)
38
+ serverUrl = serverUrl.replace(/\/$/, '');
39
+
40
+ const response = await fetch(`${serverUrl}/api/agents`, {
41
+ // Forward relevant headers if necessary, or just basic fetch
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ // Add any other needed headers
45
+ },
46
+ next: { revalidate: 600 }, // Cache for 10 minutes
47
+ });
48
+
49
+ if (!response.ok) {
50
+ console.warn(`Proxy failed to fetch agents from ${serverUrl}: ${response.status} ${response.statusText}`);
51
+ return NextResponse.json(
52
+ { error: `Failed to fetch from ${serverUrl}` },
53
+ { status: response.status }
54
+ );
55
+ }
56
+
57
+ const data = await response.json();
58
+ return NextResponse.json(data);
59
+
60
+ } catch (error) {
61
+ console.error('Proxy error fetching federated agents:', error);
62
+ return NextResponse.json(
63
+ { error: 'Internal Server Error' },
64
+ { status: 500 }
65
+ );
66
+ }
67
+ }
@@ -0,0 +1,176 @@
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 POST(
8
+ request: NextRequest,
9
+ { params }: { params: Promise<{ agentName: string }> },
10
+ ) {
11
+ const { agentName } = await params;
12
+
13
+ // Note: Authentication is handled by middleware
14
+ // If we are here, the request is either authenticated or public access is allowed (but middleware blocks it if not)
15
+
16
+ try {
17
+ const body = await request.json();
18
+ const { messages, stream, model } = body;
19
+
20
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
21
+ return NextResponse.json(
22
+ { error: { message: 'Messages array is required and cannot be empty.', type: 'invalid_request_error' } },
23
+ { status: 400 },
24
+ );
25
+ }
26
+
27
+ const collection = await $provideAgentCollectionForServer();
28
+ let agentSource;
29
+ try {
30
+ agentSource = await collection.getAgentSource(agentName);
31
+ } catch (error) {
32
+ return NextResponse.json(
33
+ { error: { message: `Agent '${agentName}' not found.`, type: 'invalid_request_error' } },
34
+ { status: 404 },
35
+ );
36
+ }
37
+
38
+ if (!agentSource) {
39
+ return NextResponse.json(
40
+ { error: { message: `Agent '${agentName}' not found.`, type: 'invalid_request_error' } },
41
+ { status: 404 },
42
+ );
43
+ }
44
+
45
+ const executionTools = await $provideExecutionToolsForServer();
46
+ const agent = new Agent({
47
+ agentSource,
48
+ executionTools,
49
+ isVerbose: true, // or false
50
+ });
51
+
52
+ // Prepare thread and content
53
+ const lastMessage = messages[messages.length - 1];
54
+ const previousMessages = messages.slice(0, -1);
55
+
56
+ const thread: ChatMessage[] = previousMessages.map((msg: TODO_any, index: number) => ({
57
+ id: `msg-${index}`, // Placeholder ID
58
+ from: msg.role === 'assistant' ? 'agent' : 'user', // Mapping standard OpenAI roles
59
+ content: msg.content,
60
+ isComplete: true,
61
+ date: new Date(), // We don't have the real date, using current
62
+ }));
63
+
64
+ const prompt: Prompt = {
65
+ title: 'OpenAI API Chat Completion',
66
+ content: lastMessage.content,
67
+ modelRequirements: {
68
+ modelVariant: 'CHAT',
69
+ // We could pass 'model' from body if we wanted to enforce it, but Agent usually has its own config
70
+ },
71
+ parameters: {},
72
+ thread,
73
+ } as Prompt;
74
+ // Note: Casting as Prompt because the type definition might require properties we don't strictly use or that are optional but TS complains
75
+
76
+ if (stream) {
77
+ const encoder = new TextEncoder();
78
+ const readableStream = new ReadableStream({
79
+ async start(controller) {
80
+ const runId = `chatcmpl-${Math.random().toString(36).substring(2, 15)}`;
81
+ const created = Math.floor(Date.now() / 1000);
82
+
83
+ let previousContent = '';
84
+
85
+ try {
86
+ await agent.callChatModelStream(prompt, (chunk: ChatPromptResult) => {
87
+ const fullContent = chunk.content;
88
+ const deltaContent = fullContent.substring(previousContent.length);
89
+ previousContent = fullContent;
90
+
91
+ if (deltaContent) {
92
+ const chunkData = {
93
+ id: runId,
94
+ object: 'chat.completion.chunk',
95
+ created,
96
+ model: model || 'promptbook-agent',
97
+ choices: [
98
+ {
99
+ index: 0,
100
+ delta: {
101
+ content: deltaContent,
102
+ },
103
+ finish_reason: null,
104
+ },
105
+ ],
106
+ };
107
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunkData)}\n\n`));
108
+ }
109
+ });
110
+
111
+ const doneChunkData = {
112
+ id: runId,
113
+ object: 'chat.completion.chunk',
114
+ created,
115
+ model: model || 'promptbook-agent',
116
+ choices: [
117
+ {
118
+ index: 0,
119
+ delta: {},
120
+ finish_reason: 'stop',
121
+ },
122
+ ],
123
+ };
124
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(doneChunkData)}\n\n`));
125
+ controller.enqueue(encoder.encode('[DONE]'));
126
+ } catch (error) {
127
+ console.error('Error during streaming:', error);
128
+ // OpenAI stream doesn't usually send error JSON in stream, just closes or sends error text?
129
+ // But we should try to close gracefully or error.
130
+ controller.error(error);
131
+ }
132
+ controller.close();
133
+ },
134
+ });
135
+
136
+ return new Response(readableStream, {
137
+ headers: {
138
+ 'Content-Type': 'text/event-stream',
139
+ 'Cache-Control': 'no-cache',
140
+ Connection: 'keep-alive',
141
+ },
142
+ });
143
+ } else {
144
+ const result = await agent.callChatModel(prompt);
145
+
146
+ return NextResponse.json({
147
+ id: `chatcmpl-${Math.random().toString(36).substring(2, 15)}`,
148
+ object: 'chat.completion',
149
+ created: Math.floor(Date.now() / 1000),
150
+ model: model || 'promptbook-agent',
151
+ choices: [
152
+ {
153
+ index: 0,
154
+ message: {
155
+ role: 'assistant',
156
+ content: result.content,
157
+ },
158
+ finish_reason: 'stop',
159
+ },
160
+ ],
161
+ usage: {
162
+ prompt_tokens: result.usage?.input?.tokensCount?.value || 0,
163
+ completion_tokens: result.usage?.output?.tokensCount?.value || 0,
164
+ total_tokens: (result.usage?.input?.tokensCount?.value || 0) + (result.usage?.output?.tokensCount?.value || 0),
165
+ },
166
+ });
167
+ }
168
+
169
+ } catch (error) {
170
+ console.error('Error in OpenAI API handler:', error);
171
+ return NextResponse.json(
172
+ { error: { message: (error as Error).message || 'Internal Server Error', type: 'server_error' } },
173
+ { status: 500 },
174
+ );
175
+ }
176
+ }
@@ -1,3 +1,4 @@
1
+ import { getMetadata } from '@/src/database/getMetadata';
1
2
  import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
2
3
  import { computeAgentHash, parseAgentSource } from '@promptbook-local/core';
3
4
  import { serializeError } from '@promptbook-local/utils';
@@ -26,6 +27,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
26
27
  const agentSource = await collection.getAgentSource(agentName);
27
28
  const agentProfile = parseAgentSource(agentSource);
28
29
  const agentHash = computeAgentHash(agentSource);
30
+ const isVoiceCallingEnabled = (await getMetadata('IS_VOICE_CALLING_ENABLED')) === 'true';
29
31
 
30
32
  return new Response(
31
33
  JSON.stringify(
@@ -33,6 +35,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
33
35
  ...agentProfile,
34
36
  agentHash,
35
37
  parameters: [], // <- TODO: [😰] Implement parameters
38
+ isVoiceCallingEnabled, // [✨✷] Add voice calling status
36
39
  },
37
40
  // <- TODO: [🐱‍🚀] Rename `serializeError` to `errorToJson`
38
41
  null,
@@ -0,0 +1,177 @@
1
+ import { $getTableName } from '@/src/database/$getTableName';
2
+ import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForServer';
3
+ import { getMetadata } from '@/src/database/getMetadata';
4
+ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
5
+ import { $provideOpenAiAssistantExecutionToolsForServer } from '@/src/tools/$provideOpenAiAssistantExecutionToolsForServer';
6
+ import { Agent, computeAgentHash, PROMPTBOOK_ENGINE_VERSION } from '@promptbook-local/core';
7
+ import { computeHash, serializeError } from '@promptbook-local/utils';
8
+ import { assertsError } from '../../../../../../../../src/errors/assertsError';
9
+
10
+ export const maxDuration = 300;
11
+
12
+ export async function OPTIONS(request: Request) {
13
+ return new Response(null, {
14
+ status: 200,
15
+ headers: {
16
+ 'Access-Control-Allow-Origin': '*',
17
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
18
+ 'Access-Control-Allow-Headers': 'Content-Type',
19
+ },
20
+ });
21
+ }
22
+
23
+ export async function POST(request: Request, { params }: { params: Promise<{ agentName: string }> }) {
24
+ // Check if voice calling is enabled
25
+ const isVoiceCallingEnabled = (await getMetadata('IS_VOICE_CALLING_ENABLED')) === 'true';
26
+ if (!isVoiceCallingEnabled) {
27
+ return new Response(JSON.stringify({ error: 'Voice calling is disabled on this server' }), {
28
+ status: 403,
29
+ headers: { 'Content-Type': 'application/json' },
30
+ });
31
+ }
32
+
33
+ let { agentName } = await params;
34
+ agentName = decodeURIComponent(agentName);
35
+
36
+ // Note: Parse FormData for audio file
37
+ const formData = await request.formData();
38
+ const audioFile = formData.get('audio') as File | null;
39
+ const threadString = formData.get('thread') as string | null;
40
+ const thread = threadString ? JSON.parse(threadString) : undefined;
41
+ const messageContext = formData.get('message') as string | null; // Optional text context or previous message?
42
+
43
+ if (!audioFile) {
44
+ return new Response(JSON.stringify({ error: 'No audio file provided' }), {
45
+ status: 400,
46
+ headers: { 'Content-Type': 'application/json' },
47
+ });
48
+ }
49
+
50
+ try {
51
+ const collection = await $provideAgentCollectionForServer();
52
+ const openAiAssistantExecutionTools = await $provideOpenAiAssistantExecutionToolsForServer();
53
+ const agentSource = await collection.getAgentSource(agentName);
54
+ const agent = new Agent({
55
+ isVerbose: true,
56
+ executionTools: {
57
+ llm: openAiAssistantExecutionTools,
58
+ },
59
+ agentSource,
60
+ });
61
+
62
+ // 1. Transcribe Audio (STT)
63
+ const client = await openAiAssistantExecutionTools.getClient();
64
+ const transcription = await client.audio.transcriptions.create({
65
+ file: audioFile,
66
+ model: 'whisper-1',
67
+ });
68
+ const message = transcription.text;
69
+
70
+ // --- Common Chat Logic Start (TODO: Extract) ---
71
+
72
+ const agentHash = computeAgentHash(agentSource);
73
+ const userAgent = request.headers.get('user-agent');
74
+ const ip =
75
+ request.headers.get('x-forwarded-for') ||
76
+ request.headers.get('x-real-ip') ||
77
+ request.headers.get('x-client-ip');
78
+ const language = request.headers.get('accept-language');
79
+ const platform = userAgent ? userAgent.match(/\(([^)]+)\)/)?.[1] : undefined;
80
+
81
+ // Identify and Record User Message
82
+ const userMessageContent = {
83
+ role: 'USER',
84
+ content: message,
85
+ isVoiceCall: true, // Mark as voice call
86
+ };
87
+ const supabase = $provideSupabaseForServer();
88
+ await supabase.from(await $getTableName('ChatHistory')).insert({
89
+ createdAt: new Date().toISOString(),
90
+ messageHash: computeHash(userMessageContent),
91
+ previousMessageHash: null,
92
+ agentName,
93
+ agentHash,
94
+ message: userMessageContent,
95
+ promptbookEngineVersion: PROMPTBOOK_ENGINE_VERSION,
96
+ url: request.url,
97
+ ip,
98
+ userAgent,
99
+ language,
100
+ platform,
101
+ });
102
+
103
+ // Call Agent
104
+ const response = await agent.callChatModel({
105
+ title: `Voice Chat with agent ${agentName}`,
106
+ parameters: {},
107
+ modelRequirements: {
108
+ modelVariant: 'CHAT',
109
+ },
110
+ content: message,
111
+ thread,
112
+ });
113
+
114
+ const agentMessageContent = {
115
+ role: 'MODEL',
116
+ content: response.content,
117
+ isVoiceCall: true,
118
+ };
119
+
120
+ // Record Agent Message
121
+ await supabase.from(await $getTableName('ChatHistory')).insert({
122
+ createdAt: new Date().toISOString(),
123
+ messageHash: computeHash(agentMessageContent),
124
+ previousMessageHash: computeHash(userMessageContent),
125
+ agentName,
126
+ agentHash,
127
+ message: agentMessageContent,
128
+ promptbookEngineVersion: PROMPTBOOK_ENGINE_VERSION,
129
+ url: request.url,
130
+ ip,
131
+ userAgent,
132
+ language,
133
+ platform,
134
+ });
135
+
136
+ // Learning
137
+ const newAgentSource = agent.agentSource.value;
138
+ if (newAgentSource !== agentSource) {
139
+ await collection.updateAgentSource(agentName, newAgentSource);
140
+ }
141
+
142
+ // --- Common Chat Logic End ---
143
+
144
+ // 2. Synthesize Audio (TTS)
145
+ const mp3 = await client.audio.speech.create({
146
+ model: 'tts-1',
147
+ voice: 'alloy',
148
+ input: response.content,
149
+ });
150
+
151
+ const buffer = Buffer.from(await mp3.arrayBuffer());
152
+ const base64Audio = buffer.toString('base64');
153
+
154
+ return new Response(
155
+ JSON.stringify({
156
+ userMessage: message,
157
+ agentMessage: response.content,
158
+ audio: base64Audio,
159
+ audioFormat: 'mp3',
160
+ }),
161
+ {
162
+ status: 200,
163
+ headers: {
164
+ 'Content-Type': 'application/json',
165
+ 'Access-Control-Allow-Origin': '*',
166
+ },
167
+ },
168
+ );
169
+ } catch (error) {
170
+ assertsError(error);
171
+ console.error(error);
172
+ return new Response(JSON.stringify(serializeError(error), null, 4), {
173
+ status: 400,
174
+ headers: { 'Content-Type': 'application/json' },
175
+ });
176
+ }
177
+ }
@@ -5,8 +5,11 @@ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentColle
5
5
  import { isUserAdmin } from '@/src/utils/isUserAdmin';
6
6
  import { headers } from 'next/headers';
7
7
  import { $sideEffect } from '../../../../../../../src/utils/organization/$sideEffect';
8
+ import { generateAgentMetadata } from '../generateAgentMetadata';
8
9
  import { BookEditorWrapper } from './BookEditorWrapper';
9
10
 
11
+ export const generateMetadata = generateAgentMetadata;
12
+
10
13
  export default async function AgentBookPage({ params }: { params: Promise<{ agentName: string }> }) {
11
14
  $sideEffect(headers());
12
15