@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,192 @@
1
+ import { DynamicStructuredTool } from '@langchain/core/tools'
2
+ import { z } from 'zod'
3
+
4
+ export interface ToolDefinition<T extends z.ZodObject<z.ZodRawShape>> {
5
+ name: string
6
+ description: string
7
+ schema: T
8
+ func: (input: z.infer<T>) => Promise<string>
9
+ }
10
+
11
+ /**
12
+ * Get the Zod type name for a field (Zod v4 compatible)
13
+ */
14
+ function getZodTypeName(zodField: z.ZodTypeAny): string {
15
+ // Zod v4: use _zod.def.type
16
+ const zodDef = (zodField as unknown as { _zod?: { def?: { type?: string } } })._zod
17
+ if (zodDef?.def?.type) {
18
+ return zodDef.def.type
19
+ }
20
+ // Fallback for edge cases
21
+ return 'unknown'
22
+ }
23
+
24
+ /**
25
+ * Get the inner type for optional/array types (Zod v4 compatible)
26
+ */
27
+ function getInnerType(zodField: z.ZodTypeAny): z.ZodTypeAny | null {
28
+ const zodDef = (zodField as unknown as { _zod?: { def?: { innerType?: z.ZodTypeAny; element?: z.ZodTypeAny } } })._zod
29
+ // For optional types
30
+ if (zodDef?.def?.innerType) {
31
+ return zodDef.def.innerType
32
+ }
33
+ // For array types
34
+ if (zodDef?.def?.element) {
35
+ return zodDef.def.element
36
+ }
37
+ return null
38
+ }
39
+
40
+ /**
41
+ * Get enum values (Zod v4 compatible)
42
+ */
43
+ function getEnumValues(zodField: z.ZodTypeAny): string[] | null {
44
+ const zodDef = (zodField as unknown as { _zod?: { def?: { entries?: Record<string, string> } } })._zod
45
+ if (zodDef?.def?.entries) {
46
+ return Object.values(zodDef.def.entries)
47
+ }
48
+ return null
49
+ }
50
+
51
+ /**
52
+ * Convert Zod schema to JSON Schema with explicit type: "object"
53
+ * LM Studio requires type: "object" at root level which zodToJsonSchema sometimes omits
54
+ *
55
+ * Zod v4 compatible implementation
56
+ */
57
+ export function zodToOpenAISchema(zodSchema: z.ZodObject<z.ZodRawShape>): Record<string, unknown> {
58
+ const shape = zodSchema.shape
59
+ const properties: Record<string, unknown> = {}
60
+ const required: string[] = []
61
+
62
+ for (const [key, value] of Object.entries(shape)) {
63
+ const zodField = value as z.ZodTypeAny
64
+ const fieldSchema: Record<string, unknown> = {}
65
+
66
+ // Get the description if available (Zod v4 uses .description property)
67
+ const description = (zodField as unknown as { description?: string }).description
68
+ if (description) {
69
+ fieldSchema.description = description
70
+ }
71
+
72
+ // Get the type name using Zod v4 API
73
+ const typeName = getZodTypeName(zodField)
74
+
75
+ // Handle different Zod types
76
+ switch (typeName) {
77
+ case 'string':
78
+ fieldSchema.type = 'string'
79
+ break
80
+ case 'number':
81
+ fieldSchema.type = 'number'
82
+ break
83
+ case 'boolean':
84
+ fieldSchema.type = 'boolean'
85
+ break
86
+ case 'array': {
87
+ fieldSchema.type = 'array'
88
+ const elementType = getInnerType(zodField)
89
+ if (elementType) {
90
+ const elementTypeName = getZodTypeName(elementType)
91
+ if (elementTypeName === 'object') {
92
+ fieldSchema.items = zodToOpenAISchema(elementType as z.ZodObject<z.ZodRawShape>)
93
+ } else if (elementTypeName === 'string') {
94
+ fieldSchema.items = { type: 'string' }
95
+ } else {
96
+ fieldSchema.items = { type: 'string' } // default fallback
97
+ }
98
+ } else {
99
+ fieldSchema.items = { type: 'string' } // default fallback
100
+ }
101
+ break
102
+ }
103
+ case 'object':
104
+ Object.assign(fieldSchema, zodToOpenAISchema(zodField as z.ZodObject<z.ZodRawShape>))
105
+ break
106
+ case 'optional': {
107
+ // Handle optional - get inner type
108
+ const innerType = getInnerType(zodField)
109
+ if (innerType) {
110
+ const innerTypeName = getZodTypeName(innerType)
111
+ if (innerTypeName === 'string') {
112
+ fieldSchema.type = 'string'
113
+ } else if (innerTypeName === 'number') {
114
+ fieldSchema.type = 'number'
115
+ } else if (innerTypeName === 'boolean') {
116
+ fieldSchema.type = 'boolean'
117
+ } else {
118
+ fieldSchema.type = 'string'
119
+ }
120
+ } else {
121
+ fieldSchema.type = 'string'
122
+ }
123
+ break
124
+ }
125
+ case 'enum': {
126
+ fieldSchema.type = 'string'
127
+ const enumValues = getEnumValues(zodField)
128
+ if (enumValues) {
129
+ fieldSchema.enum = enumValues
130
+ }
131
+ break
132
+ }
133
+ default:
134
+ // Default to string for unknown types
135
+ fieldSchema.type = 'string'
136
+ }
137
+
138
+ properties[key] = fieldSchema
139
+
140
+ // Check if required (not optional)
141
+ if (typeName !== 'optional') {
142
+ required.push(key)
143
+ }
144
+ }
145
+
146
+ return {
147
+ type: 'object',
148
+ properties,
149
+ required: required.length > 0 ? required : undefined,
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Convert tool definitions to OpenAI tool format with proper type: "object"
155
+ * Use this for LM Studio compatibility
156
+ */
157
+ export function convertToOpenAITools(definitions: ToolDefinition<z.ZodObject<z.ZodRawShape>>[]): Array<{
158
+ type: 'function'
159
+ function: {
160
+ name: string
161
+ description: string
162
+ parameters: Record<string, unknown>
163
+ }
164
+ }> {
165
+ return definitions.map(def => ({
166
+ type: 'function' as const,
167
+ function: {
168
+ name: def.name,
169
+ description: def.description,
170
+ parameters: zodToOpenAISchema(def.schema),
171
+ },
172
+ }))
173
+ }
174
+
175
+ /**
176
+ * Create a LangChain tool from a definition
177
+ */
178
+ export function createTool<T extends z.ZodObject<z.ZodRawShape>>(def: ToolDefinition<T>) {
179
+ return new DynamicStructuredTool({
180
+ name: def.name,
181
+ description: def.description,
182
+ schema: def.schema,
183
+ func: def.func,
184
+ })
185
+ }
186
+
187
+ /**
188
+ * Build multiple tools from definitions
189
+ */
190
+ export function buildTools(definitions: ToolDefinition<z.ZodObject<z.ZodRawShape>>[]) {
191
+ return definitions.map(createTool)
192
+ }
@@ -0,0 +1,342 @@
1
+ /**
2
+ * LangChain Tracing Callback Handler
3
+ *
4
+ * Captures LangChain events (LLM calls, tool executions, chain steps)
5
+ * and creates spans for observability.
6
+ */
7
+
8
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base'
9
+ import type { Serialized } from '@langchain/core/load/serializable'
10
+ import { tracer } from './tracer'
11
+ import type { SpanContext } from '../types/observability.types'
12
+
13
+ interface TracingCallbackHandlerOptions {
14
+ context: { userId: string; teamId: string }
15
+ traceId: string
16
+ /** Model name to use when LangChain doesn't provide it in callbacks */
17
+ modelName?: string
18
+ }
19
+
20
+ /**
21
+ * Callback handler that creates spans for LangChain events
22
+ */
23
+ export class TracingCallbackHandler extends BaseCallbackHandler {
24
+ name = 'tracing_callback_handler'
25
+
26
+ private context: { userId: string; teamId: string }
27
+ private traceId: string
28
+ private spans: Map<string, SpanContext>
29
+ private parentSpans: Map<string, string>
30
+ private modelName?: string
31
+
32
+ // Counters for LLM and tool calls
33
+ private _llmCallCount = 0
34
+ private _toolCallCount = 0
35
+
36
+ // Track pending async operations to ensure all callbacks complete
37
+ private pendingOperations: Promise<void>[] = []
38
+
39
+ constructor(options: TracingCallbackHandlerOptions) {
40
+ super()
41
+ this.context = options.context
42
+ this.traceId = options.traceId
43
+ this.modelName = options.modelName
44
+ this.spans = new Map()
45
+ this.parentSpans = new Map()
46
+ }
47
+
48
+ /**
49
+ * Track an async operation for later flushing
50
+ */
51
+ private trackOperation(promise: Promise<void>): void {
52
+ this.pendingOperations.push(promise)
53
+ // Clean up completed promises periodically
54
+ promise.finally(() => {
55
+ const index = this.pendingOperations.indexOf(promise)
56
+ if (index > -1) {
57
+ this.pendingOperations.splice(index, 1)
58
+ }
59
+ })
60
+ }
61
+
62
+ /**
63
+ * Wait for all pending operations to complete
64
+ * Call this before getCounts() to ensure accurate counts
65
+ */
66
+ async flush(): Promise<void> {
67
+ // Wait for all pending operations with a timeout
68
+ const timeout = new Promise<void>((resolve) => setTimeout(resolve, 5000))
69
+ await Promise.race([
70
+ Promise.all(this.pendingOperations),
71
+ timeout,
72
+ ])
73
+ }
74
+
75
+ /**
76
+ * Get the count of LLM and tool calls
77
+ */
78
+ getCounts(): { llmCalls: number; toolCalls: number } {
79
+ return {
80
+ llmCalls: this._llmCallCount,
81
+ toolCalls: this._toolCallCount,
82
+ }
83
+ }
84
+
85
+ /**
86
+ * LLM Events
87
+ */
88
+
89
+ async handleLLMStart(
90
+ llm: Serialized,
91
+ prompts: string[],
92
+ runId: string,
93
+ parentRunId?: string
94
+ ): Promise<void> {
95
+ const operation = (async () => {
96
+ try {
97
+ const provider = llm.id?.[llm.id.length - 1] || 'unknown'
98
+ // Extract model name from various possible locations (expanded for Ollama compatibility)
99
+ const llmAny = llm as any
100
+
101
+ const model =
102
+ // Standard locations
103
+ llmAny.kwargs?.model ||
104
+ llmAny.kwargs?.model_name ||
105
+ llmAny.kwargs?.modelName ||
106
+ llmAny.kwargs?.model_id ||
107
+ llmAny.model ||
108
+ llmAny.model_name ||
109
+ llmAny.modelName ||
110
+ // Ollama-specific locations
111
+ llmAny.kwargs?.configuration?.model ||
112
+ llmAny.kwargs?.options?.model ||
113
+ llmAny.lc_kwargs?.model ||
114
+ // ChatOllama specific
115
+ llmAny.kwargs?.base_url && llmAny.kwargs?.model ||
116
+ // Last resort: try to extract from id array
117
+ (Array.isArray(llm.id) && llm.id.find((id: string) => id.includes(':') || id.includes('-'))) ||
118
+ // Use model name passed from config (fallback for providers like Ollama)
119
+ this.modelName ||
120
+ 'unknown'
121
+ const depth = parentRunId ? (this.spans.get(parentRunId)?.depth || 0) + 1 : 0
122
+
123
+ const spanContext = await tracer.startSpan(this.context, this.traceId, {
124
+ name: `LLM: ${model}`,
125
+ type: 'llm',
126
+ provider,
127
+ model,
128
+ parentSpanId: parentRunId ? this.parentSpans.get(parentRunId) : undefined,
129
+ depth,
130
+ input: { prompts },
131
+ })
132
+
133
+ if (spanContext) {
134
+ this.spans.set(runId, spanContext)
135
+ if (parentRunId) {
136
+ this.parentSpans.set(runId, spanContext.spanId)
137
+ }
138
+ }
139
+ } catch (error) {
140
+ console.error('[TracingCallbackHandler] handleLLMStart error:', error)
141
+ }
142
+ })()
143
+ this.trackOperation(operation)
144
+ await operation
145
+ }
146
+
147
+ async handleLLMEnd(output: any, runId: string): Promise<void> {
148
+ // Increment counter immediately (synchronously) to avoid race condition
149
+ this._llmCallCount++
150
+
151
+ const operation = (async () => {
152
+ try {
153
+ const spanContext = this.spans.get(runId)
154
+ if (!spanContext) return
155
+
156
+ // Extract token usage from output
157
+ const usage = output.llmOutput?.tokenUsage || {}
158
+ const tokens = {
159
+ input: usage.promptTokens || usage.input_tokens || 0,
160
+ output: usage.completionTokens || usage.output_tokens || 0,
161
+ }
162
+
163
+ await tracer.endSpan(this.context, this.traceId, spanContext.spanId, {
164
+ output: {
165
+ generations: output.generations?.map((gen: any) => gen.text || gen.message?.content),
166
+ },
167
+ tokens: tokens.input || tokens.output ? tokens : undefined,
168
+ })
169
+
170
+ this.spans.delete(runId)
171
+ this.parentSpans.delete(runId)
172
+ } catch (error) {
173
+ console.error('[TracingCallbackHandler] handleLLMEnd error:', error)
174
+ }
175
+ })()
176
+ this.trackOperation(operation)
177
+ }
178
+
179
+ async handleLLMError(error: Error, runId: string): Promise<void> {
180
+ try {
181
+ const spanContext = this.spans.get(runId)
182
+ if (!spanContext) return
183
+
184
+ await tracer.endSpan(this.context, this.traceId, spanContext.spanId, {
185
+ error,
186
+ })
187
+
188
+ this.spans.delete(runId)
189
+ this.parentSpans.delete(runId)
190
+ } catch (err) {
191
+ console.error('[TracingCallbackHandler] handleLLMError error:', err)
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Tool Events
197
+ */
198
+
199
+ async handleToolStart(
200
+ tool: Serialized,
201
+ input: string,
202
+ runId: string,
203
+ parentRunId?: string
204
+ ): Promise<void> {
205
+ try {
206
+ const toolName = tool.id?.[tool.id.length - 1] || 'unknown'
207
+ const depth = parentRunId ? (this.spans.get(parentRunId)?.depth || 0) + 1 : 0
208
+
209
+ const spanContext = await tracer.startSpan(this.context, this.traceId, {
210
+ name: `Tool: ${toolName}`,
211
+ type: 'tool',
212
+ toolName,
213
+ parentSpanId: parentRunId ? this.parentSpans.get(parentRunId) : undefined,
214
+ depth,
215
+ input: { toolInput: input },
216
+ })
217
+
218
+ if (spanContext) {
219
+ this.spans.set(runId, spanContext)
220
+ if (parentRunId) {
221
+ this.parentSpans.set(runId, spanContext.spanId)
222
+ }
223
+ }
224
+ } catch (error) {
225
+ console.error('[TracingCallbackHandler] handleToolStart error:', error)
226
+ }
227
+ }
228
+
229
+ async handleToolEnd(output: string, runId: string): Promise<void> {
230
+ // Increment counter immediately (synchronously) to avoid race condition
231
+ this._toolCallCount++
232
+
233
+ const operation = (async () => {
234
+ try {
235
+ const spanContext = this.spans.get(runId)
236
+ if (!spanContext) return
237
+
238
+ await tracer.endSpan(this.context, this.traceId, spanContext.spanId, {
239
+ toolOutput: output,
240
+ })
241
+
242
+ this.spans.delete(runId)
243
+ this.parentSpans.delete(runId)
244
+ } catch (error) {
245
+ console.error('[TracingCallbackHandler] handleToolEnd error:', error)
246
+ }
247
+ })()
248
+ this.trackOperation(operation)
249
+ }
250
+
251
+ async handleToolError(error: Error, runId: string): Promise<void> {
252
+ try {
253
+ const spanContext = this.spans.get(runId)
254
+ if (!spanContext) return
255
+
256
+ await tracer.endSpan(this.context, this.traceId, spanContext.spanId, {
257
+ error,
258
+ })
259
+
260
+ this.spans.delete(runId)
261
+ this.parentSpans.delete(runId)
262
+ } catch (err) {
263
+ console.error('[TracingCallbackHandler] handleToolError error:', err)
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Chain Events
269
+ */
270
+
271
+ async handleChainStart(
272
+ chain: Serialized,
273
+ inputs: Record<string, unknown>,
274
+ runId: string,
275
+ parentRunId?: string
276
+ ): Promise<void> {
277
+ try {
278
+ const chainName = chain.id?.[chain.id.length - 1] || 'unknown'
279
+ const depth = parentRunId ? (this.spans.get(parentRunId)?.depth || 0) + 1 : 0
280
+
281
+ const spanContext = await tracer.startSpan(this.context, this.traceId, {
282
+ name: `Chain: ${chainName}`,
283
+ type: 'chain',
284
+ parentSpanId: parentRunId ? this.parentSpans.get(parentRunId) : undefined,
285
+ depth,
286
+ input: inputs,
287
+ })
288
+
289
+ if (spanContext) {
290
+ this.spans.set(runId, spanContext)
291
+ if (parentRunId) {
292
+ this.parentSpans.set(runId, spanContext.spanId)
293
+ }
294
+ }
295
+ } catch (error) {
296
+ console.error('[TracingCallbackHandler] handleChainStart error:', error)
297
+ }
298
+ }
299
+
300
+ async handleChainEnd(outputs: Record<string, unknown>, runId: string): Promise<void> {
301
+ try {
302
+ const spanContext = this.spans.get(runId)
303
+ if (!spanContext) return
304
+
305
+ await tracer.endSpan(this.context, this.traceId, spanContext.spanId, {
306
+ output: outputs,
307
+ })
308
+
309
+ this.spans.delete(runId)
310
+ this.parentSpans.delete(runId)
311
+ } catch (error) {
312
+ console.error('[TracingCallbackHandler] handleChainEnd error:', error)
313
+ }
314
+ }
315
+
316
+ async handleChainError(error: Error, runId: string): Promise<void> {
317
+ try {
318
+ const spanContext = this.spans.get(runId)
319
+ if (!spanContext) return
320
+
321
+ await tracer.endSpan(this.context, this.traceId, spanContext.spanId, {
322
+ error,
323
+ })
324
+
325
+ this.spans.delete(runId)
326
+ this.parentSpans.delete(runId)
327
+ } catch (err) {
328
+ console.error('[TracingCallbackHandler] handleChainError error:', err)
329
+ }
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Factory function to create tracing callbacks
335
+ */
336
+ export function createTracingCallbacks(
337
+ context: { userId: string; teamId: string },
338
+ traceId: string,
339
+ modelName?: string
340
+ ): TracingCallbackHandler {
341
+ return new TracingCallbackHandler({ context, traceId, modelName })
342
+ }