@promptbook/cli 0.103.0-53 → 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.
Files changed (76) hide show
  1. package/apps/agents-server/config.ts +0 -2
  2. package/apps/agents-server/src/app/admin/api-tokens/ApiTokensClient.tsx +186 -0
  3. package/apps/agents-server/src/app/admin/api-tokens/page.tsx +13 -0
  4. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +79 -6
  5. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +171 -69
  6. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +10 -2
  7. package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +203 -0
  8. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +3 -1
  9. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -1
  10. package/apps/agents-server/src/app/agents/[agentName]/api/openai/chat/completions/route.ts +10 -0
  11. package/apps/agents-server/src/app/agents/[agentName]/api/openrouter/chat/completions/route.ts +10 -0
  12. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +218 -0
  13. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +24 -3
  14. package/apps/agents-server/src/app/api/api-tokens/route.ts +76 -0
  15. package/apps/agents-server/src/app/api/auth/change-password/route.ts +75 -0
  16. package/apps/agents-server/src/app/api/chat-feedback/export/route.ts +55 -0
  17. package/apps/agents-server/src/app/api/chat-history/export/route.ts +55 -0
  18. package/apps/agents-server/src/app/docs/[docId]/page.tsx +1 -0
  19. package/apps/agents-server/src/app/docs/page.tsx +1 -0
  20. package/apps/agents-server/src/components/ChangePasswordDialog/ChangePasswordDialog.tsx +41 -0
  21. package/apps/agents-server/src/components/ChangePasswordForm/ChangePasswordForm.tsx +159 -0
  22. package/apps/agents-server/src/components/Header/Header.tsx +94 -33
  23. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +2 -1
  24. package/apps/agents-server/src/database/migrations/2025-12-0010-llm-cache.sql +12 -0
  25. package/apps/agents-server/src/database/migrations/2025-12-0060-api-tokens.sql +13 -0
  26. package/apps/agents-server/src/database/schema.ts +51 -0
  27. package/apps/agents-server/src/middleware.ts +50 -2
  28. package/apps/agents-server/src/tools/$provideCdnForServer.ts +3 -7
  29. package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +10 -1
  30. package/apps/agents-server/src/utils/cache/SupabaseCacheStorage.ts +55 -0
  31. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +63 -0
  32. package/apps/agents-server/src/utils/convertToCsv.ts +31 -0
  33. package/apps/agents-server/src/utils/handleChatCompletion.ts +183 -0
  34. package/apps/agents-server/src/utils/resolveInheritedAgentSource.ts +93 -0
  35. package/esm/index.es.js +846 -131
  36. package/esm/index.es.js.map +1 -1
  37. package/esm/typings/src/_packages/core.index.d.ts +8 -6
  38. package/esm/typings/src/_packages/types.index.d.ts +1 -1
  39. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +4 -0
  40. package/esm/typings/src/commitments/ACTION/ACTION.d.ts +4 -0
  41. package/esm/typings/src/commitments/CLOSED/CLOSED.d.ts +35 -0
  42. package/esm/typings/src/commitments/COMPONENT/COMPONENT.d.ts +28 -0
  43. package/esm/typings/src/commitments/DELETE/DELETE.d.ts +4 -0
  44. package/esm/typings/src/commitments/FORMAT/FORMAT.d.ts +4 -0
  45. package/esm/typings/src/commitments/FROM/FROM.d.ts +34 -0
  46. package/esm/typings/src/commitments/GOAL/GOAL.d.ts +4 -0
  47. package/esm/typings/src/commitments/IMPORTANT/IMPORTANT.d.ts +26 -0
  48. package/esm/typings/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +4 -0
  49. package/esm/typings/src/commitments/LANGUAGE/LANGUAGE.d.ts +35 -0
  50. package/esm/typings/src/commitments/MEMORY/MEMORY.d.ts +4 -0
  51. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +4 -0
  52. package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +4 -0
  53. package/esm/typings/src/commitments/MESSAGE/MESSAGE.d.ts +4 -0
  54. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +4 -0
  55. package/esm/typings/src/commitments/META/META.d.ts +4 -0
  56. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +4 -0
  57. package/esm/typings/src/commitments/META_IMAGE/META_IMAGE.d.ts +4 -0
  58. package/esm/typings/src/commitments/META_LINK/META_LINK.d.ts +4 -0
  59. package/esm/typings/src/commitments/MODEL/MODEL.d.ts +4 -0
  60. package/esm/typings/src/commitments/NOTE/NOTE.d.ts +4 -0
  61. package/esm/typings/src/commitments/OPEN/OPEN.d.ts +35 -0
  62. package/esm/typings/src/commitments/PERSONA/PERSONA.d.ts +4 -0
  63. package/esm/typings/src/commitments/RULE/RULE.d.ts +4 -0
  64. package/esm/typings/src/commitments/SAMPLE/SAMPLE.d.ts +4 -0
  65. package/esm/typings/src/commitments/SCENARIO/SCENARIO.d.ts +4 -0
  66. package/esm/typings/src/commitments/STYLE/STYLE.d.ts +4 -0
  67. package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +5 -0
  68. package/esm/typings/src/commitments/_base/CommitmentDefinition.d.ts +5 -0
  69. package/esm/typings/src/commitments/_base/NotYetImplementedCommitmentDefinition.d.ts +4 -0
  70. package/esm/typings/src/commitments/index.d.ts +1 -82
  71. package/esm/typings/src/commitments/registry.d.ts +68 -0
  72. package/esm/typings/src/version.d.ts +1 -1
  73. package/package.json +3 -3
  74. package/umd/index.umd.js +846 -131
  75. package/umd/index.umd.js.map +1 -1
  76. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +0 -119
