Add scripts/ingest.py
This commit is contained in:
1 parent
900c0faa03
commit
d9fad6f9f8
1 file changed
+232
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cyborg Architecture — Knowledge Ingestion Script
|
||||
Phase 1: File-based knowledge extraction and graph building
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# --- Configuration ---
|
||||
MEMGRAPH_URI = os.getenv("MEMGRAPH_URI", "bolt://localhost:7687")
|
||||
MEMGRAPH_USER = os.getenv("MEMGRAPH_USER", "memgraph")
|
||||
MEMGRAPH_PASS = os.getenv("MEMGRAPH_PASS", "memgraph")
|
||||
|
||||
QDRANT_URL = os.getenv("QDRANT_URL", "http://localhost:6333")
|
||||
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")
|
||||
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:7b")
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
def run_cmd(cmd: list[str]) -> tuple[int, str]:
|
||||
"""Run a shell command, return (exit_code, stdout)."""
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
return result.returncode, result.stdout.strip()
|
||||
|
||||
|
||||
def file_hash(path: str) -> str:
|
||||
"""Simple SHA-256 hash of file content."""
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()[:16]
|
||||
|
||||
|
||||
def get_file_metadata(path: str) -> dict:
|
||||
"""Extract metadata from a file."""
|
||||
p = Path(path)
|
||||
lang = p.suffix.lstrip(".") or "text"
|
||||
lang = classify_language(lang)
|
||||
|
||||
# Count lines
|
||||
lines = 0
|
||||
try:
|
||||
with open(path, "r", errors="ignore") as f:
|
||||
for _ in f:
|
||||
lines += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"path": path,
|
||||
"name": p.name,
|
||||
"lang": lang,
|
||||
"lines": lines,
|
||||
"size": p.stat().st_size,
|
||||
"hash": file_hash(path),
|
||||
"modified": p.stat().st_mtime,
|
||||
}
|
||||
|
||||
|
||||
def classify_language(ext: str) -> str:
|
||||
"""Map file extension to language category."""
|
||||
lang_map = {
|
||||
"py": "python", "js": "javascript", "ts": "typescript",
|
||||
"go": "go", "rs": "rust", "java": "java", "cpp": "cpp",
|
||||
"c": "c", "h": "c-header", "hpp": "cpp-header",
|
||||
"html": "html", "css": "css", "scss": "scss", "sass": "sass",
|
||||
"md": "markdown", "yaml": "yaml", "yml": "yaml", "json": "json",
|
||||
"xml": "xml", "toml": "toml", "ini": "ini", "cfg": "ini",
|
||||
"sh": "shell", "bash": "shell", "zsh": "shell",
|
||||
"dockerfile": "docker", "docker-compose": "docker",
|
||||
"sql": "sql", "graphql": "graphql", "proto": "protobuf",
|
||||
"tf": "hcl", "hcl": "hcl", "vue": "vue", "svelte": "svelte",
|
||||
}
|
||||
return lang_map.get(ext, ext)
|
||||
|
||||
|
||||
# --- Graph Operations (via Cypher) ---
|
||||
|
||||
def create_file_node(cypher_conn, meta: dict):
|
||||
"""Create or update a file node in Memgraph."""
|
||||
# This would use the mgclient Python library in production
|
||||
# For now, return the Cypher statement
|
||||
cypher = f"""
|
||||
MERGE (f:File {{path: '{meta['path']}'}})
|
||||
SET f.name = '{meta['name']}',
|
||||
f.lang = '{meta['lang']}',
|
||||
f.lines = {meta['lines']},
|
||||
f.size = {meta['size']},
|
||||
f.hash = '{meta['hash']}',
|
||||
f.modified = {meta['modified']},
|
||||
f.indexed_at = timestamp()
|
||||
RETURN f
|
||||
"""
|
||||
return cypher
|
||||
|
||||
|
||||
def create_import_dependency(cypher_conn, from_file: str, to_file: str):
|
||||
"""Create an IMPORTS relationship between files."""
|
||||
cypher = f"""
|
||||
MATCH (a:File {{path: '{from_file}'}}), (b:File {{path: '{to_file}'}})
|
||||
CREATE (a)-[:IMPORTS]->(b)
|
||||
RETURN a, b
|
||||
"""
|
||||
return cypher
|
||||
|
||||
|
||||
def create_project_structure(cypher_conn, path: str, level: int = 0):
|
||||
"""Build a directory tree as a graph."""
|
||||
p = Path(path)
|
||||
if not p.is_dir():
|
||||
return []
|
||||
|
||||
statements = []
|
||||
current = f"MATCH (d:Directory {{path: '{p}'}}) RETURN d"
|
||||
|
||||
for item in sorted(p.iterdir()):
|
||||
if item.is_dir():
|
||||
child_path = str(item)
|
||||
statements.append(f"""
|
||||
MERGE (parent:Directory {{path: '{p}'}})
|
||||
MERGE (child:Directory {{path: '{child_path}'}})
|
||||
MERGE (parent)-[:CONTAINS]->(child)
|
||||
""")
|
||||
statements.extend(create_project_structure(str(item), level + 1))
|
||||
elif item.is_file():
|
||||
meta = get_file_metadata(str(item))
|
||||
statements.append(create_file_node(None, meta))
|
||||
statements.append(f"""
|
||||
MERGE (d:Directory {{path: '{p}'}})
|
||||
MERGE (f:File {{path: '{meta['path']}'}})
|
||||
MERGE (d)-[:CONTAINS]->(f)
|
||||
""")
|
||||
|
||||
return statements
|
||||
|
||||
|
||||
# --- Dependency Extraction ---
|
||||
|
||||
def extract_imports_python(file_path: str) -> list[str]:
|
||||
"""Parse Python imports from a file."""
|
||||
imports = []
|
||||
try:
|
||||
with open(file_path, "r", errors="ignore") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("import ") or line.startswith("from "):
|
||||
module = line.split()[1].split(".")[0]
|
||||
imports.append(module)
|
||||
except Exception:
|
||||
pass
|
||||
return imports
|
||||
|
||||
|
||||
def extract_imports_js(file_path: str) -> list[str]:
|
||||
"""Parse JS/TS imports from a file."""
|
||||
imports = []
|
||||
try:
|
||||
with open(file_path, "r", errors="ignore") as f:
|
||||
content = f.read()
|
||||
# Simple regex for require/import
|
||||
import re
|
||||
for m in re.finditer(r'''(?:import\s+.*?from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))''', content):
|
||||
mod = m.group(1) or m.group(2)
|
||||
if not mod.startswith(".") and not mod.startswith("/"):
|
||||
imports.append(mod.split("/")[0])
|
||||
except Exception:
|
||||
pass
|
||||
return imports
|
||||
|
||||
|
||||
def extract_imports_generic(file_path: str, lang: str) -> list[str]:
|
||||
"""Generic import extraction."""
|
||||
if lang == "python":
|
||||
return extract_imports_python(file_path)
|
||||
elif lang in ("javascript", "typescript"):
|
||||
return extract_imports_js(file_path)
|
||||
return []
|
||||
|
||||
|
||||
# --- Main ---
|
||||
|
||||
def ingest_directory(directory: str, dry_run: bool = False) -> dict:
|
||||
"""Main ingestion function."""
|
||||
results = {
|
||||
"files": [],
|
||||
"dependencies": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
for root, dirs, files in os.walk(directory):
|
||||
# Skip hidden dirs and venvs
|
||||
dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ("node_modules", "__pycache__", ".git", "venv")]
|
||||
|
||||
for fname in files:
|
||||
fpath = os.path.join(root, fname)
|
||||
rel_path = os.path.relpath(fpath, directory)
|
||||
|
||||
meta = get_file_metadata(fpath)
|
||||
results["files"].append(meta)
|
||||
|
||||
# Extract imports
|
||||
imports = extract_imports_generic(fpath, meta["lang"])
|
||||
if imports:
|
||||
results["dependencies"].append({
|
||||
"file": fpath,
|
||||
"imports": imports,
|
||||
})
|
||||
|
||||
if dry_run:
|
||||
print(f"Would ingest {len(results['files'])} files")
|
||||
print(f"Would create {len(results['dependencies'])} dependency records")
|
||||
print(json.dumps(results, indent=2)[:2000])
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Cyborg Architecture - Knowledge Ingest")
|
||||
parser.add_argument("directory", nargs="?", default=".", help="Directory to ingest")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Show what would happen")
|
||||
args = parser.parse_args()
|
||||
|
||||
results = ingest_directory(args.directory, dry_run=args.dry_run)
|
||||
print(json.dumps(results, indent=2, default=str))
|
||||
Reference in new issue
Block a user