This is page 9 of 13. Use http://codebase.md/tejpalvirk/contextmanager?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .gitignore
├── build-all-domains.sh
├── developer
│ ├── .gitattributes
│ ├── developer_advancedcontext.txt
│ ├── developer_buildcontext.txt
│ ├── developer_deletecontext.txt
│ ├── developer_endsession_examples.txt
│ ├── developer_endsession.txt
│ ├── developer_loadcontext.txt
│ ├── developer_startsession.txt
│ ├── Dockerfile
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── README.md
│ └── tsconfig.json
├── dist
│ ├── developer
│ │ ├── index.d.ts
│ │ └── index.js
│ ├── main
│ │ ├── descriptions
│ │ │ ├── common_advancedcontext.txt
│ │ │ ├── common_buildcontext.txt
│ │ │ ├── common_deletecontext.txt
│ │ │ ├── common_endsession.txt
│ │ │ ├── common_loadcontext.txt
│ │ │ ├── common_startsession.txt
│ │ │ ├── developer_advancedcontext.txt
│ │ │ ├── developer_buildcontext.txt
│ │ │ ├── developer_deletecontext.txt
│ │ │ ├── developer_endsession_examples.txt
│ │ │ ├── developer_endsession.txt
│ │ │ ├── developer_loadcontext.txt
│ │ │ ├── developer_startsession.txt
│ │ │ ├── project_advancedcontext.txt
│ │ │ ├── project_buildcontext.txt
│ │ │ ├── project_deletecontext.txt
│ │ │ ├── project_endsession_examples.txt
│ │ │ ├── project_endsession.txt
│ │ │ ├── project_loadcontext.txt
│ │ │ ├── project_startsession.txt
│ │ │ ├── qualitativeresearch_advancedcontext.txt
│ │ │ ├── qualitativeresearch_buildcontext.txt
│ │ │ ├── qualitativeresearch_deletecontext.txt
│ │ │ ├── qualitativeresearch_endsession_examples.txt
│ │ │ ├── qualitativeresearch_endsession.txt
│ │ │ ├── qualitativeresearch_loadcontext.txt
│ │ │ ├── qualitativeresearch_startsession.txt
│ │ │ ├── quantitativeresearch_advancedcontext.txt
│ │ │ ├── quantitativeresearch_buildcontext.txt
│ │ │ ├── quantitativeresearch_deletecontext.txt
│ │ │ ├── quantitativeresearch_endsession_examples.txt
│ │ │ ├── quantitativeresearch_endsession.txt
│ │ │ ├── quantitativeresearch_loadcontext.txt
│ │ │ ├── quantitativeresearch_startsession.txt
│ │ │ ├── student_advancedcontext.txt
│ │ │ ├── student_buildcontext.txt
│ │ │ ├── student_deletecontext.txt
│ │ │ ├── student_endsession_examples.txt
│ │ │ ├── student_endsession.txt
│ │ │ ├── student_loadcontext.txt
│ │ │ └── student_startsession.txt
│ │ ├── index.d.ts
│ │ ├── index.js
│ │ ├── mcp.d.ts
│ │ └── mcp.js
│ ├── project
│ │ ├── index.d.ts
│ │ └── index.js
│ ├── qualitativeresearch
│ │ ├── index.d.ts
│ │ └── index.js
│ ├── quantitativeresearch
│ │ ├── index.d.ts
│ │ └── index.js
│ └── student
│ ├── index.d.ts
│ └── index.js
├── main
│ ├── descriptions
│ │ ├── common_advancedcontext.txt
│ │ ├── common_buildcontext.txt
│ │ ├── common_deletecontext.txt
│ │ ├── common_endsession.txt
│ │ ├── common_loadcontext.txt
│ │ ├── common_startsession.txt
│ │ ├── developer_advancedcontext.txt
│ │ ├── developer_buildcontext.txt
│ │ ├── developer_deletecontext.txt
│ │ ├── developer_endsession_examples.txt
│ │ ├── developer_endsession.txt
│ │ ├── developer_loadcontext.txt
│ │ ├── developer_startsession.txt
│ │ ├── project_advancedcontext.txt
│ │ ├── project_buildcontext.txt
│ │ ├── project_deletecontext.txt
│ │ ├── project_endsession_examples.txt
│ │ ├── project_endsession.txt
│ │ ├── project_loadcontext.txt
│ │ ├── project_startsession.txt
│ │ ├── qualitativeresearch_advancedcontext.txt
│ │ ├── qualitativeresearch_buildcontext.txt
│ │ ├── qualitativeresearch_deletecontext.txt
│ │ ├── qualitativeresearch_endsession_examples.txt
│ │ ├── qualitativeresearch_endsession.txt
│ │ ├── qualitativeresearch_loadcontext.txt
│ │ ├── qualitativeresearch_startsession.txt
│ │ ├── quantitativeresearch_advancedcontext.txt
│ │ ├── quantitativeresearch_buildcontext.txt
│ │ ├── quantitativeresearch_deletecontext.txt
│ │ ├── quantitativeresearch_endsession_examples.txt
│ │ ├── quantitativeresearch_endsession.txt
│ │ ├── quantitativeresearch_loadcontext.txt
│ │ ├── quantitativeresearch_startsession.txt
│ │ ├── student_advancedcontext.txt
│ │ ├── student_buildcontext.txt
│ │ ├── student_deletecontext.txt
│ │ ├── student_endsession_examples.txt
│ │ ├── student_endsession.txt
│ │ ├── student_loadcontext.txt
│ │ └── student_startsession.txt
│ ├── index.js
│ ├── index.ts
│ ├── mcp.ts
│ ├── package.json
│ ├── README.md
│ └── tsconfig.json
├── package-lock.json
├── package.json
├── project
│ ├── .gitattributes
│ ├── Dockerfile
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── project_advancedcontext.txt
│ ├── project_buildcontext.txt
│ ├── project_deletecontext.txt
│ ├── project_endsession_examples.txt
│ ├── project_endsession.txt
│ ├── project_loadcontext.txt
│ ├── project_startsession.txt
│ ├── README.md
│ └── tsconfig.json
├── qualitativeresearch
│ ├── .gitattributes
│ ├── Dockerfile
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── qualitativeresearch_advancedcontext.txt
│ ├── qualitativeresearch_buildcontext.txt
│ ├── qualitativeresearch_deletecontext.txt
│ ├── qualitativeresearch_endsession_examples.txt
│ ├── qualitativeresearch_endsession.txt
│ ├── qualitativeresearch_loadcontext.txt
│ ├── qualitativeresearch_startsession.txt
│ ├── README.md
│ └── tsconfig.json
├── quantitativeresearch
│ ├── .gitattributes
│ ├── Dockerfile
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── quantitativeresearch_advancedcontext.txt
│ ├── quantitativeresearch_buildcontext.txt
│ ├── quantitativeresearch_deletecontext.txt
│ ├── quantitativeresearch_endsession_examples.txt
│ ├── quantitativeresearch_endsession.txt
│ ├── quantitativeresearch_loadcontext.txt
│ ├── quantitativeresearch_startsession.txt
│ ├── README.md
│ └── tsconfig.json
├── README.md
├── student
│ ├── .gitattributes
│ ├── Dockerfile
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── README.md
│ ├── student_advancedcontext.txt
│ ├── student_buildcontext.txt
│ ├── student_deletecontext.txt
│ ├── student_endsession_examples.txt
│ ├── student_endsession.txt
│ ├── student_loadcontext.txt
│ ├── student_startsession.txt
│ └── tsconfig.json
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/qualitativeresearch/index.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4 | import { promises as fs } from 'fs';
5 | import path from 'path';
6 | import { fileURLToPath } from 'url';
7 | import { z } from "zod";
8 | import { readFileSync, existsSync } from "fs";
9 | // Define memory file path using environment variable with fallback
10 | const parentPath = path.dirname(fileURLToPath(import.meta.url));
11 | const defaultMemoryPath = path.join(parentPath, 'memory.json');
12 | const defaultSessionsPath = path.join(parentPath, 'sessions.json');
13 | // Properly handle absolute and relative paths for MEMORY_FILE_PATH
14 | const MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH
15 | ? path.isAbsolute(process.env.MEMORY_FILE_PATH)
16 | ? process.env.MEMORY_FILE_PATH // Use absolute path as is
17 | : path.join(process.cwd(), process.env.MEMORY_FILE_PATH) // Relative to current working directory
18 | : defaultMemoryPath; // Default fallback
19 | // Properly handle absolute and relative paths for SESSIONS_FILE_PATH
20 | const SESSIONS_FILE_PATH = process.env.SESSIONS_FILE_PATH
21 | ? path.isAbsolute(process.env.SESSIONS_FILE_PATH)
22 | ? process.env.SESSIONS_FILE_PATH // Use absolute path as is
23 | : path.join(process.cwd(), process.env.SESSIONS_FILE_PATH) // Relative to current working directory
24 | : defaultSessionsPath; // Default fallback
25 | // Qualitative Research specific entity types
26 | const VALID_ENTITY_TYPES = [
27 | 'project', // Overall research study
28 | 'participant', // Research subjects
29 | 'interview', // Formal conversation with participants
30 | 'observation', // Field notes from observational research
31 | 'document', // External materials being analyzed
32 | 'code', // Labels applied to data segments
33 | 'codeGroup', // Categories or families of related codes
34 | 'memo', // Researcher's analytical notes
35 | 'theme', // Emergent patterns across data
36 | 'quote', // Notable excerpts from data sources
37 | 'literature', // Academic sources
38 | 'researchQuestion', // Formal questions guiding the study
39 | 'finding', // Results or conclusions
40 | 'status', // Status entity type
41 | 'priority' // Priority entity type
42 | ];
43 | // Qualitative Research specific relation types
44 | const VALID_RELATION_TYPES = [
45 | 'participated_in', // Links participants to interviews/observations
46 | 'codes', // Shows which codes apply to which data
47 | 'contains', // Hierarchical relationship (e.g., codegroup contains codes)
48 | 'supports', // Data supporting a theme or finding
49 | 'contradicts', // Data contradicting a theme or finding
50 | 'answers', // Data addressing a research question
51 | 'cites', // References to literature
52 | 'followed_by', // Temporal sequence
53 | 'related_to', // General connection
54 | 'reflects_on', // Memo reflecting on data/code/theme
55 | 'compares', // Comparative relationship
56 | 'conducted_by', // Person who conducted data collection
57 | 'transcribed_by', // Person who transcribed data
58 | 'part_of', // Entity is part of another entity
59 | 'derived_from', // Entity is derived from another entity
60 | 'collected_on', // Data collection date
61 | 'analyzes', // Analysis relationship
62 | 'triangulates_with', // Triangulation between data sources
63 | 'has_status', // Entity has a specific status
64 | 'has_priority', // Entity has a specific priority
65 | 'precedes' // Entity comes before another entity in sequence
66 | ];
67 | // Status values for different entity types in qualitative research
68 | const STATUS_VALUES = {
69 | project: ['planning', 'data_collection', 'analysis', 'writing', 'complete'],
70 | interview: ['scheduled', 'conducted', 'transcribed', 'coded', 'analyzed'],
71 | observation: ['planned', 'conducted', 'documented', 'coded', 'analyzed'],
72 | code: ['initial', 'revised', 'final'],
73 | theme: ['emerging', 'developing', 'established'],
74 | finding: ['preliminary', 'draft', 'final']
75 | };
76 | // Define valid status values for all entity types
77 | const VALID_STATUS_VALUES = [
78 | 'planning', 'data_collection', 'analysis', 'writing', 'complete',
79 | 'scheduled', 'conducted', 'transcribed', 'coded', 'analyzed',
80 | 'planned', 'documented',
81 | 'initial', 'revised', 'final',
82 | 'emerging', 'developing', 'established',
83 | 'preliminary', 'draft',
84 | 'active', 'in_progress', 'not_started'
85 | ];
86 | // Define valid priority values
87 | const VALID_PRIORITY_VALUES = [
88 | 'high', 'low'
89 | ];
90 | // Basic validation functions
91 | function validateEntityType(entityType) {
92 | return VALID_ENTITY_TYPES.includes(entityType);
93 | }
94 | function validateRelationType(relationType) {
95 | return VALID_RELATION_TYPES.includes(relationType);
96 | }
97 | const __filename = fileURLToPath(import.meta.url);
98 | const __dirname = path.dirname(__filename);
99 | // Collect tool descriptions from text files
100 | const toolDescriptions = {
101 | 'startsession': '',
102 | 'loadcontext': '',
103 | 'deletecontext': '',
104 | 'buildcontext': '',
105 | 'advancedcontext': '',
106 | 'endsession': '',
107 | };
108 | for (const tool of Object.keys(toolDescriptions)) {
109 | const descriptionFilePath = path.resolve(__dirname, `qualitativeresearch_${tool}.txt`);
110 | if (existsSync(descriptionFilePath)) {
111 | toolDescriptions[tool] = readFileSync(descriptionFilePath, 'utf-8');
112 | }
113 | }
114 | // Session management functions
115 | async function loadSessionStates() {
116 | try {
117 | const fileContent = await fs.readFile(SESSIONS_FILE_PATH, 'utf-8');
118 | const sessions = JSON.parse(fileContent);
119 | // Convert from object to Map
120 | const sessionsMap = new Map();
121 | for (const [key, value] of Object.entries(sessions)) {
122 | sessionsMap.set(key, value);
123 | }
124 | return sessionsMap;
125 | }
126 | catch (error) {
127 | if (error instanceof Error && 'code' in error && error.code === "ENOENT") {
128 | return new Map();
129 | }
130 | throw error;
131 | }
132 | }
133 | async function saveSessionStates(sessionsMap) {
134 | // Convert from Map to object
135 | const sessions = {};
136 | for (const [key, value] of sessionsMap.entries()) {
137 | sessions[key] = value;
138 | }
139 | await fs.writeFile(SESSIONS_FILE_PATH, JSON.stringify(sessions, null, 2), 'utf-8');
140 | }
141 | // Generate a unique session ID
142 | function generateSessionId() {
143 | return `qual_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
144 | }
145 | // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
146 | class KnowledgeGraphManager {
147 | async loadGraph() {
148 | try {
149 | const fileContent = await fs.readFile(MEMORY_FILE_PATH, 'utf-8');
150 | return JSON.parse(fileContent);
151 | }
152 | catch (error) {
153 | // If the file doesn't exist, return an empty graph
154 | return {
155 | entities: [],
156 | relations: []
157 | };
158 | }
159 | }
160 | async saveGraph(graph) {
161 | await fs.writeFile(MEMORY_FILE_PATH, JSON.stringify(graph, null, 2), 'utf-8');
162 | }
163 | // Initialize status and priority entities
164 | async initializeStatusAndPriority() {
165 | const graph = await this.loadGraph();
166 | // Create status entities if they don't exist
167 | for (const statusValue of VALID_STATUS_VALUES) {
168 | const statusName = `status:${statusValue}`;
169 | if (!graph.entities.some(e => e.name === statusName && e.entityType === 'status')) {
170 | graph.entities.push({
171 | name: statusName,
172 | entityType: 'status',
173 | observations: [`A ${statusValue} status value`]
174 | });
175 | }
176 | }
177 | // Create priority entities if they don't exist
178 | for (const priorityValue of VALID_PRIORITY_VALUES) {
179 | const priorityName = `priority:${priorityValue}`;
180 | if (!graph.entities.some(e => e.name === priorityName && e.entityType === 'priority')) {
181 | graph.entities.push({
182 | name: priorityName,
183 | entityType: 'priority',
184 | observations: [`A ${priorityValue} priority value`]
185 | });
186 | }
187 | }
188 | await this.saveGraph(graph);
189 | }
190 | // Helper method to get status of an entity
191 | async getEntityStatus(entityName) {
192 | const graph = await this.loadGraph();
193 | // Find status relation for this entity
194 | const statusRelation = graph.relations.find(r => r.from === entityName &&
195 | r.relationType === 'has_status');
196 | if (statusRelation) {
197 | // Extract status value from the status entity name (status:value)
198 | return statusRelation.to.split(':')[1];
199 | }
200 | return null;
201 | }
202 | // Helper method to get priority of an entity
203 | async getEntityPriority(entityName) {
204 | const graph = await this.loadGraph();
205 | // Find priority relation for this entity
206 | const priorityRelation = graph.relations.find(r => r.from === entityName &&
207 | r.relationType === 'has_priority');
208 | if (priorityRelation) {
209 | // Extract priority value from the priority entity name (priority:value)
210 | return priorityRelation.to.split(':')[1];
211 | }
212 | return null;
213 | }
214 | // Helper method to set status of an entity
215 | async setEntityStatus(entityName, statusValue) {
216 | if (!VALID_STATUS_VALUES.includes(statusValue)) {
217 | throw new Error(`Invalid status value: ${statusValue}. Valid values are: ${VALID_STATUS_VALUES.join(', ')}`);
218 | }
219 | const graph = await this.loadGraph();
220 | // Remove any existing status relations for this entity
221 | graph.relations = graph.relations.filter(r => !(r.from === entityName && r.relationType === 'has_status'));
222 | // Add new status relation
223 | graph.relations.push({
224 | from: entityName,
225 | to: `status:${statusValue}`,
226 | relationType: 'has_status'
227 | });
228 | await this.saveGraph(graph);
229 | }
230 | // Helper method to set priority of an entity
231 | async setEntityPriority(entityName, priorityValue) {
232 | if (!VALID_PRIORITY_VALUES.includes(priorityValue)) {
233 | throw new Error(`Invalid priority value: ${priorityValue}. Valid values are: ${VALID_PRIORITY_VALUES.join(', ')}`);
234 | }
235 | const graph = await this.loadGraph();
236 | // Remove any existing priority relations for this entity
237 | graph.relations = graph.relations.filter(r => !(r.from === entityName && r.relationType === 'has_priority'));
238 | // Add new priority relation
239 | graph.relations.push({
240 | from: entityName,
241 | to: `priority:${priorityValue}`,
242 | relationType: 'has_priority'
243 | });
244 | await this.saveGraph(graph);
245 | }
246 | async createEntities(entities) {
247 | const graph = await this.loadGraph();
248 | const existingEntityNames = new Set(graph.entities.map(e => e.name));
249 | // Validate entity types
250 | entities.forEach(entity => {
251 | if (!validateEntityType(entity.entityType)) {
252 | throw new Error(`Invalid entity type: ${entity.entityType}. Valid types are: ${VALID_ENTITY_TYPES.join(', ')}`);
253 | }
254 | });
255 | const newEntities = entities.filter(entity => !existingEntityNames.has(entity.name));
256 | graph.entities.push(...newEntities);
257 | await this.saveGraph(graph);
258 | return newEntities;
259 | }
260 | async createRelations(relations) {
261 | const graph = await this.loadGraph();
262 | const existingEntityNames = new Set(graph.entities.map(e => e.name));
263 | // Check that entities exist and validate relation types
264 | relations.forEach(relation => {
265 | if (!existingEntityNames.has(relation.from)) {
266 | throw new Error(`Entity '${relation.from}' not found`);
267 | }
268 | if (!existingEntityNames.has(relation.to)) {
269 | throw new Error(`Entity '${relation.to}' not found`);
270 | }
271 | if (!validateRelationType(relation.relationType)) {
272 | throw new Error(`Invalid relation type: ${relation.relationType}. Valid types are: ${VALID_RELATION_TYPES.join(', ')}`);
273 | }
274 | });
275 | // Filter out duplicate relations
276 | const existingRelations = new Set(graph.relations.map(r => `${r.from}:${r.to}:${r.relationType}`));
277 | const newRelations = relations.filter(r => !existingRelations.has(`${r.from}:${r.to}:${r.relationType}`));
278 | graph.relations.push(...newRelations);
279 | await this.saveGraph(graph);
280 | return newRelations;
281 | }
282 | async addObservations(observations) {
283 | const graph = await this.loadGraph();
284 | const results = [];
285 | for (const observation of observations) {
286 | const entity = graph.entities.find(e => e.name === observation.entityName);
287 | if (!entity) {
288 | throw new Error(`Entity '${observation.entityName}' not found`);
289 | }
290 | // Filter out duplicate observations
291 | const existingObservations = new Set(entity.observations);
292 | const newObservations = observation.contents.filter(o => !existingObservations.has(o));
293 | entity.observations.push(...newObservations);
294 | results.push({
295 | entityName: observation.entityName,
296 | addedObservations: newObservations
297 | });
298 | }
299 | await this.saveGraph(graph);
300 | return results;
301 | }
302 | async deleteEntities(entityNames) {
303 | const graph = await this.loadGraph();
304 | // Remove the entities
305 | graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
306 | // Remove relations that involve the deleted entities
307 | graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
308 | await this.saveGraph(graph);
309 | }
310 | async deleteObservations(deletions) {
311 | const graph = await this.loadGraph();
312 | for (const deletion of deletions) {
313 | const entity = graph.entities.find(e => e.name === deletion.entityName);
314 | if (entity) {
315 | // Remove the specified observations
316 | entity.observations = entity.observations.filter(o => !deletion.observations.includes(o));
317 | }
318 | }
319 | await this.saveGraph(graph);
320 | }
321 | async deleteRelations(relations) {
322 | const graph = await this.loadGraph();
323 | // Remove specified relations
324 | graph.relations = graph.relations.filter(r => !relations.some(toDelete => r.from === toDelete.from &&
325 | r.to === toDelete.to &&
326 | r.relationType === toDelete.relationType));
327 | await this.saveGraph(graph);
328 | }
329 | async readGraph() {
330 | return this.loadGraph();
331 | }
332 | async searchNodes(query) {
333 | const graph = await this.loadGraph();
334 | // Split query into search terms
335 | const terms = query.toLowerCase().split(/\s+/);
336 | // Find matching entities
337 | const matchingEntityNames = new Set();
338 | for (const entity of graph.entities) {
339 | // Check if all terms match
340 | const matchesAllTerms = terms.every(term => {
341 | // Check entity name
342 | if (entity.name.toLowerCase().includes(term)) {
343 | return true;
344 | }
345 | // Check entity type
346 | if (entity.entityType.toLowerCase().includes(term)) {
347 | return true;
348 | }
349 | // Check observations
350 | for (const observation of entity.observations) {
351 | if (observation.toLowerCase().includes(term)) {
352 | return true;
353 | }
354 | }
355 | return false;
356 | });
357 | if (matchesAllTerms) {
358 | matchingEntityNames.add(entity.name);
359 | }
360 | }
361 | // Find relations between matching entities
362 | const matchingRelations = graph.relations.filter(r => matchingEntityNames.has(r.from) && matchingEntityNames.has(r.to));
363 | // Return matching entities and their relations
364 | return {
365 | entities: graph.entities.filter(e => matchingEntityNames.has(e.name)),
366 | relations: matchingRelations
367 | };
368 | }
369 | async openNodes(names) {
370 | const graph = await this.loadGraph();
371 | // Find the specified entities
372 | const entities = graph.entities.filter(e => names.includes(e.name));
373 | // Find relations between the specified entities
374 | const relations = graph.relations.filter(r => names.includes(r.from) && names.includes(r.to));
375 | return {
376 | entities,
377 | relations
378 | };
379 | }
380 | // Get project overview including research questions, methodology, participants, data sources
381 | async getProjectOverview(projectName) {
382 | const graph = await this.loadGraph();
383 | // Find the project
384 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
385 | if (!project) {
386 | throw new Error(`Project '${projectName}' not found`);
387 | }
388 | // Find research questions
389 | const researchQuestions = [];
390 | for (const relation of graph.relations) {
391 | if (relation.relationType === 'part_of' && relation.to === projectName) {
392 | const question = graph.entities.find(e => e.name === relation.from && e.entityType === 'researchQuestion');
393 | if (question) {
394 | researchQuestions.push(question);
395 | }
396 | }
397 | }
398 | // Find participants
399 | const participants = [];
400 | for (const relation of graph.relations) {
401 | if (relation.relationType === 'part_of' && relation.to === projectName) {
402 | const participant = graph.entities.find(e => e.name === relation.from && e.entityType === 'participant');
403 | if (participant) {
404 | participants.push(participant);
405 | }
406 | }
407 | }
408 | // Find data sources (interviews, observations, documents)
409 | const interviews = [];
410 | const observations = [];
411 | const documents = [];
412 | for (const relation of graph.relations) {
413 | if (relation.relationType === 'part_of' && relation.to === projectName) {
414 | const entity = graph.entities.find(e => e.name === relation.from);
415 | if (entity) {
416 | if (entity.entityType === 'interview') {
417 | interviews.push(entity);
418 | }
419 | else if (entity.entityType === 'observation') {
420 | observations.push(entity);
421 | }
422 | else if (entity.entityType === 'document') {
423 | documents.push(entity);
424 | }
425 | }
426 | }
427 | }
428 | // Find findings
429 | const findings = [];
430 | for (const relation of graph.relations) {
431 | if (relation.relationType === 'part_of' && relation.to === projectName) {
432 | const finding = graph.entities.find(e => e.name === relation.from && e.entityType === 'finding');
433 | if (finding) {
434 | findings.push(finding);
435 | }
436 | }
437 | }
438 | // Find themes
439 | const themes = [];
440 | for (const relation of graph.relations) {
441 | if (relation.relationType === 'part_of' && relation.to === projectName) {
442 | const theme = graph.entities.find(e => e.name === relation.from && e.entityType === 'theme');
443 | if (theme) {
444 | themes.push(theme);
445 | }
446 | }
447 | }
448 | // Get methodology info from project observations
449 | const methodologyObs = project.observations.filter(o => o.toLowerCase().includes('method') || o.toLowerCase().includes('approach'));
450 | return {
451 | project,
452 | researchQuestions,
453 | methodology: methodologyObs,
454 | dataCollection: {
455 | participants: participants.length,
456 | interviews: interviews.length,
457 | observations: observations.length,
458 | documents: documents.length,
459 | participantsList: participants,
460 | interviewsList: interviews,
461 | observationsList: observations,
462 | documentsList: documents
463 | },
464 | analysis: {
465 | themes: themes.length,
466 | themesList: themes
467 | },
468 | findings
469 | };
470 | }
471 | // Get all data related to a specific participant
472 | async getParticipantProfile(participantName) {
473 | const graph = await this.loadGraph();
474 | // Find the participant
475 | const participant = graph.entities.find(e => e.name === participantName && e.entityType === 'participant');
476 | if (!participant) {
477 | throw new Error(`Participant '${participantName}' not found`);
478 | }
479 | // Find interviews with this participant
480 | const interviews = [];
481 | for (const relation of graph.relations) {
482 | if (relation.relationType === 'participated_in' && relation.from === participantName) {
483 | const interview = graph.entities.find(e => e.name === relation.to && e.entityType === 'interview');
484 | if (interview) {
485 | interviews.push(interview);
486 | }
487 | }
488 | }
489 | // Find observations including this participant
490 | const observations = [];
491 | for (const relation of graph.relations) {
492 | if (relation.relationType === 'participated_in' && relation.from === participantName) {
493 | const observation = graph.entities.find(e => e.name === relation.to && e.entityType === 'observation');
494 | if (observation) {
495 | observations.push(observation);
496 | }
497 | }
498 | }
499 | // Find quotes from this participant
500 | const quotes = [];
501 | for (const relation of graph.relations) {
502 | if (relation.relationType === 'contains' && relation.to === participantName) {
503 | const quote = graph.entities.find(e => e.name === relation.from && e.entityType === 'quote');
504 | if (quote) {
505 | quotes.push(quote);
506 | }
507 | }
508 | }
509 | // Find any memos about this participant
510 | const memos = [];
511 | for (const relation of graph.relations) {
512 | if (relation.relationType === 'reflects_on' && relation.to === participantName) {
513 | const memo = graph.entities.find(e => e.name === relation.from && e.entityType === 'memo');
514 | if (memo) {
515 | memos.push(memo);
516 | }
517 | }
518 | }
519 | // Extract demographic information from observations
520 | const demographicObs = participant.observations.filter(o => o.toLowerCase().includes('age') ||
521 | o.toLowerCase().includes('gender') ||
522 | o.toLowerCase().includes('occupation') ||
523 | o.toLowerCase().includes('education'));
524 | return {
525 | participant,
526 | demographics: demographicObs,
527 | interviews,
528 | observations,
529 | quotes,
530 | memos
531 | };
532 | }
533 | // Get themes with supporting codes and data
534 | async getThematicAnalysis(projectName) {
535 | const graph = await this.loadGraph();
536 | // Find the project
537 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
538 | if (!project) {
539 | throw new Error(`Project '${projectName}' not found`);
540 | }
541 | // Find all themes related to this project
542 | const themes = [];
543 | for (const relation of graph.relations) {
544 | if (relation.relationType === 'part_of' && relation.to === projectName) {
545 | const theme = graph.entities.find(e => e.name === relation.from && e.entityType === 'theme');
546 | if (theme) {
547 | themes.push(theme);
548 | }
549 | }
550 | }
551 | // For each theme, find supporting data
552 | const thematicAnalysis = themes.map(theme => {
553 | // Find codes supporting this theme
554 | const supportingCodes = [];
555 | for (const relation of graph.relations) {
556 | if (relation.relationType === 'supports' && relation.to === theme.name) {
557 | const code = graph.entities.find(e => e.name === relation.from && e.entityType === 'code');
558 | if (code) {
559 | supportingCodes.push(code);
560 | }
561 | }
562 | }
563 | // For each code, find supporting quotes
564 | const codeData = supportingCodes.map(code => {
565 | const quotes = [];
566 | for (const relation of graph.relations) {
567 | if (relation.relationType === 'codes' && relation.from === code.name) {
568 | const quote = graph.entities.find(e => e.name === relation.to && e.entityType === 'quote');
569 | if (quote) {
570 | quotes.push(quote);
571 | }
572 | }
573 | }
574 | return {
575 | code,
576 | quotes
577 | };
578 | });
579 | // Find any memos reflecting on this theme
580 | const memos = [];
581 | for (const relation of graph.relations) {
582 | if (relation.relationType === 'reflects_on' && relation.to === theme.name) {
583 | const memo = graph.entities.find(e => e.name === relation.from && e.entityType === 'memo');
584 | if (memo) {
585 | memos.push(memo);
586 | }
587 | }
588 | }
589 | // Find status of the theme
590 | const statusObs = theme.observations.find(o => o.startsWith('Status:'));
591 | const status = statusObs ? statusObs.split(':')[1].trim() : 'unknown';
592 | return {
593 | theme,
594 | status,
595 | supportingData: codeData,
596 | codes: supportingCodes,
597 | memos
598 | };
599 | });
600 | return {
601 | project,
602 | themes: thematicAnalysis
603 | };
604 | }
605 | // Get all data segments tagged with a specific code
606 | async getCodedData(codeName) {
607 | const graph = await this.loadGraph();
608 | // Find the code
609 | const code = graph.entities.find(e => e.name === codeName && e.entityType === 'code');
610 | if (!code) {
611 | throw new Error(`Code '${codeName}' not found`);
612 | }
613 | // Find which code group this code belongs to, if any
614 | const codeGroups = [];
615 | for (const relation of graph.relations) {
616 | if (relation.relationType === 'contains' && relation.to === codeName) {
617 | const codeGroup = graph.entities.find(e => e.name === relation.from && e.entityType === 'codeGroup');
618 | if (codeGroup) {
619 | codeGroups.push(codeGroup);
620 | }
621 | }
622 | }
623 | // Find all quotes tagged with this code
624 | const quotes = [];
625 | for (const relation of graph.relations) {
626 | if (relation.relationType === 'codes' && relation.from === codeName) {
627 | const quote = graph.entities.find(e => e.name === relation.to && e.entityType === 'quote');
628 | if (quote) {
629 | quotes.push(quote);
630 | }
631 | }
632 | }
633 | // Find which sources (interviews, observations, documents) these quotes come from
634 | const sources = new Map();
635 | for (const quote of quotes) {
636 | for (const relation of graph.relations) {
637 | if (relation.relationType === 'contains' && relation.from !== codeName && relation.to === quote.name) {
638 | const source = graph.entities.find(e => e.name === relation.from);
639 | if (source) {
640 | sources.set(source.name, source);
641 | }
642 | }
643 | }
644 | }
645 | // Find themes this code supports
646 | const themes = [];
647 | for (const relation of graph.relations) {
648 | if (relation.relationType === 'supports' && relation.from === codeName) {
649 | const theme = graph.entities.find(e => e.name === relation.to && e.entityType === 'theme');
650 | if (theme) {
651 | themes.push(theme);
652 | }
653 | }
654 | }
655 | // Find any memos reflecting on this code
656 | const memos = [];
657 | for (const relation of graph.relations) {
658 | if (relation.relationType === 'reflects_on' && relation.to === codeName) {
659 | const memo = graph.entities.find(e => e.name === relation.from && e.entityType === 'memo');
660 | if (memo) {
661 | memos.push(memo);
662 | }
663 | }
664 | }
665 | return {
666 | code,
667 | codeGroups,
668 | quotes,
669 | sourceCount: sources.size,
670 | sources: Array.from(sources.values()),
671 | themes,
672 | memos
673 | };
674 | }
675 | // Shows data organized by research questions with findings
676 | async getResearchQuestionAnalysis(projectName) {
677 | const graph = await this.loadGraph();
678 | // Find the project
679 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
680 | if (!project) {
681 | throw new Error(`Project '${projectName}' not found`);
682 | }
683 | // Find all research questions for this project
684 | const researchQuestions = [];
685 | for (const relation of graph.relations) {
686 | if (relation.relationType === 'part_of' && relation.to === projectName) {
687 | const question = graph.entities.find(e => e.name === relation.from && e.entityType === 'researchQuestion');
688 | if (question) {
689 | researchQuestions.push(question);
690 | }
691 | }
692 | }
693 | // For each research question, find related data
694 | const questionAnalysis = researchQuestions.map(question => {
695 | // Find findings that answer this question
696 | const findings = [];
697 | for (const relation of graph.relations) {
698 | if (relation.relationType === 'answers' && relation.to === question.name) {
699 | const finding = graph.entities.find(e => e.name === relation.from && e.entityType === 'finding');
700 | if (finding) {
701 | findings.push(finding);
702 | }
703 | }
704 | }
705 | // Find themes related to this question
706 | const themes = [];
707 | for (const relation of graph.relations) {
708 | if (relation.relationType === 'answers' && relation.to === question.name) {
709 | const theme = graph.entities.find(e => e.name === relation.from && e.entityType === 'theme');
710 | if (theme) {
711 | themes.push(theme);
712 | }
713 | }
714 | }
715 | // Find data directly addressing this question
716 | const quotes = [];
717 | for (const relation of graph.relations) {
718 | if (relation.relationType === 'answers' && relation.to === question.name) {
719 | const quote = graph.entities.find(e => e.name === relation.from && e.entityType === 'quote');
720 | if (quote) {
721 | quotes.push(quote);
722 | }
723 | }
724 | }
725 | return {
726 | question,
727 | findings,
728 | themes,
729 | quotes
730 | };
731 | });
732 | return {
733 | project,
734 | researchQuestions: questionAnalysis
735 | };
736 | }
737 | // Returns data in temporal sequence
738 | async getChronologicalData(projectName, dataType) {
739 | const graph = await this.loadGraph();
740 | // Find the project
741 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
742 | if (!project) {
743 | throw new Error(`Project '${projectName}' not found`);
744 | }
745 | // Find all data collection entities for this project
746 | let dataEntities = [];
747 | for (const relation of graph.relations) {
748 | if (relation.relationType === 'part_of' && relation.to === projectName) {
749 | let entity;
750 | // Filter by data type if specified
751 | if (dataType) {
752 | entity = graph.entities.find(e => e.name === relation.from && e.entityType === dataType);
753 | }
754 | else {
755 | entity = graph.entities.find(e => e.name === relation.from &&
756 | (e.entityType === 'interview' ||
757 | e.entityType === 'observation' ||
758 | e.entityType === 'document'));
759 | }
760 | if (entity) {
761 | dataEntities.push(entity);
762 | }
763 | }
764 | }
765 | // Extract date information for each entity
766 | const dataWithDates = dataEntities.map(entity => {
767 | const dateObs = entity.observations.find(o => o.startsWith('Date:') || o.startsWith('Collected on:') || o.startsWith('Created:'));
768 | let date = new Date(0);
769 | if (dateObs) {
770 | // Extract date string and try to parse it
771 | const dateString = dateObs.split(':')[1].trim();
772 | const parsedDate = new Date(dateString);
773 | if (!isNaN(parsedDate.getTime())) {
774 | date = parsedDate;
775 | }
776 | }
777 | return {
778 | entity,
779 | date
780 | };
781 | });
782 | // Sort by date
783 | dataWithDates.sort((a, b) => a.date.getTime() - b.date.getTime());
784 | // Create a timeline of data
785 | const timeline = dataWithDates.map(item => {
786 | // For each entity, find related quotes
787 | const quotes = [];
788 | for (const relation of graph.relations) {
789 | if (relation.relationType === 'contains' && relation.from === item.entity.name) {
790 | const quote = graph.entities.find(e => e.name === relation.to && e.entityType === 'quote');
791 | if (quote) {
792 | quotes.push(quote);
793 | }
794 | }
795 | }
796 | return {
797 | date: item.date,
798 | entity: item.entity,
799 | quotes
800 | };
801 | });
802 | return {
803 | project,
804 | timeline
805 | };
806 | }
807 | // Finds where multiple codes appear together
808 | async getCodeCooccurrence(codeName) {
809 | const graph = await this.loadGraph();
810 | // Find the code
811 | const code = graph.entities.find(e => e.name === codeName && e.entityType === 'code');
812 | if (!code) {
813 | throw new Error(`Code '${codeName}' not found`);
814 | }
815 | // Find all quotes tagged with this code
816 | const quotes = [];
817 | for (const relation of graph.relations) {
818 | if (relation.relationType === 'codes' && relation.from === codeName) {
819 | const quote = graph.entities.find(e => e.name === relation.to && e.entityType === 'quote');
820 | if (quote) {
821 | quotes.push(quote);
822 | }
823 | }
824 | }
825 | // For each quote, find other codes that also tag it
826 | const codeOccurrences = new Map();
827 | for (const quote of quotes) {
828 | for (const relation of graph.relations) {
829 | if (relation.relationType === 'codes' && relation.from !== codeName && relation.to === quote.name) {
830 | const otherCode = graph.entities.find(e => e.name === relation.from && e.entityType === 'code');
831 | if (otherCode) {
832 | if (codeOccurrences.has(otherCode.name)) {
833 | const occurrence = codeOccurrences.get(otherCode.name);
834 | occurrence.count++;
835 | occurrence.quotes.push(quote);
836 | }
837 | else {
838 | codeOccurrences.set(otherCode.name, {
839 | code: otherCode,
840 | count: 1,
841 | quotes: [quote]
842 | });
843 | }
844 | }
845 | }
846 | }
847 | }
848 | // Sort codes by co-occurrence frequency
849 | const cooccurringCodes = Array.from(codeOccurrences.values())
850 | .sort((a, b) => b.count - a.count);
851 | return {
852 | code,
853 | quotesCount: quotes.length,
854 | cooccurringCodes
855 | };
856 | }
857 | // Gets all memos related to a specific entity
858 | async getMemosByFocus(entityName) {
859 | const graph = await this.loadGraph();
860 | // Find the entity
861 | const entity = graph.entities.find(e => e.name === entityName);
862 | if (!entity) {
863 | throw new Error(`Entity '${entityName}' not found`);
864 | }
865 | // Find all memos reflecting on this entity
866 | const memos = [];
867 | for (const relation of graph.relations) {
868 | if (relation.relationType === 'reflects_on' && relation.to === entityName) {
869 | const memo = graph.entities.find(e => e.name === relation.from && e.entityType === 'memo');
870 | if (memo) {
871 | memos.push(memo);
872 | }
873 | }
874 | }
875 | // Sort memos by date if possible
876 | const memosWithDates = memos.map(memo => {
877 | const dateObs = memo.observations.find(o => o.startsWith('Date:') || o.startsWith('Created:'));
878 | let date = new Date(0);
879 | if (dateObs) {
880 | const dateString = dateObs.split(':')[1].trim();
881 | const parsedDate = new Date(dateString);
882 | if (!isNaN(parsedDate.getTime())) {
883 | date = parsedDate;
884 | }
885 | }
886 | return {
887 | memo,
888 | date
889 | };
890 | });
891 | // Sort by date, most recent first
892 | memosWithDates.sort((a, b) => b.date.getTime() - a.date.getTime());
893 | return {
894 | entity,
895 | memos: memosWithDates.map(m => m.memo)
896 | };
897 | }
898 | // Returns information about methods, sampling, analysis approach
899 | async getMethodologyDetails(projectName) {
900 | const graph = await this.loadGraph();
901 | // Find the project
902 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
903 | if (!project) {
904 | throw new Error(`Project '${projectName}' not found`);
905 | }
906 | // Extract methodology information from project observations
907 | const methodologyObs = project.observations.filter(o => o.toLowerCase().includes('method') ||
908 | o.toLowerCase().includes('approach') ||
909 | o.toLowerCase().includes('sampling') ||
910 | o.toLowerCase().includes('analysis') ||
911 | o.toLowerCase().includes('validity') ||
912 | o.toLowerCase().includes('reliability'));
913 | // Find methodology-related memos
914 | const memos = [];
915 | for (const relation of graph.relations) {
916 | if (relation.relationType === 'reflects_on' && relation.to === projectName) {
917 | const memo = graph.entities.find(e => e.name === relation.from && e.entityType === 'memo');
918 | if (memo && memo.observations.some(o => o.toLowerCase().includes('method') ||
919 | o.toLowerCase().includes('approach') ||
920 | o.toLowerCase().includes('sampling') ||
921 | o.toLowerCase().includes('analysis'))) {
922 | memos.push(memo);
923 | }
924 | }
925 | }
926 | // Calculate data collection statistics
927 | // 1. Get all interviews
928 | const interviews = [];
929 | for (const relation of graph.relations) {
930 | if (relation.relationType === 'part_of' && relation.to === projectName) {
931 | const interview = graph.entities.find(e => e.name === relation.from && e.entityType === 'interview');
932 | if (interview) {
933 | interviews.push(interview);
934 | }
935 | }
936 | }
937 | // 2. Get all observations
938 | const observations = [];
939 | for (const relation of graph.relations) {
940 | if (relation.relationType === 'part_of' && relation.to === projectName) {
941 | const observation = graph.entities.find(e => e.name === relation.from && e.entityType === 'observation');
942 | if (observation) {
943 | observations.push(observation);
944 | }
945 | }
946 | }
947 | // 3. Get all documents
948 | const documents = [];
949 | for (const relation of graph.relations) {
950 | if (relation.relationType === 'part_of' && relation.to === projectName) {
951 | const document = graph.entities.find(e => e.name === relation.from && e.entityType === 'document');
952 | if (document) {
953 | documents.push(document);
954 | }
955 | }
956 | }
957 | // 4. Get all participants
958 | const participants = [];
959 | for (const relation of graph.relations) {
960 | if (relation.relationType === 'part_of' && relation.to === projectName) {
961 | const participant = graph.entities.find(e => e.name === relation.from && e.entityType === 'participant');
962 | if (participant) {
963 | participants.push(participant);
964 | }
965 | }
966 | }
967 | // Find literature cited in this project
968 | const literature = [];
969 | for (const relation of graph.relations) {
970 | if (relation.relationType === 'cites' && relation.from === projectName) {
971 | const source = graph.entities.find(e => e.name === relation.to && e.entityType === 'literature');
972 | if (source) {
973 | literature.push(source);
974 | }
975 | }
976 | }
977 | return {
978 | project,
979 | methodology: methodologyObs,
980 | dataCollection: {
981 | participants: participants.length,
982 | interviews: interviews.length,
983 | observations: observations.length,
984 | documents: documents.length
985 | },
986 | memos,
987 | literature
988 | };
989 | }
990 | // First, let's add the missing getRelatedEntities method to the KnowledgeGraphManager class
991 | // Add this before the async main() function
992 | async getRelatedEntities(entityName, relationTypes) {
993 | const graph = await this.loadGraph();
994 | // Find the entity
995 | const entity = graph.entities.find(e => e.name === entityName);
996 | if (!entity) {
997 | throw new Error(`Entity '${entityName}' not found`);
998 | }
999 | // Find all relations involving this entity
1000 | let relevantRelations = graph.relations.filter(r => r.from === entityName || r.to === entityName);
1001 | // Filter by relation types if specified
1002 | if (relationTypes && relationTypes.length > 0) {
1003 | relevantRelations = relevantRelations.filter(r => relationTypes.includes(r.relationType));
1004 | }
1005 | // Get all related entities grouped by relation type
1006 | const related = {};
1007 | for (const relation of relevantRelations) {
1008 | const relationType = relation.relationType;
1009 | if (!related[relationType]) {
1010 | related[relationType] = [];
1011 | }
1012 | if (relation.from === entityName) {
1013 | const target = graph.entities.find(e => e.name === relation.to);
1014 | if (target) {
1015 | related[relationType].push(target);
1016 | }
1017 | }
1018 | else {
1019 | const source = graph.entities.find(e => e.name === relation.from);
1020 | if (source) {
1021 | related[relationType].push(source);
1022 | }
1023 | }
1024 | }
1025 | return {
1026 | entity,
1027 | related
1028 | };
1029 | }
1030 | }
1031 | // Main function to set up the MCP server
1032 | async function main() {
1033 | try {
1034 | const knowledgeGraphManager = new KnowledgeGraphManager();
1035 | // Initialize status and priority entities
1036 | await knowledgeGraphManager.initializeStatusAndPriority();
1037 | // Create the MCP server with a name and version
1038 | const server = new McpServer({
1039 | name: "Context Manager",
1040 | version: "1.0.0"
1041 | });
1042 | // Define a resource that exposes the entire graph
1043 | server.resource("graph", "graph://researcher/qualitative", async (uri) => ({
1044 | contents: [{
1045 | uri: uri.href,
1046 | text: JSON.stringify(await knowledgeGraphManager.readGraph(), null, 2)
1047 | }]
1048 | }));
1049 | // Define tools using zod for parameter validation
1050 | /**
1051 | * Load context for a specific entity
1052 | */
1053 | server.tool("loadcontext", toolDescriptions["loadcontext"], {
1054 | entityName: z.string(),
1055 | entityType: z.string().optional(),
1056 | sessionId: z.string().optional() // Optional to maintain backward compatibility
1057 | }, async ({ entityName, entityType = "project", sessionId }) => {
1058 | try {
1059 | // Validate session if ID is provided
1060 | if (sessionId) {
1061 | const sessionStates = await loadSessionStates();
1062 | if (!sessionStates.has(sessionId)) {
1063 | console.warn(`Warning: Session ${sessionId} not found, but proceeding with context load`);
1064 | // Initialize it anyway for more robustness
1065 | sessionStates.set(sessionId, []);
1066 | await saveSessionStates(sessionStates);
1067 | }
1068 | // Track that this entity was loaded in this session
1069 | const sessionState = sessionStates.get(sessionId) || [];
1070 | const loadEvent = {
1071 | type: 'context_loaded',
1072 | timestamp: new Date().toISOString(),
1073 | entityName,
1074 | entityType
1075 | };
1076 | sessionState.push(loadEvent);
1077 | sessionStates.set(sessionId, sessionState);
1078 | await saveSessionStates(sessionStates);
1079 | }
1080 | // Get the entity
1081 | // Changed from using 'name:' prefix to directly searching by the entity name
1082 | const entityGraph = await knowledgeGraphManager.searchNodes(entityName);
1083 | if (entityGraph.entities.length === 0) {
1084 | throw new Error(`Entity ${entityName} not found`);
1085 | }
1086 | // Find the exact entity by name (case-sensitive match)
1087 | const entity = entityGraph.entities.find(e => e.name === entityName);
1088 | if (!entity) {
1089 | throw new Error(`Entity ${entityName} not found`);
1090 | }
1091 | // Different context loading based on entity type
1092 | let contextMessage = "";
1093 | if (entityType === "project") {
1094 | // Get project overview
1095 | const projectOverview = await knowledgeGraphManager.getProjectOverview(entityName);
1096 | // Get thematic analysis
1097 | let thematicAnalysis;
1098 | try {
1099 | thematicAnalysis = await knowledgeGraphManager.getThematicAnalysis(entityName);
1100 | }
1101 | catch (error) {
1102 | thematicAnalysis = { themes: [] };
1103 | }
1104 | // Get research question analysis
1105 | let researchQuestions;
1106 | try {
1107 | researchQuestions = await knowledgeGraphManager.getResearchQuestionAnalysis(entityName);
1108 | }
1109 | catch (error) {
1110 | researchQuestions = { researchQuestions: [] };
1111 | }
1112 | // Get methodology details
1113 | let methodology;
1114 | try {
1115 | methodology = await knowledgeGraphManager.getMethodologyDetails(entityName);
1116 | }
1117 | catch (error) {
1118 | methodology = { methodology: [] };
1119 | }
1120 | // Get status and priority using the relation-based approach
1121 | const status = await knowledgeGraphManager.getEntityStatus(entityName) || "Unknown";
1122 | const priority = await knowledgeGraphManager.getEntityPriority(entityName);
1123 | const priorityText = priority ? `- **Priority**: ${priority}` : "";
1124 | // Format observations
1125 | const observationsList = entity.observations.length > 0
1126 | ? entity.observations.map(obs => `- ${obs}`).join("\n")
1127 | : "No observations";
1128 | // Extract methodology information
1129 | const methodologyText = methodology.methodology?.map((m) => `- ${m}`).join("\n") || "No methodology details available";
1130 | // Extract research questions
1131 | const questionsText = researchQuestions.researchQuestions?.map((q) => {
1132 | const findings = q.findings?.map((f) => ` - ${f.name}`).join("\n") || " - No findings yet";
1133 | return `- **${q.question.name}**\n${findings}`;
1134 | }).join("\n") || "No research questions found";
1135 | // Format data collection stats
1136 | const participantCount = projectOverview.dataCollection?.participants || 0;
1137 | const interviewCount = projectOverview.dataCollection?.interviews || 0;
1138 | const observationCount = projectOverview.dataCollection?.observations || 0;
1139 | const documentCount = projectOverview.dataCollection?.documents || 0;
1140 | // Format theme analysis
1141 | const themesText = thematicAnalysis.themes?.map(async (t) => {
1142 | const codeCount = t.supportingData?.length || 0;
1143 | const themeStatus = await knowledgeGraphManager.getEntityStatus(t.theme.name) || "unknown";
1144 | return `- **${t.theme.name}** (Status: ${themeStatus}): ${codeCount} codes`;
1145 | });
1146 | const resolvedThemesText = themesText ?
1147 | await Promise.all(themesText).then(texts => texts.join("\n")) :
1148 | "No themes identified yet";
1149 | // Get recent data collection - without date references
1150 | const recentInterviews = projectOverview.dataCollection?.interviewsList?.slice(0, 5).map(async (i) => {
1151 | const participant = i.observations.find(o => o.startsWith("participant:"))?.substring(12) || "Unknown";
1152 | const interviewStatus = await knowledgeGraphManager.getEntityStatus(i.name) || "unknown";
1153 | return `- **${i.name}** with ${participant} (Status: ${interviewStatus})`;
1154 | });
1155 | const resolvedInterviewsText = recentInterviews ?
1156 | await Promise.all(recentInterviews).then(texts => texts.join("\n")) :
1157 | "No recent interviews";
1158 | // Get findings with status from relations
1159 | const findingsText = projectOverview.findings?.map(async (f) => {
1160 | const findingStatus = await knowledgeGraphManager.getEntityStatus(f.name) || "preliminary";
1161 | const findingObs = f.observations.length > 0 ? f.observations[0] : "No description";
1162 | return `- **${f.name}** (Status: ${findingStatus}): ${findingObs}`;
1163 | });
1164 | const resolvedFindingsText = findingsText ?
1165 | await Promise.all(findingsText).then(texts => texts.join("\n")) :
1166 | "No findings recorded yet";
1167 | contextMessage = `# Qualitative Research Project Context: ${entityName}
1168 |
1169 | ## Project Details
1170 | - **Status**: ${status}
1171 | ${priorityText}
1172 |
1173 | ## Observations
1174 | ${observationsList}
1175 |
1176 | ## Research Design
1177 | ${methodologyText}
1178 |
1179 | ## Research Questions
1180 | ${questionsText}
1181 |
1182 | ## Data Collection Stats
1183 | - **Participants**: ${participantCount}
1184 | - **Interviews**: ${interviewCount}
1185 | - **Observations**: ${observationCount}
1186 | - **Documents**: ${documentCount}
1187 |
1188 | ## Recent Interviews
1189 | ${resolvedInterviewsText}
1190 |
1191 | ## Analysis Progress
1192 | ### Themes
1193 | ${resolvedThemesText}
1194 |
1195 | ## Findings
1196 | ${resolvedFindingsText}`;
1197 | }
1198 | else if (entityType === "participant") {
1199 | // Get participant profile
1200 | const participantProfile = await knowledgeGraphManager.getParticipantProfile(entityName);
1201 | // Get status and priority using the relation-based approach
1202 | const status = await knowledgeGraphManager.getEntityStatus(entityName) || "Unknown";
1203 | const priority = await knowledgeGraphManager.getEntityPriority(entityName);
1204 | const priorityText = priority ? `- **Priority**: ${priority}` : "";
1205 | // Format observations
1206 | const observationsList = entity.observations.length > 0
1207 | ? entity.observations.map(obs => `- ${obs}`).join("\n")
1208 | : "No observations";
1209 | // Format demographics without relying on patterns
1210 | const demographics = participantProfile.demographics?.map((d) => `- ${d}`).join("\n") || "No demographic information available";
1211 | // Format interviews with status from relations
1212 | const interviewsText = participantProfile.interviews?.map(async (i) => {
1213 | const interviewStatus = await knowledgeGraphManager.getEntityStatus(i.name) || "unknown";
1214 | return `- **${i.name}** (Status: ${interviewStatus})`;
1215 | });
1216 | const resolvedInterviewsText = interviewsText ?
1217 | await Promise.all(interviewsText).then(texts => texts.join("\n")) :
1218 | "No interviews recorded";
1219 | // Format observations with status from relations
1220 | const observationsText = participantProfile.observations?.map(async (o) => {
1221 | const observationStatus = await knowledgeGraphManager.getEntityStatus(o.name) || "unknown";
1222 | return `- **${o.name}** (Status: ${observationStatus})`;
1223 | });
1224 | const resolvedObservationsText = observationsText ?
1225 | await Promise.all(observationsText).then(texts => texts.join("\n")) :
1226 | "No observations recorded";
1227 | // Format quotes
1228 | const quotesText = participantProfile.quotes?.map((q) => {
1229 | // Show the full quote
1230 | const quote = q.observations.find(o => !o.startsWith("source:") && !o.startsWith("context:"));
1231 | const source = q.observations.find(o => o.startsWith("source:"))?.substring(7) || "Unknown source";
1232 | return `- "${quote || "No text available"}" (Source: ${source})`;
1233 | }).join("\n") || "No quotes recorded";
1234 | // Format memos
1235 | const memosText = participantProfile.memos?.map(async (m) => {
1236 | const memoStatus = await knowledgeGraphManager.getEntityStatus(m.name) || "unknown";
1237 | const topic = m.observations.find(o => o.startsWith("topic:"))?.substring(6) || "Untitled";
1238 | return `- **${topic}** (Status: ${memoStatus})`;
1239 | });
1240 | const resolvedMemosText = memosText ?
1241 | await Promise.all(memosText).then(texts => texts.join("\n")) :
1242 | "No memos about this participant";
1243 | contextMessage = `# Participant Context: ${entityName}
1244 |
1245 | ## Status and Priority
1246 | - **Status**: ${status}
1247 | ${priorityText}
1248 |
1249 | ## Observations
1250 | ${observationsList}
1251 |
1252 | ## Demographics
1253 | ${demographics}
1254 |
1255 | ## Interviews
1256 | ${resolvedInterviewsText}
1257 |
1258 | ## Observations
1259 | ${resolvedObservationsText}
1260 |
1261 | ## Quotes
1262 | ${quotesText}
1263 |
1264 | ## Research Memos
1265 | ${resolvedMemosText}`;
1266 | }
1267 | else if (entityType === "interview") {
1268 | // Find which project this interview belongs to
1269 | let projectName = 'Unknown project';
1270 | for (const relation of entityGraph.relations) {
1271 | if (relation.relationType === 'part_of' && relation.from === entityName) {
1272 | const project = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'project');
1273 | if (project) {
1274 | projectName = project.name;
1275 | break;
1276 | }
1277 | }
1278 | }
1279 | // Get status and priority using the relation-based approach
1280 | const status = await knowledgeGraphManager.getEntityStatus(entityName) || "Unknown";
1281 | const priority = await knowledgeGraphManager.getEntityPriority(entityName);
1282 | const priorityText = priority ? `- **Priority**: ${priority}` : "";
1283 | // Format observations
1284 | const observationsList = entity.observations.length > 0
1285 | ? entity.observations.map(obs => `- ${obs}`).join("\n")
1286 | : "No observations";
1287 | // Get interview details without parsing date
1288 | const participant = entity.observations.find(o => o.startsWith("participant:"))?.substring(12) || "Unknown";
1289 | // Find codes applied to this interview and include their status
1290 | const codesWithStatus = [];
1291 | for (const relation of entityGraph.relations) {
1292 | if (relation.relationType === 'codes' && relation.to === entityName) {
1293 | const code = entityGraph.entities.find(e => e.name === relation.from && e.entityType === 'code');
1294 | if (code) {
1295 | const codeStatus = await knowledgeGraphManager.getEntityStatus(code.name) || "unknown";
1296 | codesWithStatus.push({
1297 | code,
1298 | status: codeStatus
1299 | });
1300 | }
1301 | }
1302 | }
1303 | const codesText = codesWithStatus.map(c => `- **${c.code.name}** (Status: ${c.status}): ${c.code.observations[0] || "No description"}`).join("\n") || "No codes applied yet";
1304 | // Find quotes from this interview
1305 | const quotes = [];
1306 | for (const relation of entityGraph.relations) {
1307 | if (relation.relationType === 'contains' && relation.from === entityName) {
1308 | const quote = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'quote');
1309 | if (quote) {
1310 | quotes.push(quote);
1311 | }
1312 | }
1313 | }
1314 | const quotesText = quotes.map(q => {
1315 | // Get the full quote text
1316 | const quoteText = q.observations.find(o => !o.startsWith("context:") && !o.startsWith("speaker:")) || "No text";
1317 | return `- "${quoteText}"`;
1318 | }).join("\n") || "No notable quotes recorded";
1319 | contextMessage = `# Interview Context: ${entityName}
1320 |
1321 | ## Overview
1322 | - **Project**: ${projectName}
1323 | - **Participant**: ${participant}
1324 | - **Status**: ${status}
1325 | ${priorityText}
1326 |
1327 | ## Observations
1328 | ${observationsList}
1329 |
1330 | ## Applied Codes
1331 | ${codesText}
1332 |
1333 | ## Notable Quotes
1334 | ${quotesText}`;
1335 | }
1336 | else if (entityType === "code") {
1337 | // Get coded data for this code
1338 | const codedData = await knowledgeGraphManager.getCodedData(entityName);
1339 | // Format code context
1340 | const definition = entity.observations.find(o => !o.startsWith("status:") && !o.startsWith("created:"));
1341 | const created = entity.observations.find(o => o.startsWith("created:"))?.substring(8) || "Unknown";
1342 | const status = entity.observations.find(o => o.startsWith("status:"))?.substring(7) || "active";
1343 | // Format code groups
1344 | const codeGroupsText = codedData.codeGroups?.map((group) => {
1345 | const description = group.observations.find(o => !o.startsWith("created:"));
1346 | return `- **${group.name}**: ${description || "No description"}`;
1347 | }).join("\n") || "Not part of any code groups";
1348 | // Format quotes
1349 | const quotesText = codedData.quotes?.map((quote) => {
1350 | const source = quote.observations.find(o => o.startsWith("source:"))?.substring(7) || "Unknown source";
1351 | const text = quote.observations.find(o => !o.startsWith("source:") && !o.startsWith("context:"));
1352 | return `- "${text || "No text"}" (Source: ${source})`;
1353 | }).join("\n") || "No quotes tagged with this code";
1354 | // Format sources
1355 | const sourcesText = codedData.sources?.map((source) => {
1356 | return `- **${source.name}** (${source.entityType})`;
1357 | }).join("\n") || "No sources found";
1358 | // Format themes
1359 | const themesText = codedData.themes?.map((theme) => {
1360 | const description = theme.observations.find(o => !o.startsWith("status:") && !o.startsWith("created:"));
1361 | return `- **${theme.name}**: ${description || "No description"}`;
1362 | }).join("\n") || "Not associated with any themes";
1363 | // Get co-occurrence data
1364 | let cooccurrenceData;
1365 | try {
1366 | cooccurrenceData = await knowledgeGraphManager.getCodeCooccurrence(entityName);
1367 | // Format co-occurrence
1368 | const cooccurrenceText = cooccurrenceData.cooccurringCodes?.map((c) => {
1369 | return `- **${c.code.name}** (${c.count} co-occurrences)`;
1370 | }).slice(0, 5).join("\n") || "No code co-occurrence data";
1371 | contextMessage = `# Code Context: ${entityName}
1372 |
1373 | ## Code Details
1374 | - **Definition**: ${definition || "No definition provided"}
1375 | - **Created**: ${created}
1376 | - **Status**: ${status}
1377 | - **Items Coded**: ${codedData.quotes?.length || 0}
1378 |
1379 | ## Part of Code Groups
1380 | ${codeGroupsText}
1381 |
1382 | ## Supporting Themes
1383 | ${themesText}
1384 |
1385 | ## Top Co-occurring Codes
1386 | ${cooccurrenceText}
1387 |
1388 | ## Example Quotes
1389 | ${quotesText}
1390 |
1391 | ## Used in These Sources
1392 | ${sourcesText}`;
1393 | }
1394 | catch (error) {
1395 | contextMessage = `# Code Context: ${entityName}
1396 |
1397 | ## Code Details
1398 | - **Definition**: ${definition || "No definition provided"}
1399 | - **Created**: ${created}
1400 | - **Status**: ${status}
1401 | - **Items Coded**: ${codedData.quotes?.length || 0}
1402 |
1403 | ## Part of Code Groups
1404 | ${codeGroupsText}
1405 |
1406 | ## Supporting Themes
1407 | ${themesText}
1408 |
1409 | ## Example Quotes
1410 | ${quotesText}
1411 |
1412 | ## Used in These Sources
1413 | ${sourcesText}`;
1414 | }
1415 | }
1416 | else if (entityType === "theme") {
1417 | // Get thematic analysis data
1418 | let projectName = "";
1419 | // Find which project this theme belongs to
1420 | for (const relation of entityGraph.relations) {
1421 | if (relation.relationType === 'part_of' && relation.from === entityName) {
1422 | const project = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'project');
1423 | if (project) {
1424 | projectName = project.name;
1425 | break;
1426 | }
1427 | }
1428 | }
1429 | let thematicAnalysis;
1430 | try {
1431 | thematicAnalysis = await knowledgeGraphManager.getThematicAnalysis(projectName);
1432 | // Find this theme in the analysis
1433 | const themeAnalysis = thematicAnalysis.themes?.find((t) => t.theme.name === entityName);
1434 | if (themeAnalysis) {
1435 | const description = entity.observations.find(o => !o.startsWith("created:") && !o.startsWith("status:"));
1436 | const status = entity.observations.find(o => o.startsWith("status:"))?.substring(7) || "emerging";
1437 | const created = entity.observations.find(o => o.startsWith("created:"))?.substring(8) || "Unknown";
1438 | // Format codes
1439 | const codesText = themeAnalysis.codes?.map((code) => {
1440 | const definition = code.observations.find(o => !o.startsWith("status:") && !o.startsWith("created:"));
1441 | return `- **${code.name}**: ${definition || "No definition"}`;
1442 | }).join("\n") || "No supporting codes";
1443 | // Format supporting quotes
1444 | const quotesText = themeAnalysis.supportingData?.flatMap((codeData) => codeData.quotes.map((quote) => {
1445 | const text = quote.observations.find(o => !o.startsWith("source:") && !o.startsWith("context:"));
1446 | return `- "${text || "No text"}" [Code: ${codeData.code.name}]`;
1447 | })).slice(0, 10).join("\n") || "No supporting quotes";
1448 | // Format memos
1449 | const memosText = themeAnalysis.memos?.map((memo) => {
1450 | const date = memo.observations.find(o => o.startsWith("date:"))?.substring(5) || "Unknown date";
1451 | const topic = memo.observations.find(o => o.startsWith("topic:"))?.substring(6) || "Untitled";
1452 | const content = memo.observations.find(o => !o.startsWith("date:") && !o.startsWith("topic:"));
1453 | return `- **${topic}** (${date}): ${content ? (content.length > 100 ? content.substring(0, 100) + "..." : content) : "No content"}`;
1454 | }).join("\n") || "No analytical memos about this theme";
1455 | contextMessage = `# Theme Context: ${entityName}
1456 |
1457 | ## Theme Details
1458 | - **Description**: ${description || "No description provided"}
1459 | - **Status**: ${status}
1460 | - **Created**: ${created}
1461 | - **Project**: ${projectName || "Not associated with a specific project"}
1462 |
1463 | ## Supporting Codes
1464 | ${codesText}
1465 |
1466 | ## Example Supporting Quotes
1467 | ${quotesText}
1468 |
1469 | ## Analytical Memos
1470 | ${memosText}`;
1471 | }
1472 | else {
1473 | const description = entity.observations.find(o => !o.startsWith("created:") && !o.startsWith("status:"));
1474 | const status = entity.observations.find(o => o.startsWith("status:"))?.substring(7) || "emerging";
1475 | contextMessage = `# Theme Context: ${entityName}
1476 |
1477 | ## Theme Details
1478 | - **Description**: ${description || "No description provided"}
1479 | - **Status**: ${status}
1480 | - **Project**: ${projectName || "Not associated with a specific project"}
1481 |
1482 | No detailed analysis available for this theme.`;
1483 | }
1484 | }
1485 | catch (error) {
1486 | const description = entity.observations.find(o => !o.startsWith("created:") && !o.startsWith("status:"));
1487 | const status = entity.observations.find(o => o.startsWith("status:"))?.substring(7) || "emerging";
1488 | contextMessage = `# Theme Context: ${entityName}
1489 |
1490 | ## Theme Details
1491 | - **Description**: ${description || "No description provided"}
1492 | - **Status**: ${status}
1493 | - **Project**: ${projectName || "Not associated with a specific project"}
1494 |
1495 | No detailed analysis available for this theme.`;
1496 | }
1497 | }
1498 | else if (entityType === "memo") {
1499 | // Get memo details
1500 | const topic = entity.observations.find(o => o.startsWith("topic:"))?.substring(6) || "Untitled";
1501 | const date = entity.observations.find(o => o.startsWith("date:"))?.substring(5) || "Unknown date";
1502 | const content = entity.observations.find(o => !o.startsWith("topic:") && !o.startsWith("date:"));
1503 | // Find what this memo reflects on
1504 | const relatedEntities = [];
1505 | for (const relation of entityGraph.relations) {
1506 | if (relation.relationType === 'reflects_on' && relation.from === entityName) {
1507 | const relatedEntity = entityGraph.entities.find(e => e.name === relation.to);
1508 | if (relatedEntity) {
1509 | relatedEntities.push(relatedEntity);
1510 | }
1511 | }
1512 | }
1513 | // Find which project this memo belongs to
1514 | let projectName = 'Unknown project';
1515 | for (const relation of entityGraph.relations) {
1516 | if (relation.relationType === 'part_of' && relation.from === entityName) {
1517 | const project = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'project');
1518 | if (project) {
1519 | projectName = project.name;
1520 | break;
1521 | }
1522 | }
1523 | }
1524 | // Format related entities
1525 | const relatedText = relatedEntities.map((e) => `- **${e.name}** (${e.entityType})`).join("\n") || "Not specifically linked to any entities";
1526 | contextMessage = `# Memo Context: ${entityName}
1527 |
1528 | ## Memo Details
1529 | - **Topic**: ${topic}
1530 | - **Date**: ${date}
1531 | - **Project**: ${projectName}
1532 |
1533 | ## Content
1534 | ${content || "No content available"}
1535 |
1536 | ## Related Entities
1537 | ${relatedText}`;
1538 | }
1539 | else if (entityType === "researchQuestion") {
1540 | // Find which project this research question belongs to
1541 | let projectName = 'Unknown project';
1542 | for (const relation of entityGraph.relations) {
1543 | if (relation.relationType === 'part_of' && relation.from === entityName) {
1544 | const project = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'project');
1545 | if (project) {
1546 | projectName = project.name;
1547 | break;
1548 | }
1549 | }
1550 | }
1551 | // Get research question analysis
1552 | let analysisData;
1553 | try {
1554 | analysisData = await knowledgeGraphManager.getResearchQuestionAnalysis(projectName);
1555 | // Find this question in the analysis
1556 | const questionAnalysis = analysisData.researchQuestions?.find((q) => q.question.name === entityName);
1557 | if (questionAnalysis) {
1558 | // Format findings
1559 | const findingsText = questionAnalysis.findings?.map((finding) => {
1560 | const status = finding.observations.find(o => o.startsWith("status:"))?.substring(7) || "preliminary";
1561 | const description = finding.observations.find(o => !o.startsWith("status:") && !o.startsWith("created:"));
1562 | return `- **${finding.name}** (${status}): ${description || "No description"}`;
1563 | }).join("\n") || "No findings recorded yet";
1564 | // Format themes
1565 | const themesText = questionAnalysis.themes?.map((theme) => {
1566 | const description = theme.observations.find(o => !o.startsWith("status:") && !o.startsWith("created:"));
1567 | return `- **${theme.name}**: ${description || "No description"}`;
1568 | }).join("\n") || "No themes associated with this question";
1569 | // Format quotes
1570 | const quotesText = questionAnalysis.quotes?.map((quote) => {
1571 | const source = quote.observations.find(o => o.startsWith("source:"))?.substring(7) || "Unknown source";
1572 | const text = quote.observations.find(o => !o.startsWith("source:") && !o.startsWith("context:"));
1573 | return `- "${text || "No text"}" (Source: ${source})`;
1574 | }).slice(0, 5).join("\n") || "No direct quotes addressing this question";
1575 | contextMessage = `# Research Question Context: ${entityName}
1576 |
1577 | ## Question
1578 | ${entity.observations.find(o => !o.startsWith("created:")) || entityName}
1579 |
1580 | ## Project
1581 | ${projectName}
1582 |
1583 | ## Findings
1584 | ${findingsText}
1585 |
1586 | ## Related Themes
1587 | ${themesText}
1588 |
1589 | ## Supporting Quotes
1590 | ${quotesText}`;
1591 | }
1592 | else {
1593 | contextMessage = `# Research Question Context: ${entityName}
1594 |
1595 | ## Question
1596 | ${entity.observations.find(o => !o.startsWith("created:")) || entityName}
1597 |
1598 | ## Project
1599 | ${projectName}
1600 |
1601 | No analysis data available for this research question.`;
1602 | }
1603 | }
1604 | catch (error) {
1605 | contextMessage = `# Research Question Context: ${entityName}
1606 |
1607 | ## Question
1608 | ${entity.observations.find(o => !o.startsWith("created:")) || entityName}
1609 |
1610 | ## Project
1611 | ${projectName}
1612 |
1613 | No analysis data available for this research question.`;
1614 | }
1615 | }
1616 | else {
1617 | // Generic entity context for other entity types
1618 | // Get related entities
1619 | const relatedEntitiesData = await knowledgeGraphManager.getRelatedEntities(entityName);
1620 | // Format observations
1621 | const observationsText = entity.observations.map((obs) => `- ${obs}`).join("\n") || "No observations";
1622 | // Format related entities
1623 | const relatedText = Object.entries(relatedEntitiesData.related || {}).map(([relation, entities]) => {
1624 | const entitiesList = entities.map(e => `- **${e.name}** (${e.entityType})`).join("\n");
1625 | return `### ${relation} (${entities.length})\n${entitiesList}`;
1626 | }).join("\n\n") || "No related entities found";
1627 | contextMessage = `# Entity Context: ${entityName} (${entityType})
1628 |
1629 | ## Observations
1630 | ${observationsText}
1631 |
1632 | ## Related Entities
1633 | ${relatedText}`;
1634 | }
1635 | return {
1636 | content: [{
1637 | type: "text",
1638 | text: contextMessage
1639 | }]
1640 | };
1641 | }
1642 | catch (error) {
1643 | return {
1644 | content: [{
1645 | type: "text",
1646 | text: JSON.stringify({
1647 | success: false,
1648 | error: error instanceof Error ? error.message : String(error)
1649 | }, null, 2)
1650 | }]
1651 | };
1652 | }
1653 | });
1654 | // Helper function to process each stage of endsession
1655 | async function processStage(params, previousStages) {
1656 | // Process based on the stage
1657 | switch (params.stage) {
1658 | case "summary":
1659 | // Process summary stage
1660 | return {
1661 | stage: "summary",
1662 | stageNumber: params.stageNumber,
1663 | analysis: params.analysis || "",
1664 | stageData: params.stageData || {
1665 | summary: "",
1666 | duration: "",
1667 | project: ""
1668 | },
1669 | completed: !params.nextStageNeeded
1670 | };
1671 | case "interviewData":
1672 | // Process interview data stage
1673 | return {
1674 | stage: "interviewData",
1675 | stageNumber: params.stageNumber,
1676 | analysis: params.analysis || "",
1677 | stageData: params.stageData || { interviews: [] },
1678 | completed: !params.nextStageNeeded
1679 | };
1680 | case "memos":
1681 | // Process memos stage
1682 | return {
1683 | stage: "memos",
1684 | stageNumber: params.stageNumber,
1685 | analysis: params.analysis || "",
1686 | stageData: params.stageData || { memos: [] },
1687 | completed: !params.nextStageNeeded
1688 | };
1689 | case "codingActivity":
1690 | // Process coding activity stage
1691 | return {
1692 | stage: "codingActivity",
1693 | stageNumber: params.stageNumber,
1694 | analysis: params.analysis || "",
1695 | stageData: params.stageData || { codes: [] },
1696 | completed: !params.nextStageNeeded
1697 | };
1698 | case "themes":
1699 | // Process themes stage
1700 | return {
1701 | stage: "themes",
1702 | stageNumber: params.stageNumber,
1703 | analysis: params.analysis || "",
1704 | stageData: params.stageData || { themes: [] },
1705 | completed: !params.nextStageNeeded
1706 | };
1707 | case "projectStatus":
1708 | // Process project status stage
1709 | return {
1710 | stage: "projectStatus",
1711 | stageNumber: params.stageNumber,
1712 | analysis: params.analysis || "",
1713 | stageData: params.stageData || {
1714 | projectStatus: "",
1715 | projectObservation: ""
1716 | },
1717 | completed: !params.nextStageNeeded
1718 | };
1719 | case "assembly":
1720 | // Final assembly stage - compile all arguments for end-session
1721 | return {
1722 | stage: "assembly",
1723 | stageNumber: params.stageNumber,
1724 | analysis: "Final assembly of end-session arguments",
1725 | stageData: assembleEndSessionArgs(previousStages),
1726 | completed: true
1727 | };
1728 | default:
1729 | throw new Error(`Unknown stage: ${params.stage}`);
1730 | }
1731 | }
1732 | // Helper function to assemble the final end-session arguments
1733 | function assembleEndSessionArgs(stages) {
1734 | const summaryStage = stages.find(s => s.stage === "summary");
1735 | const interviewDataStage = stages.find(s => s.stage === "interviewData");
1736 | const memosStage = stages.find(s => s.stage === "memos");
1737 | const codingActivityStage = stages.find(s => s.stage === "codingActivity");
1738 | const themesStage = stages.find(s => s.stage === "themes");
1739 | const projectStatusStage = stages.find(s => s.stage === "projectStatus");
1740 | return {
1741 | summary: summaryStage?.stageData?.summary || "",
1742 | duration: summaryStage?.stageData?.duration || "unknown",
1743 | project: summaryStage?.stageData?.project || "",
1744 | interviewData: JSON.stringify(interviewDataStage?.stageData?.interviews || []),
1745 | newMemos: JSON.stringify(memosStage?.stageData?.memos || []),
1746 | codingActivity: JSON.stringify(codingActivityStage?.stageData?.codes || []),
1747 | newThemes: JSON.stringify(themesStage?.stageData?.themes || []),
1748 | projectStatus: projectStatusStage?.stageData?.projectStatus || "",
1749 | projectObservation: projectStatusStage?.stageData?.projectObservation || ""
1750 | };
1751 | }
1752 | /**
1753 | * End session by processing all stages and recording the final results.
1754 | * Only use this tool if the user asks for it.
1755 | *
1756 | * Usage examples:
1757 | *
1758 | * 1. Starting the end session process with the summary stage:
1759 | * {
1760 | * "sessionId": "qual_1234567890_abc123", // From startsession
1761 | * "stage": "summary",
1762 | * "stageNumber": 1,
1763 | * "totalStages": 6,
1764 | * "analysis": "Analyzed progress on the interview data coding",
1765 | * "stageData": {
1766 | * "summary": "Completed initial coding of participant interviews",
1767 | * "duration": "3 hours",
1768 | * "project": "Health Behavior Study" // Project name
1769 | * },
1770 | * "nextStageNeeded": true, // More stages coming
1771 | * "isRevision": false
1772 | * }
1773 | *
1774 | * 2. Middle stage for themes:
1775 | * {
1776 | * "sessionId": "qual_1234567890_abc123",
1777 | * "stage": "themes",
1778 | * "stageNumber": 2,
1779 | * "totalStages": 6,
1780 | * "analysis": "Identified emerging themes",
1781 | * "stageData": {
1782 | * "themes": [
1783 | * { "name": "Perceived Barriers", "codes": ["time_constraints", "financial_concerns"], "description": "Factors preventing healthy behaviors" },
1784 | * { "name": "Social Support", "codes": ["family_influence", "peer_encouragement"], "description": "External motivation from relationships" }
1785 | * ]
1786 | * },
1787 | * "nextStageNeeded": true,
1788 | * "isRevision": false
1789 | * }
1790 | *
1791 | * 3. Final assembly stage:
1792 | * {
1793 | * "sessionId": "qual_1234567890_abc123",
1794 | * "stage": "assembly",
1795 | * "stageNumber": 6,
1796 | * "totalStages": 6,
1797 | * "nextStageNeeded": false, // This completes the session
1798 | * "isRevision": false
1799 | * }
1800 | */
1801 | server.tool("endsession", toolDescriptions["endsession"], {
1802 | sessionId: z.string().describe("The unique session identifier obtained from startsession"),
1803 | stage: z.string().describe("Current stage of analysis: 'summary', 'themes', 'codes', 'memos', 'participantInsights', or 'assembly'"),
1804 | stageNumber: z.number().int().positive().describe("The sequence number of the current stage (starts at 1)"),
1805 | totalStages: z.number().int().positive().describe("Total number of stages in the workflow (typically 6 for standard workflow)"),
1806 | analysis: z.string().optional().describe("Text analysis or observations for the current stage"),
1807 | stageData: z.record(z.string(), z.any()).optional().describe(`Stage-specific data structure - format depends on the stage type:
1808 | - For 'summary' stage: { summary: "Session summary text", duration: "3 hours", project: "Project Name" }
1809 | - For 'themes' stage: { themes: [{ name: "Theme1", codes: ["code1", "code2"], description: "Theme description" }] }
1810 | - For 'codes' stage: { codes: [{ name: "Code1", description: "Code meaning", quotes: ["Quote text"] }] }
1811 | - For 'memos' stage: { memos: [{ title: "Memo title", content: "Detailed memo text", tags: ["tag1", "tag2"] }] }
1812 | - For 'participantInsights' stage: { insights: [{ participant: "P1", observation: "Key insight about participant" }] }
1813 | - For 'assembly' stage: no stageData needed - automatic assembly of previous stages`),
1814 | nextStageNeeded: z.boolean().describe("Whether additional stages are needed after this one (false for final stage)"),
1815 | isRevision: z.boolean().optional().describe("Whether this is revising a previous stage"),
1816 | revisesStage: z.number().int().positive().optional().describe("If revising, which stage number is being revised")
1817 | }, async (params) => {
1818 | try {
1819 | // Load session states from persistent storage
1820 | const sessionStates = await loadSessionStates();
1821 | // Validate session ID
1822 | if (!sessionStates.has(params.sessionId)) {
1823 | return {
1824 | content: [{
1825 | type: "text",
1826 | text: JSON.stringify({
1827 | success: false,
1828 | error: `Session with ID ${params.sessionId} not found. Please start a new session with startsession.`
1829 | }, null, 2)
1830 | }]
1831 | };
1832 | }
1833 | // Get or initialize session state
1834 | let sessionState = sessionStates.get(params.sessionId) || [];
1835 | // Process the current stage
1836 | const stageResult = await processStage(params, sessionState);
1837 | // Store updated state
1838 | if (params.isRevision && params.revisesStage) {
1839 | // Find the analysis stages in the session state
1840 | const analysisStages = sessionState.filter(item => item.type === 'analysis_stage') || [];
1841 | if (params.revisesStage <= analysisStages.length) {
1842 | // Replace the revised stage
1843 | analysisStages[params.revisesStage - 1] = {
1844 | type: 'analysis_stage',
1845 | ...stageResult
1846 | };
1847 | }
1848 | else {
1849 | // Add as a new stage
1850 | analysisStages.push({
1851 | type: 'analysis_stage',
1852 | ...stageResult
1853 | });
1854 | }
1855 | // Update the session state with the modified analysis stages
1856 | sessionState = [
1857 | ...sessionState.filter(item => item.type !== 'analysis_stage'),
1858 | ...analysisStages
1859 | ];
1860 | }
1861 | else {
1862 | // Add new stage
1863 | sessionState.push({
1864 | type: 'analysis_stage',
1865 | ...stageResult
1866 | });
1867 | }
1868 | // Update in persistent storage
1869 | sessionStates.set(params.sessionId, sessionState);
1870 | await saveSessionStates(sessionStates);
1871 | // Check if this is the final assembly stage and no more stages are needed
1872 | if (params.stage === "assembly" && !params.nextStageNeeded) {
1873 | // Get the assembled arguments
1874 | const args = stageResult.stageData;
1875 | try {
1876 | // Parse arguments
1877 | const summary = args.summary;
1878 | const duration = args.duration;
1879 | const project = args.project;
1880 | const interviewData = args.interviewData ? JSON.parse(args.interviewData) : [];
1881 | const newMemos = args.newMemos ? JSON.parse(args.newMemos) : [];
1882 | const codingActivity = args.codingActivity ? JSON.parse(args.codingActivity) : [];
1883 | const newThemes = args.newThemes ? JSON.parse(args.newThemes) : [];
1884 | const projectStatus = args.projectStatus;
1885 | const projectObservation = args.projectObservation;
1886 | // Update project status using the relation-based approach
1887 | try {
1888 | // Set the project status using our helper method
1889 | if (projectStatus) {
1890 | await knowledgeGraphManager.setEntityStatus(project, projectStatus);
1891 | }
1892 | // Add observation if provided
1893 | if (projectObservation) {
1894 | await knowledgeGraphManager.addObservations([{
1895 | entityName: project,
1896 | contents: [projectObservation]
1897 | }]);
1898 | }
1899 | }
1900 | catch (error) {
1901 | console.error(`Error updating status for project ${project}:`, error);
1902 | }
1903 | // Record session completion in persistent storage
1904 | sessionState.push({
1905 | type: 'session_completed',
1906 | timestamp: new Date().toISOString(),
1907 | project
1908 | });
1909 | sessionStates.set(params.sessionId, sessionState);
1910 | await saveSessionStates(sessionStates);
1911 | // Prepare the summary message
1912 | const summaryMessage = `# Qualitative Research Session Recorded
1913 |
1914 | I've recorded your research session focusing on the ${project} project.
1915 |
1916 | ## Session Summary
1917 | ${summary}
1918 |
1919 | ${interviewData.length > 0 ? `## Interviews Conducted
1920 | ${interviewData.map((i) => `- Interview with ${i.participant}`).join('\n')}` : "No interviews were recorded."}
1921 |
1922 | ${newMemos.length > 0 ? `## Research Memos Created
1923 | ${newMemos.map((m) => `- ${m.topic}`).join('\n')}` : "No memos were created."}
1924 |
1925 | ${codingActivity.length > 0 ? `## Coding Activity
1926 | ${codingActivity.map((c) => `- Coded ${c.dataItem} with "${c.code}"${c.note ? `: ${c.note}` : ''}`).join('\n')}` : "No coding was performed."}
1927 |
1928 | ${newThemes.length > 0 ? `## Themes Identified
1929 | ${newThemes.map((t) => `- ${t.name}: ${t.description}`).join('\n')}` : "No themes were identified."}
1930 |
1931 | ## Project Status
1932 | Project ${project} has been updated to: ${projectStatus}
1933 |
1934 | Would you like me to perform any additional updates to your qualitative research knowledge graph?`;
1935 | // Return the final result with the session recorded message
1936 | return {
1937 | content: [{
1938 | type: "text",
1939 | text: JSON.stringify({
1940 | success: true,
1941 | stageCompleted: params.stage,
1942 | nextStageNeeded: false,
1943 | stageResult: stageResult,
1944 | sessionRecorded: true,
1945 | summaryMessage: summaryMessage
1946 | }, null, 2)
1947 | }]
1948 | };
1949 | }
1950 | catch (error) {
1951 | return {
1952 | content: [{
1953 | type: "text",
1954 | text: JSON.stringify({
1955 | success: false,
1956 | error: `Error assembling end-session arguments: ${error instanceof Error ? error.message : String(error)}`
1957 | }, null, 2)
1958 | }]
1959 | };
1960 | }
1961 | }
1962 | else {
1963 | // This is not the final stage or more stages are needed
1964 | // Return intermediate result
1965 | return {
1966 | content: [{
1967 | type: "text",
1968 | text: JSON.stringify({
1969 | success: true,
1970 | stageCompleted: params.stage,
1971 | nextStageNeeded: params.nextStageNeeded,
1972 | stageResult: stageResult,
1973 | endSessionArgs: params.stage === "assembly" ? stageResult.stageData : null
1974 | }, null, 2)
1975 | }]
1976 | };
1977 | }
1978 | }
1979 | catch (error) {
1980 | return {
1981 | content: [{
1982 | type: "text",
1983 | text: JSON.stringify({
1984 | success: false,
1985 | error: `Error recording qualitative research session: ${error instanceof Error ? error.message : String(error)}`
1986 | }, null, 2)
1987 | }]
1988 | };
1989 | }
1990 | });
1991 | /**
1992 | * Start a new session for qualitative research. Returns session ID, recent sessions, active projects, sample participants, top codes, and recent memos.
1993 | * The output allows the user to easily choose what to focus on and which specific context to load.
1994 | */
1995 | server.tool("startsession", toolDescriptions["startsession"], {}, async () => {
1996 | try {
1997 | // Generate a unique session ID
1998 | const sessionId = generateSessionId();
1999 | // Get recent sessions from persistent storage
2000 | const sessionStates = await loadSessionStates();
2001 | // Initialize the session state
2002 | sessionStates.set(sessionId, []);
2003 | await saveSessionStates(sessionStates);
2004 | // Convert sessions map to array and retrieve the most recent sessions
2005 | const recentSessions = Array.from(sessionStates.entries())
2006 | .map(([id, stages]) => {
2007 | // Extract summary data from the first stage (if it exists)
2008 | const summaryStage = stages.find(s => s.stage === "summary");
2009 | return {
2010 | id,
2011 | project: summaryStage?.stageData?.project || "Unknown project",
2012 | summary: summaryStage?.stageData?.summary || "No summary available"
2013 | };
2014 | })
2015 | .slice(0, 3); // Default to showing 3 recent sessions
2016 | // Query for all research projects and filter by status
2017 | const projectsQuery = await knowledgeGraphManager.searchNodes("entityType:project");
2018 | const projects = [];
2019 | // Filter for active projects based on has_status relation
2020 | for (const project of projectsQuery.entities) {
2021 | const status = await knowledgeGraphManager.getEntityStatus(project.name);
2022 | if (status === "active" || status === "in_progress" || status === "data_collection" || status === "analysis") {
2023 | projects.push(project);
2024 | }
2025 | }
2026 | // Query for a sample of participants
2027 | const participantsQuery = await knowledgeGraphManager.searchNodes("entityType:participant");
2028 | const participants = participantsQuery.entities.slice(0, 5); // Limit to 5 participants for initial display
2029 | // Get all codes
2030 | const codesQuery = await knowledgeGraphManager.searchNodes("entityType:code");
2031 | const codes = codesQuery.entities.slice(0, 10); // Top 10 codes
2032 | // Get recent memos
2033 | const memosQuery = await knowledgeGraphManager.searchNodes("entityType:memo");
2034 | const memos = memosQuery.entities.slice(0, 3); // Most recent 3 memos
2035 | // Format the context information using entity-relation approach
2036 | const projectsText = await Promise.all(projects.map(async (p) => {
2037 | const status = await knowledgeGraphManager.getEntityStatus(p.name) || "Unknown";
2038 | const priority = await knowledgeGraphManager.getEntityPriority(p.name);
2039 | const priorityText = priority ? `, Priority: ${priority}` : "";
2040 | // Show truncated preview of first observation
2041 | const preview = p.observations.length > 0
2042 | ? `${p.observations[0].substring(0, 60)}${p.observations[0].length > 60 ? '...' : ''}`
2043 | : "No description";
2044 | return `- **${p.name}** (Status: ${status}${priorityText}): ${preview}`;
2045 | }));
2046 | const participantsText = await Promise.all(participants.map(async (p) => {
2047 | const status = await knowledgeGraphManager.getEntityStatus(p.name) || "Active";
2048 | // Show truncated preview of first observation for demographics
2049 | const preview = p.observations.length > 0
2050 | ? `${p.observations[0].substring(0, 60)}${p.observations[0].length > 60 ? '...' : ''}`
2051 | : "No demographics";
2052 | return `- **${p.name}** (Status: ${status}): ${preview}`;
2053 | }));
2054 | const codesText = await Promise.all(codes.map(async (c) => {
2055 | const status = await knowledgeGraphManager.getEntityStatus(c.name) || "initial";
2056 | // Show truncated preview of first observation for description
2057 | const preview = c.observations.length > 0
2058 | ? `${c.observations[0].substring(0, 60)}${c.observations[0].length > 60 ? '...' : ''}`
2059 | : "No description";
2060 | return `- **${c.name}** (Status: ${status}): ${preview}`;
2061 | }));
2062 | const memosText = await Promise.all(memos.map(async (m) => {
2063 | const status = await knowledgeGraphManager.getEntityStatus(m.name) || "draft";
2064 | // Show truncated preview of first observation for content
2065 | const preview = m.observations.length > 0
2066 | ? `${m.observations[0].substring(0, 60)}${m.observations[0].length > 60 ? '...' : ''}`
2067 | : "No content";
2068 | return `- **${m.name}** (Status: ${status}): ${preview}`;
2069 | }));
2070 | const sessionsText = recentSessions.map(s => {
2071 | return `- ${s.project} - ${s.summary.substring(0, 60)}${s.summary.length > 60 ? '...' : ''}`;
2072 | }).join("\n");
2073 | return {
2074 | content: [{
2075 | type: "text",
2076 | text: `# Choose what to focus on in this session
2077 |
2078 | ## Session ID
2079 | \`${sessionId}\`
2080 |
2081 | ## Recent Research Sessions
2082 | ${sessionsText || "No recent sessions found."}
2083 |
2084 | ## Active Research Projects
2085 | ${projectsText.join("\n") || "No active projects found."}
2086 |
2087 | ## Sample Participants
2088 | ${participantsText.join("\n") || "No participants found."}
2089 |
2090 | ## Top Codes
2091 | ${codesText.join("\n") || "No codes found."}
2092 |
2093 | ## Recent Memos
2094 | ${memosText.join("\n") || "No memos found."}
2095 |
2096 | To load specific context, use the \`loadcontext\` tool with the entity name and session ID - ${sessionId}`
2097 | }]
2098 | };
2099 | }
2100 | catch (error) {
2101 | return {
2102 | content: [{
2103 | type: "text",
2104 | text: JSON.stringify({
2105 | success: false,
2106 | error: error instanceof Error ? error.message : String(error)
2107 | }, null, 2)
2108 | }]
2109 | };
2110 | }
2111 | });
2112 | /**
2113 | * Create new entities, relations, and observations.
2114 | */
2115 | server.tool("buildcontext", toolDescriptions["buildcontext"], {
2116 | type: z.enum(["entities", "relations", "observations"]).describe("Type of creation operation: 'entities', 'relations', or 'observations'"),
2117 | data: z.array(z.any()).describe("Data for the creation operation, structure varies by type but must be an array")
2118 | }, async ({ type, data }) => {
2119 | try {
2120 | let result;
2121 | switch (type) {
2122 | case "entities":
2123 | // Validate entity types
2124 | for (const entity of data) {
2125 | if (!validateEntityType(entity.entityType)) {
2126 | throw new Error(`Invalid entity type: ${entity.entityType}`);
2127 | }
2128 | }
2129 | // Ensure entities match the Entity interface
2130 | const typedEntities = data.map((e) => ({
2131 | name: e.name,
2132 | entityType: e.entityType,
2133 | observations: e.observations
2134 | }));
2135 | result = await knowledgeGraphManager.createEntities(typedEntities);
2136 | return {
2137 | content: [{
2138 | type: "text",
2139 | text: JSON.stringify({ success: true, created: result }, null, 2)
2140 | }]
2141 | };
2142 | case "relations":
2143 | // Validate relation types
2144 | for (const relation of data) {
2145 | if (!validateRelationType(relation.relationType)) {
2146 | throw new Error(`Invalid relation type: ${relation.relationType}`);
2147 | }
2148 | }
2149 | // Ensure relations match the Relation interface
2150 | const typedRelations = data.map((r) => ({
2151 | from: r.from,
2152 | to: r.to,
2153 | relationType: r.relationType
2154 | }));
2155 | result = await knowledgeGraphManager.createRelations(typedRelations);
2156 | return {
2157 | content: [{
2158 | type: "text",
2159 | text: JSON.stringify({ success: true, created: result }, null, 2)
2160 | }]
2161 | };
2162 | case "observations":
2163 | // For qualitative researcher domain, addObservations takes an array
2164 | // Ensure observations match the required interface
2165 | const typedObservations = Array.isArray(data) ? data.map((o) => ({
2166 | entityName: o.entityName,
2167 | contents: Array.isArray(o.contents) ? o.contents :
2168 | Array.isArray(o.observations) ? o.observations : []
2169 | })) : [data];
2170 | result = await knowledgeGraphManager.addObservations(typedObservations);
2171 | return {
2172 | content: [{
2173 | type: "text",
2174 | text: JSON.stringify({ success: true, added: result }, null, 2)
2175 | }]
2176 | };
2177 | default:
2178 | throw new Error(`Invalid type: ${type}. Must be 'entities', 'relations', or 'observations'.`);
2179 | }
2180 | }
2181 | catch (error) {
2182 | return {
2183 | content: [{
2184 | type: "text",
2185 | text: JSON.stringify({
2186 | success: false,
2187 | error: error instanceof Error ? error.message : String(error)
2188 | }, null, 2)
2189 | }]
2190 | };
2191 | }
2192 | });
2193 | /**
2194 | * Delete entities, relations, or observations.
2195 | */
2196 | server.tool("deletecontext", toolDescriptions["deletecontext"], {
2197 | type: z.enum(["entities", "relations", "observations"]).describe("Type of deletion operation: 'entities', 'relations', or 'observations'"),
2198 | data: z.array(z.any()).describe("Data for the deletion operation, structure varies by type but must be an array")
2199 | }, async ({ type, data }) => {
2200 | try {
2201 | switch (type) {
2202 | case "entities":
2203 | await knowledgeGraphManager.deleteEntities(data);
2204 | return {
2205 | content: [{
2206 | type: "text",
2207 | text: JSON.stringify({ success: true, message: `Deleted ${data.length} entities` }, null, 2)
2208 | }]
2209 | };
2210 | case "relations":
2211 | // Ensure relations match the Relation interface
2212 | const typedRelations = data.map((r) => ({
2213 | from: r.from,
2214 | to: r.to,
2215 | relationType: r.relationType
2216 | }));
2217 | await knowledgeGraphManager.deleteRelations(typedRelations);
2218 | return {
2219 | content: [{
2220 | type: "text",
2221 | text: JSON.stringify({ success: true, message: `Deleted ${data.length} relations` }, null, 2)
2222 | }]
2223 | };
2224 | case "observations":
2225 | // Ensure deletions match the required interface
2226 | const typedDeletions = data.map((d) => ({
2227 | entityName: d.entityName,
2228 | observations: d.observations
2229 | }));
2230 | await knowledgeGraphManager.deleteObservations(typedDeletions);
2231 | return {
2232 | content: [{
2233 | type: "text",
2234 | text: JSON.stringify({ success: true, message: `Deleted observations from ${data.length} entities` }, null, 2)
2235 | }]
2236 | };
2237 | default:
2238 | throw new Error(`Invalid type: ${type}. Must be 'entities', 'relations', or 'observations'.`);
2239 | }
2240 | }
2241 | catch (error) {
2242 | return {
2243 | content: [{
2244 | type: "text",
2245 | text: JSON.stringify({
2246 | success: false,
2247 | error: error instanceof Error ? error.message : String(error)
2248 | }, null, 2)
2249 | }]
2250 | };
2251 | }
2252 | });
2253 | /**
2254 | * Get information about the graph, search for nodes, open nodes, get project overview, get participant profile, get codes, get themes, get transcript, get memo, get analysis, get codebook, or get related entities.
2255 | */
2256 | server.tool("advancedcontext", toolDescriptions["advancedcontext"], {
2257 | type: z.enum([
2258 | "graph",
2259 | "search",
2260 | "nodes",
2261 | "project",
2262 | "participant",
2263 | "codes",
2264 | "themes",
2265 | "transcript",
2266 | "memo",
2267 | "analysis",
2268 | "codebook",
2269 | "related"
2270 | ]).describe("Type of get operation"),
2271 | params: z.record(z.string(), z.any()).describe("Parameters for the get operation, structure varies by type")
2272 | }, async ({ type, params }) => {
2273 | try {
2274 | let result;
2275 | switch (type) {
2276 | case "graph":
2277 | result = await knowledgeGraphManager.readGraph();
2278 | return {
2279 | content: [{
2280 | type: "text",
2281 | text: JSON.stringify({ success: true, graph: result }, null, 2)
2282 | }]
2283 | };
2284 | case "search":
2285 | result = await knowledgeGraphManager.searchNodes(params.query);
2286 | return {
2287 | content: [{
2288 | type: "text",
2289 | text: JSON.stringify({ success: true, results: result }, null, 2)
2290 | }]
2291 | };
2292 | case "nodes":
2293 | result = await knowledgeGraphManager.openNodes(params.names);
2294 | return {
2295 | content: [{
2296 | type: "text",
2297 | text: JSON.stringify({ success: true, nodes: result }, null, 2)
2298 | }]
2299 | };
2300 | case "project":
2301 | result = await knowledgeGraphManager.getProjectOverview(params.projectName);
2302 | return {
2303 | content: [{
2304 | type: "text",
2305 | text: JSON.stringify({ success: true, project: result }, null, 2)
2306 | }]
2307 | };
2308 | case "participant":
2309 | result = await knowledgeGraphManager.getParticipantProfile(params.participantName);
2310 | return {
2311 | content: [{
2312 | type: "text",
2313 | text: JSON.stringify({ success: true, participant: result }, null, 2)
2314 | }]
2315 | };
2316 | case "codes":
2317 | // Use searchNodes for codes instead of a specialized method
2318 | result = await knowledgeGraphManager.searchNodes(`entityType:code ${params.projectName ? `project:${params.projectName}` : ""}`);
2319 | return {
2320 | content: [{
2321 | type: "text",
2322 | text: JSON.stringify({ success: true, codes: result }, null, 2)
2323 | }]
2324 | };
2325 | case "themes":
2326 | // Use searchNodes for themes
2327 | result = await knowledgeGraphManager.searchNodes(`entityType:theme ${params.projectName ? `project:${params.projectName}` : ""}`);
2328 | return {
2329 | content: [{
2330 | type: "text",
2331 | text: JSON.stringify({ success: true, themes: result }, null, 2)
2332 | }]
2333 | };
2334 | case "transcript":
2335 | // Use searchNodes to find the transcript
2336 | const transcriptQuery = await knowledgeGraphManager.searchNodes(`entityType:transcript participant:${params.participantName} ${params.interviewId ? `interview:${params.interviewId}` : ""}`);
2337 | return {
2338 | content: [{
2339 | type: "text",
2340 | text: JSON.stringify({ success: true, transcript: transcriptQuery }, null, 2)
2341 | }]
2342 | };
2343 | case "memo":
2344 | // Use openNodes to get the specific memo
2345 | const memoResult = await knowledgeGraphManager.openNodes([params.memoName]);
2346 | return {
2347 | content: [{
2348 | type: "text",
2349 | text: JSON.stringify({ success: true, memo: memoResult }, null, 2)
2350 | }]
2351 | };
2352 | case "analysis":
2353 | // Use searchNodes to get analysis artifacts
2354 | const analysisQuery = await knowledgeGraphManager.searchNodes(`entityType:analysis project:${params.projectName}`);
2355 | return {
2356 | content: [{
2357 | type: "text",
2358 | text: JSON.stringify({ success: true, analysis: analysisQuery }, null, 2)
2359 | }]
2360 | };
2361 | case "codebook":
2362 | // Use searchNodes to get codebook entries
2363 | const codebookQuery = await knowledgeGraphManager.searchNodes(`entityType:code project:${params.projectName}`);
2364 | return {
2365 | content: [{
2366 | type: "text",
2367 | text: JSON.stringify({ success: true, codebook: codebookQuery }, null, 2)
2368 | }]
2369 | };
2370 | case "related":
2371 | // For the related case, we don't have a specialized method in the manager
2372 | // So we'll use the generic KnowledgeGraph search capabilities
2373 | const entityGraph = await knowledgeGraphManager.searchNodes(params.entityName);
2374 | const entity = entityGraph.entities.find(e => e.name === params.entityName);
2375 | if (!entity) {
2376 | throw new Error(`Entity "${params.entityName}" not found`);
2377 | }
2378 | // Find related entities
2379 | const relations = entityGraph.relations.filter(r => r.from === params.entityName || r.to === params.entityName);
2380 | const relatedNames = relations.map(r => r.from === params.entityName ? r.to : r.from);
2381 | if (relatedNames.length === 0) {
2382 | return {
2383 | content: [{
2384 | type: "text",
2385 | text: JSON.stringify({ success: true, related: { entity, relatedEntities: [] } }, null, 2)
2386 | }]
2387 | };
2388 | }
2389 | const relatedEntitiesGraph = await knowledgeGraphManager.openNodes(relatedNames);
2390 | return {
2391 | content: [{
2392 | type: "text",
2393 | text: JSON.stringify({
2394 | success: true,
2395 | related: {
2396 | entity,
2397 | relations,
2398 | relatedEntities: relatedEntitiesGraph.entities
2399 | }
2400 | }, null, 2)
2401 | }]
2402 | };
2403 | default:
2404 | throw new Error(`Invalid type: ${type}. Must be one of the supported get operation types.`);
2405 | }
2406 | }
2407 | catch (error) {
2408 | return {
2409 | content: [{
2410 | type: "text",
2411 | text: JSON.stringify({
2412 | success: false,
2413 | error: error instanceof Error ? error.message : String(error)
2414 | }, null, 2)
2415 | }]
2416 | };
2417 | }
2418 | });
2419 | // Connect the server to the transport
2420 | const transport = new StdioServerTransport();
2421 | await server.connect(transport);
2422 | }
2423 | catch (error) {
2424 | console.error("Error starting server:", error);
2425 | process.exit(1);
2426 | }
2427 | }
2428 | // Run the main function
2429 | main().catch(error => {
2430 | console.error("Unhandled error:", error);
2431 | process.exit(1);
2432 | });
2433 | // Export the KnowledgeGraphManager for testing
2434 | export { KnowledgeGraphManager };
2435 |
```