Timothy Eastridge commited on
Commit
84473fd
·
1 Parent(s): 795ed51

commit step 6

Browse files
.gitignore CHANGED
@@ -1,2 +1,3 @@
1
  *.db
2
  /graph-agentic-system/neo4j/data
 
 
1
  *.db
2
  /graph-agentic-system/neo4j/data
3
+ /neo4j/data
agent/main.py CHANGED
@@ -16,12 +16,110 @@ def call_mcp(tool, params=None):
16
  )
17
  return response.json()
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  def main():
20
  print(f"[{datetime.now()}] Agent starting, polling every {POLL_INTERVAL}s")
21
 
22
  while True:
23
  try:
24
- # Get next instruction
25
  result = call_mcp("get_next_instruction")
26
  instruction = result.get("instruction")
27
 
@@ -34,15 +132,22 @@ def main():
34
  "parameters": {"id": instruction['id']}
35
  })
36
 
37
- # Create dummy execution for now
38
- exec_result = call_mcp("write_graph", {
 
 
 
 
 
 
 
39
  "action": "create_node",
40
  "label": "Execution",
41
  "properties": {
42
  "id": f"exec-{instruction['id']}-{int(time.time())}",
43
  "started_at": datetime.now().isoformat(),
44
  "completed_at": datetime.now().isoformat(),
45
- "result": "Dummy execution completed"
46
  }
47
  })
48
 
@@ -54,17 +159,18 @@ def main():
54
  """,
55
  "parameters": {
56
  "iid": instruction['id'],
57
- "eid": exec_result['created']['id']
58
  }
59
  })
60
 
61
- # Mark complete
 
62
  call_mcp("query_graph", {
63
- "query": "MATCH (i:Instruction {id: $id}) SET i.status = 'complete'",
64
- "parameters": {"id": instruction['id']}
65
  })
66
 
67
- print(f"[{datetime.now()}] Completed instruction: {instruction['id']}")
68
  else:
69
  print(f"[{datetime.now()}] No pending instructions")
70
 
@@ -74,4 +180,4 @@ def main():
74
  time.sleep(POLL_INTERVAL)
75
 
76
  if __name__ == "__main__":
77
- main()
 
16
  )
17
  return response.json()
18
 
19
+ def handle_discover_schema(instruction):
20
+ """Discover PostgreSQL schema and store in Neo4j"""
21
+ print(f"[{datetime.now()}] Discovering PostgreSQL schema...")
22
+
23
+ # Call MCP to discover schema
24
+ schema_result = call_mcp("discover_postgres_schema")
25
+
26
+ if "error" in schema_result:
27
+ return {"status": "failed", "error": schema_result["error"]}
28
+
29
+ schema = schema_result["schema"]
30
+
31
+ # Create SourceSystem node
32
+ call_mcp("write_graph", {
33
+ "action": "create_node",
34
+ "label": "SourceSystem",
35
+ "properties": {
36
+ "id": "postgres-main",
37
+ "name": "Main PostgreSQL Database",
38
+ "type": "postgresql",
39
+ "discovered_at": datetime.now().isoformat()
40
+ }
41
+ })
42
+
43
+ # For each table, create nodes
44
+ for table_name, columns in schema.items():
45
+ # Create Table node
46
+ table_result = call_mcp("write_graph", {
47
+ "action": "create_node",
48
+ "label": "Table",
49
+ "properties": {
50
+ "name": table_name,
51
+ "schema": "public",
52
+ "column_count": len(columns)
53
+ }
54
+ })
55
+
56
+ # Link Table to SourceSystem
57
+ call_mcp("query_graph", {
58
+ "query": """
59
+ MATCH (s:SourceSystem {id: 'postgres-main'}),
60
+ (t:Table {name: $table_name})
61
+ MERGE (s)-[:HAS_TABLE]->(t)
62
+ """,
63
+ "parameters": {"table_name": table_name}
64
+ })
65
+
66
+ # Create Column nodes
67
+ for col in columns:
68
+ col_result = call_mcp("write_graph", {
69
+ "action": "create_node",
70
+ "label": "Column",
71
+ "properties": {
72
+ "name": col['column_name'],
73
+ "data_type": col['data_type'],
74
+ "nullable": col['is_nullable'] == 'YES',
75
+ "table_name": table_name
76
+ }
77
+ })
78
+
79
+ # Link Column to Table
80
+ call_mcp("query_graph", {
81
+ "query": """
82
+ MATCH (t:Table {name: $table_name}),
83
+ (c:Column {name: $col_name, table_name: $table_name})
84
+ MERGE (t)-[:HAS_COLUMN]->(c)
85
+ """,
86
+ "parameters": {
87
+ "table_name": table_name,
88
+ "col_name": col['column_name']
89
+ }
90
+ })
91
+
92
+ # Generate sample queries
93
+ for table_name in schema.keys():
94
+ sample_queries = [
95
+ f"SELECT * FROM {table_name} LIMIT 10",
96
+ f"SELECT COUNT(*) FROM {table_name}",
97
+ f"SELECT * FROM {table_name} WHERE id = 1"
98
+ ]
99
+
100
+ for idx, query in enumerate(sample_queries):
101
+ call_mcp("write_graph", {
102
+ "action": "create_node",
103
+ "label": "QueryTemplate",
104
+ "properties": {
105
+ "id": f"template-{table_name}-{idx}",
106
+ "table_name": table_name,
107
+ "query": query,
108
+ "description": f"Sample query {idx+1} for {table_name}"
109
+ }
110
+ })
111
+
112
+ return {
113
+ "status": "success",
114
+ "tables_discovered": len(schema),
115
+ "columns_discovered": sum(len(cols) for cols in schema.values())
116
+ }
117
+
118
  def main():
119
  print(f"[{datetime.now()}] Agent starting, polling every {POLL_INTERVAL}s")
120
 
121
  while True:
122
  try:
 
123
  result = call_mcp("get_next_instruction")
124
  instruction = result.get("instruction")
125
 
 
132
  "parameters": {"id": instruction['id']}
133
  })
134
 
135
+ # Handle different instruction types
136
+ if instruction['type'] == 'discover_schema':
137
+ exec_result = handle_discover_schema(instruction)
138
+ else:
139
+ # Default dummy execution
140
+ exec_result = {"status": "success", "result": "Dummy execution"}
141
+
142
+ # Create Execution node
143
+ exec_node = call_mcp("write_graph", {
144
  "action": "create_node",
145
  "label": "Execution",
146
  "properties": {
147
  "id": f"exec-{instruction['id']}-{int(time.time())}",
148
  "started_at": datetime.now().isoformat(),
149
  "completed_at": datetime.now().isoformat(),
150
+ "result": json.dumps(exec_result)
151
  }
152
  })
153
 
 
159
  """,
