@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.
- package/.env.example +41 -0
- package/api/observability/metrics/route.ts +110 -0
- package/api/observability/traces/[traceId]/route.ts +398 -0
- package/api/observability/traces/route.ts +205 -0
- package/api/sessions/route.ts +332 -0
- package/components/observability/CollapsibleJson.tsx +71 -0
- package/components/observability/CompactTimeline.tsx +75 -0
- package/components/observability/ConversationFlow.tsx +271 -0
- package/components/observability/DisabledMessage.tsx +21 -0
- package/components/observability/FiltersPanel.tsx +82 -0
- package/components/observability/ObservabilityDashboard.tsx +230 -0
- package/components/observability/SpansList.tsx +210 -0
- package/components/observability/TraceDetail.tsx +335 -0
- package/components/observability/TraceStatusBadge.tsx +39 -0
- package/components/observability/TracesTable.tsx +97 -0
- package/components/observability/index.ts +7 -0
- package/docs/01-getting-started/01-overview.md +196 -0
- package/docs/01-getting-started/02-installation.md +368 -0
- package/docs/01-getting-started/03-configuration.md +794 -0
- package/docs/02-core-concepts/01-architecture.md +566 -0
- package/docs/02-core-concepts/02-agents.md +597 -0
- package/docs/02-core-concepts/03-tools.md +689 -0
- package/docs/03-orchestration/01-graph-orchestrator.md +809 -0
- package/docs/03-orchestration/02-legacy-react.md +650 -0
- package/docs/04-advanced/01-observability.md +645 -0
- package/docs/04-advanced/02-token-tracking.md +469 -0
- package/docs/04-advanced/03-streaming.md +476 -0
- package/docs/04-advanced/04-guardrails.md +597 -0
- package/docs/05-reference/01-api-reference.md +1403 -0
- package/docs/05-reference/02-customization.md +646 -0
- package/docs/05-reference/03-examples.md +881 -0
- package/docs/index.md +85 -0
- package/hooks/observability/useMetrics.ts +31 -0
- package/hooks/observability/useTraceDetail.ts +48 -0
- package/hooks/observability/useTraces.ts +59 -0
- package/lib/agent-factory.ts +354 -0
- package/lib/agent-helpers.ts +201 -0
- package/lib/db-memory-store.ts +417 -0
- package/lib/graph/index.ts +58 -0
- package/lib/graph/nodes/combiner.ts +399 -0
- package/lib/graph/nodes/router.ts +440 -0
- package/lib/graph/orchestrator-graph.ts +386 -0
- package/lib/graph/prompts/combiner.md +131 -0
- package/lib/graph/prompts/router.md +193 -0
- package/lib/graph/types.ts +365 -0
- package/lib/guardrails.ts +230 -0
- package/lib/index.ts +44 -0
- package/lib/logger.ts +70 -0
- package/lib/memory-store.ts +168 -0
- package/lib/message-serializer.ts +110 -0
- package/lib/prompt-renderer.ts +94 -0
- package/lib/providers.ts +226 -0
- package/lib/streaming.ts +232 -0
- package/lib/token-tracker.ts +298 -0
- package/lib/tools-builder.ts +192 -0
- package/lib/tracer-callbacks.ts +342 -0
- package/lib/tracer.ts +350 -0
- package/migrations/001_langchain_memory.sql +83 -0
- package/migrations/002_token_usage.sql +127 -0
- package/migrations/003_observability.sql +257 -0
- package/package.json +28 -0
- package/plugin.config.ts +170 -0
- package/presets/lib/langchain.config.ts.preset +142 -0
- package/presets/templates/sector7/ai-observability/[traceId]/page.tsx +91 -0
- package/presets/templates/sector7/ai-observability/page.tsx +54 -0
- package/types/langchain.types.ts +274 -0
- 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.
|