"""Bounded, read-only graph neighborhoods with checkpoint-validated provenance.""" from __future__ import annotations from dataclasses import dataclass, field import json from neo4j import READ_ACCESS from tools.knowledge import store from tools.knowledge.concepts import concept_key, substantive_concept from tools.knowledge.evidence import claim_eligible, relation_has_local_support from tools.graphrag.graph_db import create_neo4j_driver, get_neo4j_settings from tools.answers.context_policy import normalized TIMEOUT = 8 SEED_LIMIT = 32 CONCEPT_LIMIT = 24 EVIDENCE_LIMIT = 800 # Fixed templates only. User/model strings enter exclusively as parameters. ROOT_QUERY = '''MATCH (p:KnowledgePerson {graph_scope:$scope})-[:HAS_EVIDENCE]->(e:KnowledgeEvidence {graph_scope:$scope}) OPTIONAL MATCH (p)-[:HAS_WORK]->(w:KnowledgeWork {graph_scope:$scope})-[:EVIDENCED_BY]->(e) WITH p,w,e WHERE (p.source_path IN $profiles OR EXISTS { MATCH (w)-[:SUPERVISED_BY]->(v:KnowledgeAdvisor {graph_scope:$scope}) WHERE v.id IN $supervisors } OR EXISTS { MATCH (w)-[:CONSULTED_BY]->(v:KnowledgeAdvisor {graph_scope:$scope}) WHERE v.id IN $consultants }) AND ((w.id IN $works AND e.work_id=w.id) OR ($profile_records AND e.work_id IS NULL)) AND ($date_field IS NULL OR ($start <= e[$date_field] AND e[$date_field] <= $end)) OPTIONAL MATCH (w)-[:HAS_ACTIVITY]->(a:KnowledgeActivity {graph_scope:$scope})-[:SUPPORTED_BY]->(e) OPTIONAL MATCH (e)-[:ABOUT]->(c:KnowledgeConcept {graph_scope:$scope}) RETURN properties(p) AS person,properties(w) AS work,properties(e) AS evidence,a.id AS activity, collect(DISTINCT c.key) AS concepts ORDER BY CASE WHEN evidence.kind='work_record' THEN 0 ELSE 1 END,work.year DESC,evidence.line LIMIT $budget''' SEED_QUERY = '''MATCH (p:KnowledgePerson {graph_scope:$scope})-[:HAS_WORK]->(w:KnowledgeWork {graph_scope:$scope}) -[:EVIDENCED_BY]->(e:KnowledgeEvidence {graph_scope:$scope}) WHERE e.id IN $ids AND e.work_id=w.id AND w.id IN $works AND ($date_field IS NULL OR ($start <= e[$date_field] AND e[$date_field] <= $end)) OPTIONAL MATCH (w)-[:HAS_ACTIVITY]->(a:KnowledgeActivity {graph_scope:$scope})-[:SUPPORTED_BY]->(e) OPTIONAL MATCH (e)-[:ABOUT]->(c:KnowledgeConcept {graph_scope:$scope}) RETURN properties(p) AS person,properties(w) AS work,properties(e) AS evidence,a.id AS activity, collect(DISTINCT c.key) AS concepts LIMIT $budget''' PERIOD_QUERY = SEED_QUERY.replace('e.id IN $ids AND ', '') NEIGHBOR_QUERY = '''MATCH (p:KnowledgePerson {graph_scope:$scope})-[:HAS_WORK]->(w:KnowledgeWork {graph_scope:$scope}) -[:EVIDENCED_BY]->(e:KnowledgeEvidence {graph_scope:$scope})-[:ABOUT]->(c:KnowledgeConcept {graph_scope:$scope}) WHERE c.key IN $keys AND NOT p.source_path IN $exclude AND w.id IN $works AND e.work_id=w.id AND e.actor=p.name AND e.annotation_status='knowledge' AND e.actor_status IN ['explicit_owner','profile_context'] AND ($date_field IS NULL OR ($start <= e[$date_field] AND e[$date_field] <= $end)) OPTIONAL MATCH (w)-[:HAS_ACTIVITY]->(a:KnowledgeActivity {graph_scope:$scope})-[:SUPPORTED_BY]->(e) WITH p,w,e,a,collect(DISTINCT c.key) AS concepts ORDER BY CASE WHEN e.phase IN ['result','progress'] THEN 0 ELSE 1 END,e.record_date DESC,e.line WITH p,collect({work:properties(w),evidence:properties(e),activity:a.id,concepts:concepts})[..4] AS hits, reduce(shared=[],group IN collect(concepts) | shared + [key IN group WHERE NOT key IN shared]) AS shared ORDER BY size(shared) DESC,CASE WHEN any(hit IN hits WHERE hit.evidence.phase IN ['result','progress']) THEN 0 ELSE 1 END, p.source_path LIMIT $peers UNWIND hits AS hit RETURN properties(p) AS person,hit.work AS work,hit.evidence AS evidence,hit.activity AS activity,hit.concepts AS concepts''' RELATION_QUERY = '''MATCH (r:KnowledgeRelation {graph_scope:$scope,origin:'llm_checked',verification:'supported'}) -[:EVIDENCED_BY]->(e:KnowledgeEvidence {graph_scope:$scope}), (r)-[:SUBJECT]->(s:KnowledgeConcept {graph_scope:$scope}), (r)-[:OBJECT]->(o:KnowledgeConcept {graph_scope:$scope}) WHERE e.id IN $ids AND r.evidence=e.id AND r.work_id=e.work_id AND r.source_path=e.source_path RETURN properties(r) AS relation,e.id AS evidence,s.key AS subject,o.key AS object LIMIT $budget''' EXPAND_QUERY = '''MATCH (r:KnowledgeRelation {graph_scope:$scope,origin:'llm_checked',verification:'supported'}) -[:EVIDENCED_BY]->(e:KnowledgeEvidence {graph_scope:$scope}), (r)-[:SUBJECT]->(s:KnowledgeConcept {graph_scope:$scope}), (r)-[:OBJECT]->(o:KnowledgeConcept {graph_scope:$scope}), (p:KnowledgePerson {graph_scope:$scope})-[:HAS_WORK]->(w:KnowledgeWork {graph_scope:$scope})-[:EVIDENCED_BY]->(e) WHERE (s.key IN $keys OR o.key IN $keys) AND w.id IN $works AND r.evidence=e.id AND ($any_owner OR p.source_path IN $profiles) AND e.work_id=w.id AND r.work_id=w.id AND r.source_path=p.source_path AND ($date_field IS NULL OR ($start <= e[$date_field] AND e[$date_field] <= $end)) OPTIONAL MATCH (w)-[:HAS_ACTIVITY]->(a:KnowledgeActivity {graph_scope:$scope})-[:SUPPORTED_BY]->(e) RETURN properties(p) AS person,properties(w) AS work,properties(e) AS evidence,a.id AS activity, [s.key,o.key] AS concepts,properties(r) AS relation,e.id AS evidence_id,s.key AS subject,o.key AS object LIMIT $budget''' @dataclass class LocalGraph: units: list[dict] = field(default_factory=list) paths: list[dict] = field(default_factory=list) relations: list[dict] = field(default_factory=list) seeds: list[dict] = field(default_factory=list) truncation_reasons: list[str] = field(default_factory=list) def validated_row(record, by_id): actual = dict(record['evidence']) original = by_id.get(actual.get('id')) if not original: raise ValueError('Unknown graph evidence') fields = ('person','actor','actor_status','source_path','source_url','text','line','end_line', 'work_id','work_label','work_type','work_year','phase','section_scope','kind', 'annotation_status','record_date','event_date','event_year','target_date','target_year') if any(actual.get(k) != original.get(k) for k in fields): raise ValueError('Graph assertion differs from its checkpoint') person, work = dict(record['person']), dict(record['work'] or {}) if person.get('source_path') != original['source_path'] or person.get('name') != original['person']: raise ValueError('Cross-person graph edge') if original.get('work_id') and any(work.get(k) != original.get(v) for k,v in [('id','work_id'),('source_path','source_path'),('label','work_label'),('type','work_type'),('year','work_year')]): raise ValueError('Cross-work graph edge') if record.get('activity') and record['activity'] != original['id']: raise ValueError('Cross-activity graph edge') if (original.get('annotation_status')=='knowledge' and original.get('kind') in {'assignment','plan','progress','result'} and claim_eligible(original) and record.get('activity')!=original['id']): raise ValueError('Missing work-activity-evidence path') keys = set(record['concepts']) if not keys <= {concept_key(c) for c in original.get('concepts',[])}: raise ValueError('Unverified concept edge') return {**original, '_graph_keys': sorted(keys), '_graph_activity': record.get('activity')} def seed_ids(question, index, spec): """Classical retrieval proposes entry points; Neo4j must prove every path.""" from tools.knowledge.query import _terms, activity_supported, concept_families from tools.answers.content_selection import administrative_only terms = _terms(question); families = concept_families(question) candidates = [u for u in index['units'] if u.get('work_id') and claim_eligible(u) and u.get('annotation_status')=='knowledge' and not administrative_only(u['text']) and spec.matches_work(u['person'],u.get('work_label'),u.get('work_type'),u.get('work_year')) and activity_supported(question,u)] if spec.period and spec.period.field!='work_year': candidates=[u for u in candidates if u.get(spec.period.field) and spec.period.start<=u[spec.period.field]<=spec.period.end] scores = {} if index.get('vectors') and candidates: import base64, zlib import numpy as np from tools.rag.embedding_utils import embed_query, model_profile, embedding_model_name if model_profile(embedding_model_name())['fingerprint'] != index['embedding_fingerprint']: raise RuntimeError('Graph seed embeddings belong to another model') matrix=np.frombuffer(zlib.decompress(base64.b64decode(index['vectors'])),dtype='=.74: ranked.append((hits+int(equivalent)+semantic,row)) ranked.sort(key=lambda item:(-item[0],item[1]['source_path'],item[1]['line'])) return [row['id'] for _,row in ranked[:SEED_LIMIT+1]] def validated_relation(record, by_id): row=by_id[record.get('evidence_id') or record['evidence']]; actual=dict(record['relation']) match=next((r for r in row.get('relations',[]) if r.get('verification')=='supported' and concept_key(r['subject'])==record['subject'] and concept_key(r['object'])==record['object'] and r['predicate']==actual.get('predicate') and r['quote']==actual.get('quote') and relation_has_local_support(row,r)),None) if not match or any(actual.get(k)!=row.get(k) for k in ('actor','work_id','phase','source_path','source_url','line')): raise ValueError('Unverified semantic graph relation') return {'evidence_id':row['id'],**match} def retrieve_local_graph(question, index, bound, *, limit=5): spec=bound.spec; result=LocalGraph(); by_id={u['id']:u for u in index['units']} people=[p for p in spec.people if p.role=='student'] profiles=[p.source_document for p in people if p.source_document] works=sorted({u['work_id'] for u in index['units'] if u.get('work_id') and spec.matches_work(u['person'],u.get('work_label'),u.get('work_type'),u.get('work_year'))}) ids=seed_ids(question,index,spec) if not profiles and not any(p.role in {'supervisor','consultant'} for p in spec.people) else [] if len(ids)>SEED_LIMIT:result.truncation_reasons.append('graph_seed_limit') ids=ids[:SEED_LIMIT] period=spec.period if spec.period and spec.period.field!='work_year' else None params={'scope':store.SCOPE,'profiles':profiles,'works':works,'ids':ids, 'supervisors':[store.identifier(normalized(p.name)) for p in spec.people if p.role=='supervisor'], 'consultants':[store.identifier(normalized(p.name)) for p in spec.people if p.role=='consultant'], 'date_field':period.field if period else None,'start':period.start if period else None,'end':period.end if period else None, 'profile_records':not any(w.work_type or w.years or w.labels or w.numbers or w.excluded_labels or w.excluded_numbers or w.unnumbered for w in spec.works), 'budget':EVIDENCE_LIMIT+1,'peers':min(61,max(11,limit*3+1))} settings=get_neo4j_settings() with create_neo4j_driver(settings,connection_timeout=TIMEOUT,max_transaction_retry_time=0) as driver,\ driver.session(database=settings.database,default_access_mode=READ_ACCESS) as session, session.begin_transaction(timeout=TIMEOUT*4) as tx: state=tx.run('MATCH (s:KnowledgeState {scope:$scope}) RETURN properties(s) AS state',scope=store.SCOPE).single() state=dict(state['state']) if state else {} if (state.get('version')!=index['version'] or state.get('projection_schema')!=store.CONTENT_GRAPH_SCHEMA or state.get('index_version_json')!=json.dumps(index['index_version'],sort_keys=True) or index.get('_checkpoint_sha256') and state.get('checkpoint_sha256')!=index['_checkpoint_sha256']): raise RuntimeError('Content graph projection is stale or differs from checkpoint') def fetch(query, **extra): return [validated_row(dict(r),by_id) for r in tx.run(query,**{**params,**extra})] root=fetch(ROOT_QUERY) if profiles or params['supervisors'] or params['consultants'] else fetch( PERIOD_QUERY if spec.intent=='period' else SEED_QUERY) if len(root)>EVIDENCE_LIMIT:result.truncation_reasons.append('graph_evidence_limit') root=root[:EVIDENCE_LIMIT] result.seeds=root result.units=root if spec.intent in {'similar','discovery','period'}: ranked={} for row in root: if row.get('annotation_status')!='knowledge' or not claim_eligible(row):continue for key in row['_graph_keys']: if spec.intent!='similar' or substantive_concept(key): ranked.setdefault(key,[]).append(row) keys=sorted(ranked,key=lambda k:(-len(ranked[k]),k))[:CONCEPT_LIMIT] if len(ranked)>CONCEPT_LIMIT:result.truncation_reasons.append('graph_concept_limit') expansions=list(tx.run(EXPAND_QUERY,**{**params,'keys':keys, 'any_owner':spec.intent!='similar','budget':SEED_LIMIT+1})) if keys else [] if len(expansions)>SEED_LIMIT:result.truncation_reasons.append('graph_relation_limit') for record in expansions[:SEED_LIMIT]: row=validated_row(dict(record),by_id) relation=validated_relation(dict(record),by_id) result.relations.append(relation) for key in row['_graph_keys']: if (spec.intent!='similar' or substantive_concept(key)) and key not in keys: if len(keys)>=CONCEPT_LIMIT+8: result.truncation_reasons.append('graph_concept_limit');continue keys.append(key);ranked.setdefault(key,[]).append(row) root.append(row) peers=fetch(NEIGHBOR_QUERY,keys=keys,exclude=profiles if spec.intent=='similar' else []) if keys else [] if len({r['source_path'] for r in peers})>=params['peers']: result.truncation_reasons.append('graph_peer_limit') for row in peers: for key in row['_graph_keys']: if key not in keys:continue anchors=[a for a in ranked[key] if spec.intent!='similar' or a['source_path']!=row['source_path']] for anchor in sorted(anchors,key=lambda a:(a['phase'] not in {'result','progress'},a['line']))[:2]: result.paths.append({'kind':'shared_topic','concept':key,'anchor_id':anchor['id'], 'peer_id':row['id'],'anchor_work_id':anchor['work_id'],'peer_work_id':row['work_id'], 'anchor_activity_id':anchor.get('_graph_activity'),'peer_activity_id':row.get('_graph_activity'), 'concept_match':'approved_equivalent_key'}) result.units=list({r['id']:r for r in [*root,*peers]}.values()) records=tx.run(RELATION_QUERY,scope=store.SCOPE, ids=[u['id'] for u in result.units],budget=EVIDENCE_LIMIT+1) for record in records: relation=validated_relation(dict(record),by_id) if relation not in result.relations:result.relations.append(relation) tx.commit() return result