Compare commits
2 Commits
99064f5f24
...
a36a44d5e7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a36a44d5e7 | ||
|
|
78c30d315d |
@ -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 });
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Repository, FindOptionsWhere, LessThan, MoreThanOrEqual } from 'typeorm';
|
import { Repository, FindOptionsWhere } 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 {
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
* Basado en: michangarrito MCH-012/MCH-013
|
* Basado en: michangarrito MCH-012/MCH-013
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Repository, DataSource } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
AIModel,
|
AIModel,
|
||||||
AIConversation,
|
AIConversation,
|
||||||
@ -19,7 +19,6 @@ import {
|
|||||||
import { AIService } from './ai.service';
|
import { AIService } from './ai.service';
|
||||||
import {
|
import {
|
||||||
ERPRole,
|
ERPRole,
|
||||||
ERP_ROLES,
|
|
||||||
getERPRole,
|
getERPRole,
|
||||||
hasToolAccess,
|
hasToolAccess,
|
||||||
getToolsForRole,
|
getToolsForRole,
|
||||||
@ -246,7 +245,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 {
|
||||||
const result = await tool.handler(toolCall.arguments, context);
|
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) {
|
||||||
@ -271,9 +270,10 @@ 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,
|
||||||
inputTokens: response.tokensUsed.input,
|
promptTokens: response.tokensUsed.input,
|
||||||
outputTokens: response.tokensUsed.output,
|
completionTokens: response.tokensUsed.output,
|
||||||
costUsd: this.calculateCost(model, response.tokensUsed),
|
totalTokens: response.tokensUsed.total,
|
||||||
|
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.externalId || model.code,
|
model: model.modelId || 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 error = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}) as Record<string, any>);
|
||||||
throw new Error(error.error?.message || 'AI provider error');
|
throw new Error((errorData as any).error?.message || 'AI provider error');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json() as Record<string, any>;
|
||||||
const choice = data.choices?.[0];
|
const choice = (data.choices as any[])?.[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?.prompt_tokens || 0,
|
input: (data.usage as any)?.prompt_tokens || 0,
|
||||||
output: data.usage?.completion_tokens || 0,
|
output: (data.usage as any)?.completion_tokens || 0,
|
||||||
total: data.usage?.total_tokens || 0,
|
total: (data.usage as any)?.total_tokens || 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -422,22 +422,19 @@ 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 / 1000000) * (model.inputCostPer1m || 0);
|
const inputCost = (tokens.input / 1000) * (model.inputCostPer1k || 0);
|
||||||
const outputCost = (tokens.output / 1000000) * (model.outputCostPer1m || 0);
|
const outputCost = (tokens.output / 1000) * (model.outputCostPer1k || 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, _] of this.conversationHistory) {
|
for (const [_key, _value] of this.conversationHistory) {
|
||||||
// Implementar lógica de limpieza basada en timestamp
|
// TODO: Implementar lógica de limpieza basada en timestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: result.meta,
|
pagination: { total: result.total, page: result.page, limit: result.limit, totalPages: result.totalPages },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@ -7,10 +7,22 @@
|
|||||||
* @module Budgets
|
* @module Budgets
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Repository, IsNull } from 'typeorm';
|
import { Repository, IsNull, FindOptionsWhere } 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;
|
||||||
@ -29,9 +41,96 @@ export interface UpdateConceptoDto {
|
|||||||
isComposite?: boolean;
|
isComposite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConceptoService extends BaseService<Concepto> {
|
export class ConceptoService {
|
||||||
|
private repository: Repository<Concepto>;
|
||||||
|
|
||||||
constructor(repository: Repository<Concepto>) {
|
constructor(repository: Repository<Concepto>) {
|
||||||
super(repository);
|
this.repository = 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,7 +169,7 @@ export class ConceptoService extends BaseService<Concepto> {
|
|||||||
return this.findAll(ctx, {
|
return this.findAll(ctx, {
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
where: { parentId: IsNull() } as any,
|
where: { parentId: IsNull() } as FindOptionsWhere<Concepto>,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +181,7 @@ export class ConceptoService extends BaseService<Concepto> {
|
|||||||
parentId: string
|
parentId: string
|
||||||
): Promise<Concepto[]> {
|
): Promise<Concepto[]> {
|
||||||
return this.find(ctx, {
|
return this.find(ctx, {
|
||||||
where: { parentId } as any,
|
where: { parentId } as FindOptionsWhere<Concepto>,
|
||||||
order: { code: 'ASC' },
|
order: { code: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -99,7 +198,7 @@ export class ConceptoService extends BaseService<Concepto> {
|
|||||||
: { parentId: IsNull() };
|
: { parentId: IsNull() };
|
||||||
|
|
||||||
const roots = await this.find(ctx, {
|
const roots = await this.find(ctx, {
|
||||||
where: where as any,
|
where: where as FindOptionsWhere<Concepto>,
|
||||||
order: { code: 'ASC' },
|
order: { code: 'ASC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -151,7 +250,7 @@ export class ConceptoService extends BaseService<Concepto> {
|
|||||||
* 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 any);
|
return this.exists(ctx, { code } as FindOptionsWhere<Concepto>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user