feat(WAI): 托管号码给trip planner Agent
parent
38e61ed4d1
commit
3d5b8dba73
@ -0,0 +1,120 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const generateId = require('../../utils/generateId.util');
|
||||||
|
const plannerEvents = require('../emitter');
|
||||||
|
// const createAsyncQueueProcessor = require('../emitter/queueProcessor');
|
||||||
|
const { getAgentSession, createAgentSession } = require('../../services/agent_sessions.service');
|
||||||
|
// const { getOutboundMessage, upsertOutboundMessage } = require('../../services/outbound_messages.service');
|
||||||
|
const { isEmpty } = require('../../utils/commons.util');
|
||||||
|
const { logger, getUserLogger } = require('../../utils/logger.util');
|
||||||
|
const { callAgentRounds } = require('../vendor/bailian.aliyun');
|
||||||
|
const { sendMessage } = require('../handler/whatsappHandler');
|
||||||
|
|
||||||
|
const userMessageEventNames = ['user:message:received'];
|
||||||
|
const agentMessageEventNames = ['agent:message:received'];
|
||||||
|
|
||||||
|
// const AGENT_PHONE_NO = ['8613557032060'];
|
||||||
|
const AGENT_PHONE_NO = require('../../config').agentHosted.split(',');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收客人的回复
|
||||||
|
* - 发送给agent
|
||||||
|
*/
|
||||||
|
const setupUserMessageHandler = async () => {
|
||||||
|
const messageListner = async ({ eventName, msgRow }) => {
|
||||||
|
const { from, to, direction } = msgRow;
|
||||||
|
const _whatsAppNo = to;
|
||||||
|
const _contact = from;
|
||||||
|
if (!AGENT_PHONE_NO.includes(_whatsAppNo) || direction !== 'inbound') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
getUserLogger(_whatsAppNo).info({ eventName, msgRow });
|
||||||
|
try {
|
||||||
|
const agentSession = await getAgentSession({ hosted: _whatsAppNo, contact: _contact });
|
||||||
|
|
||||||
|
// if (isEmpty(agentSession) || isEmpty(savedMsg?.text_body)) {
|
||||||
|
// 只处理文本
|
||||||
|
if (isEmpty(msgRow?.text_body)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// contact -> agent
|
||||||
|
const agentPost = {
|
||||||
|
agent_session_id: agentSession?.agent_session_id || '',
|
||||||
|
content: msgRow.text_body,
|
||||||
|
};
|
||||||
|
const agentRes = await callAgentRounds(agentPost);
|
||||||
|
plannerEvents.emit('agent:message:received', { msgRow, agentSession, agentRes });
|
||||||
|
// #
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ messageData: msgRow, error }, 'error call agent');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
userMessageEventNames.forEach(eventName => {
|
||||||
|
plannerEvents.on(eventName, async msgRow => messageListner({ eventName, msgRow }));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupAgentMessageHandler = async () => {
|
||||||
|
const messageListner = async ({ eventName, payload }) => {
|
||||||
|
const { msgRow, agentSession, agentRes } = payload;
|
||||||
|
const { from, to, whatsAppNo, direction } = msgRow;
|
||||||
|
const _whatsAppNo = whatsAppNo || to;
|
||||||
|
const _contact = from;
|
||||||
|
|
||||||
|
const { output: agentMsg } = agentRes;
|
||||||
|
const { session_id: agentSessionId } = agentMsg;
|
||||||
|
getUserLogger(_whatsAppNo).info({ eventName, agentRes });
|
||||||
|
try {
|
||||||
|
const now = new Date(new Date().getTime() + 60 * 60 * 1000).toISOString();
|
||||||
|
// 号码直接托管
|
||||||
|
// - todo: 按会话托管时另外处理
|
||||||
|
if (isEmpty(agentSession)) {
|
||||||
|
await createAgentSession({
|
||||||
|
agent_session_id: agentSessionId,
|
||||||
|
hosted: _whatsAppNo,
|
||||||
|
contact: _contact,
|
||||||
|
agent_id: agentRes.agent.id,
|
||||||
|
agent_name: agentRes.agent.name,
|
||||||
|
model_id: agentRes.usage.models[0].model_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// agent -> contact
|
||||||
|
const msgReady = { from: _whatsAppNo, to: _contact, msgcontent: { body: agentMsg.text }, msgtype: 'text', actionId: `.${generateId()}.${agentSessionId}` };
|
||||||
|
await sendMessage(msgReady);
|
||||||
|
// #
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ msgRow, agentRes, error }, 'error call agent');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
agentMessageEventNames.forEach(eventName => {
|
||||||
|
plannerEvents.on(eventName, async payload => messageListner({ eventName, payload }));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function startAgentSession(messageData) {
|
||||||
|
// const now = new Date(new Date().getTime() + 60 * 60 * 1000).toISOString();
|
||||||
|
|
||||||
|
// const { from, to, whatsAppNo, direction } = messageData;
|
||||||
|
// const _whatsAppNo = whatsAppNo || to;
|
||||||
|
// const _contact = from;
|
||||||
|
|
||||||
|
// // -> agent
|
||||||
|
// const agentRes = await callAgentFirst({
|
||||||
|
// // agent_session_id: agentSession.agent_session_id,
|
||||||
|
// content: messageData.text_body,
|
||||||
|
// });
|
||||||
|
// const { text: agentMsg, session_id: agentSessionId } = agentRes;
|
||||||
|
// await createAgentSession({ agent_session_id: agentSessionId, hosted: _whatsAppNo, contact: _contact, created_at: now });
|
||||||
|
// const msgReady = { from: _whatsAppNo, to: _contact, body: agentMsg, msgtype: 'text', actionId: `.${generateId()}` };
|
||||||
|
// await sendMessage(msgReady);
|
||||||
|
// //
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupAgentHandler() {
|
||||||
|
setupAgentMessageHandler();
|
||||||
|
setupUserMessageHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { setupAgentHandler, startAgentSession };
|
@ -0,0 +1,69 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
const { logger, getUserLogger } = require('../../utils/logger.util');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
publicUrl: 'https://dashscope.aliyuncs.com/api/v1/apps/{appId}/completion',
|
||||||
|
apiKey: 'sk-7d059313bd8943318b91cf4c25d91470', // process.env.ALIYUN_API_KEY,
|
||||||
|
agent: {
|
||||||
|
id: '3abf8008c77b4ccabb6ed37c6f42a882',
|
||||||
|
name: 'sales-7',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ! system prompt 将会覆盖 agent的system prompt
|
||||||
|
*/
|
||||||
|
async function callAgentFirst(payload) {
|
||||||
|
const url = config.publicUrl.replace('{appId}', config.agent.id);
|
||||||
|
try {
|
||||||
|
if (!url) {
|
||||||
|
logger.error('no agent url provided\n', payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
input: {
|
||||||
|
// prompt: payload.content,
|
||||||
|
messages: [
|
||||||
|
// { role: 'system', content: `Start contacting the guest named ${payload.customer_name}, here is the guest's reservation request information: ${payload.background}` },
|
||||||
|
// { role: 'assistant', content: payload.hello },
|
||||||
|
{ role: 'user', content: payload.content },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// parameters: {
|
||||||
|
// messages: '',
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
const { output } = await axios.post(url, body, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${config.apiKey}` } });
|
||||||
|
return output;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(JSON.stringify({ url, payload, error: error.message }, undefined, 2), 'Error calling agent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {object} agent response { session_id, text }
|
||||||
|
*/
|
||||||
|
async function callAgentRounds(payload) {
|
||||||
|
const url = config.publicUrl.replace('{appId}', config.agent.id);
|
||||||
|
try {
|
||||||
|
if (!url) {
|
||||||
|
logger.error('no agent url provided\n', payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// getUserLogger(payload.whatsAppNo).info({ url, payload });
|
||||||
|
const body = {
|
||||||
|
input: { prompt: payload.content, session_id: payload.agent_session_id },
|
||||||
|
// parameters: {
|
||||||
|
// messages: '',
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
const res = await axios.post(url, body, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${config.apiKey}` } });
|
||||||
|
return { ...res.data, agent: config.agent };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(JSON.stringify({ url, payload, error: error.message }, undefined, 2), 'Error calling agent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { callAgentFirst, callAgentRounds };
|
@ -0,0 +1,61 @@
|
|||||||
|
const Sequelize = require('sequelize');
|
||||||
|
module.exports = function(sequelize, DataTypes) {
|
||||||
|
return sequelize.define('agent_sessions', {
|
||||||
|
sn: {
|
||||||
|
autoIncrement: true,
|
||||||
|
type: DataTypes.BIGINT,
|
||||||
|
allowNull: false,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
agent_session_id: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
comment: "联系人"
|
||||||
|
},
|
||||||
|
hosted: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
comment: "托管的号码"
|
||||||
|
},
|
||||||
|
agent_id: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
agent_name: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
model_id: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
createtime: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: Sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
|
||||||
|
},
|
||||||
|
updatetime: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: Sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
tableName: 'agent_sessions',
|
||||||
|
timestamps: false,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
name: "PRIMARY",
|
||||||
|
unique: true,
|
||||||
|
using: "BTREE",
|
||||||
|
fields: [
|
||||||
|
{ name: "sn" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const db = require('../config').database;
|
||||||
|
const { domain, name } = require('../config').server;
|
||||||
|
const { objectMapper, pick, isEmpty } = require('../utils/commons.util');
|
||||||
|
const initModels = require('../models/init-models');
|
||||||
|
|
||||||
|
const Sequelize = db.sequelize;
|
||||||
|
const models = initModels(Sequelize);
|
||||||
|
|
||||||
|
const AgentSessionsModelModel = models.agent_sessions;
|
||||||
|
|
||||||
|
const getAgentSession = async where => {
|
||||||
|
const r = await AgentSessionsModelModel.findOne({
|
||||||
|
where,
|
||||||
|
});
|
||||||
|
return r?.toJSON() || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAgentSession = async data => {
|
||||||
|
const r = await AgentSessionsModelModel.create(data);
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { getAgentSession, createAgentSession };
|
Loading…
Reference in New Issue