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>
172 lines
5.0 KiB
TypeScript
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';
|
|
}
|
|
}
|