UNPKG

@agentscope/studio

Version:

AgentScope Studio is a powerful local monitoring and visualization tool designed to provide real-time insights into your system's performance and behavior.

639 lines (638 loc) 31.8 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SpanDao = void 0; const objectUtils_1 = require("../../../shared/src/utils/objectUtils"); const ModelInvocationView_1 = require("../models/ModelInvocationView"); const Trace_1 = require("../models/Trace"); class SpanDao { static saveSpans(dataArray) { return __awaiter(this, void 0, void 0, function* () { try { // Create SpanTable instances with embedded resource and scope data const spans = dataArray.map((data) => { // Extract key fields for indexing const serviceName = this.extractServiceName(data.resource); const operationName = this.extractOperationName(data.attributes); const instrumentationName = this.extractInstrumentationName(data.scope); const instrumentationVersion = this.extractInstrumentationVersion(data.scope); const model = this.extractModel(data.attributes); const inputTokens = this.extractInputTokens(data.attributes); const outputTokens = this.extractOutputTokens(data.attributes); const totalTokens = this.calculateTotalTokens(inputTokens, outputTokens); const statusCode = data.status.code || 0; const span = new Trace_1.SpanTable(); Object.assign(span, { id: data.spanId, // Use spanId as the primary key traceId: data.traceId, spanId: data.spanId, traceState: data.traceState, parentSpanId: data.parentSpanId, flags: data.flags, name: data.name, kind: data.kind, // Now it's a number (OpenTelemetry API enum) startTimeUnixNano: data.startTimeUnixNano, endTimeUnixNano: data.endTimeUnixNano, attributes: data.attributes, droppedAttributesCount: data.droppedAttributesCount, events: data.events, droppedEventsCount: data.droppedEventsCount, links: data.links, droppedLinksCount: data.droppedLinksCount, status: data.status, resource: data.resource, scope: data.scope, // Additional fields for our application statusCode: statusCode, serviceName: serviceName, operationName: operationName, instrumentationName: instrumentationName, instrumentationVersion: instrumentationVersion, model: model, inputTokens: inputTokens, outputTokens: outputTokens, totalTokens: totalTokens, conversationId: data.conversationId, latencyNs: data.latencyNs, }); return span; }); // Save all spans in a single transaction return yield Trace_1.SpanTable.save(spans); } catch (error) { console.error('Error saving spans:', error); throw error; } }); } static getSpansByConversationId(conversationId) { return __awaiter(this, void 0, void 0, function* () { try { const spans = yield Trace_1.SpanTable.find({ where: { conversationId }, order: { startTimeUnixNano: 'ASC' }, }); return spans.map((span) => ({ traceId: span.traceId, spanId: span.spanId, traceState: span.traceState, parentSpanId: span.parentSpanId, flags: span.flags, name: span.name, kind: span.kind, startTimeUnixNano: span.startTimeUnixNano, endTimeUnixNano: span.endTimeUnixNano, attributes: span.attributes, droppedAttributesCount: span.droppedAttributesCount || 0, events: (span.events || []), droppedEventsCount: span.droppedEventsCount || 0, links: (span.links || []), droppedLinksCount: span.droppedLinksCount || 0, status: span.status, resource: span.resource, scope: span.scope, conversationId: span.conversationId, latencyNs: span.latencyNs, })); } catch (error) { console.error(`Error fetching spans for conversationId ${conversationId}:`, error); throw error; } }); } // Helper methods to extract key fields from nested data static calculateTotalTokens(inputTokens, outputTokens) { if (typeof inputTokens === 'number' && typeof outputTokens === 'number') { return inputTokens + outputTokens; } if (typeof inputTokens === 'number') { return inputTokens; } if (typeof outputTokens === 'number') { return outputTokens; } return undefined; } static extractServiceName(resource) { const value = (0, objectUtils_1.getNestedValue)(resource.attributes, 'service.name'); return typeof value === 'string' ? value : undefined; } static extractOperationName(attributes) { const value = (0, objectUtils_1.getNestedValue)(attributes, 'gen_ai.operation.name'); return typeof value === 'string' ? value : undefined; } static extractInstrumentationName(scope) { const value = (0, objectUtils_1.getNestedValue)(scope.attributes, 'server.name'); return typeof value === 'string' ? value : undefined; } static extractInstrumentationVersion(scope) { const value = (0, objectUtils_1.getNestedValue)(scope.attributes, 'server.version'); return typeof value === 'string' ? value : undefined; } static extractModel(attributes) { const value = (0, objectUtils_1.getNestedValue)(attributes, 'gen_ai.request.model'); return typeof value === 'string' ? value : undefined; } static extractInputTokens(attributes) { const value = (0, objectUtils_1.getNestedValue)(attributes, 'gen_ai.usage.input_tokens'); return typeof value === 'number' ? value : undefined; } static extractOutputTokens(attributes) { const value = (0, objectUtils_1.getNestedValue)(attributes, 'gen_ai.usage.output_tokens'); return typeof value === 'number' ? value : undefined; } // Trace listing and filtering methods static getLatestTraces() { return __awaiter(this, arguments, void 0, function* (limit = 10) { return yield Trace_1.SpanTable.find({ order: { startTimeUnixNano: 'DESC' }, take: limit, }); }); } static getTracesByTraceId(traceId) { return __awaiter(this, void 0, void 0, function* () { return yield Trace_1.SpanTable.find({ where: { traceId }, order: { startTimeUnixNano: 'ASC' }, }); }); } static getSpanById(spanId) { return __awaiter(this, void 0, void 0, function* () { return yield Trace_1.SpanTable.findOne({ where: { spanId }, }); }); } static getModelInvocationViewData() { return __awaiter(this, void 0, void 0, function* () { const res = yield ModelInvocationView_1.ModelInvocationView.find(); if (res.length > 0) { return res[0]; } else { throw new Error('ModelInvocationView data not found'); } }); } static getModelInvocationData(conversationId) { return __awaiter(this, void 0, void 0, function* () { // 1. Basic statistics const basicStats = yield Trace_1.SpanTable.createQueryBuilder('span') .select(`COUNT(CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') THEN 1 END)`, 'totalInvocations') .addSelect(`COUNT(CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') AND span.totalTokens IS NOT NULL THEN 1 END)`, 'chatInvocations') .where('span.conversationId = :conversationId', { conversationId }) .getRawOne(); // 2. Chat token statistics (total and average) const chatTokenStats = yield Trace_1.SpanTable.createQueryBuilder('span') .select([ // Total - input tokens `COALESCE(SUM( CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') AND span.totalTokens IS NOT NULL THEN CAST(COALESCE(span.inputTokens, 0) AS INTEGER) ELSE 0 END ), 0) as totalPromptTokens`, // Total - output tokens `COALESCE(SUM( CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') AND span.totalTokens IS NOT NULL THEN CAST(COALESCE(span.outputTokens, 0) AS INTEGER) ELSE 0 END ), 0) as totalCompletionTokens`, // Total - total tokens `COALESCE(SUM( CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') AND span.totalTokens IS NOT NULL THEN CAST(COALESCE(span.totalTokens, 0) AS INTEGER) ELSE 0 END ), 0) as totalTokens`, // Average - input tokens `COALESCE( CAST(SUM( CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') AND span.totalTokens IS NOT NULL THEN CAST(COALESCE(span.inputTokens, 0) AS INTEGER) ELSE 0 END ) AS FLOAT) / NULLIF(COUNT(CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') AND span.totalTokens IS NOT NULL THEN 1 END), 0) , 0) as avgPromptTokens`, // Average - output tokens `COALESCE( CAST(SUM( CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') AND span.totalTokens IS NOT NULL THEN CAST(COALESCE(span.outputTokens, 0) AS INTEGER) ELSE 0 END ) AS FLOAT) / NULLIF(COUNT(CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') AND span.totalTokens IS NOT NULL THEN 1 END), 0) , 0) as avgCompletionTokens`, // Average - total tokens `COALESCE( CAST(SUM( CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') AND span.totalTokens IS NOT NULL THEN CAST(COALESCE(span.totalTokens, 0) AS INTEGER) ELSE 0 END ) AS FLOAT) / NULLIF(COUNT(CASE WHEN (span.operationName = 'chat' OR span.operationName = 'chat_model') AND span.totalTokens IS NOT NULL THEN 1 END), 0) , 0) as avgTotalTokens`, ]) .where('span.conversationId = :conversationId', { conversationId }) .getRawOne(); // 3. Model invocation statistics (grouped by model) const modelInvocations = yield Trace_1.SpanTable.createQueryBuilder('span') .select(['span.model as modelName', 'COUNT(*) as invocations']) .where('span.conversationId = :conversationId', { conversationId }) .andWhere("(span.operationName = 'chat' OR span.operationName = 'chat_model')") .andWhere('span.totalTokens IS NOT NULL') .groupBy('modelName') .getRawMany(); // 4. Model token statistics (grouped by model) const modelTokenStats = yield Trace_1.SpanTable.createQueryBuilder('span') .select([ 'span.model as modelName', // Total `SUM(CAST(COALESCE(span.inputTokens, 0) AS INTEGER)) as totalPromptTokens`, `SUM(CAST(COALESCE(span.outputTokens, 0) AS INTEGER)) as totalCompletionTokens`, `SUM(CAST(COALESCE(span.totalTokens, 0) AS INTEGER)) as totalTokens`, // Average `CAST(SUM(CAST(COALESCE(span.inputTokens, 0) AS INTEGER)) AS FLOAT) / COUNT(*) as avgPromptTokens`, `CAST(SUM(CAST(COALESCE(span.outputTokens, 0) AS INTEGER)) AS FLOAT) / COUNT(*) as avgCompletionTokens`, `CAST(SUM(CAST(COALESCE(span.totalTokens, 0) AS INTEGER)) AS FLOAT) / COUNT(*) as avgTotalTokens`, ]) .where('span.conversationId = :conversationId', { conversationId }) .andWhere("(span.operationName = 'chat' OR span.operationName = 'chat_model')") .andWhere('span.totalTokens IS NOT NULL') .groupBy('modelName') .getRawMany(); // 5. Build return structure return { modelInvocations: Number(basicStats.totalInvocations), chat: { modelInvocations: Number(basicStats.chatInvocations), totalTokens: { promptTokens: Number(chatTokenStats.totalPromptTokens), completionTokens: Number(chatTokenStats.totalCompletionTokens), totalTokens: Number(chatTokenStats.totalTokens), }, avgTokens: { promptTokens: Number(chatTokenStats.avgPromptTokens), completionTokens: Number(chatTokenStats.avgCompletionTokens), totalTokens: Number(chatTokenStats.avgTotalTokens), }, modelInvocationsByModel: modelInvocations.map((stat) => ({ modelName: stat.modelName || 'unknown', invocations: Number(stat.invocations), })), totalTokensByModel: modelTokenStats.map((stat) => ({ modelName: stat.modelName || 'unknown', promptTokens: Number(stat.totalPromptTokens), completionTokens: Number(stat.totalCompletionTokens), totalTokens: Number(stat.totalTokens), })), avgTokensByModel: modelTokenStats.map((stat) => ({ modelName: stat.modelName || 'unknown', promptTokens: Number(stat.avgPromptTokens), completionTokens: Number(stat.avgCompletionTokens), totalTokens: Number(stat.avgTotalTokens), })), }, }; }); } static deleteSpansByConversationIds(conversationIds) { return __awaiter(this, void 0, void 0, function* () { try { if (conversationIds.length === 0) { return 0; } const result = yield Trace_1.SpanTable.createQueryBuilder() .delete() .where('conversationId IN (:...conversationIds)', { conversationIds, }) .execute(); return result.affected || 0; } catch (error) { console.error('Error deleting spans by conversationIds:', error); throw error; } }); } /** * Get unique trace IDs with aggregated information * Uses the same parameter pattern as RunDao.getProjects (TableRequestParams) * * @param params - TableRequestParams containing pagination, sort, and filters * @returns TableData<Trace> with list, total, page, pageSize */ static getTraces(params) { return __awaiter(this, void 0, void 0, function* () { var _a; try { const { pagination, sort, filters } = params; // Build base query - group by traceId and aggregate information const spanCountSubquery = `( SELECT COUNT(*) FROM span_table s WHERE s.traceId = span.traceId )`; const totalTokensSubquery = `( SELECT SUM(COALESCE(s.totalTokens, 0)) FROM span_table s WHERE s.traceId = span.traceId )`; // Subquery to get the name of the span with the earliest startTimeUnixNano const nameSubquery = `( SELECT s.name FROM span_table s WHERE s.traceId = span.traceId AND s.startTimeUnixNano = ( SELECT MIN(s2.startTimeUnixNano) FROM span_table s2 WHERE s2.traceId = span.traceId ) LIMIT 1 )`; const queryBuilder = Trace_1.SpanTable.createQueryBuilder('span') .select('span.traceId', 'traceId') .addSelect('MIN(span.startTimeUnixNano)', 'startTime') .addSelect('MAX(span.endTimeUnixNano)', 'endTime') .addSelect('MAX(span.statusCode)', 'status') .addSelect(nameSubquery, 'name') .addSelect(spanCountSubquery, 'spanCount') .addSelect(totalTokensSubquery, 'totalTokens') .groupBy('span.traceId'); // Apply name filter - use the subquery directly in HAVING if (filters === null || filters === void 0 ? void 0 : filters.traceName) { const filterValue = typeof filters.traceName === 'object' && filters.traceName !== null && 'value' in filters.traceName ? filters.traceName.value : String(filters.traceName); if (filterValue) { // Use the same subquery in HAVING clause to filter by name queryBuilder.having(`${nameSubquery} LIKE :traceNameFilter`, { traceNameFilter: `%${filterValue}%`, }); } } // Apply traceId filter if (filters === null || filters === void 0 ? void 0 : filters.traceId) { const filterValue = typeof filters.traceId === 'object' && filters.traceId !== null && 'value' in filters.traceId ? filters.traceId.value : String(filters.traceId); if (filterValue) { queryBuilder.having('span.traceId LIKE :traceIdFilter', { traceIdFilter: `%${filterValue}%`, }); } } // Apply time range filter if (filters === null || filters === void 0 ? void 0 : filters.timeRange) { const timeRangeFilter = filters.timeRange; if (timeRangeFilter.operator === 'between' && Array.isArray(timeRangeFilter.value) && timeRangeFilter.value.length === 2) { const [rangeStart, rangeEnd] = timeRangeFilter.value; if (rangeStart && rangeEnd) { queryBuilder.andWhere('span.startTimeUnixNano >= :rangeStart AND span.startTimeUnixNano <= :rangeEnd', { rangeStart, rangeEnd }); } else if (rangeStart) { queryBuilder.andWhere('span.startTimeUnixNano >= :rangeStart', { rangeStart }); } else if (rangeEnd) { queryBuilder.andWhere('span.startTimeUnixNano <= :rangeEnd', { rangeEnd }); } } } // Apply sorting const sortField = (sort === null || sort === void 0 ? void 0 : sort.field) || 'startTime'; const sortOrder = ((_a = sort === null || sort === void 0 ? void 0 : sort.order) === null || _a === void 0 ? void 0 : _a.toUpperCase()) === 'ASC' ? 'ASC' : 'DESC'; switch (sortField) { case 'startTime': queryBuilder.orderBy('MIN(span.startTimeUnixNano)', sortOrder); break; case 'duration': queryBuilder.orderBy('(MAX(span.endTimeUnixNano) - MIN(span.startTimeUnixNano))', sortOrder); break; case 'status': queryBuilder.orderBy('MAX(span.statusCode)', sortOrder); break; case 'totalTokens': queryBuilder.orderBy(`(SELECT SUM(COALESCE(s.totalTokens, 0)) FROM span_table s WHERE s.traceId = span.traceId)`, sortOrder); break; default: queryBuilder.orderBy('MIN(span.startTimeUnixNano)', 'DESC'); } // Get total count (before pagination) const countQuery = queryBuilder.clone(); const totalResult = yield countQuery.getRawMany(); const total = totalResult.length; // Apply pagination const page = pagination.page || 1; const pageSize = pagination.pageSize || 10; const skip = (page - 1) * pageSize; queryBuilder.skip(skip).take(pageSize); // Query paginated traces const results = yield queryBuilder.getRawMany(); // Simple mapping - return raw data const traces = results.map((row) => ({ traceId: row.traceId || '', traceName: row.name || 'Unknown', startTime: row.startTime || '0', endTime: row.endTime || '0', status: Number(row.status) || 0, spanCount: Number(row.spanCount) || 0, totalTokens: row.totalTokens !== null && row.totalTokens !== undefined ? Number(row.totalTokens) : undefined, })); // Return paginated data return { list: traces, total, page, pageSize, }; } catch (error) { console.error('Error in getTraces:', error); throw error; } }); } // Get a single trace with all spans static getTrace(traceId) { return __awaiter(this, void 0, void 0, function* () { try { const spans = yield this.getTracesByTraceId(traceId); if (spans.length === 0) { throw new Error(`Trace with id ${traceId} not found`); } // Calculate trace-level statistics const startTimes = spans.map((s) => BigInt(s.startTimeUnixNano)); const endTimes = spans.map((s) => BigInt(s.endTimeUnixNano)); const minStartTime = startTimes.reduce((a, b) => (a < b ? a : b)); const maxEndTime = endTimes.reduce((a, b) => (a > b ? a : b)); const duration = Number(maxEndTime - minStartTime) / 1e9; // Get status (ERROR if any span has error status) const status = spans.some((s) => s.statusCode === 2) ? 2 : 1; // Calculate total tokens const totalTokens = spans.reduce((sum, s) => sum + (s.totalTokens || 0), 0); const spanDataArray = spans.map((span) => ({ traceId: span.traceId, spanId: span.spanId, traceState: span.traceState, parentSpanId: span.parentSpanId, flags: span.flags, name: span.name, kind: span.kind, startTimeUnixNano: span.startTimeUnixNano, endTimeUnixNano: span.endTimeUnixNano, attributes: span.attributes, droppedAttributesCount: span.droppedAttributesCount || 0, events: (span.events || []), droppedEventsCount: span.droppedEventsCount || 0, links: (span.links || []), droppedLinksCount: span.droppedLinksCount || 0, status: span.status, resource: span.resource, scope: span.scope, conversationId: span.conversationId, latencyNs: span.latencyNs, })); return { traceId, spans: spanDataArray, startTime: minStartTime.toString(), endTime: maxEndTime.toString(), duration, status, totalTokens: totalTokens > 0 ? totalTokens : undefined, }; } catch (error) { console.error(`Error getting trace ${traceId}:`, error); throw error; } }); } // Get trace statistics static getTraceStatistic(filters) { return __awaiter(this, void 0, void 0, function* () { try { const queryBuilder = Trace_1.SpanTable.createQueryBuilder('span'); if (filters === null || filters === void 0 ? void 0 : filters.startTime) { queryBuilder.andWhere('span.startTimeUnixNano >= :startTime', { startTime: filters.startTime, }); } if (filters === null || filters === void 0 ? void 0 : filters.endTime) { queryBuilder.andWhere('span.startTimeUnixNano <= :endTime', { endTime: filters.endTime, }); } if (filters === null || filters === void 0 ? void 0 : filters.traceId) { queryBuilder.andWhere('span.traceId LIKE :traceId', { traceId: `%${filters.traceId}%`, }); } // Get total spans const totalSpans = yield queryBuilder.getCount(); // Get unique trace count const uniqueTracesQuery = queryBuilder .clone() .select('COUNT(DISTINCT span.traceId)', 'count') .getRawOne(); const totalTraces = Number((yield uniqueTracesQuery).count || 0); // Get error traces count const errorTracesQuery = queryBuilder .clone() .select('COUNT(DISTINCT span.traceId)', 'count') .andWhere('span.statusCode = :statusCode', { statusCode: 2 }) .getRawOne(); const errorTraces = Number((yield errorTracesQuery).count || 0); // Get average duration const durationQuery = yield queryBuilder .clone() .select('span.traceId', 'traceId') .addSelect('MIN(span.startTimeUnixNano)', 'startTime') .addSelect('MAX(span.endTimeUnixNano)', 'endTime') .groupBy('span.traceId') .getRawMany(); const durations = durationQuery.map((row) => { const startTimeNs = BigInt(row.startTime); const endTimeNs = BigInt(row.endTime); return Number(endTimeNs - startTimeNs) / 1e9; }); const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0; // Get total tokens const tokensQuery = queryBuilder .clone() .select('SUM(COALESCE(span.totalTokens, 0))', 'total') .getRawOne(); const totalTokens = Number((yield tokensQuery).total || 0); // Get traces by status const statusQuery = yield queryBuilder .clone() .select('span.statusCode', 'status') .addSelect('COUNT(DISTINCT span.traceId)', 'count') .groupBy('span.statusCode') .getRawMany(); const tracesByStatus = statusQuery.map((row) => ({ status: Number(row.status) || 0, count: Number(row.count) || 0, })); return { totalTraces, totalSpans, errorTraces, avgDuration, totalTokens, tracesByStatus, }; } catch (error) { console.error('Error getting trace statistics:', error); throw error; } }); } } exports.SpanDao = SpanDao;