Skip to content

Commit d76cc8b

Browse files
committed
feat: Add Contextual Pre-Briefer MCP Tool to Memory Core (#9689)
1 parent d9e7a1a commit d76cc8b

4 files changed

Lines changed: 218 additions & 0 deletions

File tree

ai/mcp/server/memory-core/openapi.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,52 @@ paths:
314314
schema:
315315
$ref: '#/components/schemas/ErrorResponse'
316316

317+
/context/pre_brief:
318+
post:
319+
summary: Contextual Pre-Briefing
320+
operationId: pre_brief_session
321+
x-pass-as-object: true
322+
description: |
323+
Instantly contextualizes the agent by targeting a specific Epic or Graph Node,
324+
loading its high-weight semantic relationships and returning a structured brief.
325+
This provides zero-friction context-switching for the LLM.
326+
tags: [System]
327+
requestBody:
328+
required: true
329+
content:
330+
application/json:
331+
schema:
332+
type: object
333+
required:
334+
- targetId
335+
properties:
336+
targetId:
337+
type: string
338+
description: The Target Epic / Node ID to brief against.
339+
limit:
340+
type: number
341+
description: Max context neighbors to pull.
342+
default: 5
343+
responses:
344+
'200':
345+
description: Successful structural brief payload.
346+
content:
347+
application/json:
348+
schema:
349+
type: object
350+
'400':
351+
description: Invalid request body
352+
content:
353+
application/json:
354+
schema:
355+
$ref: '#/components/schemas/ErrorResponse'
356+
'500':
357+
description: Internal server error.
358+
content:
359+
application/json:
360+
schema:
361+
$ref: '#/components/schemas/ErrorResponse'
362+
317363
/summaries:
318364
get:
319365
summary: Get All Summaries

ai/mcp/server/memory-core/services/MemoryService.mjs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,78 @@ class MemoryService extends Base {
264264
}
265265
}
266266

