index.js 11.7 KB
/**
 * 图谱 MCP 工具集 — Phase 1
 *
 * 6 个工具供 Master Agent 通过 [TOOL_CALL:] 调用:
 *   create_person / get_person / link_persons /
 *   create_event  / search_graph / get_relationship_path
 */

const { getGraphStore } = require('../../../db/GraphStore');

// ─── 辅助函数 ────────────────────────────────────────────────────

function store() {
    return getGraphStore();
}

/** 将节点对象格式化为 AI 可读的文本摘要 */
function nodeSummary(node) {
    if (!node) return '(未找到)';
    const { _type, _analysisStatus, _createdAt, _updatedAt, id, ...fields } = node;
    return JSON.stringify({ type: _type, status: _analysisStatus, ...fields }, null, 2);
}

/** 解析 name_or_id:先尝试按 id 精确查,再按名称模糊查 */
function resolvePerson(nameOrId) {
    const g = store();
    let node = g.getNode(nameOrId);
    if (!node) {
        const results = g.findByName(nameOrId, 'Person');
        node = results[0] || null;
    }
    return node;
}

// ─── 工具定义 ────────────────────────────────────────────────────

function createPersonTool() {
    return {
        name: 'create_person',
        description: '在知识图谱中创建或更新一个 Person 节点(联系人)。如果同名联系人已存在,会用新数据合并更新。',
        parameters: {
            type: 'object',
            required: ['name'],
            properties: {
                name:         { type: 'string',  description: '联系人姓名' },
                alias:        { type: 'string',  description: '别名或英文名(可选)' },
                company:      { type: 'string',  description: '所在公司' },
                role:         { type: 'string',  description: '职位/职务' },
                wechatId:     { type: 'string',  description: '微信 ID' },
                phone:        { type: 'string',  description: '手机号' },
                email:        { type: 'string',  description: '邮箱' },
                linkedin:     { type: 'string',  description: 'LinkedIn URL 或 username' },
                twitter:      { type: 'string',  description: 'X/Twitter handle' },
                notes:        { type: 'string',  description: '备注信息' },
            },
        },

        async execute(params) {
            const g = store();
            // 检查是否已存在同名节点,若有则 upsert
            const existing = g.findByName(params.name, 'Person');
            const id = existing[0]?.id;
            const node = g.createNode('Person', { ...(id ? { id } : {}), ...params });
            return `✅ 联系人已保存:\n${nodeSummary(node)}`;
        },
    };
}

function getPersonTool() {
    return {
        name: 'get_person',
        description: '查询一个联系人的完整档案,包括所有属性和关联关系。支持按 id 或姓名查询。',
        parameters: {
            type: 'object',
            required: ['name_or_id'],
            properties: {
                name_or_id: { type: 'string', description: '联系人姓名或节点 id' },
            },
        },

        async execute(params) {
            const g = store();
            const node = resolvePerson(params.name_or_id);
            if (!node) return `❌ 未找到联系人:${params.name_or_id}`;

            const { outgoing, incoming } = g.getEdges(node.id);

            const fmtEdge = (e, dir) => {
                const neighbor = dir === 'out' ? e.to_id : e.from_id;
                const neighborNode = g.getNode(neighbor);
                const neighborName = neighborNode?.name || neighbor;
                const meta = Object.keys(e.data).length ? ` (${JSON.stringify(e.data)})` : '';
                return dir === 'out'
                    ? `  → [${e.relation}] ${neighborName} (${neighborNode?._type || '?'})${meta}`
                    : `  ← [${e.relation}] ${neighborName} (${neighborNode?._type || '?'})${meta}`;
            };

            const lines = [
                `📋 联系人档案:${node.name}`,
                `─────────────────────────`,
                nodeSummary(node),
            ];

            if (outgoing.length) {
                lines.push('\n🔗 关联关系(出边):');
                outgoing.forEach(e => lines.push(fmtEdge(e, 'out')));
            }
            if (incoming.length) {
                lines.push('\n🔗 关联关系(入边):');
                incoming.forEach(e => lines.push(fmtEdge(e, 'in')));
            }
            if (!outgoing.length && !incoming.length) {
                lines.push('\n(暂无关联关系)');
            }

            return lines.join('\n');
        },
    };
}

function linkPersonsTool() {
    return {
        name: 'link_persons',
        description: '在图谱中建立两个节点之间的关系边。节点可以是 Person / Organization / Project 等任意类型。',
        parameters: {
            type: 'object',
            required: ['from_name_or_id', 'to_name_or_id', 'relation'],
            properties: {
                from_name_or_id: { type: 'string', description: '起始节点(姓名或 id)' },
                to_name_or_id:   { type: 'string', description: '目标节点(姓名或 id)' },
                relation: {
                    type: 'string',
                    description: '关系类型:KNOWS / BELONGS_TO / PARTICIPATED / MENTIONED_IN / ASSOCIATED / 自定义',
                },
                channel:  { type: 'string', description: '认识渠道(如 wechat / linkedin)' },
                since:    { type: 'string', description: '关系建立时间(如 2023-06)' },
                notes:    { type: 'string', description: '关系备注' },
            },
        },

        async execute(params) {
            const g = store();
            const { from_name_or_id, to_name_or_id, relation, ...meta } = params;

            // 解析 from
            let fromNode = resolvePerson(from_name_or_id);
            if (!fromNode) fromNode = g.getNode(from_name_or_id);
            if (!fromNode) return `❌ 未找到节点:${from_name_or_id}`;

            // 解析 to(支持 Person 或 Organization/Project)
            let toNode = g.getNode(to_name_or_id);
            if (!toNode) {
                const results = g.search(to_name_or_id);
                toNode = results[0] || null;
            }
            if (!toNode) return `❌ 未找到节点:${to_name_or_id}`;

            const edge = g.createEdge(fromNode.id, toNode.id, relation, meta);
            return `✅ 关系已建立:${fromNode.name} --[${relation}]--> ${toNode.name}\n边 id: ${edge.id}`;
        },
    };
}

