erp-retail-backend-v2/src/modules/mcp/services/tool-logger.service.ts
Adrian Flores Cortes 9de89aab5a [PROP-CORE-004] feat: Add Phase 6 modules from erp-core
Propagated modules:
- payment-terminals: MercadoPago + Clip TPV integration
- ai: Role-based AI access (ADMIN, GERENTE_TIENDA, CAJERO, CLIENTE)
- mcp: 18 ERP tools for AI assistants

71 files added. Critical for POS operations (P0).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 02:43:47 -06:00

172 lines
5.0 KiB
TypeScript

import { Repository } from 'typeorm';
import { ToolCall, ToolCallResult, ResultType } from '../entities';
import { StartCallData, CallHistoryFilters, PaginatedResult } from '../dto';
export class ToolLoggerService {
constructor(
private readonly toolCallRepo: Repository<ToolCall>,
private readonly resultRepo: Repository<ToolCallResult>
) {}
async startCall(data: StartCallData): Promise<string> {
const call = this.toolCallRepo.create({
tenantId: data.tenantId,
toolName: data.toolName,
parameters: data.parameters,
agentId: data.agentId,
conversationId: data.conversationId,
callerType: data.callerType,
calledByUserId: data.userId,
status: 'running',
startedAt: new Date(),
});
const saved = await this.toolCallRepo.save(call);
return saved.id;
}
async completeCall(callId: string, result: any): Promise<void> {
const call = await this.toolCallRepo.findOne({ where: { id: callId } });
if (!call) return;
const duration = Date.now() - call.startedAt.getTime();
await this.toolCallRepo.update(callId, {
status: 'success',
completedAt: new Date(),
durationMs: duration,
});
await this.resultRepo.save({
toolCallId: callId,
result,
resultType: this.getResultType(result),
});
}
async failCall(callId: string, errorMessage: string, errorCode: string): Promise<void> {
const call = await this.toolCallRepo.findOne({ where: { id: callId } });
if (!call) return;
const duration = Date.now() - call.startedAt.getTime();
await this.toolCallRepo.update(callId, {
status: 'error',
completedAt: new Date(),
durationMs: duration,
});
await this.resultRepo.save({
toolCallId: callId,
resultType: 'error',
errorMessage,
errorCode,
});
}
async timeoutCall(callId: string): Promise<void> {
const call = await this.toolCallRepo.findOne({ where: { id: callId } });
if (!call) return;
const duration = Date.now() - call.startedAt.getTime();
await this.toolCallRepo.update(callId, {
status: 'timeout',
completedAt: new Date(),
durationMs: duration,
});
await this.resultRepo.save({
toolCallId: callId,
resultType: 'error',
errorMessage: 'Tool execution timed out',
errorCode: 'TIMEOUT',
});
}
async getCallHistory(
tenantId: string,
filters: CallHistoryFilters
): Promise<PaginatedResult<ToolCall>> {
const qb = this.toolCallRepo
.createQueryBuilder('tc')
.leftJoinAndSelect('tc.result', 'result')
.where('tc.tenant_id = :tenantId', { tenantId });
if (filters.toolName) {
qb.andWhere('tc.tool_name = :toolName', { toolName: filters.toolName });
}
if (filters.status) {
qb.andWhere('tc.status = :status', { status: filters.status });
}
if (filters.startDate) {
qb.andWhere('tc.created_at >= :startDate', { startDate: filters.startDate });
}
if (filters.endDate) {
qb.andWhere('tc.created_at <= :endDate', { endDate: filters.endDate });
}
qb.orderBy('tc.created_at', 'DESC');
qb.skip((filters.page - 1) * filters.limit);
qb.take(filters.limit);
const [data, total] = await qb.getManyAndCount();
return { data, total, page: filters.page, limit: filters.limit };
}
async getCallById(id: string, tenantId: string): Promise<ToolCall | null> {
return this.toolCallRepo.findOne({
where: { id, tenantId },
relations: ['result'],
});
}
async getToolStats(
tenantId: string,
startDate: Date,
endDate: Date
): Promise<{
toolName: string;
totalCalls: number;
successfulCalls: number;
failedCalls: number;
avgDurationMs: number;
}[]> {
const result = await this.toolCallRepo
.createQueryBuilder('tc')
.select('tc.tool_name', 'toolName')
.addSelect('COUNT(*)', 'totalCalls')
.addSelect("COUNT(*) FILTER (WHERE tc.status = 'success')", 'successfulCalls')
.addSelect("COUNT(*) FILTER (WHERE tc.status = 'error')", 'failedCalls')
.addSelect('AVG(tc.duration_ms)', 'avgDurationMs')
.where('tc.tenant_id = :tenantId', { tenantId })
.andWhere('tc.created_at BETWEEN :startDate AND :endDate', { startDate, endDate })
.groupBy('tc.tool_name')
.orderBy('totalCalls', 'DESC')
.getRawMany();
return result.map((r) => ({
toolName: r.toolName,
totalCalls: parseInt(r.totalCalls) || 0,
successfulCalls: parseInt(r.successfulCalls) || 0,
failedCalls: parseInt(r.failedCalls) || 0,
avgDurationMs: parseFloat(r.avgDurationMs) || 0,
}));
}
private getResultType(result: any): ResultType {
if (result === null) return 'null';
if (Array.isArray(result)) return 'array';
const type = typeof result;
if (type === 'object') return 'object';
if (type === 'string') return 'string';
if (type === 'number') return 'number';
if (type === 'boolean') return 'boolean';
return 'object';
}
}