267+
/**
268+
* Instantly contextualizes the agent by targeting a specific Epic or Graph Node,
269+
* loading its high-weight semantic relationships and returning a structured brief.
270+
* @param {Object} options
271+
* @param {String} options.targetId The Target Epic / Node ID to brief against.
272+
* @param {Number} [options.limit=5] Max context neighbors to pull.
273+
* @returns {Promise<Object>}
274+
*/
275+
async preBriefSession({ targetId, limit = 5 }) {
276+
try {
277+
const baseNode = GraphService.getNode({ id: targetId });
278+
if (!baseNode) {
279+
return {
280+
error: `Node ${targetId} not found in the Native Graph.`,
281+
code: 'NODE_NOT_FOUND'
282+
};
283+
}
284+
285+
let neighbors = GraphService.getNeighbors({ id: targetId });
286+
287+
// Focus purely on highest-weight semantic and architectural relationships
288+
neighbors = neighbors
289+
.filter(n => n.weight >= 0.5) // filter weak noise
290+
.sort((a, b) => b.weight - a.weight)
291+
.slice(0, limit);
292+
293+
const brief = {
294+
target: baseNode,
295+
context: []
296+
};
297+
298+
const collection = await ChromaManager.getSummaryCollection();
299+
300+
for (const neighbor of neighbors) {
301+
let episodicContext = null;
302+
303+
if (neighbor.semanticVectorId) {
304+
try {
305+
const result = await collection.get({
306+
ids: [neighbor.semanticVectorId],
307+
include: ['documents']
308+
});
309+
if (result.documents && result.documents.length > 0) {
310+
episodicContext = result.documents[0];
311+
}
312+
} catch (e) {
313+
// Missing vector is fine, we still have structural graph data
314+
}
315+
}
316+
317+
brief.context.push({
318+
id: neighbor.id,
319+
type: neighbor.type,
320+
name: neighbor.name,
321+
relationship: neighbor.relationship,
322+
weight: neighbor.weight,
323+
episodicContext
324+
});
325+
}
326+
327+
return brief;
328+
329+
} catch (error) {
330+
logger.error('[MemoryService] Error in preBriefSession:', error);
331+
return {
332+
error : 'Failed to generate contextual brief',
333+
message: error.message,
334+
code : 'PRE_BRIEF_ERROR'
335+
};
336+
}
337+
}
338+
267339
/**
268340
* Mutates the active context frontier in the native knowledge graph.
269341
* @param {Object} options

ai/mcp/server/memory-core/services/toolService.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const serviceMapping = {
2525
manage_database : DatabaseLifecycleService.manageDatabase .bind(DatabaseLifecycleService),
2626
manage_database_backup: DatabaseService .manageDatabaseBackup.bind(DatabaseService),
2727
mutate_frontier : MemoryService .mutateFrontier .bind(MemoryService),
28+
pre_brief_session : MemoryService .preBriefSession .bind(MemoryService),
2829
query_raw_memories : MemoryService .queryMemories .bind(MemoryService),
2930
query_summaries : SummaryService .querySummaries .bind(SummaryService),
3031
search_nodes : GraphService .searchNodes .bind(GraphService),
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {setup} from '../../../../../../setup.mjs';
2+
3+
const appName = 'GraphServiceTest';
4+
5+
setup({
6+
neoConfig: {
7+
unitTestMode: true
8+
},
9+
appConfig: {
10+
name : appName,
11+
isMounted : () => true,
12+
vnodeInitialising: false
13+
}
14+
});
15+
16+
import {test, expect} from '@playwright/test';
17+
import Neo from '../../../../../../../../src/Neo.mjs';
18+
import * as core from '../../../../../../../../src/core/_export.mjs';
19+
import GraphService from '../../../../../../../../ai/mcp/server/memory-core/services/GraphService.mjs';
20+
import aiConfig from '../../../../../../../../ai/mcp/server/memory-core/config.mjs';
21+
import fs from 'fs-extra';
22+
import path from 'path';
23+
import os from 'os';
24+
25+
test.describe('Neo.ai.mcp.server.memory-core.services.GraphService', () => {
26+
let service;
27+
const testDbPath = path.join(os.tmpdir(), `memory-core-graph-test-${process.pid}-${Date.now()}.db`);
28+
29+
test.beforeAll(async () => {
30+
// Mock the SQLite target path to a safe pure temporary location
31+
aiConfig.sqlitePath = testDbPath;
32+
if (fs.existsSync(testDbPath)) {
33+
fs.unlinkSync(testDbPath);
34+
}
35+
await GraphService.initAsync();
36+
});
37+
38+
test.beforeEach(async () => {
39+
// Clear graph nodes and edges before each test for isolation
40+
if (GraphService.db) {
41+
GraphService.db.nodes.clear();
42+
GraphService.db.edges.clear();
43+
}
44+
});
45+
46+
test.afterEach(() => {
47+
if (GraphService.db) {
48+
GraphService.db.nodes.clear();
49+
GraphService.db.edges.clear();
50+
}
51+
});
52+
53+
test.afterAll(() => {
54+
if (fs.existsSync(testDbPath)) {
55+
try {
56+
fs.unlinkSync(testDbPath);
57+
} catch (e) {}
58+
}
59+
});
60+
61+
test('should extract node neighbors properly', async () => {
62+
GraphService.upsertNode({ id: 'EpicA', name: 'Roadmap Planner' });
63+
GraphService.upsertNode({ id: 'Task1', name: 'Implementation' });
64+
GraphService.upsertNode({ id: 'Task2', name: 'Documentation' });
65+
66+
GraphService.linkNodes('EpicA', 'Task1', 'CONTAINS', 1.0);
67+
GraphService.linkNodes('EpicA', 'Task2', 'CONTAINS', 0.8);
68+
GraphService.linkNodes('Task1', 'Task2', 'DEPENDENCY', 0.5);
69+
70+
const neighbors = GraphService.getNeighbors({ id: 'EpicA' });
71+
72+
// Validation of extraction
73+
expect(neighbors.length).toBe(2);
74+
75+
const task1 = neighbors.find(n => n.id === 'Task1');
76+
const task2 = neighbors.find(n => n.id === 'Task2');
77+
78+
expect(task1).toBeDefined();
79+
expect(task1.weight).toBe(1.0);
80+
expect(task1.relationship).toBe('CONTAINS');
81+
82+
expect(task2).toBeDefined();
83+
expect(task2.weight).toBe(0.8);
84+
});
85+
86+
test('should correctly expose getContextFrontier topology', async () => {
87+
GraphService.upsertNode({ id: 'frontier', type: 'SYSTEM_ANCHOR' });
88+
GraphService.upsertNode({ id: 'EpicB' });
89+
90+
// Weight < 0.8 should be filtered out by getContextFrontier originally
91+
GraphService.linkNodes('frontier', 'EpicB', 'STRATEGIC_PIVOT', 0.9);
92+
93+
const topology = GraphService.getContextFrontier();
94+
expect(topology).toBeDefined();
95+
expect(topology.frontier.id).toBe('frontier');
96+
expect(topology.strategicNeighbors.length).toBe(1);
97+
expect(topology.strategicNeighbors[0].id).toBe('EpicB');
98+
});
99+
});

0 commit comments

Comments
 (0)