Compare commits

..

No commits in common. "a36a44d5e748dff2b82ac5db4e54c1f3445c500e" and "99064f5f244940628b32e9ee013528f2c94aeb5a" have entirely different histories.

5 changed files with 34 additions and 130 deletions

View File

@ -49,7 +49,7 @@ export class AIController {
// MODELS // MODELS
// ============================================ // ============================================
private async findAllModels(_req: Request, res: Response, next: NextFunction): Promise<void> { private async findAllModels(req: Request, res: Response, next: NextFunction): Promise<void> {
try { try {
const models = await this.aiService.findAllModels(); const models = await this.aiService.findAllModels();
res.json({ data: models, total: models.length }); res.json({ data: models, total: models.length });

View File

@ -1,4 +1,4 @@
import { Repository, FindOptionsWhere } from 'typeorm'; import { Repository, FindOptionsWhere, LessThan, MoreThanOrEqual } from 'typeorm';
import { AIModel, AIConversation, AIMessage, AIPrompt, AIUsageLog, AITenantQuota } from '../entities'; import { AIModel, AIConversation, AIMessage, AIPrompt, AIUsageLog, AITenantQuota } from '../entities';
export interface ConversationFilters { export interface ConversationFilters {

View File

@ -7,7 +7,7 @@
* Basado en: michangarrito MCH-012/MCH-013 * Basado en: michangarrito MCH-012/MCH-013
*/ */
import { Repository } from 'typeorm'; import { Repository, DataSource } from 'typeorm';
import { import {
AIModel, AIModel,
AIConversation, AIConversation,
@ -19,6 +19,7 @@ import {
import { AIService } from './ai.service'; import { AIService } from './ai.service';
import { import {
ERPRole, ERPRole,
ERP_ROLES,
getERPRole, getERPRole,
hasToolAccess, hasToolAccess,
getToolsForRole, getToolsForRole,
@ -245,7 +246,7 @@ export class RoleBasedAIService extends AIService {
const tool = this.toolRegistry.get(toolCall.name); const tool = this.toolRegistry.get(toolCall.name);
if (tool?.handler) { if (tool?.handler) {
try { try {
await tool.handler(toolCall.arguments, context); const result = await tool.handler(toolCall.arguments, context);
// El resultado se incorpora a la respuesta // El resultado se incorpora a la respuesta
// En una implementación completa, se haría otra llamada a la API // En una implementación completa, se haría otra llamada a la API
} catch (error: any) { } catch (error: any) {
@ -270,10 +271,9 @@ export class RoleBasedAIService extends AIService {
await this.logUsage(context.tenantId, { await this.logUsage(context.tenantId, {
modelId: model.id, modelId: model.id,
conversationId: conversation.id, conversationId: conversation.id,
promptTokens: response.tokensUsed.input, inputTokens: response.tokensUsed.input,
completionTokens: response.tokensUsed.output, outputTokens: response.tokensUsed.output,
totalTokens: response.tokensUsed.total, costUsd: this.calculateCost(model, response.tokensUsed),
cost: this.calculateCost(model, response.tokensUsed),
usageType: 'chat', usageType: 'chat',
}); });
@ -338,7 +338,7 @@ export class RoleBasedAIService extends AIService {
/** /**
* Obtener modelo por defecto para el tenant * Obtener modelo por defecto para el tenant
*/ */
private async getDefaultModel(_tenantId: string): Promise<AIModel | null> { private async getDefaultModel(tenantId: string): Promise<AIModel | null> {
// Buscar configuración del tenant o usar default // Buscar configuración del tenant o usar default
const models = await this.findAllModels(); const models = await this.findAllModels();
return models.find((m) => m.isDefault) || models[0] || null; return models.find((m) => m.isDefault) || models[0] || null;
@ -372,7 +372,7 @@ export class RoleBasedAIService extends AIService {
'HTTP-Referer': process.env.APP_URL || 'https://erp.local', 'HTTP-Referer': process.env.APP_URL || 'https://erp.local',
}, },
body: JSON.stringify({ body: JSON.stringify({
model: model.modelId || model.code, model: model.externalId || model.code,
messages: messages.map((m) => ({ messages: messages.map((m) => ({
role: m.role, role: m.role,
content: m.content, content: m.content,
@ -393,12 +393,12 @@ export class RoleBasedAIService extends AIService {
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({}) as Record<string, any>); const error = await response.json().catch(() => ({}));
throw new Error((errorData as any).error?.message || 'AI provider error'); throw new Error(error.error?.message || 'AI provider error');
} }
const data = await response.json() as Record<string, any>; const data = await response.json();
const choice = (data.choices as any[])?.[0]; const choice = data.choices?.[0];
return { return {
content: choice?.message?.content || '', content: choice?.message?.content || '',
@ -408,9 +408,9 @@ export class RoleBasedAIService extends AIService {
arguments: JSON.parse(tc.function?.arguments || '{}'), arguments: JSON.parse(tc.function?.arguments || '{}'),
})), })),
tokensUsed: { tokensUsed: {
input: (data.usage as any)?.prompt_tokens || 0, input: data.usage?.prompt_tokens || 0,
output: (data.usage as any)?.completion_tokens || 0, output: data.usage?.completion_tokens || 0,
total: (data.usage as any)?.total_tokens || 0, total: data.usage?.total_tokens || 0,
}, },
}; };
} }
@ -422,19 +422,22 @@ export class RoleBasedAIService extends AIService {
model: AIModel, model: AIModel,
tokens: { input: number; output: number } tokens: { input: number; output: number }
): number { ): number {
const inputCost = (tokens.input / 1000) * (model.inputCostPer1k || 0); const inputCost = (tokens.input / 1000000) * (model.inputCostPer1m || 0);
const outputCost = (tokens.output / 1000) * (model.outputCostPer1k || 0); const outputCost = (tokens.output / 1000000) * (model.outputCostPer1m || 0);
return inputCost + outputCost; return inputCost + outputCost;
} }
/** /**
* Limpiar conversación antigua (para liberar memoria) * Limpiar conversación antigua (para liberar memoria)
*/ */
cleanupOldConversations(_maxAgeMinutes: number = 60): void { cleanupOldConversations(maxAgeMinutes: number = 60): void {
const now = Date.now();
const maxAge = maxAgeMinutes * 60 * 1000;
// En una implementación real, esto estaría en Redis o similar // En una implementación real, esto estaría en Redis o similar
// Por ahora limpiamos el Map en memoria // Por ahora limpiamos el Map en memoria
for (const [_key, _value] of this.conversationHistory) { for (const [key, _] of this.conversationHistory) {
// TODO: Implementar lógica de limpieza basada en timestamp // Implementar lógica de limpieza basada en timestamp
} }
} }
} }

View File

@ -65,7 +65,7 @@ export function createConceptoController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: { total: result.total, page: result.page, limit: result.limit, totalPages: result.totalPages }, pagination: result.meta,
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -7,22 +7,10 @@
* @module Budgets * @module Budgets
*/ */
import { Repository, IsNull, FindOptionsWhere } from 'typeorm'; import { Repository, IsNull } from 'typeorm';
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { Concepto } from '../entities/concepto.entity'; import { Concepto } from '../entities/concepto.entity';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateConceptoDto { export interface CreateConceptoDto {
code: string; code: string;
name: string; name: string;
@ -41,96 +29,9 @@ export interface UpdateConceptoDto {
isComposite?: boolean; isComposite?: boolean;
} }
export class ConceptoService { export class ConceptoService extends BaseService<Concepto> {
private repository: Repository<Concepto>;
constructor(repository: Repository<Concepto>) { constructor(repository: Repository<Concepto>) {
this.repository = repository; super(repository);
}
async create(ctx: ServiceContext, data: Partial<Concepto>): Promise<Concepto> {
const entity = this.repository.create({
...data,
tenantId: ctx.tenantId,
createdById: ctx.userId,
});
return this.repository.save(entity);
}
async findById(ctx: ServiceContext, id: string): Promise<Concepto | null> {
return this.repository.findOne({
where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() } as FindOptionsWhere<Concepto>,
});
}
async findAll(
ctx: ServiceContext,
options: { page?: number; limit?: number; where?: FindOptionsWhere<Concepto> } = {}
): Promise<PaginatedResult<Concepto>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where = {
...options.where,
tenantId: ctx.tenantId,
deletedAt: IsNull(),
} as FindOptionsWhere<Concepto>;
const [data, total] = await this.repository.findAndCount({
where,
skip,
take: limit,
order: { code: 'ASC' },
});
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async find(
ctx: ServiceContext,
options: { where?: FindOptionsWhere<Concepto>; order?: Record<string, 'ASC' | 'DESC'> } = {}
): Promise<Concepto[]> {
return this.repository.find({
where: {
...options.where,
tenantId: ctx.tenantId,
deletedAt: IsNull(),
} as FindOptionsWhere<Concepto>,
order: options.order,
});
}
async update(ctx: ServiceContext, id: string, data: Partial<Concepto>): Promise<Concepto | null> {
const entity = await this.findById(ctx, id);
if (!entity) return null;
Object.assign(entity, data);
return this.repository.save(entity);
}
async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
const entity = await this.findById(ctx, id);
if (!entity) return false;
entity.deletedAt = new Date();
await this.repository.save(entity);
return true;
}
async exists(ctx: ServiceContext, where: FindOptionsWhere<Concepto>): Promise<boolean> {
const count = await this.repository.count({
where: {
...where,
tenantId: ctx.tenantId,
deletedAt: IsNull(),
} as FindOptionsWhere<Concepto>,
});
return count > 0;
} }
/** /**
@ -169,7 +70,7 @@ export class ConceptoService {
return this.findAll(ctx, { return this.findAll(ctx, {
page, page,
limit, limit,
where: { parentId: IsNull() } as FindOptionsWhere<Concepto>, where: { parentId: IsNull() } as any,
}); });
} }
@ -181,7 +82,7 @@ export class ConceptoService {
parentId: string parentId: string
): Promise<Concepto[]> { ): Promise<Concepto[]> {
return this.find(ctx, { return this.find(ctx, {
where: { parentId } as FindOptionsWhere<Concepto>, where: { parentId } as any,
order: { code: 'ASC' }, order: { code: 'ASC' },
}); });
} }
@ -198,7 +99,7 @@ export class ConceptoService {
: { parentId: IsNull() }; : { parentId: IsNull() };
const roots = await this.find(ctx, { const roots = await this.find(ctx, {
where: where as FindOptionsWhere<Concepto>, where: where as any,
order: { code: 'ASC' }, order: { code: 'ASC' },
}); });
@ -250,7 +151,7 @@ export class ConceptoService {
* Verificar si un código ya existe * Verificar si un código ya existe
*/ */
async codeExists(ctx: ServiceContext, code: string): Promise<boolean> { async codeExists(ctx: ServiceContext, code: string): Promise<boolean> {
return this.exists(ctx, { code } as FindOptionsWhere<Concepto>); return this.exists(ctx, { code } as any);
} }
} }