@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,440 @@
1
+ /**
2
+ * Router Node (GENERIC - Theme-Agnostic)
3
+ *
4
+ * Classifies user intents using structured output.
5
+ * Single LLM call that parses intent type, action, and parameters.
6
+ * Includes retry logic with Zod validation for local model compatibility.
7
+ *
8
+ * GENERIC ARCHITECTURE:
9
+ * - Schema and prompt are generated dynamically from OrchestratorConfig
10
+ * - No hardcoded knowledge of specific entities (task, customer, page)
11
+ * - Theme configures available tools via config
12
+ */
13
+
14
+ import { z } from 'zod'
15
+ import { HumanMessage, SystemMessage } from '@langchain/core/messages'
16
+ import type { BaseMessage } from '@langchain/core/messages'
17
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models'
18
+ import { getModel, getStructuredOutputMethod } from '../../providers'
19
+ import { tracer } from '../../tracer'
20
+ import { config as pluginConfig } from '../../../plugin.config'
21
+ import type { OrchestratorState, Intent, IntentType, IntentAction, OrchestratorConfig } from '../types'
22
+ import { DEFAULT_GRAPH_CONFIG } from '../types'
23
+
24
+ // ============================================
25
+ // CONFIGURATION
26
+ // ============================================
27
+
28
+ const ROUTER_CONFIG = {
29
+ maxRetries: 3,
30
+ retryDelayMs: 500,
31
+ }
32
+
33
+ // ============================================
34
+ // DYNAMIC SCHEMA GENERATION
35
+ // ============================================
36
+
37
+ /**
38
+ * Create Zod schema dynamically from orchestrator config
39
+ */
40
+ function createIntentSchema(config: OrchestratorConfig) {
41
+ const toolNames = config.tools.map(t => t.name)
42
+ const systemIntents = config.systemIntents || ['greeting', 'clarification']
43
+ const allIntentTypes = [...toolNames, ...systemIntents]
44
+
45
+ // Create description for intent types
46
+ const toolDescriptions = config.tools
47
+ .map(t => `${t.name} for ${t.description}`)
48
+ .join(', ')
49
+ const systemDescriptions = systemIntents
50
+ .map(s => s === 'greeting' ? 'greeting for hello/hi' : 'clarification if unclear')
51
+ .join(', ')
52
+
53
+ const IntentSchema = z.object({
54
+ type: z.enum(allIntentTypes as [string, ...string[]]).describe(
55
+ `The type of intent: ${toolDescriptions}, ${systemDescriptions}`
56
+ ),
57
+ action: z.enum(['list', 'create', 'update', 'delete', 'search', 'get', 'unknown']).describe(
58
+ 'The action to perform on the entity'
59
+ ),
60
+ parameters: z.record(z.string(), z.unknown()).describe(
61
+ 'Extracted parameters like title, priority, query, name, etc.'
62
+ ),
63
+ originalText: z.string().describe(
64
+ 'The portion of the user message that maps to this intent'
65
+ ),
66
+ })
67
+
68
+ const RouterOutputSchema = z.object({
69
+ intents: z.array(IntentSchema).describe(
70
+ 'All intents extracted from the user message. Include multiple if user asks for multiple things.'
71
+ ),
72
+ needsClarification: z.boolean().describe(
73
+ 'True if the request is too vague to understand'
74
+ ),
75
+ clarificationQuestion: z.string().nullable().describe(
76
+ 'Question to ask user if clarification is needed, in their language. Null if not needed.'
77
+ ),
78
+ })
79
+
80
+ return RouterOutputSchema
81
+ }
82
+
83
+ type RouterOutput = {
84
+ intents: Array<{
85
+ type: string
86
+ action: IntentAction
87
+ parameters: Record<string, unknown>
88
+ originalText: string
89
+ }>
90
+ needsClarification: boolean
91
+ clarificationQuestion: string | null
92
+ }
93
+
94
+ // ============================================
95
+ // RETRY HELPERS
96
+ // ============================================
97
+
98
+ /**
99
+ * Extract JSON from a string that might contain markdown or extra text
100
+ */
101
+ function extractJsonFromResponse(text: string): string {
102
+ // Try to find JSON in markdown code block
103
+ const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/)
104
+ if (codeBlockMatch) {
105
+ return codeBlockMatch[1].trim()
106
+ }
107
+
108
+ // Try to find raw JSON object
109
+ const jsonMatch = text.match(/\{[\s\S]*\}/)
110
+ if (jsonMatch) {
111
+ return jsonMatch[0]
112
+ }
113
+
114
+ return text
115
+ }
116
+
117
+ /**
118
+ * Validate and parse router output with Zod
119
+ * Returns null if validation fails
120
+ */
121
+ function validateRouterOutput(data: unknown, schema: z.ZodSchema): RouterOutput | null {
122
+ try {
123
+ return schema.parse(data) as RouterOutput
124
+ } catch (error) {
125
+ if (pluginConfig.debug) {
126
+ console.log('[Router] Zod validation failed:', error)
127
+ }
128
+ return null
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Attempt to invoke the model with structured output
134
+ * Returns the validated output or null on failure
135
+ */
136
+ async function tryStructuredOutput(
137
+ model: BaseChatModel,
138
+ messages: BaseMessage[],
139
+ method: 'functionCalling' | 'jsonMode' | 'jsonSchema',
140
+ schema: z.ZodSchema
141
+ ): Promise<RouterOutput | null> {
142
+ try {
143
+ const structuredModel = model.withStructuredOutput(schema, {
144
+ name: 'extract_intents',
145
+ method,
146
+ })
147
+ const result = await structuredModel.invoke(messages)
148
+ return validateRouterOutput(result, schema)
149
+ } catch (error) {
150
+ if (pluginConfig.debug) {
151
+ console.log('[Router] Structured output failed:', error)
152
+ }
153
+ return null
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Fallback: invoke model without structured output and parse JSON manually
159
+ */
160
+ async function tryManualJsonParsing(
161
+ model: BaseChatModel,
162
+ messages: BaseMessage[],
163
+ schema: z.ZodSchema
164
+ ): Promise<RouterOutput | null> {
165
+ try {
166
+ const result = await model.invoke(messages)
167
+ const content = typeof result.content === 'string'
168
+ ? result.content
169
+ : JSON.stringify(result.content)
170
+
171
+ const jsonStr = extractJsonFromResponse(content)
172
+ const parsed = JSON.parse(jsonStr)
173
+ return validateRouterOutput(parsed, schema)
174
+ } catch (error) {
175
+ if (pluginConfig.debug) {
176
+ console.log('[Router] Manual JSON parsing failed:', error)
177
+ }
178
+ return null
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Invoke router with retry logic
184
+ * Tries structured output first, then falls back to manual parsing
185
+ */
186
+ async function invokeRouterWithRetry(
187
+ model: BaseChatModel,
188
+ messages: BaseMessage[],
189
+ structuredOutputMethod: 'functionCalling' | 'jsonMode' | 'jsonSchema',
190
+ schema: z.ZodSchema
191
+ ): Promise<RouterOutput> {
192
+ let lastError: Error | null = null
193
+
194
+ for (let attempt = 1; attempt <= ROUTER_CONFIG.maxRetries; attempt++) {
195
+ if (pluginConfig.debug && attempt > 1) {
196
+ console.log(`[Router] Retry attempt ${attempt}/${ROUTER_CONFIG.maxRetries}`)
197
+ }
198
+
199
+ // Try structured output first
200
+ const structuredResult = await tryStructuredOutput(model, messages, structuredOutputMethod, schema)
201
+ if (structuredResult) {
202
+ if (pluginConfig.debug && attempt > 1) {
203
+ console.log('[Router] Succeeded on retry with structured output')
204
+ }
205
+ return structuredResult
206
+ }
207
+
208
+ // Fallback to manual JSON parsing
209
+ if (pluginConfig.debug) {
210
+ console.log('[Router] Falling back to manual JSON parsing')
211
+ }
212
+ const manualResult = await tryManualJsonParsing(model, messages, schema)
213
+ if (manualResult) {
214
+ if (pluginConfig.debug) {
215
+ console.log('[Router] Succeeded with manual JSON parsing')
216
+ }
217
+ return manualResult
218
+ }
219
+
220
+ // Wait before retry (exponential backoff)
221
+ if (attempt < ROUTER_CONFIG.maxRetries) {
222
+ const delay = ROUTER_CONFIG.retryDelayMs * Math.pow(2, attempt - 1)
223
+ await new Promise((resolve) => setTimeout(resolve, delay))
224
+ }
225
+ }
226
+
227
+ // All retries failed - throw error
228
+ throw lastError || new Error('Router failed after all retry attempts')
229
+ }
230
+
231
+ // ============================================
232
+ // DYNAMIC PROMPT GENERATION
233
+ // ============================================
234
+
235
+ /**
236
+ * Generate router prompt dynamically from orchestrator config
237
+ */
238
+ function createRouterPrompt(config: OrchestratorConfig): string {
239
+ const toolNames = config.tools.map(t => t.name)
240
+ const systemIntents = config.systemIntents || ['greeting', 'clarification']
241
+
242
+ // Build intent types section
243
+ const intentTypesSection = [
244
+ ...config.tools.map(tool => `- ${tool.name}: ${tool.description}`),
245
+ ...systemIntents.map(intent =>
246
+ intent === 'greeting' ? '- greeting: Greeting or small talk' : '- clarification: Request is too vague to understand'
247
+ )
248
+ ].join('\n')
249
+
250
+ // Build parameter examples section
251
+ const parameterExamplesSection = config.tools
252
+ .filter(t => t.exampleParameters)
253
+ .map(t => `- ${t.name}: ${t.exampleParameters}`)
254
+ .join('\n')
255
+
256
+ // Build type union for JSON format
257
+ const typeUnion = [...toolNames, ...systemIntents].map(t => `"${t}"`).join(' | ')
258
+
259
+ // Build examples dynamically from first tool (if available)
260
+ const exampleTool = config.tools[0]
261
+ const examplePrompts = exampleTool ? `
262
+ User: "Show me my ${exampleTool.name}s"
263
+ Response: {"intents": [{"type": "${exampleTool.name}", "action": "list", "parameters": {}, "originalText": "Show me my ${exampleTool.name}s"}], "needsClarification": false}
264
+
265
+ User: "Create ${exampleTool.name} 'Example' high priority"
266
+ Response: {"intents": [{"type": "${exampleTool.name}", "action": "create", "parameters": {"title": "Example", "priority": "high"}, "originalText": "Create ${exampleTool.name} 'Example' high priority"}], "needsClarification": false}
267
+ ` : ''
268
+
269
+ const multiToolExample = config.tools.length >= 2 ? `
270
+ User: "Show my ${config.tools[0].name}s and find ${config.tools[1].name} data"
271
+ Response: {"intents": [{"type": "${config.tools[0].name}", "action": "list", "parameters": {}, "originalText": "Show my ${config.tools[0].name}s"}, {"type": "${config.tools[1].name}", "action": "search", "parameters": {"query": "data"}, "originalText": "find ${config.tools[1].name} data"}], "needsClarification": false}
272
+ ` : ''
273
+
274
+ return `You are an intent classifier for a multi-agent system. Your job is to analyze user messages and extract ALL intents.
275
+
276
+ IMPORTANT: You MUST respond with valid JSON only. No additional text or explanation.
277
+
278
+ ## Intent Types
279
+ ${intentTypesSection}
280
+
281
+ ## Rules
282
+ 1. Extract ALL intents if user asks for multiple things
283
+ 2. Be specific with parameters (title, priority, query, etc.)
284
+ 3. Preserve user's language for clarification questions
285
+ 4. Use clarification only when truly unclear
286
+ 5. Map originalText to the relevant portion of the message
287
+
288
+ ## Parameter Examples
289
+ ${parameterExamplesSection}
290
+
291
+ ## JSON Output Format
292
+ {
293
+ "intents": [
294
+ {
295
+ "type": ${typeUnion},
296
+ "action": "list" | "create" | "update" | "delete" | "search" | "get" | "unknown",
297
+ "parameters": {},
298
+ "originalText": "portion of user message"
299
+ }
300
+ ],
301
+ "needsClarification": false,
302
+ "clarificationQuestion": null
303
+ }
304
+
305
+ ## Examples
306
+ ${examplePrompts}${multiToolExample}
307
+ User: "Hola"
308
+ Response: {"intents": [{"type": "greeting", "action": "unknown", "parameters": {}, "originalText": "Hola"}], "needsClarification": false}
309
+
310
+ ${config.routerPromptExtras || ''}`
311
+ }
312
+
313
+ // ============================================
314
+ // ROUTER NODE FACTORY (GENERIC)
315
+ // ============================================
316
+
317
+ /**
318
+ * Create router node with configuration
319
+ *
320
+ * Uses structured output for reliable JSON extraction.
321
+ * Single LLM call replaces multiple ReAct iterations.
322
+ *
323
+ * @param config - Orchestrator configuration with tools
324
+ * @returns Router node function
325
+ */
326
+ export function createRouterNode(config: OrchestratorConfig) {
327
+ // Generate schema and prompt from config (done once at graph creation)
328
+ const RouterOutputSchema = createIntentSchema(config)
329
+ const routerPrompt = createRouterPrompt(config)
330
+
331
+ // Return the router node function
332
+ return async function routerNode(
333
+ state: OrchestratorState
334
+ ): Promise<Partial<OrchestratorState>> {
335
+ const { context, traceId, modelConfig } = state
336
+
337
+ // Use model config from state (injected by theme)
338
+ // Fallback to defaults if not provided
339
+ const modelCfg = modelConfig || {
340
+ provider: 'openai',
341
+ model: undefined,
342
+ temperature: DEFAULT_GRAPH_CONFIG.routerTemperature,
343
+ }
344
+
345
+ // Start span for router with provider/model info
346
+ const spanContext = traceId
347
+ ? await tracer.startSpan(
348
+ { userId: context.userId, teamId: context.teamId },
349
+ traceId,
350
+ {
351
+ name: 'router',
352
+ type: 'llm',
353
+ provider: modelCfg.provider,
354
+ model: modelCfg.model,
355
+ input: { message: state.input },
356
+ }
357
+ )
358
+ : null
359
+
360
+ try {
361
+ // Get model with orchestrator's provider and low temperature for consistent classification
362
+ const model = getModel(modelCfg)
363
+
364
+ // Automatically detect the best structured output method for the provider
365
+ // (jsonSchema for LM Studio, functionCalling for OpenAI/Anthropic/Ollama)
366
+ const structuredOutputMethod = getStructuredOutputMethod(modelCfg)
367
+
368
+ // Build messages with recent conversation context
369
+ const recentHistory = state.conversationHistory.slice(-DEFAULT_GRAPH_CONFIG.maxHistoryMessages)
370
+
371
+ const messages = [
372
+ new SystemMessage(routerPrompt),
373
+ ...recentHistory,
374
+ new HumanMessage(state.input),
375
+ ]
376
+
377
+ // Log in debug mode
378
+ if (pluginConfig.debug) {
379
+ console.log('[Router] Classifying intent for:', state.input)
380
+ }
381
+
382
+ // Invoke with retry logic and Zod validation
383
+ const result = await invokeRouterWithRetry(model, messages, structuredOutputMethod, RouterOutputSchema)
384
+
385
+ if (pluginConfig.debug) {
386
+ console.log('[Router] Classified intents:', JSON.stringify(result.intents, null, 2))
387
+ }
388
+
389
+ // Transform to our Intent type
390
+ const intents: Intent[] = result.intents.map((intent) => ({
391
+ type: intent.type as IntentType,
392
+ action: intent.action as IntentAction,
393
+ parameters: intent.parameters as Record<string, unknown>,
394
+ originalText: intent.originalText,
395
+ }))
396
+
397
+ // End span with success
398
+ if (spanContext && traceId) {
399
+ await tracer.endSpan(
400
+ { userId: context.userId, teamId: context.teamId },
401
+ traceId,
402
+ spanContext.spanId,
403
+ {
404
+ output: {
405
+ intentsCount: intents.length,
406
+ intents: intents.map((i) => ({ type: i.type, action: i.action })),
407
+ needsClarification: result.needsClarification,
408
+ },
409
+ }
410
+ )
411
+ }
412
+
413
+ return {
414
+ intents,
415
+ needsClarification: result.needsClarification,
416
+ clarificationQuestion: result.clarificationQuestion ?? undefined,
417
+ }
418
+ } catch (error) {
419
+ console.error('[Router] Error classifying intent:', error)
420
+
421
+ // End span with error
422
+ if (spanContext && traceId) {
423
+ await tracer.endSpan(
424
+ { userId: context.userId, teamId: context.teamId },
425
+ traceId,
426
+ spanContext.spanId,
427
+ { error: error instanceof Error ? error : new Error(String(error)) }
428
+ )
429
+ }
430
+
431
+ return {
432
+ intents: [],
433
+ needsClarification: true,
434
+ clarificationQuestion:
435
+ 'I encountered an error understanding your request. Could you please rephrase it?',
436
+ error: error instanceof Error ? error.message : 'Router classification failed',
437
+ }
438
+ }
439
+ }
440
+ }