function createEventTool() {
    return {
        name: 'create_event',
        description: '在图谱中创建一个事件节点(对话/会议/社媒动态等),并关联参与者。',
        parameters: {
            type: 'object',
            required: ['type', 'summary'],
            properties: {
                type:         { type: 'string', description: '事件类型:chat / meeting / post / other' },
                summary:      { type: 'string', description: '事件摘要' },
                date:         { type: 'string', description: '事件日期(YYYY-MM-DD)' },
                rawText:      { type: 'string', description: '原始文本内容(可选)' },
                participants: {
                    type: 'array',
                    items: { type: 'string' },
                    description: '参与者姓名或 id 列表',
                },
            },
        },

        async execute(params) {
            const g = store();
            const { participants = [], ...eventData } = params;

            const event = g.createNode('Event', eventData);

            const linked = [];
            for (const p of participants) {
                const person = resolvePerson(p);
                if (person) {
                    g.createEdge(person.id, event.id, 'PARTICIPATED');
                    linked.push(person.name);
                }
            }

            return `✅ 事件已创建:${event.id}\n摘要:${event.summary}\n关联参与者:${linked.join('、') || '(无)'}`;
        },
    };
}

function searchGraphTool() {
    return {
        name: 'search_graph',
        description: '在整个知识图谱中全文搜索,返回匹配的节点列表。支持按姓名、公司、备注等字段模糊查询。',
        parameters: {
            type: 'object',
            required: ['query'],
            properties: {
                query: { type: 'string', description: '搜索关键词' },
                type:  { type: 'string', description: '限定节点类型:Person / Organization / Project / Event / Asset(可选)' },
            },
        },

        async execute(params) {
            const g = store();
            const results = params.type
                ? g.search(params.query).filter(n => n._type === params.type)
                : g.search(params.query);

            if (results.length === 0) return `🔍 未找到与「${params.query}」相关的节点。`;

            const lines = [`🔍 搜索「${params.query}」,找到 ${results.length} 条结果:\n`];
            results.slice(0, 10).forEach((n, i) => {
                lines.push(`${i + 1}. [${n._type}] ${n.name || n.id}  (id: ${n.id}, 状态: ${n._analysisStatus})`);
            });
            if (results.length > 10) lines.push(`\n…共 ${results.length} 条,仅显示前 10 条`);

            return lines.join('\n');
        },
    };
}

function getRelationshipPathTool() {
    return {
        name: 'get_relationship_path',
        description: '查找两个联系人之间的关系路径(最短路径 BFS),揭示间接关系链。',
        parameters: {
            type: 'object',
            required: ['from_name_or_id', 'to_name_or_id'],
            properties: {
                from_name_or_id: { type: 'string', description: '起点联系人(姓名或 id)' },
                to_name_or_id:   { type: 'string', description: '终点联系人(姓名或 id)' },
                max_depth:       { type: 'number', description: '最大搜索深度,默认 6' },
            },
        },

        async execute(params) {
            const g = store();
            const from = resolvePerson(params.from_name_or_id);
            const to   = resolvePerson(params.to_name_or_id);

            if (!from) return `❌ 未找到:${params.from_name_or_id}`;
            if (!to)   return `❌ 未找到:${params.to_name_or_id}`;

            const path = g.getRelationshipPath(from.id, to.id, params.max_depth || 6);

            if (path === null) return `❌ ${from.name} 和 ${to.name} 之间暂无可达的关系路径。`;
            if (path.length === 0) return `✅ ${from.name} 和 ${to.name} 是同一个人。`;

            const lines = [`🔗 关系路径(${path.length} 步):\n`, `  ${from.name}`];
            for (const step of path) {
                const arrow = step.direction === 'outgoing' ? '→' : '←';
                const meta = step.data?.channel ? ` [via ${step.data.channel}]` : '';
                lines.push(`  ${arrow} [${step.relation}]${meta}`);
                lines.push(`  ${step.neighborName} (${step.neighborType})`);
            }

            return lines.join('\n');
        },
    };
}

function createGraphTools() {
    return [
        createPersonTool(),
        getPersonTool(),
        linkPersonsTool(),
        createEventTool(),
        searchGraphTool(),
        getRelationshipPathTool(),
    ];
}

module.exports = { createGraphTools };