Spaces:
Runtime error
Runtime error
File size: 5,682 Bytes
cf1f646 3c2c4aa c24f0b6 3c2c4aa cc01889 c24f0b6 3c2c4aa c24f0b6 3c2c4aa cc01889 3c2c4aa cf1f646 cc01889 c24f0b6 3c2c4aa c24f0b6 3c2c4aa c24f0b6 3c2c4aa c24f0b6 3c2c4aa cc01889 3c2c4aa cf1f646 c24f0b6 3c2c4aa c24f0b6 3c2c4aa c24f0b6 3c2c4aa c24f0b6 3c2c4aa cf1f646 3c2c4aa cf1f646 c24f0b6 3c2c4aa c24f0b6 3c2c4aa cf1f646 c24f0b6 3c2c4aa cf1f646 3c2c4aa c24f0b6 3c2c4aa cf1f646 cc01889 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | # File Objective : Build the Vantage Knowledge Graph end-to-end.
# Scope : Dev script β run after ingest + tag_topics to populate Neo4j.
# What it does : 1. Coverage β reads topic_coverage.csv (written by tag_topics.py).
# 2. Signals β Google Trends volume + velocity per topic (anchor=calculator).
# 3. Gap score β Demand Γ (1 β Coverage) per topic.
# 4. Push β (:Topic){gap metadata} + (:Topic)-[:COVERED_BY]->(:Document).
# 5. Prints top gaps with covered documents.
# What it does not: Wipe the graph (run wipe_graph.py first) or re-ingest.
#
# Usage : python scripts/build_graph.py # all topics
# SAMPLE=10 python scripts/build_graph.py # first N topics only
from __future__ import annotations
import csv
import json
import os
from collections import defaultdict
from pathlib import Path
from vantage_core.adapters.graph_neo4j import Neo4jGraphRepo
from vantage_core.adapters.signals_pytrends import fetch_signals
from vantage_core.domain.gap import compute_gaps
from vantage_core.models import SignalRecord
from vantage_core.topic_seeds import load_topics
SAMPLE = int(os.environ.get("SAMPLE", 0)) # >0 β first N topics
TOPICS_ENV = os.environ.get("TOPICS", "") # comma-separated override
_SIG_CACHE = Path("data/signals.json")
_COVERAGE_CSV = Path("data/topic_coverage.csv")
def load_coverage() -> dict[str, list[str]]:
"""Read topic_coverage.csv β {topic: [doc_id, ...]}. Written by tag_topics.py."""
coverage: dict[str, list[str]] = defaultdict(list)
with _COVERAGE_CSV.open(encoding="utf-8") as f:
for row in csv.DictReader(f):
coverage[row["topic"]].append(row["doc_id"])
return dict(coverage)
def load_signals(topics: list[str]) -> list[SignalRecord]:
"""Return signals from disk cache if present; otherwise fetch live and save cache."""
if _SIG_CACHE.exists():
print(f" Using cached signals ({_SIG_CACHE})")
data = json.loads(_SIG_CACHE.read_text(encoding="utf-8"))
cached = {r["keyword"]: SignalRecord(**r) for r in data["signals"]}
# fetch any topics missing from cache
missing = [t for t in topics if t not in cached]
if missing:
print(f" {len(missing)} topics not in cache β fetching live...")
fresh = fetch_signals(missing)
for s in fresh:
cached[s.keyword] = s
_save_signals_cache(list(cached.values()))
return [cached[t] for t in topics if t in cached]
signals = fetch_signals(topics)
_save_signals_cache(signals)
return signals
def _save_signals_cache(signals: list[SignalRecord]) -> None:
_SIG_CACHE.parent.mkdir(parents=True, exist_ok=True)
_SIG_CACHE.write_text(
json.dumps({"signals": [s.model_dump() for s in signals]}, indent=2),
encoding="utf-8",
)
print(f" Signals cached β {_SIG_CACHE} ({len(signals)} records)")
def main() -> None:
graph = Neo4jGraphRepo()
# ββ 0. Load coverage CSV, pre-filter to topics with β₯1 doc βββββββββββββββ
print(f"Loading coverage from {_COVERAGE_CSV}...")
raw_coverage = load_coverage() # topic β [doc_id]
if TOPICS_ENV:
candidates = [t.strip() for t in TOPICS_ENV.split(",")]
elif SAMPLE:
candidates = load_topics()[:SAMPLE]
else:
candidates = load_topics()
topics = [t for t in candidates if t in raw_coverage]
print(f" {len(topics)}/{len(candidates)} topics have β₯1 doc\n")
# cover: topic β list of {doc_id} dicts
cover: dict[str, list[dict]] = {t: [{"doc_id": d} for d in raw_coverage[t]] for t in topics}
print(f"Building graph for {len(topics)} topics\n")
# ββ 1. Signals ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
print("Loading signals...")
signals = load_signals(topics)
# ββ 2. Gap scores βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
counts = {t: len(docs) for t, docs in cover.items()}
gaps = compute_gaps(signals, counts)
# ββ 3. Push to Neo4j: Topic nodes + COVERED_BY edges ββββββββββββββββββββββ
print("\nPushing to Neo4j...")
graph.build(gaps)
edges = [
{"topic": g.topic, "doc_id": d["doc_id"], "title": ""}
for g in gaps
for d in cover.get(g.topic, [])
]
graph.link_coverage(edges)
print(f" {len(gaps)} Topic nodes + {len(edges)} COVERED_BY edges\n")
# ββ 4. Sample output ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
print("ββ Top 10 gaps ββββββββββββββββββββββββββββββββββββββββββββββββββββββ")
for g in gaps[:10]:
print(f" gap={g.gap_score:.4f} {g.label:<13} vol={g.volume:.2f} vel={g.velocity:+.2f} "
f"cov={g.coverage_count}docs {g.topic}")
for d in graph.docs_for_topic(g.topic, limit=2):
print(f" ββ [{d['doc_id']}] {d['title'][:60]}")
graph.close()
if __name__ == "__main__":
main()
|