Timothy Eastridge commited on
Commit
9930ba9
·
1 Parent(s): f79f9b7

commit dev

Browse files
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.db
2
+ /graph-agentic-system/neo4j/data
Makefile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ up:
2
+ docker-compose up -d
3
+
4
+ down:
5
+ docker-compose down
6
+
7
+ logs:
8
+ docker-compose logs -f
9
+
10
+ seed:
11
+ docker-compose exec mcp python /app/ops/scripts/seed.py
README.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agentic Knowledge Graph
2
+
3
+ A graph-driven agentic system that uses Neo4j as the knowledge graph backend, with MCP server for graph operations, an intelligent agent for workflow execution, and a web interface for interaction.
4
+
5
+ ## Architecture
6
+
7
+ - **Neo4j**: Knowledge graph database storing workflows, instructions, and metadata
8
+ - **MCP Server**: Single gateway for all Neo4j operations with API key authentication
9
+ - **Agent**: Executes workflows by reading instructions from Neo4j and performing actions
10
+ - **Frontend**: Next.js web interface for user interaction and monitoring
11
+ - **PostgreSQL**: Data source for schema discovery and query execution
12
+
13
+ ## Quick Start
14
+
15
+ 1. **Clone and setup**:
16
+ ```bash
17
+ git clone <repository-url>
18
+ cd 20250922_agentic_kg
19
+ cp .env.example .env
20
+ ```
21
+
22
+ 2. **Edit environment variables**:
23
+ - Update `.env` with your API keys and configuration
24
+ - Set `LLM_API_KEY` for OpenAI/Claude access
25
+ - Set `MCP_API_KEYS` for MCP server authentication
26
+
27
+ 3. **Start services**:
28
+ ```bash
29
+ make up
30
+ ```
31
+
32
+ 4. **Check health**:
33
+ ```bash
34
+ make neo4j-health
35
+ make logs
36
+ ```
37
+
38
+ ## Available Commands
39
+
40
+ - `make up` - Start all services
41
+ - `make down` - Stop all services
42
+ - `make logs` - Show service logs
43
+ - `make seed` - Seed database with initial data
44
+ - `make clean` - Stop and remove volumes
45
+ - `make restart` - Restart all services
46
+ - `make neo4j-health` - Check Neo4j health
47
+
48
+ ## Services
49
+
50
+ - **Neo4j**: http://localhost:7474 (Browser), bolt://localhost:7687 (Bolt)
51
+ - **MCP Server**: http://localhost:3001
52
+ - **Frontend**: http://localhost:3000
53
+ - **PostgreSQL**: localhost:5432
54
+
55
+ ## Development
56
+
57
+ The system is designed as a monorepo with the following structure:
58
+
59
+ ```
60
+ ├── neo4j/ # Neo4j Docker configuration
61
+ ├── mcp/ # MCP server implementation
62
+ ├── agent/ # Agent implementation
63
+ ├── frontend/ # Next.js frontend
64
+ ├── postgres/ # PostgreSQL configuration
65
+ ├── ops/ # Operations scripts
66
+ └── docker-compose.yml
67
+ ```
68
+
69
+ ## Features
70
+
71
+ - Graph-driven workflow execution
72
+ - Human-in-the-loop pauses for review
73
+ - Natural language to SQL translation
74
+ - Real-time workflow monitoring
75
+ - Graph visualization
76
+ - Audit logging
77
+
78
+ ## Status
79
+
80
+ 🚧 **In Development** - Core infrastructure setup complete, implementing MCP server and agent components.
docker-compose.yml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ neo4j:
5
+ build: ./neo4j
6
+ ports:
7
+ - "7474:7474" # Browser
8
+ - "7687:7687" # Bolt
9
+ environment:
10
+ - NEO4J_AUTH=neo4j/password
11
+ volumes:
12
+ - ./neo4j/data:/data
13
+ healthcheck:
14
+ test: ["CMD", "cypher-shell", "-u", "neo4j", "-p", "password", "MATCH (n) RETURN count(n) LIMIT 1"]
15
+ interval: 10s
16
+ timeout: 5s
17
+ retries: 5
18
+
19
+ postgres:
20
+ image: postgres:15
21
+ container_name: postgres
22
+ ports:
23
+ - "5432:5432"
24
+ environment:
25
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
26
+ - POSTGRES_DB=testdb
27
+ volumes:
28
+ - postgres_data:/var/lib/postgresql/data
29
+
30
+ mcp:
31
+ build: ./mcp
32
+ container_name: mcp
33
+ ports:
34
+ - "8000:8000"
35
+ environment:
36
+ - MCP_PORT=8000
37
+ - MCP_API_KEYS=dev-key-123,external-key-456
38
+ - NEO4J_BOLT_URL=bolt://neo4j:7687
39
+ - NEO4J_AUTH=neo4j/password
40
+ depends_on:
41
+ - neo4j
42
+
43
+ volumes:
44
+ neo4j_data:
45
+ postgres_data:
graph-agentic-system/Makefile ADDED
File without changes
graph-agentic-system/README.md ADDED
File without changes
graph-agentic-system/docker-compose.yml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ neo4j:
5
+ image: neo4j:5.15-community
6
+ container_name: neo4j
7
+ ports:
8
+ - "7474:7474"
9
+ - "7687:7687"
10
+ environment:
11
+ - NEO4J_AUTH=-Force{NEO4J_AUTH}
12
+ - NEO4J_BOLT_URL=-Force{NEO4J_BOLT_URL}
13
+ volumes:
14
+ - neo4j_data:/data
15
+
16
+ postgres:
17
+ image: postgres:15
18
+ container_name: postgres
19
+ ports:
20
+ - "5432:5432"
21
+ environment:
22
+ - POSTGRES_PASSWORD=-Force{POSTGRES_PASSWORD}
23
+ - POSTGRES_DB=testdb
24
+ volumes:
25
+ - postgres_data:/var/lib/postgresql/data
26
+
27
+ volumes:
28
+ neo4j_data:
29
+ postgres_data:
graph-agentic-system/mcp/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 8000
11
+
12
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
graph-agentic-system/mcp/main.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Header, HTTPException
2
+ from neo4j import GraphDatabase
3
+ import os
4
+ import json
5
+ from datetime import datetime
6
+
7
+ app = FastAPI()
8
+ driver = GraphDatabase.driver(
9
+ os.getenv("NEO4J_BOLT_URL"),
10
+ auth=("neo4j", os.getenv("NEO4J_AUTH").split("/")[1])
11
+ )
12
+
13
+ VALID_API_KEYS = os.getenv("MCP_API_KEYS", "").split(",")
14
+
15
+ @app.get("/health")
16
+ def health():
17
+ return {"ok": True, "timestamp": datetime.now().isoformat()}
18
+
19
+ @app.post("/mcp")
20
+ async def execute_tool(request: dict, x_api_key: str = Header(None)):
21
+ # Verify API key
22
+ if x_api_key not in VALID_API_KEYS:
23
+ raise HTTPException(status_code=401, detail="Invalid API key")
24
+
25
+ tool = request.get("tool")
26
+ params = request.get("params", {})
27
+
28
+ if tool == "get_schema":
29
+ # Return node labels and relationships
30
+ with driver.session() as session:
31
+ result = session.run("CALL db.labels() YIELD label RETURN collect(label) as labels")
32
+ return {"labels": result.single()["labels"]}
33
+
34
+ elif tool == "query_graph":
35
+ # Execute parameterized query
36
+ query = params.get("query")
37
+ query_params = params.get("parameters", {})
38
+ with driver.session() as session:
39
+ result = session.run(query, query_params)
40
+ return {"data": [dict(record) for record in result]}
41
+
42
+ elif tool == "write_graph":
43
+ # Structured write operation
44
+ action = params.get("action")
45
+ if action == "create_node":
46
+ label = params.get("label")
47
+ properties = params.get("properties", {})
48
+ with driver.session() as session:
49
+ result = session.run(f"CREATE (n:{label} ) RETURN n", props=properties)
50
+ return {"created": dict(result.single()["n"])}
51
+
52
+ elif tool == "get_next_instruction":
53
+ # Get next pending instruction
54
+ with driver.session() as session:
55
+ result = session.run("""
56
+ MATCH (i:Instruction {status: 'pending'})
57
+ RETURN i ORDER BY i.sequence LIMIT 1
58
+ """)
59
+ record = result.single()
60
+ return {"instruction": dict(record["i"]) if record else None}
61
+
62
+ return {"error": "Unknown tool"}
graph-agentic-system/mcp/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi==0.104.0
2
+ uvicorn==0.24.0
3
+ neo4j==5.14.0
4
+ pydantic==2.4.0
graph-agentic-system/neo4j/Dockerfile ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ FROM neo4j:5.23.0
2
+ ENV NEO4J_PLUGINS='["apoc"]'
3
+ ENV NEO4J_apoc_export_file_enabled=true
4
+ ENV NEO4J_apoc_import_file_enabled=true
graph-agentic-system/ops/health/neo4j.ps1 ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ docker-compose exec neo4j cypher-shell -u neo4j -p password "MATCH (n) RETURN count(n) LIMIT 1"
2
+ if (1 -eq 0) { echo " Neo4j healthy" }
graph-agentic-system/ops/health/neo4j.sh ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ #!/bin/bash
2
+ docker-compose exec neo4j cypher-shell -u neo4j -p password "MATCH (n) RETURN count(n) LIMIT 1" && echo " Neo4j healthy"
graph-agentic-system/ops/scripts/seed.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+
4
+ MCP_URL = "http://localhost:8000/mcp"
5
+ API_KEY = "dev-key-123"
6
+
7
+ def call_mcp(tool, params=None):
8
+ response = requests.post(
9
+ MCP_URL,
10
+ headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
11
+ json={"tool": tool, "params": params or {}}
12
+ )
13
+ if response.status_code == 200:
14
+ return response.json()
15
+ else:
16
+ print(f"Error: {response.status_code} - {response.text}")
17
+ return {"error": f"HTTP {response.status_code}: {response.text}"}
18
+
19
+ # Clear existing data
20
+ print("Clearing existing data...")
21
+ call_mcp("query_graph", {"query": "MATCH (n) DETACH DELETE n"})
22
+
23
+ # Create demo workflow
24
+ print("Creating workflow...")
25
+ workflow = call_mcp("write_graph", {
26
+ "action": "create_node",
27
+ "label": "Workflow",
28
+ "properties": {
29
+ "id": "demo-workflow-1",
30
+ "name": "Entity Resolution Demo",
31
+ "status": "active",
32
+ "max_iterations": 10,
33
+ "current_iteration": 0
34
+ }
35
+ })
36
+ print(f"Created workflow: {workflow}")
37
+
38
+ # Create three instructions
39
+ print("Creating instructions...")
40
+ instructions = [
41
+ {"id": "inst-1", "sequence": 1, "type": "discover_schema", "status": "pending", "pause_duration": 300},
42
+ {"id": "inst-2", "sequence": 2, "type": "generate_sql", "status": "pending", "pause_duration": 300},
43
+ {"id": "inst-3", "sequence": 3, "type": "review_results", "status": "pending", "pause_duration": 300}
44
+ ]
45
+
46
+ for inst in instructions:
47
+ result = call_mcp("write_graph", {
48
+ "action": "create_node",
49
+ "label": "Instruction",
50
+ "properties": inst
51
+ })
52
+ print(f"Created instruction: {inst['id']}")
53
+
54
+ # Create relationships
55
+ print("Creating relationships...")
56
+ call_mcp("query_graph", {
57
+ "query": "MATCH (w:Workflow {id: 'demo-workflow-1'}), (i:Instruction {id: 'inst-1'}) CREATE (w)-[:HAS_INSTRUCTION]->(i)"
58
+ })
59
+
60
+ call_mcp("query_graph", {
61
+ "query": "MATCH (w:Workflow {id: 'demo-workflow-1'}), (i:Instruction {id: 'inst-2'}) CREATE (w)-[:HAS_INSTRUCTION]->(i)"
62
+ })
63
+
64
+ call_mcp("query_graph", {
65
+ "query": "MATCH (w:Workflow {id: 'demo-workflow-1'}), (i:Instruction {id: 'inst-3'}) CREATE (w)-[:HAS_INSTRUCTION]->(i)"
66
+ })
67
+
68
+ # Create instruction chain
69
+ call_mcp("query_graph", {
70
+ "query": "MATCH (i1:Instruction {id: 'inst-1'}), (i2:Instruction {id: 'inst-2'}) CREATE (i1)-[:NEXT_INSTRUCTION]->(i2)"
71
+ })
72
+
73
+ call_mcp("query_graph", {
74
+ "query": "MATCH (i2:Instruction {id: 'inst-2'}), (i3:Instruction {id: 'inst-3'}) CREATE (i2)-[:NEXT_INSTRUCTION]->(i3)"
75
+ })
76
+
77
+ # Create indexes
78
+ print("Creating indexes...")
79
+ call_mcp("query_graph", {"query": "CREATE INDEX workflow_id IF NOT EXISTS FOR (w:Workflow) ON (w.id)"})
80
+ call_mcp("query_graph", {"query": "CREATE INDEX instruction_id IF NOT EXISTS FOR (i:Instruction) ON (i.id)"})
81
+ call_mcp("query_graph", {"query": "CREATE INDEX instruction_status_seq IF NOT EXISTS FOR (i:Instruction) ON (i.status, i.sequence)"})
82
+
83
+ print(" Seeding complete!")
mcp/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 8000
11
+
12
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
mcp/main.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Header, HTTPException
2
+ from neo4j import GraphDatabase
3
+ import os
4
+ import json
5
+ from datetime import datetime
6
+
7
+ app = FastAPI()
8
+ driver = GraphDatabase.driver(
9
+ os.getenv("NEO4J_BOLT_URL"),
10
+ auth=("neo4j", os.getenv("NEO4J_AUTH").split("/")[1])
11
+ )
12
+
13
+ VALID_API_KEYS = os.getenv("MCP_API_KEYS", "").split(",")
14
+
15
+ @app.get("/health")
16
+ def health():
17
+ return {"ok": True, "timestamp": datetime.now().isoformat()}
18
+
19
+ @app.post("/mcp")
20
+ async def execute_tool(request: dict, x_api_key: str = Header(None)):
21
+ # Verify API key
22
+ if x_api_key not in VALID_API_KEYS:
23
+ raise HTTPException(status_code=401, detail="Invalid API key")
24
+
25
+ tool = request.get("tool")
26
+ params = request.get("params", {})
27
+
28
+ if tool == "get_schema":
29
+ # Return node labels and relationships
30
+ with driver.session() as session:
31
+ result = session.run("CALL db.labels() YIELD label RETURN collect(label) as labels")
32
+ return {"labels": result.single()["labels"]}
33
+
34
+ elif tool == "query_graph":
35
+ # Execute parameterized query
36
+ query = params.get("query")
37
+ query_params = params.get("parameters", {})
38
+ with driver.session() as session:
39
+ result = session.run(query, query_params)
40
+ return {"data": [dict(record) for record in result]}
41
+
42
+ elif tool == "write_graph":
43
+ # Structured write operation
44
+ action = params.get("action")
45
+ if action == "create_node":
46
+ label = params.get("label")
47
+ properties = params.get("properties", {})
48
+ with driver.session() as session:
49
+ result = session.run(f"CREATE (n:{label} $props) RETURN n", {"props": properties})
50
+ record = result.single()
51
+ if record:
52
+ node = record["n"]
53
+ return {"created": dict(node) if hasattr(node, 'items') else {"id": str(node.id), "labels": list(node.labels), "properties": dict(node)}}
54
+ return {"created": {}}
55
+
56
+ elif tool == "get_next_instruction":
57
+ # Get next pending instruction
58
+ with driver.session() as session:
59
+ result = session.run("""
60
+ MATCH (i:Instruction {status: 'pending'})
61
+ RETURN i ORDER BY i.sequence LIMIT 1
62
+ """)
63
+ record = result.single()
64
+ return {"instruction": dict(record["i"]) if record else None}
65
+
66
+ return {"error": "Unknown tool"}
mcp/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.104.0
2
+ uvicorn==0.24.0
3
+ neo4j==5.14.0
4
+ pydantic==2.4.0
5
+ requests==2.31.0
neo4j/Dockerfile ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ FROM neo4j:5.23.0
2
+ ENV NEO4J_PLUGINS='["apoc"]'
3
+ ENV NEO4J_apoc_export_file_enabled=true
4
+ ENV NEO4J_apoc_import_file_enabled=true