@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,210 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react'
|
|
4
|
+
import { useTranslations } from 'next-intl'
|
|
5
|
+
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
6
|
+
import type { Span } from '../../types/observability.types'
|
|
7
|
+
import { TraceStatusBadge } from './TraceStatusBadge'
|
|
8
|
+
|
|
9
|
+
// Internal chain patterns to filter (LangChain infrastructure noise)
|
|
10
|
+
const INTERNAL_CHAIN_PATTERNS = [
|
|
11
|
+
/^Chain:\s*(Runnable|Channel|Compiled)/i,
|
|
12
|
+
/^RunnableSequence$/i,
|
|
13
|
+
/^RunnableLambda$/i,
|
|
14
|
+
/^ChannelWrite$/i,
|
|
15
|
+
/^ChannelRead$/i,
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
interface SpansListProps {
|
|
19
|
+
spans: Span[]
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function SpansList({ spans, className = '' }: SpansListProps) {
|
|
24
|
+
const t = useTranslations('observability')
|
|
25
|
+
const [expandedSpans, setExpandedSpans] = useState<Set<string>>(new Set())
|
|
26
|
+
const [showInternalChains, setShowInternalChains] = useState(false)
|
|
27
|
+
|
|
28
|
+
// Filter internal chain spans
|
|
29
|
+
const { displaySpans, hiddenCount } = useMemo(() => {
|
|
30
|
+
if (showInternalChains) {
|
|
31
|
+
return { displaySpans: spans, hiddenCount: 0 }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const filtered = spans.filter((span) => {
|
|
35
|
+
// Always show non-chain spans
|
|
36
|
+
if (span.type !== 'chain') return true
|
|
37
|
+
// Always show chains with errors
|
|
38
|
+
if (span.error) return true
|
|
39
|
+
// Filter internal chains by pattern
|
|
40
|
+
return !INTERNAL_CHAIN_PATTERNS.some((pattern) => pattern.test(span.name))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
displaySpans: filtered,
|
|
45
|
+
hiddenCount: spans.length - filtered.length,
|
|
46
|
+
}
|
|
47
|
+
}, [spans, showInternalChains])
|
|
48
|
+
|
|
49
|
+
const toggleSpan = (spanId: string) => {
|
|
50
|
+
setExpandedSpans((prev) => {
|
|
51
|
+
const next = new Set(prev)
|
|
52
|
+
if (next.has(spanId)) {
|
|
53
|
+
next.delete(spanId)
|
|
54
|
+
} else {
|
|
55
|
+
next.add(spanId)
|
|
56
|
+
}
|
|
57
|
+
return next
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const formatDuration = (ms?: number) => {
|
|
62
|
+
if (!ms) return '-'
|
|
63
|
+
if (ms < 1000) return `${ms}ms`
|
|
64
|
+
return `${(ms / 1000).toFixed(2)}s`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const formatTokens = (input?: number, output?: number) => {
|
|
68
|
+
if (!input && !output) return null
|
|
69
|
+
return `${input || 0}/${output || 0} tokens`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const getSpanTypeLabel = (type: string) => {
|
|
73
|
+
return type.toUpperCase()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (spans.length === 0) {
|
|
77
|
+
return (
|
|
78
|
+
<div className="text-center py-8 text-muted-foreground" data-cy="spans-list-empty">
|
|
79
|
+
{t('detail.noSpans')}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className={className} data-cy="spans-list">
|
|
86
|
+
{/* Filter toggle */}
|
|
87
|
+
{hiddenCount > 0 && (
|
|
88
|
+
<div className="flex items-center justify-between mb-3 p-2 bg-muted/50 rounded-lg" data-cy="spans-filter-info">
|
|
89
|
+
<span className="text-sm text-muted-foreground">
|
|
90
|
+
{t('detail.hiddenSpans', { count: hiddenCount })}
|
|
91
|
+
</span>
|
|
92
|
+
<Button
|
|
93
|
+
variant="ghost"
|
|
94
|
+
size="sm"
|
|
95
|
+
onClick={() => setShowInternalChains(!showInternalChains)}
|
|
96
|
+
data-cy="toggle-internal-chains"
|
|
97
|
+
>
|
|
98
|
+
{showInternalChains ? t('detail.hideInternalChains') : t('detail.showInternalChains')}
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
{showInternalChains && hiddenCount > 0 && (
|
|
103
|
+
<div className="flex justify-end mb-3">
|
|
104
|
+
<Button
|
|
105
|
+
variant="ghost"
|
|
106
|
+
size="sm"
|
|
107
|
+
onClick={() => setShowInternalChains(false)}
|
|
108
|
+
data-cy="hide-internal-chains"
|
|
109
|
+
>
|
|
110
|
+
{t('detail.hideInternalChains')}
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
<div className="space-y-2">
|
|
116
|
+
{displaySpans.map((span) => {
|
|
117
|
+
const isExpanded = expandedSpans.has(span.spanId)
|
|
118
|
+
const hasDetails =
|
|
119
|
+
(span.toolInput !== undefined && span.toolInput !== null) ||
|
|
120
|
+
(span.toolOutput !== undefined && span.toolOutput !== null) ||
|
|
121
|
+
!!span.error
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
key={span.spanId}
|
|
126
|
+
className="border border-border rounded-lg p-3 bg-card"
|
|
127
|
+
data-cy={`span-item-${span.spanId}`}
|
|
128
|
+
style={{ marginLeft: `${span.depth * 24}px` }}
|
|
129
|
+
>
|
|
130
|
+
<div className="flex items-start justify-between gap-4">
|
|
131
|
+
<div className="flex-1">
|
|
132
|
+
<div className="flex items-center gap-2 mb-1">
|
|
133
|
+
<span className="text-[10px] font-mono uppercase tracking-wider text-muted-foreground bg-muted px-1.5 py-0.5 rounded" data-cy="span-type">
|
|
134
|
+
{getSpanTypeLabel(span.type)}
|
|
135
|
+
</span>
|
|
136
|
+
<span className="font-medium text-foreground" data-cy="span-name">
|
|
137
|
+
{span.name}
|
|
138
|
+
</span>
|
|
139
|
+
<TraceStatusBadge status={span.status} />
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
|
143
|
+
{span.provider && (
|
|
144
|
+
<span data-cy="span-provider">
|
|
145
|
+
{span.provider} / {span.model}
|
|
146
|
+
</span>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{span.toolName && <span data-cy="span-tool-name">{span.toolName}</span>}
|
|
150
|
+
|
|
151
|
+
<span data-cy="span-duration">{formatDuration(span.durationMs)}</span>
|
|
152
|
+
|
|
153
|
+
{formatTokens(span.inputTokens, span.outputTokens) && (
|
|
154
|
+
<span data-cy="span-tokens">{formatTokens(span.inputTokens, span.outputTokens)}</span>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{hasDetails && (
|
|
159
|
+
<button
|
|
160
|
+
onClick={() => toggleSpan(span.spanId)}
|
|
161
|
+
className="mt-2 text-sm text-primary hover:underline"
|
|
162
|
+
data-cy={`span-toggle-${span.spanId}`}
|
|
163
|
+
>
|
|
164
|
+
{isExpanded ? t('detail.hideDetails') : t('detail.showDetails')}
|
|
165
|
+
</button>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{isExpanded && (
|
|
169
|
+
<div className="mt-3 space-y-2" data-cy={`span-details-${span.spanId}`}>
|
|
170
|
+
{span.toolInput !== undefined && span.toolInput !== null && (
|
|
171
|
+
<div>
|
|
172
|
+
<div className="text-sm font-medium text-foreground mb-1">
|
|
173
|
+
{t('detail.toolInput')}
|
|
174
|
+
</div>
|
|
175
|
+
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
|
|
176
|
+
{JSON.stringify(span.toolInput, null, 2)}
|
|
177
|
+
</pre>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{span.toolOutput !== undefined && span.toolOutput !== null && (
|
|
182
|
+
<div>
|
|
183
|
+
<div className="text-sm font-medium text-foreground mb-1">
|
|
184
|
+
{t('detail.toolOutput')}
|
|
185
|
+
</div>
|
|
186
|
+
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
|
|
187
|
+
{JSON.stringify(span.toolOutput, null, 2)}
|
|
188
|
+
</pre>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{span.error && (
|
|
193
|
+
<div>
|
|
194
|
+
<div className="text-sm font-medium text-destructive mb-1">{t('detail.error')}</div>
|
|
195
|
+
<pre className="text-xs bg-destructive/10 text-destructive p-2 rounded overflow-x-auto">
|
|
196
|
+
{span.error}
|
|
197
|
+
</pre>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
})}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
import { useTranslations } from 'next-intl'
|
|
5
|
+
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@nextsparkjs/core/components/ui/card'
|
|
7
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@nextsparkjs/core/components/ui/accordion'
|
|
8
|
+
import type { Trace, Span } from '../../types/observability.types'
|
|
9
|
+
import { TraceStatusBadge } from './TraceStatusBadge'
|
|
10
|
+
import { SpansList } from './SpansList'
|
|
11
|
+
import { ConversationFlow } from './ConversationFlow'
|
|
12
|
+
import { CompactTimeline } from './CompactTimeline'
|
|
13
|
+
|
|
14
|
+
interface ParentTraceInfo {
|
|
15
|
+
traceId: string
|
|
16
|
+
agentName: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TraceDetailProps {
|
|
20
|
+
trace: Trace
|
|
21
|
+
spans: Span[]
|
|
22
|
+
childTraces?: Trace[]
|
|
23
|
+
childSpansMap?: Record<string, Span[]>
|
|
24
|
+
parentTrace?: ParentTraceInfo
|
|
25
|
+
onBack: () => void
|
|
26
|
+
onSelectChildTrace?: (traceId: string) => void
|
|
27
|
+
onSelectParentTrace?: (traceId: string) => void
|
|
28
|
+
className?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function TraceDetail({ trace, spans, childTraces = [], childSpansMap = {}, parentTrace, onBack, onSelectChildTrace, onSelectParentTrace, className = '' }: TraceDetailProps) {
|
|
32
|
+
const t = useTranslations('observability')
|
|
33
|
+
|
|
34
|
+
const formatDuration = (ms?: number) => {
|
|
35
|
+
if (!ms) return '-'
|
|
36
|
+
if (ms < 1000) return `${ms}ms`
|
|
37
|
+
return `${(ms / 1000).toFixed(2)}s`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const formatTokens = (tokens: number) => {
|
|
41
|
+
if (tokens === 0) return '-'
|
|
42
|
+
return tokens.toLocaleString()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Extract unique tools used in this trace
|
|
46
|
+
const toolsUsed = useMemo(() => {
|
|
47
|
+
return [...new Set(
|
|
48
|
+
spans
|
|
49
|
+
.filter((s) => s.type === 'tool' && s.toolName)
|
|
50
|
+
.map((s) => s.toolName!)
|
|
51
|
+
)]
|
|
52
|
+
}, [spans])
|
|
53
|
+
|
|
54
|
+
// Extract model info from LLM spans
|
|
55
|
+
const modelInfo = useMemo(() => {
|
|
56
|
+
const llmSpan = spans.find((s) => s.type === 'llm' && s.model)
|
|
57
|
+
if (!llmSpan) return null
|
|
58
|
+
return {
|
|
59
|
+
model: llmSpan.model,
|
|
60
|
+
provider: llmSpan.provider,
|
|
61
|
+
}
|
|
62
|
+
}, [spans])
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className={className} data-cy="trace-detail">
|
|
66
|
+
<div className="mb-6">
|
|
67
|
+
<div className="flex items-center gap-2 mb-4">
|
|
68
|
+
<Button variant="ghost" onClick={onBack} data-cy="trace-back-button">
|
|
69
|
+
← {t('detail.back')}
|
|
70
|
+
</Button>
|
|
71
|
+
{parentTrace && onSelectParentTrace && (
|
|
72
|
+
<>
|
|
73
|
+
<span className="text-muted-foreground">/</span>
|
|
74
|
+
<Button
|
|
75
|
+
variant="link"
|
|
76
|
+
onClick={() => onSelectParentTrace(parentTrace.traceId)}
|
|
77
|
+
className="text-primary hover:underline p-0 h-auto font-medium"
|
|
78
|
+
data-cy="parent-trace-link"
|
|
79
|
+
>
|
|
80
|
+
{parentTrace.agentName}
|
|
81
|
+
</Button>
|
|
82
|
+
<span className="text-muted-foreground">/</span>
|
|
83
|
+
<span className="font-medium">{trace.agentName}</span>
|
|
84
|
+
</>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="flex items-start justify-between">
|
|
89
|
+
<div>
|
|
90
|
+
<h2 className="text-2xl font-bold mb-2">{t('detail.title')}</h2>
|
|
91
|
+
<p className="text-sm text-muted-foreground font-mono">{trace.traceId}</p>
|
|
92
|
+
</div>
|
|
93
|
+
<TraceStatusBadge status={trace.status} />
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
|
|
98
|
+
<Card>
|
|
99
|
+
<CardHeader className="pb-2">
|
|
100
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
101
|
+
{t('detail.agent')}
|
|
102
|
+
</CardTitle>
|
|
103
|
+
</CardHeader>
|
|
104
|
+
<CardContent>
|
|
105
|
+
<p className="text-lg font-semibold" data-cy="trace-agent">
|
|
106
|
+
{trace.agentName}
|
|
107
|
+
</p>
|
|
108
|
+
{trace.agentType && (
|
|
109
|
+
<p className="text-sm text-muted-foreground">{trace.agentType}</p>
|
|
110
|
+
)}
|
|
111
|
+
</CardContent>
|
|
112
|
+
</Card>
|
|
113
|
+
|
|
114
|
+
<Card>
|
|
115
|
+
<CardHeader className="pb-2">
|
|
116
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
117
|
+
{t('detail.model')}
|
|
118
|
+
</CardTitle>
|
|
119
|
+
</CardHeader>
|
|
120
|
+
<CardContent>
|
|
121
|
+
<p className="text-lg font-semibold" data-cy="trace-model">
|
|
122
|
+
{modelInfo?.model || '-'}
|
|
123
|
+
</p>
|
|
124
|
+
{modelInfo?.provider && (
|
|
125
|
+
<p className="text-sm text-muted-foreground" data-cy="trace-provider">
|
|
126
|
+
{modelInfo.provider}
|
|
127
|
+
</p>
|
|
128
|
+
)}
|
|
129
|
+
</CardContent>
|
|
130
|
+
</Card>
|
|
131
|
+
|
|
132
|
+
<Card>
|
|
133
|
+
<CardHeader className="pb-2">
|
|
134
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
135
|
+
{t('detail.duration')}
|
|
136
|
+
</CardTitle>
|
|
137
|
+
</CardHeader>
|
|
138
|
+
<CardContent>
|
|
139
|
+
<p className="text-lg font-semibold" data-cy="trace-duration">
|
|
140
|
+
{formatDuration(trace.durationMs)}
|
|
141
|
+
</p>
|
|
142
|
+
</CardContent>
|
|
143
|
+
</Card>
|
|
144
|
+
|
|
145
|
+
<Card>
|
|
146
|
+
<CardHeader className="pb-2">
|
|
147
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
148
|
+
{t('detail.tokens')}
|
|
149
|
+
</CardTitle>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<CardContent>
|
|
152
|
+
<p className="text-lg font-semibold" data-cy="trace-tokens">
|
|
153
|
+
{formatTokens(trace.totalTokens)}
|
|
154
|
+
</p>
|
|
155
|
+
<p className="text-xs text-muted-foreground">
|
|
156
|
+
{formatTokens(trace.inputTokens)} in / {formatTokens(trace.outputTokens)} out
|
|
157
|
+
</p>
|
|
158
|
+
</CardContent>
|
|
159
|
+
</Card>
|
|
160
|
+
|
|
161
|
+
<Card>
|
|
162
|
+
<CardHeader className="pb-2">
|
|
163
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
164
|
+
{t('detail.calls')}
|
|
165
|
+
</CardTitle>
|
|
166
|
+
</CardHeader>
|
|
167
|
+
<CardContent>
|
|
168
|
+
<p className="text-lg font-semibold" data-cy="trace-calls">
|
|
169
|
+
{trace.llmCalls} LLM / {trace.toolCalls} {t('detail.tools')}
|
|
170
|
+
</p>
|
|
171
|
+
{toolsUsed.length > 0 && (
|
|
172
|
+
<p className="text-xs text-muted-foreground mt-1" data-cy="trace-tools-used">
|
|
173
|
+
{toolsUsed.join(', ')}
|
|
174
|
+
</p>
|
|
175
|
+
)}
|
|
176
|
+
</CardContent>
|
|
177
|
+
</Card>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div className="space-y-6">
|
|
181
|
+
{/* Error box - shown first when there's an error */}
|
|
182
|
+
{trace.error && (
|
|
183
|
+
<Card className="border-destructive">
|
|
184
|
+
<CardHeader>
|
|
185
|
+
<CardTitle className="text-destructive">{t('detail.error')}</CardTitle>
|
|
186
|
+
</CardHeader>
|
|
187
|
+
<CardContent>
|
|
188
|
+
<pre
|
|
189
|
+
className="text-sm bg-destructive/10 text-destructive p-4 rounded-lg overflow-x-auto whitespace-pre-wrap"
|
|
190
|
+
data-cy="trace-error"
|
|
191
|
+
>
|
|
192
|
+
{trace.error}
|
|
193
|
+
</pre>
|
|
194
|
+
{trace.errorStack && (
|
|
195
|
+
<details className="mt-4">
|
|
196
|
+
<summary className="cursor-pointer text-sm font-medium mb-2">
|
|
197
|
+
{t('detail.stackTrace')}
|
|
198
|
+
</summary>
|
|
199
|
+
<pre className="text-xs bg-destructive/10 text-destructive p-3 rounded overflow-x-auto">
|
|
200
|
+
{trace.errorStack}
|
|
201
|
+
</pre>
|
|
202
|
+
</details>
|
|
203
|
+
)}
|
|
204
|
+
</CardContent>
|
|
205
|
+
</Card>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
<Card>
|
|
209
|
+
<CardHeader>
|
|
210
|
+
<CardTitle>{t('detail.input')}</CardTitle>
|
|
211
|
+
</CardHeader>
|
|
212
|
+
<CardContent>
|
|
213
|
+
<pre
|
|
214
|
+
className="text-sm bg-muted p-4 rounded-lg overflow-x-auto whitespace-pre-wrap"
|
|
215
|
+
data-cy="trace-input"
|
|
216
|
+
>
|
|
217
|
+
{trace.input}
|
|
218
|
+
</pre>
|
|
219
|
+
</CardContent>
|
|
220
|
+
</Card>
|
|
221
|
+
|
|
222
|
+
{/* Conversation Flow - Human-readable execution view */}
|
|
223
|
+
<ConversationFlow trace={trace} spans={spans} />
|
|
224
|
+
|
|
225
|
+
{/* Child Traces - Sub-agent invocations with accordion */}
|
|
226
|
+
{childTraces.length > 0 && (
|
|
227
|
+
<Card>
|
|
228
|
+
<CardHeader>
|
|
229
|
+
<CardTitle>{t('detail.childTraces')}</CardTitle>
|
|
230
|
+
</CardHeader>
|
|
231
|
+
<CardContent>
|
|
232
|
+
<Accordion type="multiple" className="w-full" data-cy="child-traces-list">
|
|
233
|
+
{childTraces.map((child) => (
|
|
234
|
+
<AccordionItem
|
|
235
|
+
key={child.traceId}
|
|
236
|
+
value={child.traceId}
|
|
237
|
+
className="border rounded-lg mb-3 last:mb-0"
|
|
238
|
+
data-cy={`child-trace-${child.agentName}`}
|
|
239
|
+
>
|
|
240
|
+
<AccordionTrigger className="px-4 hover:no-underline">
|
|
241
|
+
<div className="flex items-center justify-between w-full pr-4">
|
|
242
|
+
<div className="flex items-center gap-3">
|
|
243
|
+
<TraceStatusBadge status={child.status} />
|
|
244
|
+
<div className="text-left">
|
|
245
|
+
<p className="font-medium">{child.agentName}</p>
|
|
246
|
+
<p className="text-xs text-muted-foreground font-mono">
|
|
247
|
+
{child.traceId.slice(0, 8)}...
|
|
248
|
+
</p>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
<div className="text-right text-sm">
|
|
252
|
+
<p>{formatDuration(child.durationMs)}</p>
|
|
253
|
+
<p className="text-xs text-muted-foreground">
|
|
254
|
+
{formatTokens(child.totalTokens)} tokens
|
|
255
|
+
</p>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</AccordionTrigger>
|
|
259
|
+
<AccordionContent className="px-4">
|
|
260
|
+
<div className="space-y-4 pt-2">
|
|
261
|
+
{/* Stats row */}
|
|
262
|
+
<div className="grid grid-cols-3 gap-4 text-sm">
|
|
263
|
+
<div className="bg-muted/50 rounded p-2">
|
|
264
|
+
<p className="text-muted-foreground text-xs">{t('detail.duration')}</p>
|
|
265
|
+
<p className="font-medium">{formatDuration(child.durationMs)}</p>
|
|
266
|
+
</div>
|
|
267
|
+
<div className="bg-muted/50 rounded p-2">
|
|
268
|
+
<p className="text-muted-foreground text-xs">{t('detail.tokens')}</p>
|
|
269
|
+
<p className="font-medium">
|
|
270
|
+
{formatTokens(child.inputTokens)} / {formatTokens(child.outputTokens)}
|
|
271
|
+
</p>
|
|
272
|
+
</div>
|
|
273
|
+
<div className="bg-muted/50 rounded p-2">
|
|
274
|
+
<p className="text-muted-foreground text-xs">{t('detail.calls')}</p>
|
|
275
|
+
<p className="font-medium">
|
|
276
|
+
{child.llmCalls} LLM / {child.toolCalls} tools
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Compact Timeline - execution flow visualization */}
|
|
282
|
+
{childSpansMap[child.traceId] && childSpansMap[child.traceId].length > 0 && (
|
|
283
|
+
<div className="bg-muted/30 rounded-lg p-3">
|
|
284
|
+
<CompactTimeline spans={childSpansMap[child.traceId]} />
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{/* Error if exists */}
|
|
289
|
+
{child.error && (
|
|
290
|
+
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-3">
|
|
291
|
+
<p className="text-sm font-medium text-destructive mb-1">{t('detail.error')}</p>
|
|
292
|
+
<pre className="text-xs text-destructive whitespace-pre-wrap">
|
|
293
|
+
{child.error}
|
|
294
|
+
</pre>
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{/* Input */}
|
|
299
|
+
<div>
|
|
300
|
+
<p className="text-sm font-medium mb-2">{t('detail.input')}</p>
|
|
301
|
+
<pre className="text-sm bg-muted p-3 rounded-lg overflow-x-auto whitespace-pre-wrap max-h-40 overflow-y-auto">
|
|
302
|
+
{child.input}
|
|
303
|
+
</pre>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
{/* Output */}
|
|
307
|
+
{child.output && (
|
|
308
|
+
<div>
|
|
309
|
+
<p className="text-sm font-medium mb-2">{t('detail.output')}</p>
|
|
310
|
+
<pre className="text-sm bg-muted p-3 rounded-lg overflow-x-auto whitespace-pre-wrap max-h-60 overflow-y-auto">
|
|
311
|
+
{child.output}
|
|
312
|
+
</pre>
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
</AccordionContent>
|
|
317
|
+
</AccordionItem>
|
|
318
|
+
))}
|
|
319
|
+
</Accordion>
|
|
320
|
+
</CardContent>
|
|
321
|
+
</Card>
|
|
322
|
+
)}
|
|
323
|
+
|
|
324
|
+
<Card>
|
|
325
|
+
<CardHeader>
|
|
326
|
+
<CardTitle>{t('detail.spans')}</CardTitle>
|
|
327
|
+
</CardHeader>
|
|
328
|
+
<CardContent>
|
|
329
|
+
<SpansList spans={spans} />
|
|
330
|
+
</CardContent>
|
|
331
|
+
</Card>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from 'next-intl'
|
|
4
|
+
import type { TraceStatus } from '../../types/observability.types'
|
|
5
|
+
|
|
6
|
+
interface TraceStatusBadgeProps {
|
|
7
|
+
status: TraceStatus
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function TraceStatusBadge({ status, className = '' }: TraceStatusBadgeProps) {
|
|
12
|
+
const t = useTranslations('observability')
|
|
13
|
+
|
|
14
|
+
const statusConfig = {
|
|
15
|
+
running: {
|
|
16
|
+
label: t('status.running'),
|
|
17
|
+
className: 'bg-muted text-muted-foreground border-border',
|
|
18
|
+
},
|
|
19
|
+
success: {
|
|
20
|
+
label: t('status.success'),
|
|
21
|
+
className: 'bg-muted text-foreground border-border',
|
|
22
|
+
},
|
|
23
|
+
error: {
|
|
24
|
+
label: t('status.error'),
|
|
25
|
+
className: 'bg-destructive/10 text-destructive border-destructive/20',
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const config = statusConfig[status]
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<span
|
|
33
|
+
className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium border ${config.className} ${className}`}
|
|
34
|
+
data-cy={`trace-status-${status}`}
|
|
35
|
+
>
|
|
36
|
+
{config.label}
|
|
37
|
+
</span>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from 'next-intl'
|
|
4
|
+
import {
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableRow,
|
|
11
|
+
} from '@nextsparkjs/core/components/ui/table'
|
|
12
|
+
import type { Trace } from '../../types/observability.types'
|
|
13
|
+
import { TraceStatusBadge } from './TraceStatusBadge'
|
|
14
|
+
|
|
15
|
+
interface TracesTableProps {
|
|
16
|
+
traces: Trace[]
|
|
17
|
+
onSelect: (traceId: string) => void
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function TracesTable({ traces, onSelect, className = '' }: TracesTableProps) {
|
|
22
|
+
const t = useTranslations('observability')
|
|
23
|
+
|
|
24
|
+
const formatDuration = (ms?: number) => {
|
|
25
|
+
if (!ms) return '-'
|
|
26
|
+
if (ms < 1000) return `${ms}ms`
|
|
27
|
+
return `${(ms / 1000).toFixed(2)}s`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const formatTokens = (tokens: number) => {
|
|
31
|
+
if (tokens === 0) return '-'
|
|
32
|
+
return tokens.toLocaleString()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const truncateTraceId = (traceId: string) => {
|
|
36
|
+
return `${traceId.substring(0, 8)}...${traceId.substring(traceId.length - 4)}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const truncateInput = (input: string, maxLength = 60) => {
|
|
40
|
+
if (!input) return '-'
|
|
41
|
+
if (input.length <= maxLength) return input
|
|
42
|
+
return `${input.substring(0, maxLength)}...`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (traces.length === 0) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="text-center py-12" data-cy="traces-table-empty">
|
|
48
|
+
<p className="text-muted-foreground">{t('table.empty')}</p>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className={className} data-cy="traces-table">
|
|
55
|
+
<Table>
|
|
56
|
+
<TableHeader>
|
|
57
|
+
<TableRow>
|
|
58
|
+
<TableHead>{t('table.traceId')}</TableHead>
|
|
59
|
+
<TableHead className="min-w-[200px]">{t('table.input')}</TableHead>
|
|
60
|
+
<TableHead>{t('table.agent')}</TableHead>
|
|
61
|
+
<TableHead>{t('table.status')}</TableHead>
|
|
62
|
+
<TableHead>{t('table.duration')}</TableHead>
|
|
63
|
+
<TableHead>{t('table.tokens')}</TableHead>
|
|
64
|
+
<TableHead>{t('table.startedAt')}</TableHead>
|
|
65
|
+
</TableRow>
|
|
66
|
+
</TableHeader>
|
|
67
|
+
<TableBody>
|
|
68
|
+
{traces.map((trace) => (
|
|
69
|
+
<TableRow
|
|
70
|
+
key={trace.traceId}
|
|
71
|
+
onClick={() => onSelect(trace.traceId)}
|
|
72
|
+
className="cursor-pointer hover:bg-muted/50"
|
|
73
|
+
data-cy="trace-row"
|
|
74
|
+
data-trace-id={trace.traceId}
|
|
75
|
+
>
|
|
76
|
+
<TableCell className="font-mono text-xs" data-cy="trace-id">
|
|
77
|
+
{truncateTraceId(trace.traceId)}
|
|
78
|
+
</TableCell>
|
|
79
|
+
<TableCell className="text-sm max-w-[300px]" data-cy="trace-input" title={trace.input}>
|
|
80
|
+
{truncateInput(trace.input)}
|
|
81
|
+
</TableCell>
|
|
82
|
+
<TableCell data-cy="trace-agent">{trace.agentName}</TableCell>
|
|
83
|
+
<TableCell data-cy="trace-status">
|
|
84
|
+
<TraceStatusBadge status={trace.status} />
|
|
85
|
+
</TableCell>
|
|
86
|
+
<TableCell data-cy="trace-duration">{formatDuration(trace.durationMs)}</TableCell>
|
|
87
|
+
<TableCell data-cy="trace-tokens">{formatTokens(trace.totalTokens)}</TableCell>
|
|
88
|
+
<TableCell className="text-sm text-muted-foreground" data-cy="trace-started-at">
|
|
89
|
+
{new Date(trace.startedAt).toLocaleString()}
|
|
90
|
+
</TableCell>
|
|
91
|
+
</TableRow>
|
|
92
|
+
))}
|
|
93
|
+
</TableBody>
|
|
94
|
+
</Table>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ObservabilityDashboard } from './ObservabilityDashboard'
|
|
2
|
+
export { TracesTable } from './TracesTable'
|
|
3
|
+
export { TraceDetail } from './TraceDetail'
|
|
4
|
+
export { SpansList } from './SpansList'
|
|
5
|
+
export { FiltersPanel } from './FiltersPanel'
|
|
6
|
+
export { TraceStatusBadge } from './TraceStatusBadge'
|
|
7
|
+
export { DisabledMessage } from './DisabledMessage'
|