Marketplace móvil para negocios locales mexicanos. Estructura inicial: - apps/backend (NestJS API) - apps/frontend (React Web) - apps/mobile (Expo/React Native) - apps/mcp-server (Claude MCP Server) - apps/whatsapp-service (WhatsApp Business API) - database/ (PostgreSQL DDL) - docs/ (Documentación) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
254 lines
11 KiB
JavaScript
254 lines
11 KiB
JavaScript
"use strict";
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
};
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
var LlmService_1;
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.LlmService = void 0;
|
|
const common_1 = require("@nestjs/common");
|
|
const config_1 = require("@nestjs/config");
|
|
const axios_1 = __importDefault(require("axios"));
|
|
const credentials_provider_service_1 = require("../common/credentials-provider.service");
|
|
let LlmService = LlmService_1 = class LlmService {
|
|
constructor(configService, credentialsProvider) {
|
|
this.configService = configService;
|
|
this.credentialsProvider = credentialsProvider;
|
|
this.logger = new common_1.Logger(LlmService_1.name);
|
|
this.clientCache = new Map();
|
|
this.conversationHistory = new Map();
|
|
}
|
|
getClient(config) {
|
|
const cacheKey = config.isFromPlatform
|
|
? 'platform'
|
|
: `tenant:${config.tenantId}`;
|
|
if (!this.clientCache.has(cacheKey)) {
|
|
const client = axios_1.default.create({
|
|
baseURL: config.baseUrl,
|
|
headers: {
|
|
Authorization: `Bearer ${config.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
this.clientCache.set(cacheKey, client);
|
|
}
|
|
return this.clientCache.get(cacheKey);
|
|
}
|
|
invalidateClientCache(tenantId) {
|
|
const cacheKey = tenantId ? `tenant:${tenantId}` : 'platform';
|
|
this.clientCache.delete(cacheKey);
|
|
if (tenantId) {
|
|
this.credentialsProvider.invalidateCache(tenantId);
|
|
}
|
|
}
|
|
async processMessage(userMessage, context) {
|
|
try {
|
|
const llmConfig = await this.credentialsProvider.getLLMConfig(context.tenantId);
|
|
let history = this.conversationHistory.get(context.phoneNumber) || [];
|
|
if (history.length === 0) {
|
|
const systemPrompt = llmConfig.systemPrompt || this.getSystemPrompt(context);
|
|
history = [{ role: 'system', content: systemPrompt }];
|
|
}
|
|
history.push({ role: 'user', content: userMessage });
|
|
if (history.length > 21) {
|
|
history = [history[0], ...history.slice(-20)];
|
|
}
|
|
const response = await this.callLlm(history, llmConfig);
|
|
history.push({ role: 'assistant', content: response.message });
|
|
this.conversationHistory.set(context.phoneNumber, history);
|
|
return response;
|
|
}
|
|
catch (error) {
|
|
this.logger.error(`LLM error: ${error.message}`);
|
|
return this.getFallbackResponse(userMessage);
|
|
}
|
|
}
|
|
async callLlm(messages, config) {
|
|
const functions = this.getAvailableFunctions();
|
|
const client = this.getClient(config);
|
|
const requestBody = {
|
|
model: config.model,
|
|
messages,
|
|
temperature: config.temperature,
|
|
max_tokens: config.maxTokens,
|
|
};
|
|
if (functions.length > 0 && config.provider !== 'anthropic') {
|
|
requestBody.tools = functions.map((fn) => ({
|
|
type: 'function',
|
|
function: fn,
|
|
}));
|
|
requestBody.tool_choice = 'auto';
|
|
}
|
|
const source = config.isFromPlatform ? 'platform' : `tenant:${config.tenantId}`;
|
|
this.logger.debug(`Calling LLM via ${source}, model: ${config.model}`);
|
|
const { data } = await client.post('/chat/completions', requestBody);
|
|
const choice = data.choices[0];
|
|
const assistantMessage = choice.message;
|
|
if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
|
|
const toolCall = assistantMessage.tool_calls[0];
|
|
const functionName = toolCall.function.name;
|
|
const functionArgs = JSON.parse(toolCall.function.arguments || '{}');
|
|
return {
|
|
message: assistantMessage.content || this.getActionMessage(functionName),
|
|
action: functionName,
|
|
data: functionArgs,
|
|
intent: functionName,
|
|
confidence: 0.9,
|
|
};
|
|
}
|
|
return {
|
|
message: assistantMessage.content || 'Lo siento, no pude procesar tu mensaje.',
|
|
intent: 'general_response',
|
|
confidence: 0.7,
|
|
};
|
|
}
|
|
getSystemPrompt(context) {
|
|
const businessName = context.businessName || 'MiChangarrito';
|
|
const businessDescription = context.businessName
|
|
? `un negocio local`
|
|
: `una tiendita de barrio en Mexico`;
|
|
return `Eres el asistente virtual de ${businessName}, ${businessDescription}.
|
|
Tu nombre es "Asistente de ${businessName}" y ayudas a los clientes con:
|
|
- Informacion sobre productos y precios
|
|
- Hacer pedidos
|
|
- Consultar su cuenta de fiado (credito)
|
|
- Estado de sus pedidos
|
|
|
|
Informacion del cliente:
|
|
- Nombre: ${context.customerName}
|
|
- Tiene carrito con ${context.cart?.length || 0} productos
|
|
|
|
Reglas importantes:
|
|
1. Responde siempre en espanol mexicano, de forma amigable y breve
|
|
2. Usa emojis ocasionalmente para ser mas amigable
|
|
3. Si el cliente quiere hacer algo especifico, usa las funciones disponibles
|
|
4. Si no entiendes algo, pide aclaracion de forma amable
|
|
5. Nunca inventes precios o productos, di que consultaras el catalogo
|
|
6. Para fiados, siempre verifica primero el saldo disponible
|
|
7. Se proactivo sugiriendo opciones relevantes
|
|
|
|
Ejemplos de respuestas:
|
|
- "Claro! Te muestro el menu de productos"
|
|
- "Perfecto, agrego eso a tu carrito"
|
|
- "Dejame revisar tu cuenta de fiado..."`;
|
|
}
|
|
getAvailableFunctions() {
|
|
return [
|
|
{
|
|
name: 'show_menu',
|
|
description: 'Muestra el menu principal de opciones al cliente',
|
|
parameters: { type: 'object', properties: {} },
|
|
},
|
|
{
|
|
name: 'show_products',
|
|
description: 'Muestra el catalogo de productos o una categoria especifica',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
category: {
|
|
type: 'string',
|
|
description: 'Categoria de productos (bebidas, botanas, abarrotes, lacteos)',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'show_fiado',
|
|
description: 'Muestra informacion de la cuenta de fiado del cliente',
|
|
parameters: { type: 'object', properties: {} },
|
|
},
|
|
{
|
|
name: 'add_to_cart',
|
|
description: 'Agrega un producto al carrito del cliente',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
product_name: {
|
|
type: 'string',
|
|
description: 'Nombre del producto a agregar',
|
|
},
|
|
quantity: {
|
|
type: 'number',
|
|
description: 'Cantidad a agregar',
|
|
default: 1,
|
|
},
|
|
},
|
|
required: ['product_name'],
|
|
},
|
|
},
|
|
{
|
|
name: 'check_order_status',
|
|
description: 'Consulta el estado de los pedidos del cliente',
|
|
parameters: { type: 'object', properties: {} },
|
|
},
|
|
];
|
|
}
|
|
getActionMessage(action) {
|
|
const messages = {
|
|
show_menu: 'Te muestro el menu principal...',
|
|
show_products: 'Aqui tienes nuestros productos...',
|
|
show_fiado: 'Voy a revisar tu cuenta de fiado...',
|
|
add_to_cart: 'Agregando a tu carrito...',
|
|
check_order_status: 'Consultando tus pedidos...',
|
|
};
|
|
return messages[action] || 'Procesando tu solicitud...';
|
|
}
|
|
getFallbackResponse(userMessage) {
|
|
const lowerMessage = userMessage.toLowerCase();
|
|
if (lowerMessage.includes('producto') || lowerMessage.includes('comprar')) {
|
|
return {
|
|
message: 'Te muestro nuestros productos disponibles.',
|
|
action: 'show_products',
|
|
intent: 'view_products',
|
|
confidence: 0.6,
|
|
};
|
|
}
|
|
if (lowerMessage.includes('fiado') || lowerMessage.includes('credito') || lowerMessage.includes('deuda')) {
|
|
return {
|
|
message: 'Voy a revisar tu cuenta de fiado.',
|
|
action: 'show_fiado',
|
|
intent: 'check_fiado',
|
|
confidence: 0.6,
|
|
};
|
|
}
|
|
if (lowerMessage.includes('pedido') || lowerMessage.includes('orden')) {
|
|
return {
|
|
message: 'Consultando el estado de tus pedidos.',
|
|
action: 'check_order_status',
|
|
intent: 'check_orders',
|
|
confidence: 0.6,
|
|
};
|
|
}
|
|
return {
|
|
message: 'Disculpa, no entendi bien. Puedes escribir "menu" para ver las opciones disponibles, o decirme que necesitas.',
|
|
intent: 'unknown',
|
|
confidence: 0.3,
|
|
};
|
|
}
|
|
cleanupOldConversations(maxAgeMinutes = 30) {
|
|
const now = Date.now();
|
|
const maxAge = maxAgeMinutes * 60 * 1000;
|
|
for (const [phone, history] of this.conversationHistory.entries()) {
|
|
if (history.length > 0) {
|
|
if (this.conversationHistory.size > 1000) {
|
|
this.conversationHistory.delete(phone);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
exports.LlmService = LlmService;
|
|
exports.LlmService = LlmService = LlmService_1 = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__metadata("design:paramtypes", [config_1.ConfigService,
|
|
credentials_provider_service_1.CredentialsProviderService])
|
|
], LlmService);
|
|
//# sourceMappingURL=llm.service.js.map
|