@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,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
|
+
}
|