160
  "parameters": {
161
  "iid": instruction['id'],
162
+ "eid": exec_node['created']['id']
163
  }
164
  })
165
 
166
+ # Update instruction status
167
+ final_status = 'complete' if exec_result.get('status') == 'success' else 'failed'
168
  call_mcp("query_graph", {
169
+ "query": "MATCH (i:Instruction {id: $id}) SET i.status = $status",
170
+ "parameters": {"id": instruction['id'], "status": final_status}
171
  })
172
 
173
+ print(f"[{datetime.now()}] Completed instruction: {instruction['id']} with status: {final_status}")
174
  else:
175
  print(f"[{datetime.now()}] No pending instructions")
176
 
 
180
  time.sleep(POLL_INTERVAL)
181
 
182
  if __name__ == "__main__":
183
+ main()
docker-compose.yml CHANGED
@@ -22,10 +22,17 @@ services:
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
@@ -37,8 +44,10 @@ services:
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
  agent:
44
  build: ./agent
 
22
  ports:
23
  - "5432:5432"
24
  environment:
 
25
  - POSTGRES_DB=testdb
26
+ - POSTGRES_USER=postgres
27
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
28
  volumes:
29
  - postgres_data:/var/lib/postgresql/data
30
+ - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
31
+ healthcheck:
32
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
33
+ interval: 10s
34
+ timeout: 5s
35
+ retries: 5
36
 
37
  mcp:
38
  build: ./mcp
 
44
  - MCP_API_KEYS=dev-key-123,external-key-456
45
  - NEO4J_BOLT_URL=bolt://neo4j:7687
46
  - NEO4J_AUTH=neo4j/password
47
+ - POSTGRES_CONNECTION=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/testdb
48
  depends_on:
49
  - neo4j
50
+ - postgres
51
 
52
  agent:
53
  build: ./agent
mcp/main.py CHANGED
@@ -3,6 +3,8 @@ from neo4j import GraphDatabase
3
  import os
4
  import json
5
  from datetime import datetime
 
 
6
 
7
  app = FastAPI()
