@nextsparkjs/plugin-langchain 0.1.0-beta.1

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 (67) hide show
  1. package/.env.example +41 -0
  2. package/api/observability/metrics/route.ts +110 -0
  3. package/api/observability/traces/[traceId]/route.ts +398 -0
  4. package/api/observability/traces/route.ts +205 -0
  5. package/api/sessions/route.ts +332 -0
  6. package/components/observability/CollapsibleJson.tsx +71 -0
  7. package/components/observability/CompactTimeline.tsx +75 -0
  8. package/components/observability/ConversationFlow.tsx +271 -0
  9. package/components/observability/DisabledMessage.tsx +21 -0
  10. package/components/observability/FiltersPanel.tsx +82 -0
  11. package/components/observability/ObservabilityDashboard.tsx +230 -0
  12. package/components/observability/SpansList.tsx +210 -0
  13. package/components/observability/TraceDetail.tsx +335 -0
  14. package/components/observability/TraceStatusBadge.tsx +39 -0
  15. package/components/observability/TracesTable.tsx +97 -0
  16. package/components/observability/index.ts +7 -0
  17. package/docs/01-getting-started/01-overview.md +196 -0
  18. package/docs/01-getting-started/02-installation.md +368 -0
  19. package/docs/01-getting-started/03-configuration.md +794 -0
  20. package/docs/02-core-concepts/01-architecture.md +566 -0
  21. package/docs/02-core-concepts/02-agents.md +597 -0
  22. package/docs/02-core-concepts/03-tools.md +689 -0
  23. package/docs/03-orchestration/01-graph-orchestrator.md +809 -0
  24. package/docs/03-orchestration/02-legacy-react.md +650 -0
  25. package/docs/04-advanced/01-observability.md +645 -0
  26. package/docs/04-advanced/02-token-tracking.md +469 -0
  27. package/docs/04-advanced/03-streaming.md +476 -0
  28. package/docs/04-advanced/04-guardrails.md +597 -0
  29. package/docs/05-reference/01-api-reference.md +1403 -0
  30. package/docs/05-reference/02-customization.md +646 -0
  31. package/docs/05-reference/03-examples.md +881 -0
  32. package/docs/index.md +85 -0
  33. package/hooks/observability/useMetrics.ts +31 -0
  34. package/hooks/observability/useTraceDetail.ts +48 -0
  35. package/hooks/observability/useTraces.ts +59 -0
  36. package/lib/agent-factory.ts +354 -0
  37. package/lib/agent-helpers.ts +201 -0
  38. package/lib/db-memory-store.ts +417 -0
  39. package/lib/graph/index.ts +58 -0
  40. package/lib/graph/nodes/combiner.ts +399 -0
  41. package/lib/graph/nodes/router.ts +440 -0
  42. package/lib/graph/orchestrator-graph.ts +386 -0
  43. package/lib/graph/prompts/combiner.md +131 -0
  44. package/lib/graph/prompts/router.md +193 -0
  45. package/lib/graph/types.ts +365 -0
  46. package/lib/guardrails.ts +230 -0
  47. package/lib/index.ts +44 -0
  48. package/lib/logger.ts +70 -0
  49. package/lib/memory-store.ts +168 -0
  50. package/lib/message-serializer.ts +110 -0
  51. package/lib/prompt-renderer.ts +94 -0
  52. package/lib/providers.ts +226 -0
  53. package/lib/streaming.ts +232 -0
  54. package/lib/token-tracker.ts +298 -0
  55. package/lib/tools-builder.ts +192 -0
  56. package/lib/tracer-callbacks.ts +342 -0
  57. package/lib/tracer.ts +350 -0
  58. package/migrations/001_langchain_memory.sql +83 -0
  59. package/migrations/002_token_usage.sql +127 -0
  60. package/migrations/003_observability.sql +257 -0
  61. package/package.json +28 -0
  62. package/plugin.config.ts +170 -0
  63. package/presets/lib/langchain.config.ts.preset +142 -0
  64. package/presets/templates/sector7/ai-observability/[traceId]/page.tsx +91 -0
  65. package/presets/templates/sector7/ai-observability/page.tsx +54 -0
  66. package/types/langchain.types.ts +274 -0
  67. package/types/observability.types.ts +270 -0
