@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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/langchain/observability/traces
|
|
3
|
+
*
|
|
4
|
+
* List traces with pagination and filters.
|
|
5
|
+
* Admin access required (superadmin or developer).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
9
|
+
import { authenticateRequest } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
10
|
+
import { queryWithRLS } from '@nextsparkjs/core/lib/db'
|
|
11
|
+
import type { Trace } from '../../../types/observability.types'
|
|
12
|
+
|
|
13
|
+
interface TraceRow {
|
|
14
|
+
traceId: string
|
|
15
|
+
userId: string
|
|
16
|
+
teamId: string
|
|
17
|
+
sessionId: string | null
|
|
18
|
+
agentName: string
|
|
19
|
+
agentType: string | null
|
|
20
|
+
parentId: string | null
|
|
21
|
+
input: string
|
|
22
|
+
output: string | null
|
|
23
|
+
status: 'running' | 'success' | 'error'
|
|
24
|
+
error: string | null
|
|
25
|
+
errorType: string | null
|
|
26
|
+
errorStack: string | null
|
|
27
|
+
startedAt: Date
|
|
28
|
+
endedAt: Date | null
|
|
29
|
+
durationMs: number | null
|
|
30
|
+
inputTokens: number
|
|
31
|
+
outputTokens: number
|
|
32
|
+
totalTokens: number
|
|
33
|
+
totalCost: number
|
|
34
|
+
llmCalls: number
|
|
35
|
+
toolCalls: number
|
|
36
|
+
metadata: Record<string, unknown>
|
|
37
|
+
tags: string[] | null
|
|
38
|
+
createdAt: Date
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function GET(req: NextRequest) {
|
|
42
|
+
// 1. Authenticate (superadmin only)
|
|
43
|
+
const authResult = await authenticateRequest(req)
|
|
44
|
+
if (!authResult.success || !authResult.user) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ success: false, error: 'Unauthorized' },
|
|
47
|
+
{ status: 401 }
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if user has admin-level access (superadmin or developer)
|
|
52
|
+
const adminRoles = ['superadmin', 'developer']
|
|
53
|
+
if (!adminRoles.includes(authResult.user.role)) {
|
|
54
|
+
return NextResponse.json(
|
|
55
|
+
{ success: false, error: 'Forbidden: Admin access required' },
|
|
56
|
+
{ status: 403 }
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// 2. Parse query parameters
|
|
62
|
+
const { searchParams } = new URL(req.url)
|
|
63
|
+
const status = searchParams.get('status')
|
|
64
|
+
const agent = searchParams.get('agent')
|
|
65
|
+
const teamId = searchParams.get('teamId')
|
|
66
|
+
const from = searchParams.get('from')
|
|
67
|
+
const to = searchParams.get('to')
|
|
68
|
+
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 100)
|
|
69
|
+
const cursor = searchParams.get('cursor')
|
|
70
|
+
|
|
71
|
+
// 3. Build query
|
|
72
|
+
const conditions: string[] = []
|
|
73
|
+
const params: unknown[] = []
|
|
74
|
+
let paramIndex = 1
|
|
75
|
+
|
|
76
|
+
if (status) {
|
|
77
|
+
conditions.push(`status = $${paramIndex}`)
|
|
78
|
+
params.push(status)
|
|
79
|
+
paramIndex++
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (agent) {
|
|
83
|
+
conditions.push(`"agentName" = $${paramIndex}`)
|
|
84
|
+
params.push(agent)
|
|
85
|
+
paramIndex++
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (teamId) {
|
|
89
|
+
conditions.push(`"teamId" = $${paramIndex}`)
|
|
90
|
+
params.push(teamId)
|
|
91
|
+
paramIndex++
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (from) {
|
|
95
|
+
conditions.push(`"startedAt" >= $${paramIndex}`)
|
|
96
|
+
params.push(from)
|
|
97
|
+
paramIndex++
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (to) {
|
|
101
|
+
conditions.push(`"startedAt" <= $${paramIndex}`)
|
|
102
|
+
params.push(to)
|
|
103
|
+
paramIndex++
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (cursor) {
|
|
107
|
+
conditions.push(`"createdAt" < $${paramIndex}`)
|
|
108
|
+
params.push(cursor)
|
|
109
|
+
paramIndex++
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Always filter to show only root traces (no parent)
|
|
113
|
+
conditions.push(`"parentId" IS NULL`)
|
|
114
|
+
|
|
115
|
+
const whereClause = `WHERE ${conditions.join(' AND ')}`
|
|
116
|
+
|
|
117
|
+
// Add limit + 1 to check if there are more results
|
|
118
|
+
params.push(limit + 1)
|
|
119
|
+
const limitClause = `LIMIT $${paramIndex}`
|
|
120
|
+
|
|
121
|
+
const query = `
|
|
122
|
+
SELECT
|
|
123
|
+
"traceId",
|
|
124
|
+
"userId",
|
|
125
|
+
"teamId",
|
|
126
|
+
"sessionId",
|
|
127
|
+
"agentName",
|
|
128
|
+
"agentType",
|
|
129
|
+
"parentId",
|
|
130
|
+
input,
|
|
131
|
+
output,
|
|
132
|
+
status,
|
|
133
|
+
error,
|
|
134
|
+
"errorType",
|
|
135
|
+
"errorStack",
|
|
136
|
+
"startedAt",
|
|
137
|
+
"endedAt",
|
|
138
|
+
"durationMs",
|
|
139
|
+
"inputTokens",
|
|
140
|
+
"outputTokens",
|
|
141
|
+
"totalTokens",
|
|
142
|
+
"totalCost",
|
|
143
|
+
"llmCalls",
|
|
144
|
+
"toolCalls",
|
|
145
|
+
metadata,
|
|
146
|
+
tags,
|
|
147
|
+
"createdAt"
|
|
148
|
+
FROM public."langchain_traces"
|
|
149
|
+
${whereClause}
|
|
150
|
+
ORDER BY "createdAt" DESC
|
|
151
|
+
${limitClause}
|
|
152
|
+
`
|
|
153
|
+
|
|
154
|
+
const rows = await queryWithRLS<TraceRow>(query, params, authResult.user.id)
|
|
155
|
+
|
|
156
|
+
// Check if there are more results
|
|
157
|
+
const hasMore = rows.length > limit
|
|
158
|
+
const traces = hasMore ? rows.slice(0, limit) : rows
|
|
159
|
+
const nextCursor = hasMore ? traces[traces.length - 1].createdAt.toISOString() : undefined
|
|
160
|
+
|
|
161
|
+
// 4. Format response
|
|
162
|
+
const formattedTraces: Trace[] = traces.map((row) => ({
|
|
163
|
+
traceId: row.traceId,
|
|
164
|
+
userId: row.userId,
|
|
165
|
+
teamId: row.teamId,
|
|
166
|
+
sessionId: row.sessionId || undefined,
|
|
167
|
+
agentName: row.agentName,
|
|
168
|
+
agentType: row.agentType || undefined,
|
|
169
|
+
parentId: row.parentId || undefined,
|
|
170
|
+
input: row.input,
|
|
171
|
+
output: row.output || undefined,
|
|
172
|
+
status: row.status,
|
|
173
|
+
error: row.error || undefined,
|
|
174
|
+
errorType: row.errorType || undefined,
|
|
175
|
+
errorStack: row.errorStack || undefined,
|
|
176
|
+
startedAt: row.startedAt.toISOString(),
|
|
177
|
+
endedAt: row.endedAt?.toISOString(),
|
|
178
|
+
durationMs: row.durationMs || undefined,
|
|
179
|
+
inputTokens: row.inputTokens,
|
|
180
|
+
outputTokens: row.outputTokens,
|
|
181
|
+
totalTokens: row.totalTokens,
|
|
182
|
+
totalCost: row.totalCost,
|
|
183
|
+
llmCalls: row.llmCalls,
|
|
184
|
+
toolCalls: row.toolCalls,
|
|
185
|
+
metadata: row.metadata,
|
|
186
|
+
tags: row.tags || undefined,
|
|
187
|
+
createdAt: row.createdAt.toISOString(),
|
|
188
|
+
}))
|
|
189
|
+
|
|
190
|
+
return NextResponse.json({
|
|
191
|
+
success: true,
|
|
192
|
+
data: {
|
|
193
|
+
traces: formattedTraces,
|
|
194
|
+
hasMore,
|
|
195
|
+
nextCursor,
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('[Observability API] List traces error:', error)
|
|
200
|
+
return NextResponse.json(
|
|
201
|
+
{ success: false, error: 'Failed to list traces' },
|
|
202
|
+
{ status: 500 }
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { authenticateRequest } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
3
|
+
import { dbMemoryStore, CONVERSATION_LIMITS } from '../../lib/db-memory-store'
|
|
4
|
+
import { config } from '../../plugin.config'
|
|
5
|
+
import type {
|
|
6
|
+
CreateConversationRequest,
|
|
7
|
+
UpdateConversationRequest,
|
|
8
|
+
ConversationInfo,
|
|
9
|
+
} from '../../types/langchain.types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert Date to ISO string for API responses
|
|
13
|
+
*/
|
|
14
|
+
function toApiConversationInfo(conv: {
|
|
15
|
+
sessionId: string
|
|
16
|
+
name: string | null
|
|
17
|
+
messageCount: number
|
|
18
|
+
firstMessage: string | null
|
|
19
|
+
isPinned: boolean
|
|
20
|
+
createdAt: Date
|
|
21
|
+
updatedAt: Date
|
|
22
|
+
}): ConversationInfo {
|
|
23
|
+
return {
|
|
24
|
+
...conv,
|
|
25
|
+
createdAt: conv.createdAt.toISOString(),
|
|
26
|
+
updatedAt: conv.updatedAt.toISOString(),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* GET - List all conversations or get single conversation
|
|
32
|
+
*
|
|
33
|
+
* Query params:
|
|
34
|
+
* - id: Session ID to get specific conversation
|
|
35
|
+
*
|
|
36
|
+
* Without id: returns list of all conversations
|
|
37
|
+
* With id: returns single conversation details
|
|
38
|
+
*/
|
|
39
|
+
export async function GET(req: NextRequest) {
|
|
40
|
+
// 1. Auth
|
|
41
|
+
const authResult = await authenticateRequest(req)
|
|
42
|
+
if (!authResult.success || !authResult.user) {
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ success: false, error: 'Unauthorized' },
|
|
45
|
+
{ status: 401 }
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const userId = authResult.user.id
|
|
50
|
+
|
|
51
|
+
// 2. Team context
|
|
52
|
+
const teamId = req.headers.get('x-team-id')
|
|
53
|
+
if (!teamId) {
|
|
54
|
+
return NextResponse.json(
|
|
55
|
+
{ success: false, error: 'Team context required', code: 'TEAM_CONTEXT_REQUIRED' },
|
|
56
|
+
{ status: 400 }
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const context = { userId, teamId }
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Check if requesting specific session
|
|
64
|
+
const { searchParams } = new URL(req.url)
|
|
65
|
+
const sessionId = searchParams.get('id')
|
|
66
|
+
|
|
67
|
+
if (sessionId) {
|
|
68
|
+
// Get single conversation
|
|
69
|
+
const session = await dbMemoryStore.getSession(sessionId, context)
|
|
70
|
+
|
|
71
|
+
if (!session) {
|
|
72
|
+
return NextResponse.json(
|
|
73
|
+
{ success: false, error: 'Conversation not found' },
|
|
74
|
+
{ status: 404 }
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return NextResponse.json({
|
|
79
|
+
success: true,
|
|
80
|
+
data: toApiConversationInfo(session),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// List all conversations
|
|
85
|
+
const sessions = await dbMemoryStore.listSessions(context)
|
|
86
|
+
|
|
87
|
+
return NextResponse.json({
|
|
88
|
+
success: true,
|
|
89
|
+
data: {
|
|
90
|
+
sessions: sessions.map(toApiConversationInfo),
|
|
91
|
+
count: sessions.length,
|
|
92
|
+
maxAllowed: CONVERSATION_LIMITS.MAX_CONVERSATIONS,
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (config.debug) {
|
|
97
|
+
console.error('[LangChain Plugin] List sessions error:', error)
|
|
98
|
+
}
|
|
99
|
+
return NextResponse.json(
|
|
100
|
+
{ success: false, error: 'Failed to list conversations' },
|
|
101
|
+
{ status: 500 }
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* POST - Create a new conversation
|
|
108
|
+
*
|
|
109
|
+
* Body:
|
|
110
|
+
* - name: Optional name for the conversation
|
|
111
|
+
*/
|
|
112
|
+
export async function POST(req: NextRequest) {
|
|
113
|
+
// 1. Auth
|
|
114
|
+
const authResult = await authenticateRequest(req)
|
|
115
|
+
if (!authResult.success || !authResult.user) {
|
|
116
|
+
return NextResponse.json(
|
|
117
|
+
{ success: false, error: 'Unauthorized' },
|
|
118
|
+
{ status: 401 }
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const userId = authResult.user.id
|
|
123
|
+
|
|
124
|
+
// 2. Team context
|
|
125
|
+
const teamId = req.headers.get('x-team-id')
|
|
126
|
+
if (!teamId) {
|
|
127
|
+
return NextResponse.json(
|
|
128
|
+
{ success: false, error: 'Team context required', code: 'TEAM_CONTEXT_REQUIRED' },
|
|
129
|
+
{ status: 400 }
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const context = { userId, teamId }
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// 3. Check conversation limit
|
|
137
|
+
const currentCount = await dbMemoryStore.countSessions(context)
|
|
138
|
+
|
|
139
|
+
if (currentCount >= CONVERSATION_LIMITS.MAX_CONVERSATIONS) {
|
|
140
|
+
const oldestSession = await dbMemoryStore.getOldestSession(context)
|
|
141
|
+
|
|
142
|
+
return NextResponse.json({
|
|
143
|
+
success: false,
|
|
144
|
+
error: 'CONVERSATION_LIMIT_REACHED',
|
|
145
|
+
message: `Maximum of ${CONVERSATION_LIMITS.MAX_CONVERSATIONS} conversations reached. Delete an existing conversation to create a new one.`,
|
|
146
|
+
data: {
|
|
147
|
+
currentCount,
|
|
148
|
+
maxAllowed: CONVERSATION_LIMITS.MAX_CONVERSATIONS,
|
|
149
|
+
oldestSession: oldestSession
|
|
150
|
+
? {
|
|
151
|
+
sessionId: oldestSession.sessionId,
|
|
152
|
+
name: oldestSession.name,
|
|
153
|
+
updatedAt: oldestSession.updatedAt.toISOString(),
|
|
154
|
+
}
|
|
155
|
+
: null,
|
|
156
|
+
},
|
|
157
|
+
}, { status: 400 })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 4. Parse body
|
|
161
|
+
const body: CreateConversationRequest = await req.json().catch(() => ({}))
|
|
162
|
+
const { name } = body
|
|
163
|
+
|
|
164
|
+
// 5. Create session
|
|
165
|
+
const result = await dbMemoryStore.createSession(context, name)
|
|
166
|
+
|
|
167
|
+
return NextResponse.json({
|
|
168
|
+
success: true,
|
|
169
|
+
data: {
|
|
170
|
+
sessionId: result.sessionId,
|
|
171
|
+
name: name || null,
|
|
172
|
+
createdAt: result.createdAt.toISOString(),
|
|
173
|
+
},
|
|
174
|
+
}, { status: 201 })
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (config.debug) {
|
|
177
|
+
console.error('[LangChain Plugin] Create session error:', error)
|
|
178
|
+
}
|
|
179
|
+
return NextResponse.json(
|
|
180
|
+
{ success: false, error: 'Failed to create conversation' },
|
|
181
|
+
{ status: 500 }
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* PATCH - Update a conversation (rename, pin/unpin)
|
|
188
|
+
*
|
|
189
|
+
* Body:
|
|
190
|
+
* - sessionId: Session ID to update (required)
|
|
191
|
+
* - name: New name (optional)
|
|
192
|
+
* - isPinned: New pin status (optional)
|
|
193
|
+
*/
|
|
194
|
+
export async function PATCH(req: NextRequest) {
|
|
195
|
+
// 1. Auth
|
|
196
|
+
const authResult = await authenticateRequest(req)
|
|
197
|
+
if (!authResult.success || !authResult.user) {
|
|
198
|
+
return NextResponse.json(
|
|
199
|
+
{ success: false, error: 'Unauthorized' },
|
|
200
|
+
{ status: 401 }
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const userId = authResult.user.id
|
|
205
|
+
|
|
206
|
+
// 2. Team context
|
|
207
|
+
const teamId = req.headers.get('x-team-id')
|
|
208
|
+
if (!teamId) {
|
|
209
|
+
return NextResponse.json(
|
|
210
|
+
{ success: false, error: 'Team context required', code: 'TEAM_CONTEXT_REQUIRED' },
|
|
211
|
+
{ status: 400 }
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const context = { userId, teamId }
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// 3. Parse body
|
|
219
|
+
const body: UpdateConversationRequest = await req.json()
|
|
220
|
+
const { sessionId, name, isPinned } = body
|
|
221
|
+
|
|
222
|
+
if (!sessionId) {
|
|
223
|
+
return NextResponse.json(
|
|
224
|
+
{ success: false, error: 'Session ID is required' },
|
|
225
|
+
{ status: 400 }
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 4. Check if session exists
|
|
230
|
+
const existingSession = await dbMemoryStore.getSession(sessionId, context)
|
|
231
|
+
if (!existingSession) {
|
|
232
|
+
return NextResponse.json(
|
|
233
|
+
{ success: false, error: 'Conversation not found' },
|
|
234
|
+
{ status: 404 }
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 5. Update fields
|
|
239
|
+
if (name !== undefined) {
|
|
240
|
+
await dbMemoryStore.renameSession(sessionId, name, context)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (isPinned !== undefined) {
|
|
244
|
+
await dbMemoryStore.togglePinSession(sessionId, isPinned, context)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 6. Get updated session
|
|
248
|
+
const updatedSession = await dbMemoryStore.getSession(sessionId, context)
|
|
249
|
+
|
|
250
|
+
return NextResponse.json({
|
|
251
|
+
success: true,
|
|
252
|
+
data: updatedSession ? toApiConversationInfo(updatedSession) : null,
|
|
253
|
+
})
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (config.debug) {
|
|
256
|
+
console.error('[LangChain Plugin] Update session error:', error)
|
|
257
|
+
}
|
|
258
|
+
return NextResponse.json(
|
|
259
|
+
{ success: false, error: 'Failed to update conversation' },
|
|
260
|
+
{ status: 500 }
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* DELETE - Delete a conversation
|
|
267
|
+
*
|
|
268
|
+
* Body:
|
|
269
|
+
* - sessionId: Session ID to delete (required)
|
|
270
|
+
*/
|
|
271
|
+
export async function DELETE(req: NextRequest) {
|
|
272
|
+
// 1. Auth
|
|
273
|
+
const authResult = await authenticateRequest(req)
|
|
274
|
+
if (!authResult.success || !authResult.user) {
|
|
275
|
+
return NextResponse.json(
|
|
276
|
+
{ success: false, error: 'Unauthorized' },
|
|
277
|
+
{ status: 401 }
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const userId = authResult.user.id
|
|
282
|
+
|
|
283
|
+
// 2. Team context
|
|
284
|
+
const teamId = req.headers.get('x-team-id')
|
|
285
|
+
if (!teamId) {
|
|
286
|
+
return NextResponse.json(
|
|
287
|
+
{ success: false, error: 'Team context required', code: 'TEAM_CONTEXT_REQUIRED' },
|
|
288
|
+
{ status: 400 }
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const context = { userId, teamId }
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
// 3. Parse body
|
|
296
|
+
const body: { sessionId?: string } = await req.json()
|
|
297
|
+
const { sessionId } = body
|
|
298
|
+
|
|
299
|
+
if (!sessionId) {
|
|
300
|
+
return NextResponse.json(
|
|
301
|
+
{ success: false, error: 'Session ID is required' },
|
|
302
|
+
{ status: 400 }
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 4. Check if session exists
|
|
307
|
+
const existingSession = await dbMemoryStore.getSession(sessionId, context)
|
|
308
|
+
if (!existingSession) {
|
|
309
|
+
return NextResponse.json(
|
|
310
|
+
{ success: false, error: 'Conversation not found' },
|
|
311
|
+
{ status: 404 }
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 5. Delete session
|
|
316
|
+
await dbMemoryStore.clearSession(sessionId, context)
|
|
317
|
+
|
|
318
|
+
return NextResponse.json({
|
|
319
|
+
success: true,
|
|
320
|
+
message: 'Conversation deleted successfully',
|
|
321
|
+
sessionId,
|
|
322
|
+
})
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (config.debug) {
|
|
325
|
+
console.error('[LangChain Plugin] Delete session error:', error)
|
|
326
|
+
}
|
|
327
|
+
return NextResponse.json(
|
|
328
|
+
{ success: false, error: 'Failed to delete conversation' },
|
|
329
|
+
{ status: 500 }
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useTranslations } from 'next-intl'
|
|
5
|
+
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
6
|
+
|
|
7
|
+
interface CollapsibleJsonProps {
|
|
8
|
+
data: unknown
|
|
9
|
+
maxPreviewLength?: number
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Collapsible JSON viewer that shows a truncated preview by default.
|
|
15
|
+
* Useful for long JSON objects in conversation flows.
|
|
16
|
+
*/
|
|
17
|
+
export function CollapsibleJson({ data, maxPreviewLength = 100, className = '' }: CollapsibleJsonProps) {
|
|
18
|
+
const t = useTranslations('observability')
|
|
19
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
20
|
+
|
|
21
|
+
// Format the JSON
|
|
22
|
+
const formattedJson = JSON.stringify(data, null, 2)
|
|
23
|
+
|
|
24
|
+
// Check if content is long enough to need collapsing
|
|
25
|
+
const needsCollapsing = formattedJson.length > maxPreviewLength
|
|
26
|
+
|
|
27
|
+
// Create preview (first maxPreviewLength chars + ellipsis)
|
|
28
|
+
const preview = needsCollapsing
|
|
29
|
+
? formattedJson.slice(0, maxPreviewLength) + '...'
|
|
30
|
+
: formattedJson
|
|
31
|
+
|
|
32
|
+
// Check if content contains error-related keywords
|
|
33
|
+
const hasError =
|
|
34
|
+
formattedJson.toLowerCase().includes('"error"') ||
|
|
35
|
+
formattedJson.toLowerCase().includes('"error_type"') ||
|
|
36
|
+
formattedJson.toLowerCase().includes('failed') ||
|
|
37
|
+
formattedJson.toLowerCase().includes('exception')
|
|
38
|
+
|
|
39
|
+
const textColorClass = hasError
|
|
40
|
+
? 'text-destructive'
|
|
41
|
+
: 'text-foreground'
|
|
42
|
+
|
|
43
|
+
const bgColorClass = hasError
|
|
44
|
+
? 'bg-destructive/10'
|
|
45
|
+
: 'bg-muted'
|
|
46
|
+
|
|
47
|
+
if (!needsCollapsing) {
|
|
48
|
+
return (
|
|
49
|
+
<pre className={`text-xs p-2 rounded overflow-x-auto whitespace-pre-wrap ${bgColorClass} ${textColorClass} ${className}`}>
|
|
50
|
+
{formattedJson}
|
|
51
|
+
</pre>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={className}>
|
|
57
|
+
<pre className={`text-xs p-2 rounded overflow-x-auto whitespace-pre-wrap ${bgColorClass} ${textColorClass}`}>
|
|
58
|
+
{isExpanded ? formattedJson : preview}
|
|
59
|
+
</pre>
|
|
60
|
+
<Button
|
|
61
|
+
variant="ghost"
|
|
62
|
+
size="sm"
|
|
63
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
64
|
+
className="mt-1 h-6 text-xs"
|
|
65
|
+
data-cy="toggle-json"
|
|
66
|
+
>
|
|
67
|
+
{isExpanded ? t('flow.collapse') : t('flow.showFull')}
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Span } from '../../types/observability.types'
|
|
4
|
+
|
|
5
|
+
interface CompactTimelineProps {
|
|
6
|
+
spans: Span[]
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compact horizontal timeline showing execution flow with icons.
|
|
12
|
+
* Shows: User → LLM → Tool (name) ✓/✗ → LLM → Response
|
|
13
|
+
*/
|
|
14
|
+
export function CompactTimeline({ spans, className = '' }: CompactTimelineProps) {
|
|
15
|
+
// Filter to show only meaningful spans (LLM and tool)
|
|
16
|
+
const meaningfulSpans = spans.filter(
|
|
17
|
+
(span) => span.type === 'llm' || span.type === 'tool'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if (meaningfulSpans.length === 0) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const getSpanIcon = (span: Span) => {
|
|
25
|
+
if (span.type === 'llm') return '🧠'
|
|
26
|
+
if (span.type === 'tool') return '🔧'
|
|
27
|
+
return '📦'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const getStatusIcon = (status: string) => {
|
|
31
|
+
if (status === 'success') return '✓'
|
|
32
|
+
if (status === 'error') return '✗'
|
|
33
|
+
return '...'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const getStatusColor = (status: string) => {
|
|
37
|
+
if (status === 'success') return 'text-green-600 dark:text-green-400'
|
|
38
|
+
if (status === 'error') return 'text-red-600 dark:text-red-400'
|
|
39
|
+
return 'text-yellow-600 dark:text-yellow-400'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={`flex flex-wrap items-center gap-1 text-sm ${className}`} data-cy="compact-timeline">
|
|
44
|
+
{/* User icon at start */}
|
|
45
|
+
<span className="text-base" title="User input">👤</span>
|
|
46
|
+
<span className="text-muted-foreground">→</span>
|
|
47
|
+
|
|
48
|
+
{meaningfulSpans.map((span, index) => (
|
|
49
|
+
<div key={span.spanId} className="flex items-center gap-1">
|
|
50
|
+
<span className="text-base" title={span.name}>
|
|
51
|
+
{getSpanIcon(span)}
|
|
52
|
+
</span>
|
|
53
|
+
|
|
54
|
+
{span.type === 'tool' && span.toolName && (
|
|
55
|
+
<span className="text-xs text-muted-foreground bg-muted px-1 rounded">
|
|
56
|
+
{span.toolName}
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
<span className={`text-xs font-bold ${getStatusColor(span.status)}`}>
|
|
61
|
+
{getStatusIcon(span.status)}
|
|
62
|
+
</span>
|
|
63
|
+
|
|
64
|
+
{index < meaningfulSpans.length - 1 && (
|
|
65
|
+
<span className="text-muted-foreground ml-1">→</span>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
))}
|
|
69
|
+
|
|
70
|
+
{/* Response icon at end */}
|
|
71
|
+
<span className="text-muted-foreground">→</span>
|
|
72
|
+
<span className="text-base" title="AI Response">🤖</span>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|