"""Offline warm-up of the pathway report cache. Retrieval and evidence gating are deterministic in (path, corpus), but a full run costs minutes of CPU per path. This script computes every pathway report once, offline, so the running app serves them from cache instead of recomputing an identical answer on each request. Run it after the knowledge base changes (the cache key carries a corpus fingerprint, so stale entries are ignored automatically rather than served). python precompute_rag_cache.py # all reachable targets python precompute_rag_cache.py As2O3 PbO # only these targets Interrupting is safe: finished paths stay cached, so a re-run resumes. """ from __future__ import annotations import os import sys import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # Some network node names carry zero-width characters, and the tracer prints the # target name on a miss. On a GBK console that print raises UnicodeEncodeError # and aborts the run, so force UTF-8 output and never fail on an odd glyph. for _stream in (sys.stdout, sys.stderr): try: _stream.reconfigure(encoding="utf-8", errors="replace") except (AttributeError, ValueError): pass def main(argv: list[str]) -> int: from search_subgraph import CRNTracer, GRAPHML_PATH from tracernet.crn import pathways as crn_pathways from tracernet.crn.repository import CRNRepository from rag_module import RAGService project = os.path.dirname(os.path.abspath(__file__)) repository = CRNRepository.discover([project]) tracer = CRNTracer(GRAPHML_PATH, crn_repository=repository) graph = tracer.G service = RAGService(repository) print(f"[init] graph={graph.number_of_nodes()} nodes, " f"corpus signature={service._report_cache_signature()}", flush=True) targets = argv or sorted(graph.nodes()) print(f"[plan] tracing {len(targets)} candidate targets ...", flush=True) # Collect unique paths across every target. The same path is reachable from # several targets, and computing it twice would cost minutes for nothing. jobs: dict[str, tuple[str, object]] = {} for target in targets: try: _resolved, _graph, _report, all_paths = crn_pathways.trace_material_pathways( tracer, graph, target, max_depth=10, limit=100 ) except Exception as error: # a target with no upstream path is normal print(f"[trace] {target}: skipped ({type(error).__name__})", flush=True) continue for info in all_paths or []: path_str = info.get("path_str") if path_str and path_str not in jobs: jobs[path_str] = (info.get("source"), info.get("full_path")) print(f"[plan] {len(jobs)} unique pathways to warm", flush=True) started = time.time() done = 0 for index, (path_str, (source, full_path)) in enumerate(jobs.items(), 1): step = time.time() try: # Evidence only: the narrative is generated per request, so warming # it here would be discarded work -- and with an API key configured # it would spend one LLM call per path for nothing. service.cached_path_evidence(source, path_str, full_path) done += 1 status = "ok" except Exception as error: status = f"FAILED {type(error).__name__}: {error}" elapsed = time.time() - step total = time.time() - started print(f"[{index}/{len(jobs)}] {elapsed:6.1f}s total {total/60:5.1f}m " f"{status} {path_str[:90]}", flush=True) print(f"[done] warmed {done}/{len(jobs)} pathways in " f"{(time.time()-started)/60:.1f} minutes", flush=True) print(f"[done] cache directory: {service._report_cache_dir()}", flush=True) return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))