8
  driver = GraphDatabase.driver(
@@ -11,6 +13,7 @@ driver = GraphDatabase.driver(
11
  )
12
 
13
  VALID_API_KEYS = os.getenv("MCP_API_KEYS", "").split(",")
 
14
 
15
  @app.get("/health")
16
  def health():
@@ -63,4 +66,58 @@ async def execute_tool(request: dict, x_api_key: str = Header(None)):
63
  record = result.single()
64
  return {"instruction": dict(record["i"]) if record else None}
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  return {"error": "Unknown tool"}
 
3
  import os
4
  import json
5
  from datetime import datetime
6
+ import psycopg2
7
+ from psycopg2.extras import RealDictCursor
8
 
9
  app = FastAPI()
10
  driver = GraphDatabase.driver(
 
13
  )
14
 
15
  VALID_API_KEYS = os.getenv("MCP_API_KEYS", "").split(",")
16
+ POSTGRES_CONN = os.getenv("POSTGRES_CONNECTION")
17
 
18
  @app.get("/health")
19
  def health():
 
66
  record = result.single()
67
  return {"instruction": dict(record["i"]) if record else None}
68
 
69
+ elif tool == "query_postgres":
70
+ query = params.get("query")
71
+ try:
72
+ conn = psycopg2.connect(POSTGRES_CONN)
73
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
74
+ cur.execute(query)
75
+ if cur.description: # SELECT query
76
+ results = cur.fetchall()
77
+ return {"data": results, "row_count": len(results)}
78
+ else: # INSERT/UPDATE/DELETE
79
+ conn.commit()
80
+ return {"affected_rows": cur.rowcount}
81
+ except Exception as e:
82
+ return {"error": str(e)}
83
+ finally:
84
+ if 'conn' in locals():
85
+ conn.close()
86
+
87
+ elif tool == "discover_postgres_schema":
88
+ try:
89
+ conn = psycopg2.connect(POSTGRES_CONN)
90
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
91
+ # Get all tables
92
+ cur.execute("""
93
+ SELECT table_name, table_schema
94
+ FROM information_schema.tables
95
+ WHERE table_schema = 'public'
96
+ AND table_type = 'BASE TABLE'
97
+ """)
98
+ tables = cur.fetchall()
99
+
100
+ schema_info = {}
101
+ for table in tables:
102
+ table_name = table['table_name']
103
+
104
+ # Get columns for each table
105
+ cur.execute("""
106
+ SELECT column_name, data_type, is_nullable,
107
+ column_default, character_maximum_length
108
+ FROM information_schema.columns
109
+ WHERE table_schema = 'public'
110
+ AND table_name = %s
111
+ ORDER BY ordinal_position
112
+ """, (table_name,))
113
+
114
+ schema_info[table_name] = cur.fetchall()
115
+
116
+ return {"schema": schema_info}
117
+ except Exception as e:
118
+ return {"error": str(e)}
119
+ finally:
120
+ if 'conn' in locals():
121
+ conn.close()
122
+
123
  return {"error": "Unknown tool"}
mcp/requirements.txt CHANGED
@@ -3,3 +3,4 @@ uvicorn==0.24.0
3
  neo4j==5.14.0
4
  pydantic==2.4.0
5
  requests==2.31.0
 
 
3
  neo4j==5.14.0
4
  pydantic==2.4.0
5
  requests==2.31.0
6
+ psycopg2-binary==2.9.9
postgres/init.sql ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Create sample tables for testing
2
+ CREATE TABLE customers (
3
+ id SERIAL PRIMARY KEY,
4
+ email VARCHAR(255) UNIQUE NOT NULL,
5
+ first_name VARCHAR(100),
6
+ last_name VARCHAR(100),
7
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
8
+ );
9
+
10
+ CREATE TABLE orders (
11
+ id SERIAL PRIMARY KEY,
12
+ customer_id INTEGER REFERENCES customers(id),
13
+ order_date DATE NOT NULL,
14
+ total_amount DECIMAL(10,2),
15
+ status VARCHAR(50)
16
+ );
17
+
18
+ -- Insert sample data
19
+ INSERT INTO customers (email, first_name, last_name) VALUES
20
+ ('john.doe@email.com', 'John', 'Doe'),
21
+ ('jane.smith@email.com', 'Jane', 'Smith'),
22
+ ('bob.johnson@email.com', 'Bob', 'Johnson');
23
+
24
+ INSERT INTO orders (customer_id, order_date, total_amount, status) VALUES
25
+ (1, '2024-01-15', 99.99, 'completed'),
26
+ (1, '2024-02-01', 149.99, 'completed'),
27
+ (2, '2024-01-20', 79.99, 'pending'),
28
+ (3, '2024-02-10', 199.99, 'completed');
seed_localhost.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ return response.json()
14
+
15
+ # Create demo workflow
16
+ workflow = call_mcp("write_graph", {
17
+ "action": "create_node",
18
+ "label": "Workflow",
19
+ "properties": {
20
+ "id": "demo-workflow-1",
21
+ "name": "Entity Resolution Demo",
22
+ "status": "active",
23
+ "max_iterations": 10,
24
+ "current_iteration": 0
25
+ }
26
+ })
27
+ print(f"Created workflow: {workflow}")
28
+
29
+ # Create three instructions
30
+ instructions = [
31
+ {"id": "inst-1", "sequence": 1, "type": "discover_schema", "status": "pending", "pause_duration": 300},
32
+ {"id": "inst-2", "sequence": 2, "type": "generate_sql", "status": "pending", "pause_duration": 300},
33
+ {"id": "inst-3", "sequence": 3, "type": "review_results", "status": "pending", "pause_duration": 300}
34
+ ]
35
+
36
+ for inst in instructions:
37
+ result = call_mcp("write_graph", {
38
+ "action": "create_node",
39
+ "label": "Instruction",
40
+ "properties": inst
41
+ })
42
+ print(f"Created instruction: {inst['id']}")
43
+
44
+ # Link to workflow
45
+ call_mcp("query_graph", {
46
+ "query": '''
47
+ MATCH (w:Workflow {id: }), (i:Instruction {id: })
48
+ CREATE (w)-[:HAS_INSTRUCTION]->(i)
49
+ ''',
50
+ "parameters": {"wid": "demo-workflow-1", "iid": inst['id']}
51
+ })
52
+
53
+ # Create instruction chain
54
+ call_mcp("query_graph", {
55
+ "query": '''
56
+ MATCH (i1:Instruction {id: 'inst-1'}), (i2:Instruction {id: 'inst-2'})
57
+ CREATE (i1)-[:NEXT_INSTRUCTION]->(i2)
58
+ '''
59
+ })
60
+
61
+ call_mcp("query_graph", {
62
+ "query": '''
63
+ MATCH (i2:Instruction {id: 'inst-2'}), (i3:Instruction {id: 'inst-3'})
64
+ CREATE (i2)-[:NEXT_INSTRUCTION]->(i3)
65
+ '''
66
+ })
67
+
68
+ # Create indexes
69
+ call_mcp("query_graph", {"query": "CREATE INDEX workflow_id IF NOT EXISTS FOR (w:Workflow) ON (w.id)"})
70
+ call_mcp("query_graph", {"query": "CREATE INDEX instruction_id IF NOT EXISTS FOR (i:Instruction) ON (i.id)"})
71
+ call_mcp("query_graph", {"query": "CREATE INDEX instruction_status_seq IF NOT EXISTS FOR (i:Instruction) ON (i.status, i.sequence)"})
72
+
73
+ print(" Seeding complete!")
seed_simple.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ return response.json()
14
+
15
+ # Create workflow
16
+ workflow = call_mcp("write_graph", {
17
+ "action": "create_node",
18
+ "label": "Workflow",
19
+ "properties": {
20
+ "id": "demo-workflow-1",
21
+ "name": "Entity Resolution Demo",
22
+ "status": "active",
23
+ "max_iterations": 10,
24
+ "current_iteration": 0
25
+ }
26
+ })
27
+ print(f"Created workflow: {workflow}")
28
+
29
+ # Create three instructions
30
+ instructions = [
31
+ {"id": "inst-1", "sequence": 1, "type": "discover_schema", "status": "pending", "pause_duration": 300},
32
+ {"id": "inst-2", "sequence": 2, "type": "generate_sql", "status": "pending", "pause_duration": 300},
33
+ {"id": "inst-3", "sequence": 3, "type": "review_results", "status": "pending", "pause_duration": 300}
34
+ ]
35
+
36
+ for inst in instructions:
37
+ result = call_mcp("write_graph", {
38
+ "action": "create_node",
39
+ "label": "Instruction",
40
+ "properties": inst
41
+ })
42
+ print(f"Created instruction: {inst['id']}")
43
+
44
+ print(" Basic seeding complete! Workflow and instructions created.")
45
+ print("Note: Relationships skipped due to parameterized query issue - but agent should still work!")