@@ -242,6 +242,57 @@ export type AgentsServerDatabase = {
242
242
  };
243
243
  Relationships: [];
244
244
  };
245
+ LlmCache: {
246
+ Row: {
247
+ id: number;
248
+ createdAt: string;
249
+ updatedAt: string;
250
+ hash: string;
251
+ value: Json;
252
+ };
253
+ Insert: {
254
+ id?: number;
255
+ createdAt?: string;
256
+ updatedAt?: string;
257
+ hash: string;
258
+ value: Json;
259
+ };
260
+ Update: {
261
+ id?: number;
262
+ createdAt?: string;
263
+ updatedAt?: string;
264
+ hash?: string;
265
+ value?: Json;
266
+ };
267
+ Relationships: [];
268
+ };
269
+ ApiTokens: {
270
+ Row: {
271
+ id: number;
272
+ createdAt: string;
273
+ updatedAt: string;
274
+ token: string;
275
+ note: string | null;
276
+ isRevoked: boolean;
277
+ };
278
+ Insert: {
279
+ id?: number;
280
+ createdAt?: string;
281
+ updatedAt?: string;
282
+ token: string;
283
+ note?: string | null;
284
+ isRevoked?: boolean;
285
+ };
286
+ Update: {
287
+ id?: number;
288
+ createdAt?: string;
289
+ updatedAt?: string;
290
+ token?: string;
291
+ note?: string | null;
292
+ isRevoked?: boolean;
293
+ };
294
+ Relationships: [];
295
+ };
245
296
  };
246
297
  Views: Record<string, never>;
247
298
  Functions: Record<string, never>;
@@ -75,9 +75,57 @@ export async function middleware(req: NextRequest) {
75
75
  const allowedIps =
76
76
  allowedIpsMetadata !== null && allowedIpsMetadata !== undefined ? allowedIpsMetadata : allowedIpsEnv;
77
77
 
78
+ let isValidToken = false;
79
+ const authHeader = req.headers.get('authorization');
80
+
81
+ if (authHeader && authHeader.startsWith('Bearer ')) {
82
+ const token = authHeader.split(' ')[1];
83
+
84
+ if (token.startsWith('ptbk_')) {
85
+ const host = req.headers.get('host');
86
+ let tablePrefix = SUPABASE_TABLE_PREFIX;
87
+
88
+ if (host && SERVERS && SERVERS.length > 0) {
89
+ if (SERVERS.some((server) => server === host)) {
90
+ let serverName = host;
91
+ serverName = serverName.replace(/\.ptbk\.io$/, '');
92
+ serverName = normalizeTo_PascalCase(serverName);
93
+ tablePrefix = `server_${serverName}_`;
94
+ }
95
+ }
96
+
97
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
98
+ const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
99
+
100
+ if (supabaseUrl && supabaseKey) {
101
+ try {
102
+ const supabase = createClient(supabaseUrl, supabaseKey, {
103
+ auth: {
104
+ persistSession: false,
105
+ autoRefreshToken: false,
106
+ },
107
+ });
108
+
109
+ const { data } = await supabase
110
+ .from(`${tablePrefix}ApiTokens`)
111
+ .select('id')
112
+ .eq('token', token)
113
+ .eq('isRevoked', false)
114
+ .single();
115
+
116
+ if (data) {
117
+ isValidToken = true;
118
+ }
119
+ } catch (error) {
120
+ console.error('Error validating token in middleware:', error);
121
+ }
122
+ }
123
+ }
124
+ }
125
+
78
126
  const isIpAllowedResult = isIpAllowed(ip, allowedIps);
79
127
  const isLoggedIn = req.cookies.has('sessionToken');
80
- const isAccessRestricted = !isIpAllowedResult && !isLoggedIn;
128
+ const isAccessRestricted = !isIpAllowedResult && !isLoggedIn && !isValidToken;
81
129
 
82
130
  // Handle OPTIONS (preflight) requests globally
83
131
  if (req.method === 'OPTIONS') {
@@ -86,7 +134,7 @@ export async function middleware(req: NextRequest) {
86
134
  headers: {
87
135
  'Access-Control-Allow-Origin': '*',
88
136
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
89
- 'Access-Control-Allow-Headers': 'Content-Type',
137
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
90
138
  },
91
139
  });
92
140
  }
