open-context-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/assets/brand/logo-lockup-dark.svg +14 -0
- package/assets/brand/logo-lockup.svg +14 -0
- package/assets/brand/logo.svg +9 -0
- package/bin/opencontextengine.mjs +64 -0
- package/docs/QUICKSTART.md +192 -0
- package/docs/RERANKER_API.md +32 -0
- package/package.json +81 -0
- package/requirements.txt +2 -0
- package/scripts/mcp-opencontextengine.mjs +37 -0
- package/scripts/retrieval-server.py +135 -0
- package/src/client.mjs +37 -0
- package/src/config.mjs +49 -0
- package/src/environment.mjs +10 -0
- package/src/eval/remote-models.mjs +70 -0
- package/src/mcp.mjs +58 -0
- package/src/retrieval/batched.py +220 -0
- package/src/retrieval/cascade.py +142 -0
- package/src/retrieval/engine.py +195 -0
- package/src/retrieval/entities.py +187 -0
- package/src/retrieval/languages/__init__.py +129 -0
- package/src/retrieval/languages/files.py +90 -0
- package/src/retrieval/languages/go.py +154 -0
- package/src/retrieval/languages/go_ast.go +204 -0
- package/src/retrieval/languages/go_types.go +169 -0
- package/src/retrieval/languages/python.py +113 -0
- package/src/retrieval/languages/schema.py +81 -0
- package/src/retrieval/languages/text.py +39 -0
- package/src/retrieval/languages/typescript.mjs +233 -0
- package/src/retrieval/languages/typescript.py +23 -0
- package/src/retrieval/live.py +273 -0
- package/src/retrieval/reranker.py +83 -0
- package/src/retrieval/routed.py +35 -0
- package/src/runtime.mjs +60 -0
- package/src/service.mjs +77 -0
- package/src/setup.mjs +66 -0
- package/src/workspaces.mjs +59 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""One neural ranking stage after cheap, parallel facet/structure retrieval.
|
|
2
|
+
|
|
3
|
+
Query-independent code structure is prepared once. Query facets interact with
|
|
4
|
+
stored code vectors numerically; they do not trigger separate neural reranks.
|
|
5
|
+
Selection considers both individual spans and complete small function bundles.
|
|
6
|
+
"""
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
import math
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from engine import Engine, document, post
|
|
14
|
+
|
|
15
|
+
VERSION = 'structural-cascade-v2'
|
|
16
|
+
POLICY = {'candidateLimit': 96, 'facetSeeds': 4, 'neighborsPerSeed': 10,
|
|
17
|
+
'facetPool': 24, 'maxFunctionBudgetFraction': .45,
|
|
18
|
+
'embeddingScoreTemperature': .06, 'functionCompletionFactor': 1.25}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CascadeEngine(Engine):
|
|
22
|
+
def __init__(self, *args, **kwargs):
|
|
23
|
+
super().__init__(*args, **kwargs)
|
|
24
|
+
self.functions = defaultdict(list)
|
|
25
|
+
for u in self.units:
|
|
26
|
+
if u['kind'] == 'function':
|
|
27
|
+
self.functions[u['symbol']].append(u['id'])
|
|
28
|
+
for group in self.functions.values():
|
|
29
|
+
group.sort(key=lambda i: self.units[i]['start'])
|
|
30
|
+
|
|
31
|
+
def recall(self, queries, dense, facets):
|
|
32
|
+
pools, fused = [], defaultdict(float)
|
|
33
|
+
local_scores = []
|
|
34
|
+
for col, query in enumerate(queries):
|
|
35
|
+
lexical = self.lexical(query + (' ' + ' '.join(facets[col-1]['terms']) if col else ''))
|
|
36
|
+
local = defaultdict(float)
|
|
37
|
+
for scores in (dense[:, col], lexical):
|
|
38
|
+
for rank, uid in enumerate(np.argsort(-scores)[:60]):
|
|
39
|
+
if scores[uid] > 0:
|
|
40
|
+
local[int(uid)] += 1 / (30 + rank)
|
|
41
|
+
pool = sorted(local, key=lambda i: (-local[i], i))[:POLICY['facetPool']]
|
|
42
|
+
pools.append(pool)
|
|
43
|
+
local_scores.append(local)
|
|
44
|
+
for uid, score in local.items():
|
|
45
|
+
# Max preserves a facet-specific specialist; mean rewards breadth.
|
|
46
|
+
fused[uid] = max(fused[uid], score)
|
|
47
|
+
for uid in list(fused):
|
|
48
|
+
fused[uid] += .35 * sum(s.get(uid, 0) for s in local_scores) / len(queries)
|
|
49
|
+
seeds = set(uid for pool in pools for uid in pool[:POLICY['facetSeeds']])
|
|
50
|
+
expanded = set()
|
|
51
|
+
graph_prior = defaultdict(float)
|
|
52
|
+
for uid in seeds:
|
|
53
|
+
neighbors = set(self.units[uid]['edges'] + self.incoming[uid])
|
|
54
|
+
ranked = sorted(neighbors, key=lambda i: (-fused[i], i))[:POLICY['neighborsPerSeed']]
|
|
55
|
+
for neighbor in ranked:
|
|
56
|
+
expanded.add(neighbor)
|
|
57
|
+
graph_prior[neighbor] = max(graph_prior[neighbor], fused[uid] * .5)
|
|
58
|
+
candidates = set().union(*(set(pool) for pool in pools), expanded)
|
|
59
|
+
ordered = sorted(candidates, key=lambda i: (-(fused[i] + graph_prior[i]), i))
|
|
60
|
+
# Retain direct hits from every facet before applying the shared ceiling.
|
|
61
|
+
protected = list(dict.fromkeys(uid for pool in pools for uid in pool[:12]))
|
|
62
|
+
retained = list(dict.fromkeys(protected + ordered))[:POLICY['candidateLimit']]
|
|
63
|
+
return retained, expanded, len(candidates)
|
|
64
|
+
|
|
65
|
+
def pack(self, retained, scores, affinity, budget):
|
|
66
|
+
position = {uid: i for i, uid in enumerate(retained)}
|
|
67
|
+
actions = [(uid,) for uid in retained]
|
|
68
|
+
for group in self.functions.values():
|
|
69
|
+
if len(group) > 1 and all(uid in position for uid in group) and sum(self.costs[uid] for uid in group) <= budget * POLICY['maxFunctionBudgetFraction']:
|
|
70
|
+
actions.append(tuple(group))
|
|
71
|
+
relevance = np.asarray([scores[uid] for uid in retained])
|
|
72
|
+
values = relevance[:, None] * (.35 + .65 * affinity)
|
|
73
|
+
covered = np.zeros(affinity.shape[1])
|
|
74
|
+
selected, selected_set, trace, spent = [], set(), [], 0
|
|
75
|
+
while actions:
|
|
76
|
+
choices = []
|
|
77
|
+
for action in actions:
|
|
78
|
+
fresh = [uid for uid in action if uid not in selected_set]
|
|
79
|
+
cost = sum(self.costs[uid] for uid in fresh)
|
|
80
|
+
if not fresh or spent + cost > budget:
|
|
81
|
+
continue
|
|
82
|
+
indices = [position[uid] for uid in fresh]
|
|
83
|
+
benefit = values[indices].max(axis=0)
|
|
84
|
+
base = float(relevance[indices].max())
|
|
85
|
+
gain = .7 * float(np.mean(benefit / (1 + covered))) + .3 * base
|
|
86
|
+
if len(action) > 1:
|
|
87
|
+
gain *= POLICY['functionCompletionFactor']
|
|
88
|
+
gain /= (max(120, cost) / 300) ** .35
|
|
89
|
+
choices.append((gain, action, fresh, cost, benefit))
|
|
90
|
+
if not choices:
|
|
91
|
+
break
|
|
92
|
+
gain, action, fresh, cost, benefit = max(choices, key=lambda c: (c[0], -c[2][0]))
|
|
93
|
+
if gain < .015:
|
|
94
|
+
break
|
|
95
|
+
actions.remove(action)
|
|
96
|
+
selected.extend(fresh)
|
|
97
|
+
selected_set.update(fresh)
|
|
98
|
+
spent += cost
|
|
99
|
+
covered += benefit
|
|
100
|
+
trace.append({'ids': fresh, 'functionBundle': len(action) > 1,
|
|
101
|
+
'tokens': cost, 'gain': gain, 'scores': [scores[i] for i in fresh]})
|
|
102
|
+
raw = '\n'.join(self.render(self.units[uid]) for uid in selected)
|
|
103
|
+
return raw, trace
|
|
104
|
+
|
|
105
|
+
def search(self, plan, budget=4000):
|
|
106
|
+
start = time.monotonic()
|
|
107
|
+
facets = plan['facets']
|
|
108
|
+
queries = [plan['intent']] + [f['question'] for f in facets]
|
|
109
|
+
# Encode repeated intent/facet text once, without caching across requests.
|
|
110
|
+
unique_queries = list(dict.fromkeys(queries))
|
|
111
|
+
result = post(self.embed_url + '/embeddings', {'model': 'Qwen3-Embedding-4B',
|
|
112
|
+
'input': ['Instruct: Retrieve source code implementing the requested behavior.\nQuery: ' + q for q in unique_queries]}, self.embedding_key)
|
|
113
|
+
unique_vectors = [r['embedding'] for r in sorted(result['data'], key=lambda r: r['index'])]
|
|
114
|
+
vectors = np.asarray([unique_vectors[unique_queries.index(q)] for q in queries], dtype=np.float32)
|
|
115
|
+
embedded_at = time.monotonic()
|
|
116
|
+
dense = self.vectors @ vectors.T
|
|
117
|
+
retained, expanded, candidate_count = self.recall(queries, dense, facets)
|
|
118
|
+
recalled_at = time.monotonic()
|
|
119
|
+
reranked = post(self.reranker['baseUrl'] + '/rerank', {'model': self.reranker['model'],
|
|
120
|
+
'query': plan['intent'], 'documents': [document(self.units[uid], 5000) for uid in retained]}, self.reranker['apiKey'])
|
|
121
|
+
rows = reranked['results']
|
|
122
|
+
if len(rows) != len(retained) or {r['index'] for r in rows} != set(range(len(retained))) or any(not math.isfinite(r['relevance_score']) or not 0 <= r['relevance_score'] <= 1 for r in rows):
|
|
123
|
+
raise ValueError('Invalid rerank mapping or scores')
|
|
124
|
+
scores = {retained[r['index']]: r['relevance_score'] for r in rows}
|
|
125
|
+
ranked_at = time.monotonic()
|
|
126
|
+
# Soft maximum relative to each facet's best candidate avoids assuming
|
|
127
|
+
# cosine similarities are calibrated cross-language probabilities.
|
|
128
|
+
facet_dense = dense[retained, 1:]
|
|
129
|
+
affinity = np.exp(np.minimum(0, facet_dense - facet_dense.max(axis=0)) / POLICY['embeddingScoreTemperature'])
|
|
130
|
+
raw, selected = self.pack(retained, scores, affinity, budget)
|
|
131
|
+
finished = time.monotonic()
|
|
132
|
+
return raw, {'version': VERSION, 'elapsedMs': round((finished-start)*1000),
|
|
133
|
+
'tokens': len(self.encoding.encode(raw)), 'candidateCount': candidate_count,
|
|
134
|
+
'rerankedCount': len(retained), 'expandedCount': len(expanded),
|
|
135
|
+
'modelRequests': {'embedding': 1, 'rerank': 1}, 'queryCache': False,
|
|
136
|
+
'timingMs': {'embedding': round((embedded_at-start)*1000), 'recall': round((recalled_at-embedded_at)*1000),
|
|
137
|
+
'rerank': round((ranked_at-recalled_at)*1000), 'rerankModel': reranked.get('meta', {}).get('elapsed_ms'),
|
|
138
|
+
'selection': round((finished-ranked_at)*1000)},
|
|
139
|
+
'rerankInputTokens': reranked.get('usage', {}).get('input_tokens'),
|
|
140
|
+
'candidates': [{'id': uid, 'score': scores[uid], 'path': self.units[uid]['path'],
|
|
141
|
+
'start': self.units[uid]['start'], 'end': self.units[uid]['end']} for uid in retained],
|
|
142
|
+
'selected': selected, 'plan': plan, 'policy': POLICY}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Language-neutral retrieval over snapshot-bound source units. Models are called over HTTP, never loaded.
|
|
2
|
+
|
|
3
|
+
AST spans, conservative static links, lexical/dense fusion, facet reranking and
|
|
4
|
+
whole-span packing are repository independent. No evaluation labels are read.
|
|
5
|
+
"""
|
|
6
|
+
from collections import Counter, defaultdict
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from urllib.request import Request, urlopen
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import tiktoken
|
|
17
|
+
|
|
18
|
+
from languages import source_units, adapter_manifest
|
|
19
|
+
|
|
20
|
+
VERSION = 'structural-units-v2'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def terms(text):
|
|
24
|
+
text = re.sub(r'([a-z])([A-Z])', r'\1 \2', text)
|
|
25
|
+
return re.findall(r'[a-z][a-z0-9]+', text.lower().replace('_', ' '))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def post(url, payload, key='local-only', timeout=120):
|
|
29
|
+
request = Request(url, json.dumps(payload).encode(), {
|
|
30
|
+
'Content-Type': 'application/json', 'Authorization': 'Bearer ' + key})
|
|
31
|
+
with urlopen(request, timeout=timeout) as response:
|
|
32
|
+
return json.load(response)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def document(u, limit=2200):
|
|
36
|
+
return ('Path: ' + u['path'] + '\nSymbol: ' + u['name'] + '\n' + u['text'])[:limit]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Engine:
|
|
40
|
+
def __init__(self, units, vectors, embed_url, reranker, embedding_key='local-only', embedding_model='Qwen3-Embedding-4B'):
|
|
41
|
+
self.units, self.vectors = units, vectors
|
|
42
|
+
self.embed_url, self.reranker = embed_url, reranker
|
|
43
|
+
self.embedding_key = embedding_key
|
|
44
|
+
self.embedding_model = embedding_model
|
|
45
|
+
self.encoding = tiktoken.get_encoding('cl100k_base')
|
|
46
|
+
self.postings = defaultdict(list)
|
|
47
|
+
lengths = []
|
|
48
|
+
for u in units:
|
|
49
|
+
counts = Counter(terms((u['path'] + ' ' + u['name'] + ' ') * 3 + u['text']))
|
|
50
|
+
lengths.append(sum(counts.values()))
|
|
51
|
+
for term, frequency in counts.items():
|
|
52
|
+
self.postings[term].append((u['id'], frequency))
|
|
53
|
+
self.lengths = np.asarray(lengths)
|
|
54
|
+
self.average_length = max(1, self.lengths.mean())
|
|
55
|
+
self.incoming = defaultdict(list)
|
|
56
|
+
for u in units:
|
|
57
|
+
for target in u['edges']:
|
|
58
|
+
self.incoming[target].append(u['id'])
|
|
59
|
+
self.costs = [len(self.encoding.encode(self.render(u))) + 2 for u in units]
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def render(u):
|
|
63
|
+
# Text units preserve Unicode separators inside a physical source line.
|
|
64
|
+
lines = u['text'].split('\n') if u.get('language') in {'text', 'javascript', 'typescript', 'go'} else u['text'].splitlines()
|
|
65
|
+
return f"Path: {u['path']}\n" + '\n'.join(f'{i}\t{line}' for i, line in
|
|
66
|
+
enumerate(lines, u['start'])) + '\n'
|
|
67
|
+
|
|
68
|
+
def lexical(self, query):
|
|
69
|
+
scores = np.zeros(len(self.units))
|
|
70
|
+
for term in set(terms(query)):
|
|
71
|
+
matches = self.postings.get(term, [])
|
|
72
|
+
if not matches:
|
|
73
|
+
continue
|
|
74
|
+
ids, tf = np.asarray(matches).T
|
|
75
|
+
idf = math.log(1 + (len(self.units) - len(ids) + .5) / (len(ids) + .5))
|
|
76
|
+
scores[ids] += idf * tf * 2.2 / (tf + 1.2 * (.25 + .75 * self.lengths[ids] / self.average_length))
|
|
77
|
+
return scores
|
|
78
|
+
|
|
79
|
+
def rerank(self, query, ids):
|
|
80
|
+
if not ids:
|
|
81
|
+
return {}
|
|
82
|
+
data = post(self.reranker['baseUrl'] + '/rerank', {'model': self.reranker['model'],
|
|
83
|
+
'query': query, 'documents': [document(self.units[i], 5000) for i in ids]}, self.reranker['apiKey'])
|
|
84
|
+
if len(data['results']) != len(ids):
|
|
85
|
+
raise ValueError('Incomplete rerank response')
|
|
86
|
+
return {ids[r['index']]: r['relevance_score'] for r in data['results']}
|
|
87
|
+
|
|
88
|
+
def search(self, plan, budget=4000):
|
|
89
|
+
start = time.monotonic()
|
|
90
|
+
facets = plan['facets']
|
|
91
|
+
queries = [plan['intent']] + [f['question'] for f in facets]
|
|
92
|
+
embedded = post(self.embed_url + '/embeddings', {'model': self.embedding_model,
|
|
93
|
+
'input': ['Instruct: Retrieve source code implementing the requested behavior.\nQuery: ' + q for q in queries]}, self.embedding_key)
|
|
94
|
+
qvectors = np.asarray([r['embedding'] for r in sorted(embedded['data'], key=lambda r: r['index'])], dtype=np.float32)
|
|
95
|
+
dense = self.vectors @ qvectors.T
|
|
96
|
+
pools, fused = [], defaultdict(float)
|
|
97
|
+
for col, query in enumerate(queries):
|
|
98
|
+
lexical = self.lexical(query + (' ' + ' '.join(facets[col-1]['terms']) if col else ''))
|
|
99
|
+
local = defaultdict(float)
|
|
100
|
+
for scores in (dense[:, col], lexical):
|
|
101
|
+
for rank, uid in enumerate(np.argsort(-scores)[:40]):
|
|
102
|
+
if scores[uid] > 0:
|
|
103
|
+
local[int(uid)] += 1 / (30 + rank)
|
|
104
|
+
pool = sorted(local, key=local.get, reverse=True)[:28]
|
|
105
|
+
pools.append(pool)
|
|
106
|
+
for uid, score in local.items():
|
|
107
|
+
fused[uid] += score
|
|
108
|
+
facet_scores = []
|
|
109
|
+
for query, pool in zip(queries, pools):
|
|
110
|
+
facet_scores.append(self.rerank(query, pool))
|
|
111
|
+
seeds = set()
|
|
112
|
+
for scores in facet_scores:
|
|
113
|
+
seeds.update(sorted(scores, key=scores.get, reverse=True)[:3])
|
|
114
|
+
candidates = set().union(*(set(s) for s in facet_scores))
|
|
115
|
+
expanded = set()
|
|
116
|
+
for uid in seeds:
|
|
117
|
+
# Only statically resolved links. Dynamic receivers are not guessed.
|
|
118
|
+
neighbors = self.units[uid]['edges'] + self.incoming[uid]
|
|
119
|
+
expanded.update(sorted(set(neighbors), key=lambda x: fused[x], reverse=True)[:10])
|
|
120
|
+
candidates.update(expanded)
|
|
121
|
+
ranked = sorted(candidates, key=lambda uid: max(s.get(uid, 0) for s in facet_scores) + min(.15, fused[uid]), reverse=True)
|
|
122
|
+
retained = ranked[:80]
|
|
123
|
+
# Ensure graph-only discoveries get a chance in the joint rerank.
|
|
124
|
+
retained = list(dict.fromkeys(retained[:64] + sorted(expanded, key=lambda x: fused[x], reverse=True)[:16]))
|
|
125
|
+
overall = self.rerank(plan['intent'], retained)
|
|
126
|
+
# Score newly discovered graph spans against each facet, too.
|
|
127
|
+
for col, facet in enumerate(facets, 1):
|
|
128
|
+
missing = [uid for uid in retained if uid not in facet_scores[col]]
|
|
129
|
+
facet_scores[col].update(self.rerank(facet['question'], missing))
|
|
130
|
+
covered = np.zeros(len(facets))
|
|
131
|
+
selected, spent, trace = [], 0, []
|
|
132
|
+
available = set(retained)
|
|
133
|
+
while available:
|
|
134
|
+
choices = []
|
|
135
|
+
for uid in available:
|
|
136
|
+
if spent + self.costs[uid] > budget:
|
|
137
|
+
continue
|
|
138
|
+
values = np.asarray([s.get(uid, 0) for s in facet_scores[1:]])
|
|
139
|
+
# Diminishing coverage gain keeps later stages from being crowded out.
|
|
140
|
+
gain = float(np.sum(values / (1 + covered))) / len(facets)
|
|
141
|
+
gain = .7 * gain + .3 * overall.get(uid, 0)
|
|
142
|
+
gain /= (max(120, self.costs[uid]) / 300) ** .35
|
|
143
|
+
choices.append((gain, uid, values))
|
|
144
|
+
if not choices:
|
|
145
|
+
break
|
|
146
|
+
gain, uid, values = max(choices, key=lambda item: (item[0], -item[1]))
|
|
147
|
+
if gain < .015:
|
|
148
|
+
break
|
|
149
|
+
available.remove(uid)
|
|
150
|
+
selected.append(uid)
|
|
151
|
+
spent += self.costs[uid]
|
|
152
|
+
covered += values
|
|
153
|
+
trace.append({'id': uid, 'overall': overall.get(uid, 0), 'facets': values.tolist(),
|
|
154
|
+
'tokens': self.costs[uid], 'gain': gain, 'graphExpanded': uid in expanded})
|
|
155
|
+
raw = '\n'.join(self.render(self.units[uid]) for uid in selected)
|
|
156
|
+
return raw, {'elapsedMs': round((time.monotonic()-start)*1000), 'tokens': len(self.encoding.encode(raw)),
|
|
157
|
+
'candidateCount': len(candidates), 'rerankedCount': len(retained), 'expandedCount': len(expanded),
|
|
158
|
+
'selected': trace, 'plan': plan}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def build_index(root, snapshot, state, embed_url):
|
|
162
|
+
state = Path(state)
|
|
163
|
+
state.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
adapters = adapter_manifest(snapshot['files'], snapshot.get('languageOptions'))
|
|
165
|
+
identity = hashlib.sha256((VERSION + json.dumps(snapshot, sort_keys=True)
|
|
166
|
+
+ json.dumps(adapters, sort_keys=True)).encode()).hexdigest()
|
|
167
|
+
metadata = state / 'metadata.json'
|
|
168
|
+
if metadata.exists() and json.loads(metadata.read_text())['identity'] == identity:
|
|
169
|
+
return json.loads((state / 'units.json').read_text()), np.load(state / 'vectors.npy'), {
|
|
170
|
+
**json.loads(metadata.read_text()), 'cacheHit': True}
|
|
171
|
+
start = time.monotonic()
|
|
172
|
+
selection = {}
|
|
173
|
+
units = source_units(root, snapshot['files'], language_options=snapshot.get('languageOptions'), report=selection)
|
|
174
|
+
if not units:
|
|
175
|
+
raise ValueError('No indexable nonempty source text in snapshot')
|
|
176
|
+
vectors = []
|
|
177
|
+
for offset in range(0, len(units), 64):
|
|
178
|
+
result = post(embed_url + '/embeddings', {'model': 'Qwen3-Embedding-4B',
|
|
179
|
+
'input': [document(u) for u in units[offset:offset+64]]}, timeout=300)
|
|
180
|
+
rows = sorted(result['data'], key=lambda r: r['index'])
|
|
181
|
+
vectors.extend(r['embedding'] for r in rows)
|
|
182
|
+
if offset % 512 == 0:
|
|
183
|
+
print(json.dumps({'stage': 'indexing', 'done': len(vectors), 'total': len(units)}), flush=True)
|
|
184
|
+
matrix = np.asarray(vectors, dtype=np.float32)
|
|
185
|
+
if matrix.shape != (len(units), 1024) or not np.isfinite(matrix).all():
|
|
186
|
+
raise ValueError('Invalid index vectors')
|
|
187
|
+
(state / 'units.json').write_text(json.dumps(units))
|
|
188
|
+
np.save(state / 'vectors.npy', matrix)
|
|
189
|
+
summary = {'identity': identity, 'version': VERSION, 'units': len(units),
|
|
190
|
+
'files': len(snapshot['files']), 'nonemptyFiles': len({u['path'] for u in units}),
|
|
191
|
+
'selection': selection,
|
|
192
|
+
'languageAdapters': adapters, 'languageUnits': dict(Counter(u['language'] for u in units)),
|
|
193
|
+
'resolvedEdges': sum(len(u['edges']) for u in units), 'indexingMs': round((time.monotonic()-start)*1000), 'cacheHit': False}
|
|
194
|
+
metadata.write_text(json.dumps(summary, indent=2))
|
|
195
|
+
return units, matrix, summary
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Retrieve and assemble source entities, retaining their constituent spans.
|
|
2
|
+
|
|
3
|
+
Each entity has multiple stored vectors (one per source span). Cheap maximum
|
|
4
|
+
interaction with query facets locates entities; a single neural ranking stage
|
|
5
|
+
scores their representative code. Static links then supply bounded evidence
|
|
6
|
+
completion without another model call. No task-specific symbols or labels.
|
|
7
|
+
"""
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
import math
|
|
10
|
+
import time
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from engine import Engine, post
|
|
14
|
+
|
|
15
|
+
VERSION = 'entity-cascade-v3'
|
|
16
|
+
POLICY = {'candidateEntities': 80, 'perFacetEntities': 32, 'anchorEntities': 8,
|
|
17
|
+
'graphHops': 2, 'maxAssembledEntities': 128, 'maxAtomicBudgetFraction': .65,
|
|
18
|
+
'facetTemperature': .06, 'documentChars': 5000}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EntityEngine(Engine):
|
|
22
|
+
def __init__(self, *args, **kwargs):
|
|
23
|
+
super().__init__(*args, **kwargs)
|
|
24
|
+
groups = defaultdict(list)
|
|
25
|
+
for u in self.units:
|
|
26
|
+
groups[(u['symbol'], u['kind'])].append(u['id'])
|
|
27
|
+
self.entities = []
|
|
28
|
+
self.unit_entity = np.zeros(len(self.units), dtype=np.int32)
|
|
29
|
+
for (symbol, kind), ids in groups.items():
|
|
30
|
+
eid = len(self.entities)
|
|
31
|
+
ids.sort(key=lambda i: self.units[i]['start'])
|
|
32
|
+
self.entities.append({'symbol': symbol, 'kind': kind, 'ids': ids,
|
|
33
|
+
'cost': sum(self.costs[uid] for uid in ids)})
|
|
34
|
+
self.unit_entity[ids] = eid
|
|
35
|
+
self.links = [dict() for _ in self.entities]
|
|
36
|
+
for u in self.units:
|
|
37
|
+
source = int(self.unit_entity[u['id']])
|
|
38
|
+
for target_unit in u['edges']:
|
|
39
|
+
target = int(self.unit_entity[target_unit])
|
|
40
|
+
if source == target:
|
|
41
|
+
continue
|
|
42
|
+
other = self.units[target_unit]
|
|
43
|
+
# Parent context is weaker than a statically resolved callee.
|
|
44
|
+
forward = .45 if u['owner'] == other['symbol'] else .85
|
|
45
|
+
reverse = .65 if u['owner'] == other['symbol'] else .55
|
|
46
|
+
self.links[source][target] = max(self.links[source].get(target, 0), forward)
|
|
47
|
+
self.links[target][source] = max(self.links[target].get(source, 0), reverse)
|
|
48
|
+
|
|
49
|
+
def aggregate(self, unit_scores):
|
|
50
|
+
shape = (len(self.entities),) + unit_scores.shape[1:]
|
|
51
|
+
scores = np.full(shape, -np.inf, dtype=np.float32)
|
|
52
|
+
np.maximum.at(scores, self.unit_entity, unit_scores)
|
|
53
|
+
return scores
|
|
54
|
+
|
|
55
|
+
def candidate_entities(self, queries, facets, dense):
|
|
56
|
+
fused, pools = defaultdict(float), []
|
|
57
|
+
for col, query in enumerate(queries):
|
|
58
|
+
lex = self.aggregate(self.lexical(query + (' ' + ' '.join(facets[col-1]['terms']) if col else '')))
|
|
59
|
+
local = defaultdict(float)
|
|
60
|
+
for scores in [dense[:, col], lex]:
|
|
61
|
+
for rank, eid in enumerate(np.argsort(-scores)[:80]):
|
|
62
|
+
if scores[eid] > 0:
|
|
63
|
+
local[int(eid)] += 1 / (30 + rank)
|
|
64
|
+
ordered = sorted(local, key=lambda i: (-local[i], i))[:POLICY['perFacetEntities']]
|
|
65
|
+
pools.append(ordered)
|
|
66
|
+
for eid, score in local.items():
|
|
67
|
+
fused[eid] = max(fused[eid], score)
|
|
68
|
+
protected = list(dict.fromkeys(eid for pool in pools for eid in pool[:12]))
|
|
69
|
+
available = set(eid for pool in pools for eid in pool)
|
|
70
|
+
ordered = sorted(available, key=lambda i: (-fused[i], i))
|
|
71
|
+
return list(dict.fromkeys(protected + ordered))[:POLICY['candidateEntities']]
|
|
72
|
+
|
|
73
|
+
def representation(self, eid, unit_dense):
|
|
74
|
+
entity = self.entities[eid]
|
|
75
|
+
ids = entity['ids']
|
|
76
|
+
# Long functions are represented by their strongest matching code spans,
|
|
77
|
+
# not always their prefix. Every selected span remains source anchored.
|
|
78
|
+
ranked = sorted(ids, key=lambda i: (-float(unit_dense[i].max()), i))
|
|
79
|
+
header = self.units[ids[0]]
|
|
80
|
+
parts = [f"Path: {header['path']}\nSymbol: {header['name']}\n"]
|
|
81
|
+
remaining = POLICY['documentChars'] - len(parts[0])
|
|
82
|
+
for uid in ranked:
|
|
83
|
+
u = self.units[uid]
|
|
84
|
+
value = f"Lines {u['start']}-{u['end']}:\n{u['text']}\n"
|
|
85
|
+
parts.append(value[:remaining])
|
|
86
|
+
remaining -= min(len(value), remaining)
|
|
87
|
+
if remaining <= 0:
|
|
88
|
+
break
|
|
89
|
+
return ''.join(parts)
|
|
90
|
+
|
|
91
|
+
def complete(self, candidates, scores, dense):
|
|
92
|
+
expanded_scores = dict(scores)
|
|
93
|
+
provenance = {}
|
|
94
|
+
anchors = sorted(candidates, key=lambda i: (-scores[i], i))[:POLICY['anchorEntities']]
|
|
95
|
+
frontier = {eid: scores[eid] for eid in anchors if scores[eid] >= .15}
|
|
96
|
+
visited = set(frontier)
|
|
97
|
+
for hop in range(POLICY['graphHops']):
|
|
98
|
+
next_frontier = {}
|
|
99
|
+
for source, confidence in frontier.items():
|
|
100
|
+
neighbors = self.links[source]
|
|
101
|
+
# Rank by semantic compatibility before following broad class links.
|
|
102
|
+
ordered = sorted(neighbors, key=lambda i: (-float(dense[i].max()), i))[:16]
|
|
103
|
+
for target in ordered:
|
|
104
|
+
compatibility = float(np.exp(min(0, float(dense[target].max()-dense[source].max())) / .12))
|
|
105
|
+
proposal = confidence * neighbors[target] * (.4 + .6 * compatibility)
|
|
106
|
+
if proposal > expanded_scores.get(target, 0):
|
|
107
|
+
expanded_scores[target] = proposal
|
|
108
|
+
provenance[target] = {'anchor': source, 'hop': hop+1, 'kind': 'static-relationship', 'inferredScore': proposal}
|
|
109
|
+
if target not in visited and proposal > .05:
|
|
110
|
+
next_frontier[target] = max(next_frontier.get(target, 0), proposal)
|
|
111
|
+
visited.update(next_frontier)
|
|
112
|
+
frontier = next_frontier
|
|
113
|
+
selected = sorted(expanded_scores, key=lambda i: (-expanded_scores[i], i))[:POLICY['maxAssembledEntities']]
|
|
114
|
+
return selected, expanded_scores, provenance
|
|
115
|
+
|
|
116
|
+
def pack_entities(self, entities, scores, dense, unit_dense, budget):
|
|
117
|
+
affinity = np.exp(np.minimum(0, dense[:, 1:]-dense[:, 1:].max(axis=0)) / POLICY['facetTemperature'])
|
|
118
|
+
covered = np.zeros(affinity.shape[1])
|
|
119
|
+
actions = []
|
|
120
|
+
for eid in entities:
|
|
121
|
+
entity = self.entities[eid]
|
|
122
|
+
if entity['cost'] <= budget * POLICY['maxAtomicBudgetFraction']:
|
|
123
|
+
actions.append((eid, tuple(entity['ids'])))
|
|
124
|
+
else:
|
|
125
|
+
# Oversized entities remain accessible at their original AST spans.
|
|
126
|
+
actions.extend((eid, (uid,)) for uid in entity['ids'])
|
|
127
|
+
spent, selected, trace = 0, [], []
|
|
128
|
+
while actions:
|
|
129
|
+
best = None
|
|
130
|
+
for eid, ids in actions:
|
|
131
|
+
cost = sum(self.costs[i] for i in ids)
|
|
132
|
+
if spent+cost > budget:
|
|
133
|
+
continue
|
|
134
|
+
facet = affinity[eid]
|
|
135
|
+
if len(ids) == 1 and len(self.entities[eid]['ids']) > 1:
|
|
136
|
+
facet = np.exp(np.minimum(0, unit_dense[ids[0], 1:]-dense[:, 1:].max(axis=0)) / POLICY['facetTemperature'])
|
|
137
|
+
value = scores[eid] * (.35 + .65 * facet)
|
|
138
|
+
gain = .7 * float(np.mean(value/(1+covered))) + .3 * scores[eid]
|
|
139
|
+
gain /= (max(120, cost)/300) ** .35
|
|
140
|
+
choice = (gain, eid, ids, cost, value)
|
|
141
|
+
if best is None or gain > best[0]:
|
|
142
|
+
best = choice
|
|
143
|
+
if best is None or best[0] < .015:
|
|
144
|
+
break
|
|
145
|
+
gain, eid, ids, cost, value = best
|
|
146
|
+
actions.remove((eid, ids))
|
|
147
|
+
selected.extend(ids)
|
|
148
|
+
spent += cost
|
|
149
|
+
covered += value
|
|
150
|
+
trace.append({'entity': eid, 'ids': list(ids), 'symbol': self.entities[eid]['symbol'],
|
|
151
|
+
'tokens': cost, 'score': scores[eid], 'completeEntity': len(ids)==len(self.entities[eid]['ids'])})
|
|
152
|
+
return '\n'.join(self.render(self.units[i]) for i in selected), trace
|
|
153
|
+
|
|
154
|
+
def search(self, plan, budget=4000):
|
|
155
|
+
start = time.monotonic()
|
|
156
|
+
facets = plan['facets']
|
|
157
|
+
queries = [plan['intent']] + [f['question'] for f in facets]
|
|
158
|
+
unique = list(dict.fromkeys(queries))
|
|
159
|
+
result = post(self.embed_url+'/embeddings', {'model':'Qwen3-Embedding-4B',
|
|
160
|
+
'input':['Instruct: Retrieve source code implementing the requested behavior.\nQuery: '+q for q in unique]}, self.embedding_key)
|
|
161
|
+
vectors = [r['embedding'] for r in sorted(result['data'],key=lambda r:r['index'])]
|
|
162
|
+
embedded = time.monotonic()
|
|
163
|
+
unit_dense = self.vectors @ np.asarray([vectors[unique.index(q)] for q in queries],dtype=np.float32).T
|
|
164
|
+
dense = self.aggregate(unit_dense)
|
|
165
|
+
candidates = self.candidate_entities(queries,facets,dense)
|
|
166
|
+
documents = [self.representation(eid,unit_dense) for eid in candidates]
|
|
167
|
+
recalled = time.monotonic()
|
|
168
|
+
response = post(self.reranker['baseUrl']+'/rerank',{'model':self.reranker['model'],
|
|
169
|
+
'query':plan['intent'],'documents':documents},self.reranker['apiKey'])
|
|
170
|
+
rows = response['results']
|
|
171
|
+
if len(rows)!=len(candidates) or {r['index'] for r in rows}!=set(range(len(candidates))) or any(not math.isfinite(r['relevance_score']) or not 0<=r['relevance_score']<=1 for r in rows):
|
|
172
|
+
raise ValueError('Invalid rerank response')
|
|
173
|
+
scores = {candidates[r['index']]:r['relevance_score'] for r in rows}
|
|
174
|
+
ranked = time.monotonic()
|
|
175
|
+
assembled, expanded_scores, provenance = self.complete(candidates,scores,dense)
|
|
176
|
+
raw,selected = self.pack_entities(assembled,expanded_scores,dense,unit_dense,budget)
|
|
177
|
+
finished = time.monotonic()
|
|
178
|
+
return raw,{'version':VERSION,'elapsedMs':round((finished-start)*1000),'tokens':len(self.encoding.encode(raw)),
|
|
179
|
+
'queryCache':False,'modelRequests':{'embedding':1,'rerank':1},'candidateCount':len(candidates),
|
|
180
|
+
'expandedCount':len(provenance),'assembledCount':len(assembled),'policy':POLICY,'plan':plan,
|
|
181
|
+
'timingMs':{'embedding':round((embedded-start)*1000),'recall':round((recalled-embedded)*1000),
|
|
182
|
+
'rerank':round((ranked-recalled)*1000),'rerankModel':response.get('meta',{}).get('elapsed_ms'),
|
|
183
|
+
'selection':round((finished-ranked)*1000)},'rerankInputTokens':response.get('usage',{}).get('input_tokens'),
|
|
184
|
+
'candidates':[{'entity':eid,'ids':self.entities[eid]['ids'],'symbol':self.entities[eid]['symbol'],
|
|
185
|
+
'score':expanded_scores[eid],'source':'neural+graph' if eid in scores and eid in provenance else 'neural' if eid in scores else 'graph',
|
|
186
|
+
'spans':[{'path':self.units[i]['path'],'start':self.units[i]['start'],'end':self.units[i]['end']} for i in self.entities[eid]['ids']]} for eid in assembled],
|
|
187
|
+
'selected':selected,'provenance':provenance}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Snapshot-bound language adapters; downstream retrieval consumes only CodeUnit."""
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import python, typescript, text, go
|
|
10
|
+
from .files import path_exclusion, read_text
|
|
11
|
+
from .schema import SCHEMA_VERSION, SourceFile, validate_units
|
|
12
|
+
|
|
13
|
+
ADAPTERS = {'python': python, 'typescript': typescript, 'javascript': typescript, 'go': go, 'text': text}
|
|
14
|
+
EXTENSIONS = {'.py': 'python', '.ts': 'typescript', '.tsx': 'typescript',
|
|
15
|
+
'.mts': 'typescript', '.cts': 'typescript',
|
|
16
|
+
'.go': 'go', '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript'}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def language_for(path):
|
|
20
|
+
return EXTENSIONS.get(PurePosixPath(path).suffix.lower(), 'text')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def adapter_manifest(files, language_options=None):
|
|
24
|
+
languages = sorted({language_for(file['path']) for file in files})
|
|
25
|
+
options = language_options or {}
|
|
26
|
+
if not isinstance(options, dict) or set(options) - set(ADAPTERS):
|
|
27
|
+
raise ValueError('Unknown language options')
|
|
28
|
+
paths = [Path(__file__), Path(__file__).with_name('schema.py'), Path(__file__).with_name('files.py')]
|
|
29
|
+
for language in languages:
|
|
30
|
+
paths.append(Path(ADAPTERS[language].__file__))
|
|
31
|
+
if language in {'typescript', 'javascript'}:
|
|
32
|
+
paths.append(Path(typescript.__file__).with_suffix('.mjs'))
|
|
33
|
+
if language == 'go':
|
|
34
|
+
paths.append(Path(go.__file__).with_name('go_ast.go'))
|
|
35
|
+
paths.append(Path(go.__file__).with_name('go_types.go'))
|
|
36
|
+
return {'schemaVersion': SCHEMA_VERSION, 'languages': languages, 'options': options,
|
|
37
|
+
'parsers': {language: (f'python-ast-{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'
|
|
38
|
+
if language == 'python' else f'typescript-{typescript.COMPILER_VERSION}'
|
|
39
|
+
if language in {'typescript', 'javascript'} else go.compiler()[1]
|
|
40
|
+
if language == 'go' else text.VERSION)
|
|
41
|
+
for language in languages},
|
|
42
|
+
'sourceSha256': {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in paths}}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def source_units(root, files, max_lines=65, language_options=None, report=None, cache=None):
|
|
46
|
+
if type(max_lines) is not int or max_lines < 1:
|
|
47
|
+
raise ValueError('max_lines must be a positive integer')
|
|
48
|
+
root = Path(root).resolve()
|
|
49
|
+
groups, sources, seen, excluded = defaultdict(list), [], set(), []
|
|
50
|
+
options = language_options or {}
|
|
51
|
+
adapter_manifest(files, options) # Validate configuration before reading source.
|
|
52
|
+
for file in files:
|
|
53
|
+
name = file['path']
|
|
54
|
+
path = PurePosixPath(name)
|
|
55
|
+
if (path.is_absolute() or '..' in path.parts or '\\' in name or str(path) != name
|
|
56
|
+
or name in seen):
|
|
57
|
+
raise ValueError('Invalid or duplicate snapshot path: ' + name)
|
|
58
|
+
seen.add(name)
|
|
59
|
+
resolved = (root/name).resolve()
|
|
60
|
+
if not resolved.is_relative_to(root):
|
|
61
|
+
raise ValueError('Source escapes snapshot root: ' + name)
|
|
62
|
+
reason = path_exclusion(name)
|
|
63
|
+
if (root/name).is_symlink():
|
|
64
|
+
reason = 'symlink'
|
|
65
|
+
if reason:
|
|
66
|
+
excluded.append({'path': name, 'reason': reason})
|
|
67
|
+
continue
|
|
68
|
+
raw, content, reason = read_text(resolved)
|
|
69
|
+
if reason:
|
|
70
|
+
excluded.append({'path': name, 'reason': reason})
|
|
71
|
+
continue
|
|
72
|
+
if hashlib.sha256(raw).hexdigest() != file['sha256']:
|
|
73
|
+
raise ValueError('Source changed: ' + name)
|
|
74
|
+
source = SourceFile(name, content, file['sha256'])
|
|
75
|
+
sources.append(source)
|
|
76
|
+
language = language_for(name)
|
|
77
|
+
groups['typescript' if language == 'javascript' else language].append(source)
|
|
78
|
+
by_path = defaultdict(list)
|
|
79
|
+
identities = {}
|
|
80
|
+
pending_cache = {}
|
|
81
|
+
for language, subset in groups.items():
|
|
82
|
+
settings = options.get(language)
|
|
83
|
+
if language == 'go':
|
|
84
|
+
settings = go.settings(settings, sources)
|
|
85
|
+
if language == 'typescript' and 'javascript' in options:
|
|
86
|
+
if settings is not None and settings != options['javascript']:
|
|
87
|
+
raise ValueError('JavaScript and TypeScript share compiler options')
|
|
88
|
+
settings = options['javascript']
|
|
89
|
+
# Bound cache lifetime to the current snapshot. Structural languages are
|
|
90
|
+
# invalidated together so imports and callers in unchanged files refresh.
|
|
91
|
+
batches = [[source] for source in subset] if cache is not None and language == 'text' else [subset]
|
|
92
|
+
units = []
|
|
93
|
+
for batch in batches:
|
|
94
|
+
key = (language, batch[0].path if language == 'text' else '')
|
|
95
|
+
fingerprint = (hashlib.sha256(json.dumps([
|
|
96
|
+
[(s.path, s.sha256) for s in batch], max_lines, settings,
|
|
97
|
+
adapter_manifest([{'path': s.path} for s in batch], options),
|
|
98
|
+
], sort_keys=True).encode()).hexdigest() if cache is not None else None)
|
|
99
|
+
previous = cache.get(key) if cache is not None else None
|
|
100
|
+
extracted = (deepcopy(previous[1]) if previous and previous[0] == fingerprint
|
|
101
|
+
else ADAPTERS[language].extract(batch, max_lines, settings))
|
|
102
|
+
if cache is not None:
|
|
103
|
+
pending_cache[key] = (fingerprint, deepcopy(extracted))
|
|
104
|
+
offset = len(units)
|
|
105
|
+
for unit in extracted:
|
|
106
|
+
unit['id'] += offset
|
|
107
|
+
for relation in unit['relations']:
|
|
108
|
+
relation['target'] += offset
|
|
109
|
+
unit['edges'] = [target + offset for target in unit['edges']]
|
|
110
|
+
units.extend(extracted)
|
|
111
|
+
validate_units(units, subset)
|
|
112
|
+
for unit in units:
|
|
113
|
+
by_path[unit['path']].append(unit)
|
|
114
|
+
identities[id(unit)] = (language, unit['id'])
|
|
115
|
+
units = [unit for source in sources for unit in by_path[source.path]]
|
|
116
|
+
remap = {identities[id(unit)]: i for i, unit in enumerate(units)}
|
|
117
|
+
for i, unit in enumerate(units):
|
|
118
|
+
unit['id'] = i
|
|
119
|
+
for relation in unit['relations']:
|
|
120
|
+
relation['target'] = remap[(identities[id(unit)][0], relation['target'])]
|
|
121
|
+
unit['edges'] = sorted({relation['target'] for relation in unit['relations']})
|
|
122
|
+
validate_units(units, sources)
|
|
123
|
+
if cache is not None:
|
|
124
|
+
cache.clear()
|
|
125
|
+
cache.update(pending_cache)
|
|
126
|
+
if report is not None:
|
|
127
|
+
report.update(inputFiles=len(files), acceptedFiles=len(sources), excluded=excluded,
|
|
128
|
+
fallbackFiles=sum(language_for(source.path) == 'text' for source in sources))
|
|
129
|
+
return units
|