@@ -0,0 +1,881 @@
1
+ # Examples
2
+
3
+ Real-world implementation examples from the default theme. These examples demonstrate complete working implementations that you can adapt for your own themes.
4
+
5
+ ## Example 1: Task Management Agent
6
+
7
+ A complete task management agent with CRUD operations.
8
+
9
+ ### Configuration
10
+
11
+ ```typescript
12
+ // langchain.config.ts
13
+ 'task-assistant': {
14
+ provider: 'ollama',
15
+ temperature: 0.3,
16
+ description: 'Specialized agent for task management',
17
+ systemPrompt: 'task-assistant',
18
+ createTools: (context: ToolContext) => createTaskTools(context),
19
+ },
20
+ ```
21
+
22
+ ### System Prompt
23
+
24
+ ```markdown
25
+ <!-- agents/task-assistant.md -->
26
+ You are a task management AI assistant for the Boilerplate application.
27
+
28
+ ## CRITICAL RULE - MUST FOLLOW
29
+
30
+ **YOU MUST ALWAYS USE TOOLS TO GET DATA. NEVER FABRICATE OR IMAGINE TASK INFORMATION.**
31
+
32
+ Before responding with ANY task information, you MUST:
33
+ 1. Call the appropriate tool (list_tasks, search_tasks, get_task_details)
34
+ 2. Wait for the tool result
35
+ 3. ONLY THEN respond based on the REAL data from the tool
36
+
37
+ If a tool returns an error or empty results, tell the user honestly - NEVER make up fake tasks.
38
+
39
+ ## Your Capabilities
40
+ - List, search, and view tasks (using tools)
41
+ - Create new tasks with title, description, priority, and due dates
42
+ - Update existing tasks (status, priority, details)
43
+ - Suggest ideas, recipes, lists, or content to ADD to tasks when asked
44
+
45
+ ## Handling Suggestions + Task Updates
46
+
47
+ When the user asks you to "suggest X for task Y" or "add recommendations to task":
48
+ 1. First, find the task using search_tasks or get_task_details
49
+ 2. Generate your suggestions (recipes, ideas, items, etc.) using your knowledge
50
+ 3. Update the task with the suggestions using update_task
51
+ 4. Confirm what you added
52
+
53
+ ## Available Tools - USE THEM
54
+
55
+ | Tool | When to use |
56
+ |------|-------------|
57
+ | **list_tasks** | User asks to see tasks, pending items, todo list |
58
+ | **search_tasks** | User wants to find specific tasks by keyword |
59
+ | **get_task_details** | User asks about a specific task |
60
+ | **create_task** | User wants to create a new task |
61
+ | **update_task** | User wants to modify an existing task |
62
+
63
+ ## Response Format
64
+ - Use Spanish when the user writes in Spanish, English when they write in English
65
+ - Be concise but helpful
66
+ - Use bullet points for task lists
67
+ - When a task is created or updated, provide a link: [Task Title](/dashboard/tasks/{id})
68
+ - If no tasks found, say so honestly - don't invent them
69
+
70
+ ## What NOT to do
71
+ - NEVER respond with example/fake tasks like "Task 1: Description..."
72
+ - NEVER imagine what tasks the user might have
73
+ - NEVER skip calling tools before responding about tasks
74
+ ```
75
+
76
+ ### Tools Implementation
77
+
78
+ ```typescript
79
+ // tools/tasks.ts
80
+ import { z } from 'zod'
81
+ import { TasksService } from '@/themes/default/entities/tasks/tasks.service'
82
+ import type { ToolDefinition } from '@/contents/plugins/langchain/lib/tools-builder'
83
+
84
+ export interface TaskToolContext {
85
+ userId: string
86
+ teamId: string
87
+ }
88
+
89
+ export function createTaskTools(context: TaskToolContext): ToolDefinition<any>[] {
90
+ const { userId, teamId } = context
91
+
92
+ return [
93
+ {
94
+ name: 'list_tasks',
95
+ description: 'List tasks with optional filtering by status or priority.',
96
+ schema: z.object({
97
+ status: z.enum(['todo', 'in-progress', 'review', 'done', 'blocked'])
98
+ .optional()
99
+ .describe('Filter by task status'),
100
+ priority: z.enum(['low', 'medium', 'high', 'urgent'])
101
+ .optional()
102
+ .describe('Filter by priority level'),
103
+ }),
104
+ func: async (params) => {
105
+ try {
106
+ const result = await TasksService.list(userId, params)
107
+ if (result.tasks.length === 0) {
108
+ return JSON.stringify({
109
+ message: 'No tasks found',
110
+ tasks: [],
111
+ })
112
+ }
113
+ return JSON.stringify(result.tasks.map(t => ({
114
+ id: t.id,
115
+ title: t.title,
116
+ status: t.status,
117
+ priority: t.priority,
118
+ dueDate: t.dueDate,
119
+ })), null, 2)
120
+ } catch (error) {
121
+ return `Error listing tasks: ${error instanceof Error ? error.message : 'Unknown error'}`
122
+ }
123
+ },
124
+ },
125
+ {
126
+ name: 'search_tasks',
127
+ description: 'Search tasks by keyword in title or description.',
128
+ schema: z.object({
129
+ query: z.string().describe('Search term to match against task fields'),
130
+ }),
131
+ func: async ({ query }) => {
132
+ try {
133
+ const result = await TasksService.list(userId, {})
134
+ const filtered = result.tasks.filter(t =>
135
+ t.title.toLowerCase().includes(query.toLowerCase()) ||
136
+ t.description?.toLowerCase().includes(query.toLowerCase())
137
+ )
138
+ if (filtered.length === 0) {
139
+ return JSON.stringify({
140
+ message: `No tasks found matching "${query}"`,
141
+ tasks: [],
142
+ })
143
+ }
144
+ return JSON.stringify(filtered.map(t => ({
145
+ id: t.id,
146
+ title: t.title,
147
+ status: t.status,
148
+ description: t.description?.substring(0, 100),
149
+ })), null, 2)
150
+ } catch (error) {
151
+ return `Error searching tasks: ${error instanceof Error ? error.message : 'Unknown error'}`
152
+ }
153
+ },
154
+ },
155
+ {
156
+ name: 'get_task_details',
157
+ description: 'Get full details of a specific task by ID.',
158
+ schema: z.object({
159
+ taskId: z.string().describe('The task ID to retrieve'),
160
+ }),
161
+ func: async ({ taskId }) => {
162
+ try {
163
+ const task = await TasksService.getById(taskId, userId)
164
+ if (!task) {
165
+ return JSON.stringify({ error: 'Task not found', taskId })
166
+ }
167
+ return JSON.stringify(task, null, 2)
168
+ } catch (error) {
169
+ return `Error getting task: ${error instanceof Error ? error.message : 'Unknown error'}`
170
+ }
171
+ },
172
+ },
173
+ {
174
+ name: 'create_task',
175
+ description: 'Create a new task with title and optional details.',
176
+ schema: z.object({
177
+ title: z.string().min(1).describe('Task title (required)'),
178
+ description: z.string().optional().describe('Detailed description'),
179
+ priority: z.enum(['low', 'medium', 'high'])
180
+ .optional()
181
+ .default('medium')
182
+ .describe('Priority level'),
183
+ dueDate: z.string().optional().describe('Due date in ISO format (YYYY-MM-DD)'),
184
+ }),
185
+ func: async (data) => {
186
+ try {
187
+ const task = await TasksService.create(userId, {
188
+ ...data,
189
+ teamId,
190
+ status: 'todo',
191
+ })
192
+ return JSON.stringify({
193
+ success: true,
194
+ task: {
195
+ id: task.id,
196
+ title: task.title,
197
+ status: task.status,
198
+ priority: task.priority,
199
+ },
200
+ message: `Task "${task.title}" created successfully`,
201
+ link: `/dashboard/tasks/${task.id}`,
202
+ }, null, 2)
203
+ } catch (error) {
204
+ return `Error creating task: ${error instanceof Error ? error.message : 'Unknown error'}`
205
+ }
206
+ },
207
+ },
208
+ {
209
+ name: 'update_task',
210
+ description: 'Update an existing task. Only specify fields you want to change.',
211
+ schema: z.object({
212
+ taskId: z.string().describe('The task ID to update'),
213
+ title: z.string().optional().describe('New title'),
214
+ description: z.string().optional().describe('New description'),
215
+ status: z.enum(['todo', 'in-progress', 'review', 'done', 'blocked']).optional().describe('New status'),
216
+ priority: z.enum(['low', 'medium', 'high']).optional().describe('New priority'),
217
+ dueDate: z.string().optional().describe('New due date (ISO format)'),
218
+ }),
219
+ func: async ({ taskId, ...updates }) => {
220
+ try {
221
+ // Filter out undefined values
222
+ const cleanUpdates = Object.fromEntries(
223
+ Object.entries(updates).filter(([_, v]) => v !== undefined)
224
+ )
225
+
226
+ if (Object.keys(cleanUpdates).length === 0) {
227
+ return JSON.stringify({ error: 'No fields to update provided' })
228
+ }
229
+
230
+ const task = await TasksService.update(userId, taskId, cleanUpdates)
231
+ return JSON.stringify({
232
+ success: true,
233
+ task: {
234
+ id: task.id,
235
+ title: task.title,
236
+ status: task.status,
237
+ },
238
+ message: `Task "${task.title}" updated successfully`,
239
+ link: `/dashboard/tasks/${task.id}`,
240
+ }, null, 2)
241
+ } catch (error) {
242
+ return `Error updating task: ${error instanceof Error ? error.message : 'Unknown error'}`
243
+ }
244
+ },
245
+ },
246
+ ]
247
+ }
248
+ ```
249
+
250
+ ### Usage
251
+
252
+ ```typescript
253
+ import { createAgent } from '@/contents/plugins/langchain/lib/agent-factory'
254
+ import { loadSystemPrompt } from './agents'
255
+ import { getAgentModelConfig, getAgentTools } from './langchain.config'
256
+
257
+ // Create agent
258
+ const taskAgent = await createAgent({
259
+ sessionId: `user-${userId}-task-session`,
260
+ systemPrompt: loadSystemPrompt('task-assistant'),
261
+ tools: getAgentTools('task-assistant', { userId, teamId }),
262
+ modelConfig: getAgentModelConfig('task-assistant'),
263
+ context: { userId, teamId },
264
+ })
265
+
266
+ // Example conversations
267
+ await taskAgent.chat('Show me my pending tasks')
268
+ // Calls list_tasks, returns formatted list
269
+
270
+ await taskAgent.chat('Create a task to review the quarterly report')
271
+ // Calls create_task, returns success with link
272
+
273
+ await taskAgent.chat('Mark the report task as in progress')
274
+ // Calls search_tasks → update_task
275
+ ```
276
+
277
+ ---
278
+
279
+ ## Example 2: Customer Search and Update
280
+
281
+ A customer management agent with contextual updates.
282
+
283
+ ### Configuration
284
+
285
+ ```typescript
286
+ 'customer-assistant': {
287
+ provider: 'ollama',
288
+ temperature: 0.3,
289
+ description: 'Specialized agent for customer management',
290
+ systemPrompt: 'customer-assistant',
291
+ createTools: (context: ToolContext) => createCustomerTools(context),
292
+ },
293
+ ```
294
+
295
+ ### Tools Implementation
296
+
297
+ ```typescript
298
+ // tools/customers.ts
299
+ import { z } from 'zod'
300
+ import { CustomersService } from '@/themes/default/entities/customers/customers.service'
301
+ import type { DayOfWeek } from '@/themes/default/entities/customers/customers.types'
302
+
303
+ export function createCustomerTools(context: { userId: string; teamId: string }) {
304
+ const { userId, teamId } = context
305
+
306
+ return [
307
+ {
308
+ name: 'list_customers',
309
+ description: 'List all customers with optional pagination and sorting.',
310
+ schema: z.object({
311
+ limit: z.number().optional().default(20).describe('Max customers to return'),
312
+ offset: z.number().optional().default(0).describe('Offset for pagination'),
313
+ orderBy: z.enum(['name', 'account', 'office', 'salesRep', 'createdAt'])
314
+ .optional()
315
+ .describe('Field to order by'),
316
+ orderDir: z.enum(['asc', 'desc'])
317
+ .optional()
318
+ .describe('Order direction'),
319
+ }),
320
+ func: async (params) => {
321
+ try {
322
+ const result = await CustomersService.list(userId, params)
323
+ return JSON.stringify({
324
+ customers: result.customers.map(c => ({
325
+ id: c.id,
326
+ name: c.name,
327
+ account: c.account,
328
+ office: c.office,
329
+ salesRep: c.salesRep,
330
+ phone: c.phone,
331
+ })),
332
+ total: result.total,
333
+ }, null, 2)
334
+ } catch (error) {
335
+ return `Error: ${error instanceof Error ? error.message : 'Unknown'}`
336
+ }
337
+ },
338
+ },
339
+ {
340
+ name: 'search_customers',
341
+ description: 'Search customers by name, account number, office, or sales representative.',
342
+ schema: z.object({
343
+ query: z.string().describe('Search term'),
344
+ limit: z.number().optional().default(10).describe('Max results'),
345
+ }),
346
+ func: async (params) => {
347
+ try {
348
+ const results = await CustomersService.search(userId, params)
349
+ if (results.length === 0) {
350
+ return JSON.stringify({
351
+ message: `No customers found matching "${params.query}"`,
352
+ customers: [],
353
+ })
354
+ }
355
+ return JSON.stringify(results.map(c => ({
356
+ id: c.id,
357
+ name: c.name,
358
+ account: c.account,
359
+ office: c.office,
360
+ salesRep: c.salesRep,
361
+ phone: c.phone,
362
+ })), null, 2)
363
+ } catch (error) {
364
+ return `Error: ${error instanceof Error ? error.message : 'Unknown'}`
365
+ }
366
+ },
367
+ },
368
+ {
369
+ name: 'get_customer',
370
+ description: 'Get full details of a specific customer by ID.',
371
+ schema: z.object({
372
+ customerId: z.string().describe('The customer ID to retrieve'),
373
+ }),
374
+ func: async ({ customerId }) => {
375
+ try {
376
+ const customer = await CustomersService.getById(customerId, userId)
377
+ if (!customer) {
378
+ return JSON.stringify({ error: 'Customer not found' })
379
+ }
380
+ return JSON.stringify(customer, null, 2)
381
+ } catch (error) {
382
+ return `Error: ${error instanceof Error ? error.message : 'Unknown'}`
383
+ }
384
+ },
385
+ },
386
+ {
387
+ name: 'update_customer',
388
+ description: 'Update an existing customer. Only specify fields you want to change.',
389
+ schema: z.object({
390
+ customerId: z.string().describe('The customer ID to update'),
391
+ name: z.string().optional().describe('New customer name'),
392
+ account: z.number().optional().describe('New account number'),
393
+ office: z.string().optional().describe('New office location'),
394
+ phone: z.string().optional().describe('New phone number'),
395
+ salesRep: z.string().optional().describe('New sales representative'),
396
+ visitDays: z.array(z.enum(['lun', 'mar', 'mie', 'jue', 'vie']))
397
+ .optional()
398
+ .describe('New visit days'),
399
+ contactDays: z.array(z.enum(['lun', 'mar', 'mie', 'jue', 'vie']))
400
+ .optional()
401
+ .describe('New contact days'),
402
+ }),
403
+ func: async ({ customerId, ...updates }) => {
404
+ try {
405
+ const customer = await CustomersService.update(userId, customerId, {
406
+ ...updates,
407
+ visitDays: updates.visitDays as DayOfWeek[] | undefined,
408
+ contactDays: updates.contactDays as DayOfWeek[] | undefined,
409
+ })
410
+ return JSON.stringify({
411
+ success: true,
412
+ customer: {
413
+ id: customer.id,
414
+ name: customer.name,
415
+ phone: customer.phone,
416
+ },
417
+ message: `Customer "${customer.name}" updated successfully`,
418
+ link: `/dashboard/customers/${customer.id}`,
419
+ }, null, 2)
420
+ } catch (error) {
421
+ return `Error: ${error instanceof Error ? error.message : 'Unknown'}`
422
+ }
423
+ },
424
+ },
425
+ ]
426
+ }
427
+ ```
428
+
429
+ ### Example Conversation
430
+
431
+ ```
432
+ User: "Dime el teléfono de StartupXYZ"
433
+ Agent: *calls search_customers with query "StartupXYZ"*
434
+ "El teléfono de StartupXYZ es +1 512 555 0102.
435
+ Ver detalles: [StartupXYZ](/dashboard/customers/customer-002)"
436
+
437
+ User: "Modificalo, su nuevo teléfono es +1 457 45465245"
438
+ Agent: *calls update_customer with customerId="customer-002" and phone="+1 457 45465245"*
439
+ "He actualizado el teléfono de StartupXYZ a +1 457 45465245.
440
+ [StartupXYZ](/dashboard/customers/customer-002)"
441
+ ```
442
+
443
+ ---
444
+
445
+ ## Example 3: Multi-Agent Orchestration
446
+
447
+ Complete orchestrator implementation with routing.
448
+
449
+ ### Orchestrator Handler
450
+
451
+ ```typescript
452
+ // orchestrator.ts
453
+ import { createAgent } from '@/contents/plugins/langchain/lib/agent-factory'
454
+ import { loadSystemPrompt } from './agents'
455
+ import {
456
+ getAgentConfig,
457
+ getAgentModelConfig,
458
+ getAgentTools,
459
+ } from './langchain.config'
460
+ import type { RoutingResult, ClarificationResult } from './tools/orchestrator'
461
+
462
+ type AgentType = 'task' | 'customer' | 'page'
463
+
464
+ const AGENT_NAME_MAP: Record<AgentType, string> = {
465
+ task: 'task-assistant',
466
+ customer: 'customer-assistant',
467
+ page: 'page-assistant',
468
+ }
469
+
470
+ interface AgentMessage {
471
+ _getType(): string
472
+ content: string | unknown
473
+ }
474
+
475
+ export interface OrchestratorContext {
476
+ userId: string
477
+ teamId: string
478
+ sessionId: string
479
+ }
480
+
481
+ export interface OrchestratorResponse {
482
+ content: string
483
+ sessionId: string
484
+ agentUsed?: 'orchestrator' | AgentType
485
+ }
486
+
487
+ /**
488
+ * Format a clarification question for the user
489
+ */
490
+ function formatClarificationQuestion(result: ClarificationResult): string {
491
+ let response = result.question + '\n\n'
492
+ result.options.forEach((opt, i) => {
493
+ response += `${i + 1}. **${opt.label}**: ${opt.description}\n`
494
+ })
495
+ return response
496
+ }
497
+
498
+ /**
499
+ * Parse JSON content to determine routing
500
+ */
501
+ function parseRoutingDecision(content: string): RoutingResult | ClarificationResult | null {
502
+ try {
503
+ const parsed = JSON.parse(content)
504
+ if (parsed.action === 'clarify') return parsed as ClarificationResult
505
+ if (parsed.agent && ['task', 'customer', 'page'].includes(parsed.agent)) {
506
+ return parsed as RoutingResult
507
+ }
508
+ return null
509
+ } catch {
510
+ return null
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Extract routing decision from the LATEST turn only
516
+ */
517
+ function extractRoutingFromMessages(messages: AgentMessage[]): RoutingResult | ClarificationResult | null {
518
+ // Find last human message
519
+ let lastHumanIndex = -1
520
+ for (let i = messages.length - 1; i >= 0; i--) {
521
+ if (messages[i]._getType() === 'human') {
522
+ lastHumanIndex = i
523
+ break
524
+ }
525
+ }
526
+
527
+ // Search for tool results AFTER last human message
528
+ for (let i = messages.length - 1; i > lastHumanIndex; i--) {
529
+ const msg = messages[i]
530
+ if (msg._getType() === 'tool') {
531
+ const content = typeof msg.content === 'string'
532
+ ? msg.content
533
+ : JSON.stringify(msg.content)
534
+ const decision = parseRoutingDecision(content)
535
+ if (decision) return decision
536
+ }
537
+ }
538
+ return null
539
+ }
540
+
541
+ /**
542
+ * Get system prompt for an agent
543
+ */
544
+ function getSystemPromptForAgent(agentName: string): string {
545
+ const config = getAgentConfig(agentName)
546
+ if (!config?.systemPrompt) {
547
+ throw new Error(`No system prompt for agent: ${agentName}`)
548
+ }
549
+ if (config.systemPrompt.includes('\n')) {
550
+ return config.systemPrompt
551
+ }
552
+ return loadSystemPrompt(config.systemPrompt as any)
553
+ }
554
+
555
+ /**
556
+ * Invoke a specialized sub-agent
557
+ */
558
+ async function invokeSubAgent(
559
+ agentType: AgentType,
560
+ message: string,
561
+ context: OrchestratorContext
562
+ ): Promise<OrchestratorResponse> {
563
+ const { userId, teamId, sessionId } = context
564
+ const agentName = AGENT_NAME_MAP[agentType]
565
+
566
+ const agent = await createAgent({
567
+ sessionId: `${sessionId}-${agentType}`,
568
+ systemPrompt: getSystemPromptForAgent(agentName),
569
+ tools: getAgentTools(agentName, { userId, teamId }),
570
+ modelConfig: getAgentModelConfig(agentName),
571
+ context: { userId, teamId },
572
+ })
573
+
574
+ const response = await agent.chat(message)
575
+
576
+ return {
577
+ content: response.content,
578
+ sessionId,
579
+ agentUsed: agentType,
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Process a message through the orchestrator
585
+ */
586
+ export async function processWithOrchestrator(
587
+ message: string,
588
+ context: OrchestratorContext
589
+ ): Promise<OrchestratorResponse> {
590
+ const { userId, teamId, sessionId } = context
591
+
592
+ try {
593
+ // Create orchestrator agent
594
+ const orchestratorAgent = await createAgent({
595
+ sessionId: `${sessionId}-orchestrator`,
596
+ systemPrompt: getSystemPromptForAgent('orchestrator'),
597
+ tools: getAgentTools('orchestrator', { userId, teamId }),
598
+ modelConfig: getAgentModelConfig('orchestrator'),
599
+ context: { userId, teamId },
600
+ })
601
+
602
+ // Get routing decision
603
+ const routingResponse = await orchestratorAgent.chat(message)
604
+
605
+ // Extract routing from tool results
606
+ const decision = extractRoutingFromMessages(routingResponse.messages || [])
607
+
608
+ if (!decision) {
609
+ // Direct response (greeting, meta-question)
610
+ return {
611
+ content: routingResponse.content,
612
+ sessionId,
613
+ agentUsed: 'orchestrator',
614
+ }
615
+ }
616
+
617
+ // Handle clarification
618
+ if ('action' in decision && decision.action === 'clarify') {
619
+ return {
620
+ content: formatClarificationQuestion(decision),
621
+ sessionId,
622
+ agentUsed: 'orchestrator',
623
+ }
624
+ }
625
+
626
+ // Route to specialized agent
627
+ if ('agent' in decision) {
628
+ return await invokeSubAgent(
629
+ decision.agent,
630
+ decision.message || message,
631
+ context
632
+ )
633
+ }
634
+
635
+ return {
636
+ content: routingResponse.content,
637
+ sessionId,
638
+ agentUsed: 'orchestrator',
639
+ }
640
+ } catch (error) {
641
+ console.error('[Orchestrator] Error:', error)
642
+ throw new Error(
643
+ error instanceof Error ? error.message : 'Failed to process message'
644
+ )
645
+ }
646
+ }
647
+ ```
648
+
649
+ ### API Route
650
+
651
+ ```typescript
652
+ // api/ai/chat/route.ts
653
+ import { NextRequest, NextResponse } from 'next/server'
654
+ import { authenticateRequest } from '@/core/lib/auth/server'
655
+ import { processWithOrchestrator } from '@/themes/default/lib/langchain/orchestrator'
656
+ import { memoryStore } from '@/contents/plugins/langchain/lib/memory-store'
657
+
658
+ export async function POST(request: NextRequest) {
659
+ try {
660
+ // Authenticate
661
+ const { user, teamId } = await authenticateRequest(request)
662
+ if (!user || !teamId) {
663
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
664
+ }
665
+
666
+ const { message, sessionId: providedSessionId, mode = 'orchestrator' } = await request.json()
667
+
668
+ if (!message?.trim()) {
669
+ return NextResponse.json({ error: 'Message is required' }, { status: 400 })
670
+ }
671
+
672
+ // Check conversation limits
673
+ const count = await memoryStore.countSessions({ userId: user.id, teamId })
674
+ if (count >= 50 && !providedSessionId) {
675
+ return NextResponse.json({
676
+ error: 'Maximum conversations reached (50). Delete some to continue.',
677
+ }, { status: 400 })
678
+ }
679
+
680
+ const sessionId = providedSessionId || `session-${Date.now()}`
681
+
682
+ // Process with orchestrator
683
+ const response = await processWithOrchestrator(message, {
684
+ userId: user.id,
685
+ teamId,
686
+ sessionId,
687
+ })
688
+
689
+ return NextResponse.json({
690
+ ...response,
691
+ mode: 'orchestrator',
692
+ })
693
+ } catch (error) {
694
+ console.error('[Chat API] Error:', error)
695
+ return NextResponse.json(
696
+ { error: error instanceof Error ? error.message : 'Internal error' },
697
+ { status: 500 }
698
+ )
699
+ }
700
+ }
701
+
702
+ export async function GET(request: NextRequest) {
703
+ try {
704
+ const { user, teamId } = await authenticateRequest(request)
705
+ if (!user || !teamId) {
706
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
707
+ }
708
+
709
+ const sessionId = request.nextUrl.searchParams.get('sessionId')
710
+ if (!sessionId) {
711
+ return NextResponse.json({ error: 'Session ID required' }, { status: 400 })
712
+ }
713
+
714
+ const messages = await memoryStore.getMessages(sessionId, {
715
+ userId: user.id,
716
+ teamId,
717
+ })
718
+
719
+ return NextResponse.json({ messages, sessionId })
720
+ } catch (error) {
721
+ return NextResponse.json(
722
+ { error: error instanceof Error ? error.message : 'Internal error' },
723
+ { status: 500 }
724
+ )
725
+ }
726
+ }
727
+ ```
728
+
729
+ ---
730
+
731
+ ## Example 4: Page Builder Agent
732
+
733
+ Agent for managing pages and blocks.
734
+
735
+ ### Tools Implementation
736
+
737
+ ```typescript
738
+ // tools/pages.ts (simplified)
739
+ import { z } from 'zod'
740
+ import { PagesManagementService } from '@/themes/default/entities/pages/pages-management.service'
741
+ import { BLOCK_REGISTRY } from '@/core/lib/registries/block-registry'
742
+
743
+ export function createPageTools(context: { userId: string; teamId: string }) {
744
+ const { userId, teamId } = context
745
+
746
+ return [
747
+ {
748
+ name: 'list_pages',
749
+ description: 'List all pages with optional filtering.',
750
+ schema: z.object({
751
+ status: z.enum(['draft', 'published']).optional(),
752
+ limit: z.number().optional().default(20),
753
+ }),
754
+ func: async (params) => {
755
+ const pages = await PagesManagementService.list(userId, params)
756
+ return JSON.stringify(pages.map(p => ({
757
+ id: p.id,
758
+ title: p.title,
759
+ slug: p.slug,
760
+ status: p.status,
761
+ blocksCount: p.blocks?.length || 0,
762
+ })), null, 2)
763
+ },
764
+ },
765
+ {
766
+ name: 'create_page',
767
+ description: 'Create a new page.',
768
+ schema: z.object({
769
+ title: z.string().describe('Page title'),
770
+ slug: z.string().describe('URL slug'),
771
+ seoTitle: z.string().optional(),
772
+ seoDescription: z.string().optional(),
773
+ }),
774
+ func: async (data) => {
775
+ const page = await PagesManagementService.create(userId, {
776
+ ...data,
777
+ teamId,
778
+ status: 'draft',
779
+ })
780
+ return JSON.stringify({
781
+ success: true,
782
+ page: { id: page.id, title: page.title, slug: page.slug },
783
+ link: `/dashboard/pages/${page.id}`,
784
+ }, null, 2)
785
+ },
786
+ },
787
+ {
788
+ name: 'add_block',
789
+ description: 'Add a block to a page.',
790
+ schema: z.object({
791
+ pageId: z.string().describe('The page ID'),
792
+ blockSlug: z.string().describe('Block type slug from available blocks'),
793
+ props: z.record(z.any()).optional().describe('Block properties'),
794
+ position: z.number().optional().describe('Position in page'),
795
+ }),
796
+ func: async ({ pageId, blockSlug, props, position }) => {
797
+ const page = await PagesManagementService.addBlock(
798
+ userId,
799
+ pageId,
800
+ blockSlug,
801
+ props || {},
802
+ position
803
+ )
804
+ return JSON.stringify({
805
+ success: true,
806
+ message: `Added ${blockSlug} block to page`,
807
+ blocksCount: page.blocks?.length,
808
+ }, null, 2)
809
+ },
810
+ },
811
+ {
812
+ name: 'list_available_blocks',
813
+ description: 'List all available block types that can be added to pages.',
814
+ schema: z.object({
815
+ category: z.string().optional().describe('Filter by category'),
816
+ }),
817
+ func: async ({ category }) => {
818
+ const blocks = Object.entries(BLOCK_REGISTRY)
819
+ .filter(([_, block]) => !category || block.category === category)
820
+ .map(([slug, block]) => ({
821
+ slug,
822
+ name: block.name,
823
+ description: block.description,
824
+ category: block.category,
825
+ }))
826
+ return JSON.stringify(blocks, null, 2)
827
+ },
828
+ },
829
+ {
830
+ name: 'publish_page',
831
+ description: 'Publish a page to make it publicly visible.',
832
+ schema: z.object({
833
+ pageId: z.string().describe('The page ID to publish'),
834
+ }),
835
+ func: async ({ pageId }) => {
836
+ const page = await PagesManagementService.publish(userId, pageId)
837
+ return JSON.stringify({
838
+ success: true,
839
+ message: `Page "${page.title}" is now published`,
840
+ url: `/${page.slug}`,
841
+ }, null, 2)
842
+ },
843
+ },
844
+ ]
845
+ }
846
+ ```
847
+
848
+ ### Example Conversation
849
+
850
+ ```
851
+ User: "Create a landing page for our new product"
852
+ Agent: "What would you like to call the page and what URL slug should it have?"
853
+
854
+ User: "Call it 'New Product Launch' with slug 'new-product'"
855
+ Agent: *calls create_page*
856
+ "Created page 'New Product Launch'. Now let's add some blocks.
857
+ [Edit page](/dashboard/pages/page-123)"
858
+
859
+ User: "Add a hero section with a title"
860
+ Agent: *calls list_available_blocks to find hero block*
861
+ *calls add_block with blockSlug="hero"*
862
+ "Added a hero block to the page. You can customize it in the editor."
863
+
864
+ User: "Publish it"
865
+ Agent: *calls publish_page*
866
+ "Page 'New Product Launch' is now live at /new-product"
867
+ ```
868
+
869
+ ---
870
+
871
+ ## Summary
872
+
873
+ These examples demonstrate:
874
+
875
+ 1. **Tool Patterns**: CRUD operations with proper error handling
876
+ 2. **Context Usage**: userId/teamId for multi-tenancy
877
+ 3. **Response Format**: Structured JSON with links and messages
878
+ 4. **Orchestration**: Routing between specialized agents
879
+ 5. **System Prompts**: Clear rules and examples for agent behavior
880
+
881
+ Use these as templates for your own implementations, adapting the entities and business logic to your needs.