feat: sync runtime search and schema quality updates from app repo
- port retrieval, validation, and eval improvements relevant to os - align prompts and dimensions with the flat single-agent model - replace the old eval suite with the focused core scenarios Generated with Codex
This commit is contained in:
@@ -44,7 +44,7 @@ export async function GET(
|
||||
const context: DimensionContext = {
|
||||
name: dimension.name,
|
||||
description: dimension.description,
|
||||
isPriority: dimension.is_priority === 1,
|
||||
isPriority: false,
|
||||
nodeCount: countResult?.count || 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getSQLiteClient } from '@/services/database/sqlite-client';
|
||||
import { eventBroadcaster } from '@/services/events';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -35,13 +34,12 @@ async function getPopularDimensionsSQLite() {
|
||||
)
|
||||
SELECT ad.name AS dimension,
|
||||
COALESCE(dc.count, 0) AS count,
|
||||
COALESCE(dim.is_priority, 0) AS is_priority,
|
||||
dim.description
|
||||
FROM all_dimensions ad
|
||||
LEFT JOIN dimension_counts dc ON dc.dimension = ad.name
|
||||
LEFT JOIN dimensions dim ON dim.name = ad.name
|
||||
WHERE ad.name IS NOT NULL
|
||||
ORDER BY is_priority DESC, LOWER(ad.name) ASC
|
||||
ORDER BY LOWER(ad.name) ASC
|
||||
`);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -50,7 +48,7 @@ async function getPopularDimensionsSQLite() {
|
||||
data: result.rows.map((row: any) => ({
|
||||
dimension: row.dimension,
|
||||
count: Number(row.count),
|
||||
isPriority: Boolean(row.is_priority),
|
||||
isPriority: false,
|
||||
description: row.description || null
|
||||
}))
|
||||
});
|
||||
@@ -67,7 +65,14 @@ export async function POST(request: NextRequest) {
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
return togglePrioritySQLite(dimension);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
dimension,
|
||||
is_priority: false
|
||||
},
|
||||
message: 'Priority dimensions are no longer part of the product model.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling dimension priority:', error);
|
||||
return NextResponse.json({
|
||||
@@ -76,34 +81,3 @@ export async function POST(request: NextRequest) {
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PostgreSQL path removed in SQLite-only consolidation
|
||||
|
||||
async function togglePrioritySQLite(dimension: string) {
|
||||
const sqlite = getSQLiteClient();
|
||||
|
||||
const result = sqlite.query(`
|
||||
INSERT INTO dimensions(name, is_priority, updated_at)
|
||||
VALUES (?, 1, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
is_priority = CASE WHEN is_priority=1 THEN 0 ELSE 1 END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING is_priority
|
||||
`, [dimension]);
|
||||
|
||||
const isPriority = Boolean(result.rows[0].is_priority);
|
||||
|
||||
// Broadcast dimension update event
|
||||
eventBroadcaster.broadcast({
|
||||
type: 'DIMENSION_UPDATED',
|
||||
data: { dimension, isPriority }
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
dimension,
|
||||
is_priority: isPriority
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+53
-77
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getSQLiteClient } from '@/services/database/sqlite-client';
|
||||
import { eventBroadcaster } from '@/services/events';
|
||||
import { DimensionService } from '@/services/database/dimensionService';
|
||||
import { normalizeDimensionName, validateDimensionDescription } from '@/services/database/quality';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -20,11 +20,10 @@ export async function GET() {
|
||||
d.name AS dimension,
|
||||
d.description,
|
||||
d.icon,
|
||||
d.is_priority AS isPriority,
|
||||
COALESCE(dc.count, 0) AS count
|
||||
FROM dimensions d
|
||||
LEFT JOIN dimension_counts dc ON dc.dimension = d.name
|
||||
ORDER BY d.is_priority DESC, d.name ASC
|
||||
ORDER BY d.name ASC
|
||||
`);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -33,7 +32,7 @@ export async function GET() {
|
||||
dimension: row.dimension,
|
||||
description: row.description,
|
||||
icon: row.icon || null,
|
||||
isPriority: Boolean(row.isPriority),
|
||||
isPriority: false,
|
||||
count: Number(row.count)
|
||||
}))
|
||||
});
|
||||
@@ -49,10 +48,9 @@ export async function GET() {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const rawName = typeof body?.name === 'string' ? body.name.trim() : '';
|
||||
const rawName = typeof body?.name === 'string' ? normalizeDimensionName(body.name) : '';
|
||||
const description = typeof body?.description === 'string' ? body.description.trim() : null;
|
||||
const icon = typeof body?.icon === 'string' ? body.icon.trim() : null;
|
||||
const isPriority = typeof body?.isPriority === 'boolean' ? body.isPriority : false;
|
||||
const icon = typeof body?.icon === 'string' ? body.icon.trim() || null : null;
|
||||
|
||||
if (!rawName) {
|
||||
return NextResponse.json({
|
||||
@@ -61,10 +59,11 @@ export async function POST(request: NextRequest) {
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
if (description && description.length > 500) {
|
||||
const descriptionError = validateDimensionDescription(description || '');
|
||||
if (descriptionError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Description must be 500 characters or less'
|
||||
error: descriptionError
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
@@ -75,10 +74,9 @@ export async function POST(request: NextRequest) {
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
description = COALESCE(?, description),
|
||||
icon = COALESCE(?, icon),
|
||||
is_priority = COALESCE(?, is_priority),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING name, description, icon, is_priority
|
||||
`, [rawName, description, icon, isPriority ? 1 : 0, description, icon, isPriority ? 1 : 0]);
|
||||
`, [rawName, description, icon, 0, description, icon]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Failed to create dimension');
|
||||
@@ -86,12 +84,12 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const row = result.rows[0];
|
||||
const dimension = row.name as string;
|
||||
const isPriorityValue = Boolean(row.is_priority);
|
||||
const descriptionValue = row.description as string | null;
|
||||
const iconValue = (row.icon as string | null) || null;
|
||||
|
||||
eventBroadcaster.broadcast({
|
||||
type: 'DIMENSION_UPDATED',
|
||||
data: { dimension, isPriority: isPriorityValue, description: descriptionValue, count: 0 }
|
||||
data: { dimension, isPriority: false, description: descriptionValue, icon: iconValue, count: 0 }
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -99,7 +97,8 @@ export async function POST(request: NextRequest) {
|
||||
data: {
|
||||
dimension,
|
||||
description: descriptionValue,
|
||||
isPriority: isPriorityValue
|
||||
icon: iconValue,
|
||||
isPriority: false
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -114,38 +113,11 @@ export async function POST(request: NextRequest) {
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const currentName = typeof body?.currentName === 'string' ? body.currentName.trim() : '';
|
||||
const newName = typeof body?.newName === 'string' ? body.newName.trim() : '';
|
||||
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
||||
const currentName = typeof body?.currentName === 'string' ? normalizeDimensionName(body.currentName) : '';
|
||||
const newName = typeof body?.newName === 'string' ? normalizeDimensionName(body.newName) : '';
|
||||
const name = typeof body?.name === 'string' ? normalizeDimensionName(body.name) : '';
|
||||
const description = typeof body?.description === 'string' ? body.description.trim() : '';
|
||||
const isPriority = typeof body?.isPriority === 'boolean' ? body.isPriority : undefined;
|
||||
|
||||
// Handle isPriority update (lock/unlock) - simple case
|
||||
if (isPriority !== undefined && name && !currentName && !newName) {
|
||||
const sqlite = getSQLiteClient();
|
||||
const updateResult = sqlite.prepare(`
|
||||
UPDATE dimensions
|
||||
SET is_priority = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE name = ?
|
||||
`).run(isPriority ? 1 : 0, name);
|
||||
|
||||
if (updateResult.changes === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Dimension not found'
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
eventBroadcaster.broadcast({
|
||||
type: 'DIMENSION_UPDATED',
|
||||
data: { dimension: name, isPriority }
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { dimension: name, isPriority }
|
||||
});
|
||||
}
|
||||
const icon = body?.icon !== undefined ? (typeof body.icon === 'string' ? body.icon.trim() || null : null) : undefined;
|
||||
|
||||
// Handle dimension name change
|
||||
if (currentName && newName && currentName !== newName) {
|
||||
@@ -180,16 +152,16 @@ export async function PUT(request: NextRequest) {
|
||||
updates.push('description = ?');
|
||||
values.push(description || null);
|
||||
}
|
||||
|
||||
if (isPriority !== undefined) {
|
||||
updates.push('is_priority = ?');
|
||||
values.push(isPriority ? 1 : 0);
|
||||
|
||||
if (icon !== undefined) {
|
||||
updates.push('icon = ?');
|
||||
values.push(icon);
|
||||
}
|
||||
|
||||
|
||||
values.push(currentName);
|
||||
|
||||
|
||||
const dimUpdate = sqlite.prepare(`
|
||||
UPDATE dimensions
|
||||
UPDATE dimensions
|
||||
SET ${updates.join(', ')}
|
||||
WHERE name = ?
|
||||
`).run(...values);
|
||||
@@ -220,7 +192,7 @@ export async function PUT(request: NextRequest) {
|
||||
dimension: newName,
|
||||
previousName: currentName,
|
||||
description: description || undefined,
|
||||
isPriority: isPriority !== undefined ? isPriority : undefined,
|
||||
isPriority: false,
|
||||
renamed: true
|
||||
}
|
||||
});
|
||||
@@ -231,7 +203,7 @@ export async function PUT(request: NextRequest) {
|
||||
dimension: newName,
|
||||
previousName: currentName,
|
||||
description: description || undefined,
|
||||
isPriority: isPriority !== undefined ? isPriority : undefined,
|
||||
isPriority: false,
|
||||
nodeLinksUpdated: updateResult.nodeLinksUpdated
|
||||
}
|
||||
});
|
||||
@@ -246,34 +218,37 @@ export async function PUT(request: NextRequest) {
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
if (description && description.length > 500) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Description must be 500 characters or less'
|
||||
}, { status: 400 });
|
||||
if (description) {
|
||||
const descriptionError = validateDimensionDescription(description);
|
||||
if (descriptionError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: descriptionError
|
||||
}, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const sqlite = getSQLiteClient();
|
||||
|
||||
// Build update query
|
||||
if (description !== '' || isPriority !== undefined) {
|
||||
if (description !== '' || icon !== undefined) {
|
||||
const sqlite = getSQLiteClient();
|
||||
|
||||
// Build update query
|
||||
const updates: string[] = ['updated_at = CURRENT_TIMESTAMP'];
|
||||
const values: any[] = [];
|
||||
|
||||
|
||||
if (description !== '') {
|
||||
updates.push('description = ?');
|
||||
values.push(description || null);
|
||||
}
|
||||
|
||||
if (isPriority !== undefined) {
|
||||
updates.push('is_priority = ?');
|
||||
values.push(isPriority ? 1 : 0);
|
||||
|
||||
if (icon !== undefined) {
|
||||
updates.push('icon = ?');
|
||||
values.push(icon);
|
||||
}
|
||||
|
||||
|
||||
values.push(targetName);
|
||||
|
||||
|
||||
const updateResult = sqlite.prepare(`
|
||||
UPDATE dimensions
|
||||
UPDATE dimensions
|
||||
SET ${updates.join(', ')}
|
||||
WHERE name = ?
|
||||
`).run(...values);
|
||||
@@ -285,19 +260,19 @@ export async function PUT(request: NextRequest) {
|
||||
}, { status: 404 });
|
||||
}
|
||||
} else {
|
||||
// No updates provided
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'At least one update field (description or isPriority) must be provided'
|
||||
error: 'At least one update field (description, icon, or newName) must be provided'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
eventBroadcaster.broadcast({
|
||||
type: 'DIMENSION_UPDATED',
|
||||
data: {
|
||||
dimension: targetName,
|
||||
data: {
|
||||
dimension: targetName,
|
||||
description: description !== '' ? description : undefined,
|
||||
isPriority: isPriority !== undefined ? isPriority : undefined
|
||||
icon: icon !== undefined ? icon : undefined,
|
||||
isPriority: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -306,7 +281,8 @@ export async function PUT(request: NextRequest) {
|
||||
data: {
|
||||
dimension: targetName,
|
||||
description: description !== '' ? description : undefined,
|
||||
isPriority: isPriority !== undefined ? isPriority : undefined
|
||||
icon: icon !== undefined ? icon : undefined,
|
||||
isPriority: false
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
+10
-1
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { edgeService } from '@/services/database';
|
||||
import { validateEdgeExplanation } from '@/services/database/quality';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -57,8 +58,16 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const fromId = parseInt(body.from_node_id);
|
||||
const toId = parseInt(body.to_node_id);
|
||||
// Explanation can be empty - service will auto-generate
|
||||
const explanation = String(body.explanation || '').trim();
|
||||
if (explanation) {
|
||||
const explanationError = validateEdgeExplanation(explanation);
|
||||
if (explanationError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: explanationError
|
||||
}, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const skipInference = Boolean(body.skip_inference);
|
||||
const createdVia = (() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { nodeService } from '@/services/database';
|
||||
import { autoEmbedQueue } from '@/services/embedding/autoEmbedQueue';
|
||||
import { hasSufficientContent } from '@/services/embedding/constants';
|
||||
import { normalizeDimensions, validateExplicitDescription } from '@/services/database/quality';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -71,6 +72,20 @@ export async function PUT(
|
||||
const updates: Record<string, unknown> = { ...body };
|
||||
let shouldQueueEmbed = false;
|
||||
|
||||
if (typeof body.description === 'string') {
|
||||
const descriptionError = validateExplicitDescription(body.description);
|
||||
if (descriptionError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: descriptionError
|
||||
}, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(body.dimensions)) {
|
||||
updates.dimensions = normalizeDimensions(body.dimensions, 5);
|
||||
}
|
||||
|
||||
const incomingChunk = typeof body.chunk === 'string' ? body.chunk : undefined;
|
||||
const incomingNotes = typeof body.notes === 'string' ? body.notes : undefined;
|
||||
const existingChunk = existingNode.chunk ?? '';
|
||||
|
||||
+30
-30
@@ -2,10 +2,9 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { nodeService } from '@/services/database';
|
||||
import { Node, NodeFilters } from '@/types/database';
|
||||
import { autoEmbedQueue } from '@/services/embedding/autoEmbedQueue';
|
||||
import { hasSufficientContent } from '@/services/embedding/constants';
|
||||
import { DimensionService } from '@/services/database/dimensionService';
|
||||
import { generateDescription } from '@/services/database/descriptionService';
|
||||
import { scheduleAutoEdgeCreation } from '@/services/agents/autoEdge';
|
||||
import { normalizeDimensions, validateExplicitDescription } from '@/services/database/quality';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -97,11 +96,7 @@ export async function POST(request: NextRequest) {
|
||||
const eventDate = typeof body.event_date === 'string' ? body.event_date : null;
|
||||
|
||||
// Process provided dimensions first (needed for description generation)
|
||||
const providedDimensions = Array.isArray(body.dimensions) ? body.dimensions : [];
|
||||
const trimmedProvidedDimensions = providedDimensions
|
||||
.map((dim: unknown) => typeof dim === 'string' ? dim.trim() : '')
|
||||
.filter(Boolean)
|
||||
.slice(0, 8);
|
||||
const trimmedProvidedDimensions = normalizeDimensions(body.dimensions, 5);
|
||||
|
||||
// Use provided description if present, otherwise auto-generate
|
||||
let nodeDescription: string | undefined = typeof body.description === 'string' && body.description.trim()
|
||||
@@ -127,41 +122,46 @@ export async function POST(request: NextRequest) {
|
||||
nodeDescription = body.title.slice(0, 280);
|
||||
}
|
||||
|
||||
const finalDescription = nodeDescription ?? body.title.slice(0, 280);
|
||||
|
||||
const descriptionError = validateExplicitDescription(finalDescription);
|
||||
if (descriptionError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: descriptionError
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Monitor description quality
|
||||
if (nodeDescription && WEAK_PATTERNS.test(nodeDescription)) {
|
||||
console.warn(`[DescriptionQuality] Weak description for node "${body.title}": "${nodeDescription}"`);
|
||||
if (WEAK_PATTERNS.test(finalDescription)) {
|
||||
console.warn(`[DescriptionQuality] Weak description for node "${body.title}": "${finalDescription}"`);
|
||||
}
|
||||
|
||||
// Auto-assign locked dimensions + keyword dimensions for all new nodes
|
||||
const { locked, keywords } = await DimensionService.assignDimensions({
|
||||
title: body.title,
|
||||
notes: rawNotes || undefined,
|
||||
link: body.link,
|
||||
description: nodeDescription
|
||||
});
|
||||
|
||||
// Ensure keyword dimensions exist in the database (create if new)
|
||||
for (const keyword of keywords) {
|
||||
await DimensionService.ensureKeywordDimension(keyword);
|
||||
}
|
||||
|
||||
// Combine provided, locked, and keyword dimensions, remove duplicates
|
||||
const finalDimensions = [...new Set([...trimmedProvidedDimensions, ...locked, ...keywords])]
|
||||
.slice(0, 8); // max 8 total
|
||||
// Use only provided dimensions (no auto-assignment)
|
||||
const finalDimensions = trimmedProvidedDimensions;
|
||||
const rawChunk = typeof body.chunk === 'string' ? body.chunk : null;
|
||||
let chunkToStore = rawChunk;
|
||||
let chunkStatus: Node['chunk_status'];
|
||||
|
||||
if (chunkToStore && chunkToStore.trim().length > 0) {
|
||||
chunkStatus = 'not_chunked';
|
||||
} else if (!chunkToStore && hasSufficientContent(rawNotes)) {
|
||||
chunkToStore = rawNotes;
|
||||
chunkStatus = 'not_chunked';
|
||||
} else {
|
||||
// Build chunk from all available notes if not provided
|
||||
// This ensures every node gets at least one chunk for search
|
||||
const fallbackContent = [body.title, nodeDescription, rawNotes]
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
.trim();
|
||||
|
||||
if (fallbackContent) {
|
||||
chunkToStore = fallbackContent;
|
||||
chunkStatus = 'not_chunked';
|
||||
}
|
||||
}
|
||||
|
||||
const node = await nodeService.createNode({
|
||||
title: body.title,
|
||||
description: nodeDescription,
|
||||
description: finalDescription,
|
||||
notes: rawNotes ?? undefined,
|
||||
event_date: eventDate ?? undefined,
|
||||
link: body.link,
|
||||
@@ -171,7 +171,7 @@ export async function POST(request: NextRequest) {
|
||||
metadata: body.metadata || {}
|
||||
});
|
||||
|
||||
if (chunkStatus === 'not_chunked' && node.id && process.env.DISABLE_EMBEDDINGS !== 'true') {
|
||||
if (chunkStatus === 'not_chunked' && node.id) {
|
||||
autoEmbedQueue.enqueue(node.id, { reason: 'node_created' });
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,16 @@ function prettyJson(value: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonArray(value: string | null): string[] {
|
||||
if (!value) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default function EvalsClient({ traces, scenarioList }: Props) {
|
||||
const [openTraceId, setOpenTraceId] = useState<string | null>(traces[0]?.chat.trace_id || null);
|
||||
const [comments, setComments] = useState<Record<string, string>>(() => {
|
||||
@@ -74,7 +84,7 @@ export default function EvalsClient({ traces, scenarioList }: Props) {
|
||||
cost: chat.estimated_cost_usd ?? null,
|
||||
cache: chat.cache_hit == null ? 'n/a' : chat.cache_hit ? 'hit' : 'miss',
|
||||
cacheTokens: `${chat.cache_read_tokens ?? 0}/${chat.cache_write_tokens ?? 0}`,
|
||||
toolCount: toolCalls.length,
|
||||
toolCount: chat.tool_calls_count ?? toolCalls.length,
|
||||
status,
|
||||
userPreview: formatPreview(chat.user_message),
|
||||
timestamp: chat.ts,
|
||||
@@ -290,12 +300,29 @@ export default function EvalsClient({ traces, scenarioList }: Props) {
|
||||
<div><strong>Mode:</strong> {openTrace.chat.mode || 'n/a'}</div>
|
||||
<div><strong>Workflow:</strong> {openTrace.chat.workflow_key || '—'}</div>
|
||||
<div><strong>Latency:</strong> {openTrace.chat.latency_ms ?? 'n/a'} ms</div>
|
||||
<div><strong>First chunk:</strong> {openTrace.chat.first_chunk_latency_ms ?? 'n/a'} ms</div>
|
||||
<div><strong>First token:</strong> {openTrace.chat.first_token_latency_ms ?? 'n/a'} ms</div>
|
||||
<div><strong>Tokens:</strong> {openTrace.chat.input_tokens ?? 0}/{openTrace.chat.output_tokens ?? 0} (total {openTrace.chat.total_tokens ?? 0})</div>
|
||||
<div><strong>Cost:</strong> {openTrace.chat.estimated_cost_usd == null ? 'n/a' : `$${openTrace.chat.estimated_cost_usd.toFixed(4)}`}</div>
|
||||
<div><strong>Cache:</strong> {openTrace.chat.cache_hit == null ? 'n/a' : openTrace.chat.cache_hit ? 'hit' : 'miss'} (read {openTrace.chat.cache_read_tokens ?? 0} / write {openTrace.chat.cache_write_tokens ?? 0})</div>
|
||||
<div><strong>Tools:</strong> {openTrace.toolCalls.length}</div>
|
||||
<div><strong>Tools:</strong> {openTrace.chat.tool_calls_count ?? openTrace.toolCalls.length}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap', color: '#444' }}>
|
||||
{(openTrace.chat.prompt_build_ms ?? 0) > 0 && <div><strong>Prompt build:</strong> {openTrace.chat.prompt_build_ms} ms</div>}
|
||||
{(openTrace.chat.tools_build_ms ?? 0) > 0 && <div><strong>Tools build:</strong> {openTrace.chat.tools_build_ms} ms</div>}
|
||||
{(openTrace.chat.model_resolve_ms ?? 0) > 0 && <div><strong>Model resolve:</strong> {openTrace.chat.model_resolve_ms} ms</div>}
|
||||
{(openTrace.chat.message_assembly_ms ?? 0) > 0 && <div><strong>Message assembly:</strong> {openTrace.chat.message_assembly_ms} ms</div>}
|
||||
{(openTrace.chat.stream_setup_ms ?? 0) > 0 && <div><strong>Stream setup:</strong> {openTrace.chat.stream_setup_ms} ms</div>}
|
||||
{(openTrace.chat.tool_loop_ms ?? 0) > 0 && <div><strong>Tool loop:</strong> {openTrace.chat.tool_loop_ms} ms</div>}
|
||||
</div>
|
||||
|
||||
{parseJsonArray(openTrace.chat.tools_used_json).length > 0 ? (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>Tools used:</strong> {parseJsonArray(openTrace.chat.tools_used_json).join(', ')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div><strong>Comment:</strong></div>
|
||||
<div style={{ color: '#555' }}>{comments[openTrace.chat.trace_id] || '—'}</div>
|
||||
|
||||
Reference in New Issue
Block a user