code-graph-context 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -0
- package/LICENSE +21 -0
- package/README.md +870 -0
- package/dist/constants.js +1 -0
- package/dist/core/config/fairsquare-framework-schema.js +832 -0
- package/dist/core/config/graph-v2.js +1595 -0
- package/dist/core/config/nestjs-framework-schema.js +894 -0
- package/dist/core/config/schema.js +799 -0
- package/dist/core/embeddings/embeddings.service.js +26 -0
- package/dist/core/embeddings/natural-language-to-cypher.service.js +148 -0
- package/dist/core/parsers/parser-factory.js +102 -0
- package/dist/core/parsers/typescript-parser-v2.js +590 -0
- package/dist/core/parsers/typescript-parser.js +717 -0
- package/dist/mcp/constants.js +141 -0
- package/dist/mcp/handlers/graph-generator.handler.js +143 -0
- package/dist/mcp/handlers/traversal.handler.js +304 -0
- package/dist/mcp/mcp.server.js +47 -0
- package/dist/mcp/services.js +158 -0
- package/dist/mcp/tools/hello.tool.js +13 -0
- package/dist/mcp/tools/index.js +24 -0
- package/dist/mcp/tools/natural-language-to-cypher.tool.js +59 -0
- package/dist/mcp/tools/parse-typescript-project.tool.js +101 -0
- package/dist/mcp/tools/search-codebase.tool.js +97 -0
- package/dist/mcp/tools/test-neo4j-connection.tool.js +39 -0
- package/dist/mcp/tools/traverse-from-node.tool.js +97 -0
- package/dist/mcp/utils.js +152 -0
- package/dist/parsers/cypher-result.parser.js +44 -0
- package/dist/storage/neo4j/neo4j.service.js +277 -0
- package/dist/utils/test.js +19 -0
- package/package.json +81 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
export class EmbeddingsService {
|
|
3
|
+
openai;
|
|
4
|
+
model;
|
|
5
|
+
constructor(model = 'text-embedding-3-large') {
|
|
6
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
7
|
+
if (!apiKey) {
|
|
8
|
+
throw new Error('OPENAI_API_KEY environment variable is required');
|
|
9
|
+
}
|
|
10
|
+
this.openai = new OpenAI({ apiKey });
|
|
11
|
+
this.model = model;
|
|
12
|
+
}
|
|
13
|
+
async embedText(text) {
|
|
14
|
+
try {
|
|
15
|
+
const response = await this.openai.embeddings.create({
|
|
16
|
+
model: this.model,
|
|
17
|
+
input: text,
|
|
18
|
+
});
|
|
19
|
+
return response.data[0].embedding;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error('Error creating embedding:', error);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import OpenAI from 'openai';
|
|
3
|
+
export class NaturalLanguageToCypherService {
|
|
4
|
+
assistantId;
|
|
5
|
+
openai;
|
|
6
|
+
MODEL = 'gpt-4o-mini'; // Using GPT-4 Turbo
|
|
7
|
+
messageInstructions = `
|
|
8
|
+
The schema file (neo4j-apoc-schema.json) contains two sections:
|
|
9
|
+
1. rawSchema: Complete Neo4j APOC schema with all node labels, properties, and relationships in the graph
|
|
10
|
+
2. domainContext: Framework-specific semantics including:
|
|
11
|
+
- nodeTypes: Descriptions and example queries for each node type
|
|
12
|
+
- relationships: How nodes connect with context about relationship properties
|
|
13
|
+
- commonQueryPatterns: Pre-built example queries for common use cases
|
|
14
|
+
|
|
15
|
+
Your response must be a valid JSON object with this exact structure:
|
|
16
|
+
{
|
|
17
|
+
"cypher": "MATCH (n:NodeType) WHERE n.property = $param RETURN n",
|
|
18
|
+
"parameters": { "param": "value" } | null,
|
|
19
|
+
"explanation": "Concise explanation of what the query does and why it matches the user's request"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Query Generation Process:
|
|
23
|
+
1. CHECK DOMAIN CONTEXT: Look at domainContext.nodeTypes to understand available node types and their properties
|
|
24
|
+
2. REVIEW EXAMPLES: Check domainContext.commonQueryPatterns for similar query examples
|
|
25
|
+
3. CHECK RELATIONSHIPS: Look at domainContext.relationships to understand how nodes connect
|
|
26
|
+
4. EXAMINE NODE PROPERTIES: Use rawSchema to see exact property names and types
|
|
27
|
+
5. HANDLE JSON PROPERTIES: If properties or relationship context are stored as JSON strings, use apoc.convert.fromJsonMap() to parse them
|
|
28
|
+
6. GENERATE QUERY: Write the Cypher query using only node labels, relationships, and properties that exist in the schema
|
|
29
|
+
|
|
30
|
+
Critical Rules:
|
|
31
|
+
- Use the schema information from the file_search tool - do not guess node labels or relationships
|
|
32
|
+
- Use ONLY node labels and properties found in rawSchema
|
|
33
|
+
- For nested JSON data in properties, use: apoc.convert.fromJsonMap(node.propertyName) or apoc.convert.fromJsonMap(relationship.context)
|
|
34
|
+
- Check domainContext for parsing instructions specific to certain node types (e.g., some nodes may store arrays of objects in JSON format)
|
|
35
|
+
- Follow the example queries in commonQueryPatterns for proper syntax patterns
|
|
36
|
+
- Use parameterized queries with $ syntax for any dynamic values
|
|
37
|
+
- Return only the data relevant to the user's request
|
|
38
|
+
|
|
39
|
+
Provide ONLY the JSON response with no additional text, markdown formatting, or explanations outside the JSON structure.
|
|
40
|
+
`;
|
|
41
|
+
constructor() {
|
|
42
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
43
|
+
if (!apiKey) {
|
|
44
|
+
throw new Error('OPENAI_API_KEY environment variable is required');
|
|
45
|
+
}
|
|
46
|
+
this.openai = new OpenAI({ apiKey });
|
|
47
|
+
}
|
|
48
|
+
async getOrCreateAssistant(schemaPath) {
|
|
49
|
+
if (process.env.OPENAI_ASSISTANT_ID) {
|
|
50
|
+
this.assistantId = process.env.OPENAI_ASSISTANT_ID;
|
|
51
|
+
console.log(`Using existing assistant with ID: ${this.assistantId} `);
|
|
52
|
+
return this.assistantId;
|
|
53
|
+
}
|
|
54
|
+
const schemaFile = await this.openai.files.create({
|
|
55
|
+
file: fs.createReadStream(schemaPath),
|
|
56
|
+
purpose: 'assistants',
|
|
57
|
+
});
|
|
58
|
+
// Create a vector store for the schema file
|
|
59
|
+
const vectorStore = await this.openai.vectorStores.create({
|
|
60
|
+
name: 'Neo4j APOC Schema Vector Store',
|
|
61
|
+
file_ids: [schemaFile.id],
|
|
62
|
+
metadata: { type: 'neo4j_apoc_schema' },
|
|
63
|
+
});
|
|
64
|
+
const vectorStoreId = vectorStore.id;
|
|
65
|
+
// Create a new assistant
|
|
66
|
+
const assistantConfig = {
|
|
67
|
+
name: 'Neo4j Cypher Query Agent',
|
|
68
|
+
description: 'An agent that helps convert natural language to Neo4j Cypher queries',
|
|
69
|
+
model: this.MODEL,
|
|
70
|
+
instructions: `
|
|
71
|
+
You are a specialized assistant that helps convert natural language requests into Neo4j Cypher queries.
|
|
72
|
+
When users ask questions about their codebase data, you'll analyze their intent and generate appropriate
|
|
73
|
+
Cypher queries based on the Neo4j schema provided in files.
|
|
74
|
+
${this.messageInstructions}
|
|
75
|
+
`,
|
|
76
|
+
tools: [
|
|
77
|
+
{
|
|
78
|
+
type: 'code_interpreter',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'file_search',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
tool_resources: {
|
|
85
|
+
code_interpreter: {
|
|
86
|
+
file_ids: [schemaFile.id],
|
|
87
|
+
},
|
|
88
|
+
file_search: {
|
|
89
|
+
vector_store_ids: [vectorStoreId],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const assistant = await this.openai.beta.assistants.create(assistantConfig);
|
|
94
|
+
this.assistantId = assistant.id;
|
|
95
|
+
return this.assistantId;
|
|
96
|
+
}
|
|
97
|
+
async promptToQuery(userPrompt) {
|
|
98
|
+
const prompt = `Please convert this request to a valid Neo4j Cypher query: ${userPrompt}.
|
|
99
|
+
Use the Neo4j schema provided and follow the format specified in the instructions.
|
|
100
|
+
`;
|
|
101
|
+
console.log('Prompt:', prompt);
|
|
102
|
+
const run = await this.openai.beta.threads.createAndRunPoll({
|
|
103
|
+
assistant_id: this.assistantId,
|
|
104
|
+
thread: {
|
|
105
|
+
messages: [
|
|
106
|
+
{
|
|
107
|
+
role: 'user',
|
|
108
|
+
content: prompt,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const threadId = run.thread_id;
|
|
114
|
+
console.log(`Thread ID: ${threadId}`);
|
|
115
|
+
console.log('Run status:', run.status);
|
|
116
|
+
console.log('Required actions:', run.required_action);
|
|
117
|
+
console.log('Last error:', run.last_error);
|
|
118
|
+
// Validate run completed successfully
|
|
119
|
+
if (run.status !== 'completed') {
|
|
120
|
+
console.error('Full run object:', JSON.stringify(run, null, 2));
|
|
121
|
+
throw new Error(`Assistant run did not complete. Status: ${run.status}. ` +
|
|
122
|
+
`Last error: ${run.last_error ? JSON.stringify(run.last_error) : 'none'}`);
|
|
123
|
+
}
|
|
124
|
+
const messages = await this.openai.beta.threads.messages.list(threadId);
|
|
125
|
+
// Find the first text content in the latest message
|
|
126
|
+
const latestMessage = messages.data[0];
|
|
127
|
+
console.log('Latest message:', JSON.stringify(latestMessage, null, 2));
|
|
128
|
+
const textContent = latestMessage.content.find((content) => content.type === 'text');
|
|
129
|
+
if (!textContent) {
|
|
130
|
+
throw new Error(`No text content found in assistant response. Run status: ${run.status}`);
|
|
131
|
+
}
|
|
132
|
+
// Validate that the text property exists and extract the value safely
|
|
133
|
+
const textValue = textContent.text?.value;
|
|
134
|
+
if (!textValue) {
|
|
135
|
+
throw new Error(`Invalid text content structure in assistant response. Run status: ${run.status}. ` +
|
|
136
|
+
`Text content: ${JSON.stringify(textContent)}`);
|
|
137
|
+
}
|
|
138
|
+
console.log('text value:', textValue);
|
|
139
|
+
return JSON.parse(textValue);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Create a new thread for a user
|
|
143
|
+
*/
|
|
144
|
+
async createThread() {
|
|
145
|
+
const thread = await this.openai.beta.threads.create();
|
|
146
|
+
return thread.id;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser Factory
|
|
3
|
+
* Creates TypeScript parsers with appropriate framework schemas
|
|
4
|
+
*/
|
|
5
|
+
import { FAIRSQUARE_FRAMEWORK_SCHEMA } from '../config/fairsquare-framework-schema.js';
|
|
6
|
+
import { NESTJS_FRAMEWORK_SCHEMA } from '../config/nestjs-framework-schema.js';
|
|
7
|
+
import { CORE_TYPESCRIPT_SCHEMA, CoreNodeType } from '../config/schema.js';
|
|
8
|
+
import { TypeScriptParser } from './typescript-parser.js';
|
|
9
|
+
export var ProjectType;
|
|
10
|
+
(function (ProjectType) {
|
|
11
|
+
ProjectType["NESTJS"] = "nestjs";
|
|
12
|
+
ProjectType["FAIRSQUARE"] = "fairsquare";
|
|
13
|
+
ProjectType["BOTH"] = "both";
|
|
14
|
+
ProjectType["VANILLA"] = "vanilla";
|
|
15
|
+
})(ProjectType || (ProjectType = {}));
|
|
16
|
+
export class ParserFactory {
|
|
17
|
+
/**
|
|
18
|
+
* Create a parser with appropriate framework schemas
|
|
19
|
+
*/
|
|
20
|
+
static createParser(options) {
|
|
21
|
+
const { workspacePath, tsConfigPath = 'tsconfig.json', projectType = ProjectType.NESTJS, // Default to NestJS (use auto-detect for best results)
|
|
22
|
+
customFrameworkSchemas = [], excludePatterns = ['node_modules', 'dist', 'build', '.spec.', '.test.'], excludedNodeTypes = [CoreNodeType.PARAMETER_DECLARATION], } = options;
|
|
23
|
+
// Select framework schemas based on project type
|
|
24
|
+
const frameworkSchemas = this.selectFrameworkSchemas(projectType, customFrameworkSchemas);
|
|
25
|
+
console.log(`📦 Creating parser for ${projectType} project`);
|
|
26
|
+
console.log(`📚 Framework schemas: ${frameworkSchemas.map((s) => s.name).join(', ')}`);
|
|
27
|
+
return new TypeScriptParser(workspacePath, tsConfigPath, CORE_TYPESCRIPT_SCHEMA, frameworkSchemas, {
|
|
28
|
+
excludePatterns,
|
|
29
|
+
excludedNodeTypes,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Select framework schemas based on project type
|
|
34
|
+
*/
|
|
35
|
+
static selectFrameworkSchemas(projectType, customSchemas) {
|
|
36
|
+
const schemas = [];
|
|
37
|
+
switch (projectType) {
|
|
38
|
+
case ProjectType.NESTJS:
|
|
39
|
+
schemas.push(NESTJS_FRAMEWORK_SCHEMA);
|
|
40
|
+
break;
|
|
41
|
+
case ProjectType.FAIRSQUARE:
|
|
42
|
+
schemas.push(FAIRSQUARE_FRAMEWORK_SCHEMA);
|
|
43
|
+
break;
|
|
44
|
+
case ProjectType.BOTH:
|
|
45
|
+
// Apply FairSquare first (higher priority), then NestJS
|
|
46
|
+
schemas.push(FAIRSQUARE_FRAMEWORK_SCHEMA);
|
|
47
|
+
schemas.push(NESTJS_FRAMEWORK_SCHEMA);
|
|
48
|
+
break;
|
|
49
|
+
case ProjectType.VANILLA:
|
|
50
|
+
// No framework schemas
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
// Add any custom schemas
|
|
54
|
+
schemas.push(...customSchemas);
|
|
55
|
+
return schemas;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Auto-detect project type from workspace
|
|
59
|
+
*/
|
|
60
|
+
static async detectProjectType(workspacePath) {
|
|
61
|
+
const fs = await import('fs/promises');
|
|
62
|
+
const path = await import('path');
|
|
63
|
+
try {
|
|
64
|
+
const packageJsonPath = path.join(workspacePath, 'package.json');
|
|
65
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
|
|
66
|
+
const deps = {
|
|
67
|
+
...packageJson.dependencies,
|
|
68
|
+
...packageJson.devDependencies,
|
|
69
|
+
};
|
|
70
|
+
const hasNestJS = '@nestjs/common' in deps || '@nestjs/core' in deps;
|
|
71
|
+
const hasFairSquare = '@fairsquare/core' in deps || '@fairsquare/server' in deps;
|
|
72
|
+
if (hasFairSquare && hasNestJS) {
|
|
73
|
+
return ProjectType.BOTH;
|
|
74
|
+
}
|
|
75
|
+
else if (hasFairSquare) {
|
|
76
|
+
return ProjectType.FAIRSQUARE;
|
|
77
|
+
}
|
|
78
|
+
else if (hasNestJS) {
|
|
79
|
+
return ProjectType.NESTJS;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
return ProjectType.VANILLA;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.warn('Could not detect project type, defaulting to vanilla TypeScript');
|
|
87
|
+
return ProjectType.VANILLA;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create parser with auto-detection
|
|
92
|
+
*/
|
|
93
|
+
static async createParserWithAutoDetection(workspacePath, tsConfigPath) {
|
|
94
|
+
const projectType = await this.detectProjectType(workspacePath);
|
|
95
|
+
console.log(`🔍 Auto-detected project type: ${projectType}`);
|
|
96
|
+
return this.createParser({
|
|
97
|
+
workspacePath,
|
|
98
|
+
tsConfigPath,
|
|
99
|
+
projectType,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|