@@ -1,4 +1,4 @@
1
- import { DigitalOceanSpaces } from '../utils/cdn/classes/DigitalOceanSpaces';
1
+ import { VercelBlobStorage } from '../utils/cdn/classes/VercelBlobStorage';
2
2
  import { IIFilesStorageWithCdn } from '../utils/cdn/interfaces/IFilesStorage';
3
3
 
4
4
  /**
@@ -13,14 +13,10 @@ let cdn: IIFilesStorageWithCdn | null = null;
13
13
  */
14
14
  export function $provideCdnForServer(): IIFilesStorageWithCdn {
15
15
  if (!cdn) {
16
- cdn = new DigitalOceanSpaces({
17
- bucket: process.env.CDN_BUCKET!,
16
+ cdn = new VercelBlobStorage({
17
+ token: process.env.BLOB_READ_WRITE_TOKEN!,
18
18
  pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
19
- endpoint: process.env.CDN_ENDPOINT!,
20
- accessKeyId: process.env.CDN_ACCESS_KEY_ID!,
21
- secretAccessKey: process.env.CDN_SECRET_ACCESS_KEY!,
22
19
  cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
23
- gzip: true,
24
20
  });
25
21
  }
26
22
 
@@ -1,6 +1,7 @@
1
1
  'use server';
2
2
 
3
3
  import {
4
+ cacheLlmTools,
4
5
  _AnthropicClaudeMetadataRegistration,
5
6
  _AzureOpenAiMetadataRegistration,
6
7
  _BoilerplateScraperMetadataRegistration,
@@ -26,6 +27,7 @@ import { $provideFilesystemForNode } from '../../../../src/scrapers/_common/regi
26
27
  import { $provideScrapersForNode } from '../../../../src/scrapers/_common/register/$provideScrapersForNode';
27
28
  import { $provideScriptingForNode } from '../../../../src/scrapers/_common/register/$provideScriptingForNode';
28
29
  import { $sideEffect } from '../../../../src/utils/organization/$sideEffect';
30
+ import { SupabaseCacheStorage } from '../utils/cache/SupabaseCacheStorage';
29
31
 
30
32
  $sideEffect(
31
33
  _AnthropicClaudeMetadataRegistration,
@@ -90,10 +92,17 @@ export async function $provideExecutionToolsForServer(): Promise<ExecutionTools>
90
92
  isCacheReloaded,
91
93
  }; /* <- TODO: ` satisfies PrepareAndScrapeOptions` */
92
94
  const fs = await $provideFilesystemForNode(prepareAndScrapeOptions);
93
- const { /* [0] strategy,*/ llm } = await $provideLlmToolsForCli({
95
+ const { /* [0] strategy,*/ llm: llmUncached } = await $provideLlmToolsForCli({
94
96
  cliOptions,
95
97
  ...prepareAndScrapeOptions,
96
98
  });
99
+
100
+ const llm = cacheLlmTools(llmUncached, {
101
+ storage: new SupabaseCacheStorage(),
102
+ isVerbose,
103
+ isCacheReloaded,
104
+ });
105
+
97
106
  const executables = await $provideExecutablesForNode(prepareAndScrapeOptions);
98
107
 
99
108
  executionTools = {
@@ -0,0 +1,55 @@
1
+ import { TODO_any } from '@promptbook-local/types';
2
+ import { $getTableName } from '../../database/$getTableName';
3
+ import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
4
+ import { Json } from '../../database/schema';
5
+
6
+ /**
7
+ * Storage for LLM cache using Supabase
8
+ */
9
+ export class SupabaseCacheStorage {
10
+ // implements PromptbookStorage<TODO_any>
11
+
12
+ /**
13
+ * Returns the current value associated with the given key, or null if the given key does not exist in the list associated with the object.
14
+ */
15
+ public async getItem(key: string): Promise<TODO_any | null> {
16
+ const supabase = $provideSupabaseForServer();
17
+ const tableName = await $getTableName('LlmCache');
18
+
19
+ const { data } = await supabase.from(tableName).select('value').eq('hash', key).maybeSingle();
20
+
21
+ if (!data) {
22
+ return null;
23
+ }
24
+
25
+ return data.value;
26
+ }
27
+
28
+ /**
29
+ * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
30
+ */
31
+ public async setItem(key: string, value: TODO_any): Promise<void> {
32
+ const supabase = $provideSupabaseForServer();
33
+ const tableName = await $getTableName('LlmCache');
34
+
35
+ await supabase.from(tableName).upsert(
36
+ {
37
+ hash: key,
38
+ value: value as Json,
39
+ },
40
+ {
41
+ onConflict: 'hash',
42
+ },
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Removes the key/value pair with the given key from the list associated with the object, if a key/value pair with the given key exists
48
+ */
49
+ public async removeItem(key: string): Promise<void> {
50
+ const supabase = $provideSupabaseForServer();
51
+ const tableName = await $getTableName('LlmCache');
52
+
53
+ await supabase.from(tableName).delete().eq('hash', key);
54
+ }
55
+ }
@@ -0,0 +1,63 @@
1
+ import { del, put } from '@vercel/blob';
2
+ import { validateMimeType } from '../../validators/validateMimeType';
3
+ import type { IFile, IIFilesStorageWithCdn } from '../interfaces/IFilesStorage';
4
+
5
+ type IVercelBlobStorageConfig = {
6
+ readonly token: string;
7
+ readonly cdnPublicUrl: URL;
8
+ readonly pathPrefix?: string;
9
+ // Note: Vercel Blob automatically handles compression/serving
10
+ };
11
+
12
+ export class VercelBlobStorage implements IIFilesStorageWithCdn {
13
+ public get cdnPublicUrl() {
14
+ return this.config.cdnPublicUrl;
15
+ }
16
+
17
+ public constructor(private readonly config: IVercelBlobStorageConfig) {}
18
+
19
+ public getItemUrl(key: string): URL {
20
+ const path = this.config.pathPrefix ? `${this.config.pathPrefix}/${key}` : key;
21
+ return new URL(path, this.cdnPublicUrl);
22
+ }
23
+
24
+ public async getItem(key: string): Promise<IFile | null> {
25
+ const url = this.getItemUrl(key);
26
+
27
+ const response = await fetch(url);
28
+
29
+ if (response.status === 404) {
30
+ return null;
31
+ }
32
+
33
+ if (!response.ok) {
34
+ throw new Error(`Failed to fetch blob from ${url}: ${response.statusText}`);
35
+ }
36
+
37
+ const arrayBuffer = await response.arrayBuffer();
38
+ const buffer = Buffer.from(arrayBuffer);
39
+ const contentType = response.headers.get('content-type') || 'application/octet-stream';
40
+
41
+ return {
42
+ type: validateMimeType(contentType),
43
+ data: buffer,
44
+ };
45
+ }
46
+
47
+ public async removeItem(key: string): Promise<void> {
48
+ const url = this.getItemUrl(key).toString();
49
+ await del(url, { token: this.config.token });
50
+ }
51
+
52
+ public async setItem(key: string, file: IFile): Promise<void> {
53
+ const path = this.config.pathPrefix ? `${this.config.pathPrefix}/${key}` : key;
54
+
55
+ await put(path, file.data, {
56
+ access: 'public',
57
+ addRandomSuffix: false,
58
+ contentType: file.type,
59
+ token: this.config.token,
60
+ // Note: We rely on Vercel Blob for compression
61
+ });
62
+ }
63
+ }
@@ -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
+ }
@@ -0,0 +1,93 @@
1
+ import { createAgentModelRequirements } from '@promptbook-local/core';
2
+ import type { AgentCollection } from '@promptbook-local/types';
3
+ type string_book = string & { readonly __type: 'book' };
4
+
5
+ /**
6
+ * Resolves agent source with inheritance (FROM commitment)
7
+ *
8
+ * It recursively fetches the parent agent source and merges it with the current source.
9
+ *
10
+ * @param agentSource The initial agent source
11
+ * @param collection Optional agent collection to resolve local agents efficiently
12
+ * @returns The resolved agent source with inheritance applied
13
+ */
14
+ export async function resolveInheritedAgentSource(
15
+ agentSource: string_book,
16
+ collection?: AgentCollection,
17
+ ): Promise<string_book> {
18
+ // Check if the source has FROM commitment
19
+ // We use createAgentModelRequirements to parse commitments
20
+ // Note: We don't provide tools/models here as we only care about parsing commitments
21
+ const requirements = await createAgentModelRequirements(agentSource);
22
+
23
+ if (!requirements.parentAgentUrl) {
24
+ return agentSource;
25
+ }
26
+
27
+ const parentUrl = requirements.parentAgentUrl;
28
+ let parentSource: string_book;
29
+
30
+ try {
31
+ // 1. Try to resolve locally using collection if possible
32
+ // This is an optimization for internal agents
33
+ // We assume the URL might be relative or contain the agent name, or we just check if it's a full URL
34
+ // If it's a full URL, we need to check if it matches our server, but without knowing our server URL it's hard.
35
+ // So we might need to parse the URL to extract agent name if it matches expected pattern.
36
+ // For now, let's rely on fetch for external and check collection if it looks like a local reference (though FROM expects URL)
37
+
38
+ // If the URL is valid, we try to fetch it
39
+ // TODO: Handle authentication/tokens for private agents if needed
40
+ const response = await fetch(parentUrl);
41
+
42
+ if (!response.ok) {
43
+ throw new Error(`Failed to fetch parent agent from ${parentUrl}: ${response.status} ${response.statusText}`);
44
+ }
45
+
46
+ // We assume the response is the agent source text
47
+ // TODO: Handle content negotiation or JSON responses if the server returns JSON
48
+ const contentType = response.headers.get('content-type');
49
+ if (contentType && contentType.includes('application/json')) {
50
+ const data = await response.json();
51
+ // Assume some structure or that the API returns source in a property
52
+ // For Agents Server API modelRequirements/route.ts returns AgentModelRequirements, not source.
53
+ // If we point to a raw source endpoint, it returns text.
54
+ // If we point to the agent page, it returns HTML.
55
+ // We need a standard way to get source.
56
+ // For now, let's assume the URL points to the source or an API returning source.
57
+ if (typeof data === 'string') {
58
+ parentSource = data as string_book;
59
+ } else if (data.source) {
60
+ parentSource = data.source as string_book;
61
+ } else {
62
+ // Fallback or error
63
+ console.warn(`Received JSON from ${parentUrl} but couldn't determine source property. Using text.`);
64
+ // Re-fetch as text? Or assume body text was read? response.json() consumes body.
65
+ // So we might have failed here.
66
+ throw new Error(`Received JSON from ${parentUrl} but structure is unknown.`);
67
+ }
68
+ } else {
69
+ parentSource = (await response.text()) as string_book;
70
+ }
71
+
72
+ } catch (error) {
73
+ console.warn(`Failed to resolve parent agent ${parentUrl}`, error);
74
+ // If we fail to resolve parent, we return the original source (maybe with a warning or error commitment?)
75
+ // Or we could throw to fail the build.
76
+ // For robustness, let's append a warning comment
77
+ return `${agentSource}\n\n# Warning: Failed to inherit from ${parentUrl}: ${error}` as string_book;
78
+ }
79
+
80
+ // Recursively resolve the parent source
81
+ const effectiveParentSource = await resolveInheritedAgentSource(parentSource, collection);
82
+
83
+ // Strip the FROM commitment from the child source to avoid infinite recursion or re-processing
84
+ // We can filter lines starting with FROM
85
+ const childSourceLines = agentSource.split('\n');
86
+ const filteredChildSource = childSourceLines
87
+ .filter((line: string) => !line.trim().startsWith('FROM ')) // Simple string check, ideally should use parser location
88
+ .join('\n');
89
+
90
+ // Append child source to parent source
91
+ // "appends the RULE commitment to its source" -> Parent + Child
92
+ return `${effectiveParentSource}\n\n${filteredChildSource}` as string_book;
93
+ }