@hupan56/wlkj 3.3.14 → 3.3.15
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/bin/cli.js +16 -1
- package/package.json +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +4 -2
- package/templates/qoder/commands/wl-commit.md +3 -1
- package/templates/qoder/commands/wl-init.md +2 -2
- package/templates/qoder/commands/wl-prd.md +24 -0
- package/templates/qoder/commands/wl-search.md +18 -11
- package/templates/qoder/commands/wl-task.md +3 -1
- package/templates/qoder/config.yaml +0 -6
- package/templates/qoder/contracts/insight.md +55 -0
- package/templates/qoder/hooks/pre-tool-use-commit.py +124 -0
- package/templates/qoder/hooks/session-start.py +20 -1
- package/templates/qoder/hooks/stop-eval.py +129 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +9 -1
- package/templates/qoder/scripts/capability/registry.py +0 -3
- package/templates/qoder/scripts/capability/registry_mcp.py +10 -4
- package/templates/qoder/scripts/deployment/setup/init_doctor.py +6 -2
- package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +7 -7
- package/templates/qoder/scripts/deployment/setup/setup.py +5 -2
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +4 -2
- package/templates/qoder/scripts/domain/integration/spec_upload.py +4 -4
- package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
- package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +41 -5
- package/templates/qoder/scripts/domain/kg/kg.py +14 -0
- package/templates/qoder/scripts/domain/kg/switch_project.py +2 -2
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +169 -14
- package/templates/qoder/scripts/foundation/io/context_cache.py +94 -0
- package/templates/qoder/scripts/orchestration/wlkj.py +106 -0
- package/templates/qoder/scripts/tool_guide.md +70 -0
- package/templates/qoder/scripts/validation/eval/alignment_matrix.py +176 -0
- package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +110 -0
- package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +104 -0
- package/templates/qoder/scripts/validation/eval/bf_score.py +218 -0
- package/templates/qoder/scripts/validation/eval/code_flywheel.py +150 -0
- package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +156 -0
- package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +23 -0
- package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +143 -0
- package/templates/qoder/scripts/validation/eval/gradient_matrix.py +261 -0
- package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +33 -0
- package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +105 -0
- package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +118 -0
- package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +128 -0
- package/templates/qoder/scripts/validation/eval/prd_flywheel.py +148 -0
- package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +166 -0
- package/templates/qoder/scripts/validation/eval/recall_flywheel.py +148 -0
- package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +139 -0
- package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +188 -0
- package/templates/qoder/scripts/validation/eval/task_flywheel.py +124 -0
- package/templates/qoder/scripts/validation/eval/token_flywheel.py +88 -0
- package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +177 -0
- package/templates/qoder/scripts/validation/metrics/lint_cases.py +170 -0
- package/templates/qoder/scripts/validation/test/test_context_cache.py +78 -0
- package/templates/qoder/scripts/validation/test/test_lint_cases.py +60 -0
- package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +70 -0
- package/templates/qoder/settings.json +8 -0
- package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
- package/templates/qoder/agents/design-agent.md +0 -20
- package/templates/qoder/agents/spec-generator.md +0 -21
- package/templates/qoder/scripts/capability/smoke_test_report.json.new +0 -94
- package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +0 -10
- package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +0 -57
- package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +0 -62
- package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +0 -146
- package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +0 -64
- package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +0 -52
- package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +0 -104
- package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +0 -55
- package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +0 -95
- package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +0 -8
- package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +0 -22
- package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +0 -55
- package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +0 -90
- package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +0 -9
- package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +0 -206
- package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +0 -132
- package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +0 -80
- package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +0 -59
- package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +0 -93
- package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +0 -421
- package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +0 -9
- package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +0 -79
- package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +0 -99
- package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +0 -69
- package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +0 -78
- package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +0 -8
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +0 -105
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +0 -153
- package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +0 -120
- package/templates/qoder/scripts/domain/kg/graph/kg_link_db.py +0 -235
- package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +0 -238
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +0 -197
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +0 -109
- package/templates/qoder/scripts/engine/poller.py +0 -219
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""命名同源匹配: 不依赖 embedding, 用命名规则建弱关联。
|
|
3
|
-
|
|
4
|
-
AssetController ↔ asset 表 ↔ 资产管理 PRD → 词干相同 → 弱边。
|
|
5
|
-
这是 embedding 不可用时的主路径, 也是 embedding 的补充 (硬规则置信度高)。
|
|
6
|
-
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
import re
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def extract_stem(name: str) -> str:
|
|
12
|
-
"""提取名称的词干 (去掉类型后缀 + 小写 + 驼峰拆词)。
|
|
13
|
-
|
|
14
|
-
AssetController → 'asset'
|
|
15
|
-
EmployeeOnline → 'employee online' → 'employee'
|
|
16
|
-
IQrCodeLoginService → 'qr code login'
|
|
17
|
-
test_demo → 'test demo'
|
|
18
|
-
资产管理 → '资产管理'
|
|
19
|
-
"""
|
|
20
|
-
if not name:
|
|
21
|
-
return ''
|
|
22
|
-
s = name.strip()
|
|
23
|
-
# 去掉常见类型后缀
|
|
24
|
-
for suffix in ['Controller', 'ServiceImpl', 'Service', 'Mapper',
|
|
25
|
-
'Repository', 'Entity', 'Vo', 'BO', 'Bo', 'Dto', 'DTO',
|
|
26
|
-
'Form', 'Body', 'Request', 'Response', 'Result']:
|
|
27
|
-
if s.endswith(suffix) and len(s) > len(suffix):
|
|
28
|
-
s = s[:-len(suffix)]
|
|
29
|
-
break
|
|
30
|
-
# 去掉接口前缀 I
|
|
31
|
-
if s.startswith('I') and len(s) > 1 and s[1].isupper():
|
|
32
|
-
s = s[1:]
|
|
33
|
-
# 驼峰拆词
|
|
34
|
-
words = re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)|[\u4e00-\u9fff]+', s)
|
|
35
|
-
if not words:
|
|
36
|
-
return s.lower()
|
|
37
|
-
# 中文直接返回, 英文取第一个词 (主词)
|
|
38
|
-
if any('\u4e00' <= c <= '\u9fff' for c in s):
|
|
39
|
-
return ''.join(w for w in words if '\u4e00' <= w[0] <= '\u9fff')[:6]
|
|
40
|
-
return words[0].lower() if words else s.lower()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def naming_similarity(name_a: str, name_b: str) -> float:
|
|
44
|
-
"""两个名称的命名同源相似度 → [0, 1]。
|
|
45
|
-
|
|
46
|
-
词干相同 → 0.8
|
|
47
|
-
词干包含关系 → 0.6
|
|
48
|
-
无关 → 0
|
|
49
|
-
"""
|
|
50
|
-
stem_a = extract_stem(name_a)
|
|
51
|
-
stem_b = extract_stem(name_b)
|
|
52
|
-
if not stem_a or not stem_b:
|
|
53
|
-
return 0.0
|
|
54
|
-
if stem_a == stem_b:
|
|
55
|
-
return 0.8
|
|
56
|
-
if stem_a in stem_b or stem_b in stem_a:
|
|
57
|
-
return 0.6
|
|
58
|
-
# 词重叠 (多词情况)
|
|
59
|
-
words_a = set(stem_a.split())
|
|
60
|
-
words_b = set(stem_b.split())
|
|
61
|
-
if words_a and words_b:
|
|
62
|
-
overlap = len(words_a & words_b)
|
|
63
|
-
if overlap > 0:
|
|
64
|
-
return 0.5 * overlap / max(len(words_a), len(words_b))
|
|
65
|
-
return 0.0
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def match_to_anchors(entity_canonical: str, anchors: list) -> list:
|
|
69
|
-
"""把实体和 anchors 做命名匹配 → [(anchor_id, score)]。
|
|
70
|
-
|
|
71
|
-
anchors: [{'id':..., 'title':..., 'kind':...}]
|
|
72
|
-
Returns: 按相似度降序, score >= 0.6 的
|
|
73
|
-
"""
|
|
74
|
-
out = []
|
|
75
|
-
for a in anchors:
|
|
76
|
-
score = naming_similarity(entity_canonical, a.get('title', '') or a.get('id', ''))
|
|
77
|
-
if score >= 0.6:
|
|
78
|
-
out.append((a['id'], score))
|
|
79
|
-
out.sort(key=lambda x: x[1], reverse=True)
|
|
80
|
-
return out
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""弱边升格: 出现强关联时, 对应弱边自动删除。
|
|
3
|
-
|
|
4
|
-
避免同一对 (entity, anchor) 既有强边又有弱边, 强边优先。
|
|
5
|
-
"""
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
import os
|
|
8
|
-
import sys
|
|
9
|
-
|
|
10
|
-
_THIS = os.path.dirname(os.path.abspath(__file__))
|
|
11
|
-
for _i in range(8):
|
|
12
|
-
_p = os.path.dirname(_THIS)
|
|
13
|
-
if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
|
|
14
|
-
sys.path.insert(0, _p); break
|
|
15
|
-
_THIS = _p
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _get_engine():
|
|
19
|
-
from domain.kg.extract.java import pg_upsert
|
|
20
|
-
return pg_upsert._get_pg_engine()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def promote_strong_over_weak(logger=print):
|
|
24
|
-
"""删除有强关联的实体的弱边。
|
|
25
|
-
|
|
26
|
-
逻辑: entity 如果有 source='explicit'/'commit' 的 implements_req 边,
|
|
27
|
-
则该 entity 的所有 source='inferred' 的 relates_to 边删除 (被强边替代)。
|
|
28
|
-
|
|
29
|
-
Returns: {deleted}
|
|
30
|
-
"""
|
|
31
|
-
engine, _ = _get_engine()
|
|
32
|
-
if engine is None:
|
|
33
|
-
return {'deleted': 0}
|
|
34
|
-
from sqlalchemy import text
|
|
35
|
-
|
|
36
|
-
deleted = 0
|
|
37
|
-
with engine.begin() as conn:
|
|
38
|
-
# 找有强关联的实体
|
|
39
|
-
strong_entities = conn.execute(text("""
|
|
40
|
-
SELECT DISTINCT from_id FROM edges
|
|
41
|
-
WHERE source IN ('explicit', 'commit')
|
|
42
|
-
AND edge_type IN ('implements_req', 'anchors')
|
|
43
|
-
""")).all()
|
|
44
|
-
strong_ids = [r[0] for r in strong_entities]
|
|
45
|
-
# 也包括 prd: 锚点本身 (它们的实体不该有弱边)
|
|
46
|
-
if strong_ids:
|
|
47
|
-
# 删除这些实体的弱边
|
|
48
|
-
result = conn.execute(text("""
|
|
49
|
-
DELETE FROM edges
|
|
50
|
-
WHERE source='inferred' AND from_id = ANY(:ids)
|
|
51
|
-
"""), {'ids': strong_ids})
|
|
52
|
-
deleted = result.rowcount or 0
|
|
53
|
-
|
|
54
|
-
logger(' [promote] 删除 %d 条冗余弱边 (升格到强关联)' % deleted)
|
|
55
|
-
return {'deleted': deleted}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if __name__ == '__main__':
|
|
59
|
-
print(promote_strong_over_weak())
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""C 方案指纹重算: 过期弱边对比指纹 → 续期/重推理/删除。
|
|
3
|
-
|
|
4
|
-
每天 celery 定时跑:
|
|
5
|
-
1. 找 expires_at < now 的弱边
|
|
6
|
-
2. 对比 fingerprint: 没变 → 续期; 变了 → 删除重推理
|
|
7
|
-
3. 升格: 弱边的 from 出现强关联 → 弱边删除 (在 promote.py)
|
|
8
|
-
"""
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
import os
|
|
11
|
-
import sys
|
|
12
|
-
from datetime import datetime
|
|
13
|
-
|
|
14
|
-
_THIS = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
-
for _i in range(8):
|
|
16
|
-
_p = os.path.dirname(_THIS)
|
|
17
|
-
if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
|
|
18
|
-
sys.path.insert(0, _p); break
|
|
19
|
-
_THIS = _p
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _get_engine():
|
|
23
|
-
from domain.kg.extract.java import pg_upsert
|
|
24
|
-
return pg_upsert._get_pg_engine()
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def recompute_expired(logger=print):
|
|
28
|
-
"""重算所有过期弱边 (C 方案主入口)。
|
|
29
|
-
|
|
30
|
-
Returns: {expired, renewed, deleted, errors}
|
|
31
|
-
"""
|
|
32
|
-
engine, _ = _get_engine()
|
|
33
|
-
if engine is None:
|
|
34
|
-
return {'error': 'PG 不可用'}
|
|
35
|
-
from sqlalchemy import text
|
|
36
|
-
|
|
37
|
-
now = datetime.utcnow().isoformat()
|
|
38
|
-
stats = {'expired': 0, 'renewed': 0, 'deleted': 0, 'errors': 0}
|
|
39
|
-
|
|
40
|
-
with engine.begin() as conn:
|
|
41
|
-
# 找过期弱边
|
|
42
|
-
rows = conn.execute(text("""
|
|
43
|
-
SELECT from_id, to_id, fingerprint FROM edges
|
|
44
|
-
WHERE source='inferred' AND expires_at IS NOT NULL
|
|
45
|
-
AND expires_at < CAST(:now AS TIMESTAMPTZ)
|
|
46
|
-
LIMIT 200
|
|
47
|
-
"""), {'now': now}).all()
|
|
48
|
-
stats['expired'] = len(rows)
|
|
49
|
-
logger(' [recompute] %d 条过期弱边' % len(rows))
|
|
50
|
-
|
|
51
|
-
for from_id, to_id, fingerprint in rows:
|
|
52
|
-
try:
|
|
53
|
-
# 对比当前指纹: 用和建边时相同的 _get_file_fingerprint 函数算
|
|
54
|
-
# (保证一致性: file_signatures 真实 hash 优先, 无 file 时 canonical hash 兜底)
|
|
55
|
-
ent_row = conn.execute(text("""
|
|
56
|
-
SELECT id, repo_id, canonical, cn, props FROM entities WHERE id=:id
|
|
57
|
-
"""), {'id': from_id}).first()
|
|
58
|
-
if not ent_row:
|
|
59
|
-
# 实体已删 → 删弱边
|
|
60
|
-
conn.execute(text("DELETE FROM edges WHERE from_id=:f AND to_id=:t AND source='inferred'"),
|
|
61
|
-
{'f': from_id, 't': to_id})
|
|
62
|
-
stats['deleted'] += 1
|
|
63
|
-
continue
|
|
64
|
-
ent = {'id': ent_row[0], 'repo_id': ent_row[1], 'canonical': ent_row[2],
|
|
65
|
-
'cn': ent_row[3], 'props': ent_row[4] or {}}
|
|
66
|
-
# 用 weak_link 的统一 fingerprint 函数 (和建边时一致)
|
|
67
|
-
from domain.kg.extract.inference.weak_link import _get_file_fingerprint
|
|
68
|
-
cur_fp = _get_file_fingerprint(ent)
|
|
69
|
-
|
|
70
|
-
if cur_fp and fingerprint and str(cur_fp)[:16] == str(fingerprint)[:16]:
|
|
71
|
-
# ★ 指纹没变 → 续期 30 天
|
|
72
|
-
conn.execute(text("""
|
|
73
|
-
UPDATE edges SET expires_at = now() + interval '30 days'
|
|
74
|
-
WHERE from_id=:f AND to_id=:t AND source='inferred'
|
|
75
|
-
"""), {'f': from_id, 't': to_id})
|
|
76
|
-
stats['renewed'] += 1
|
|
77
|
-
else:
|
|
78
|
-
# ★ 指纹变了 → 删除旧弱边 (下次全量推理重建)
|
|
79
|
-
conn.execute(text("""
|
|
80
|
-
DELETE FROM edges WHERE from_id=:f AND to_id=:t AND source='inferred'
|
|
81
|
-
"""), {'f': from_id, 't': to_id})
|
|
82
|
-
stats['deleted'] += 1
|
|
83
|
-
except Exception:
|
|
84
|
-
stats['errors'] += 1
|
|
85
|
-
|
|
86
|
-
logger(' [recompute] 续期%d 删除%d 错%d' % (
|
|
87
|
-
stats['renewed'], stats['deleted'], stats['errors']))
|
|
88
|
-
return stats
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if __name__ == '__main__':
|
|
92
|
-
r = recompute_expired()
|
|
93
|
-
print(r)
|
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""embedding 弱关联推理 (L3 核心)。
|
|
3
|
-
|
|
4
|
-
对没有 REQ-ID 标注的老代码:
|
|
5
|
-
1. 算实体 embedding (DashScope text-embedding-v3)
|
|
6
|
-
2. 向量召回最近 anchor (pgvector HNSW; 不可用降级命名匹配)
|
|
7
|
-
3. 按相似度建弱边:
|
|
8
|
-
> 0.85 → confidence=0.9, expires_at=now+30天
|
|
9
|
-
0.6-0.85 → confidence=0.7, expires_at=now+14天
|
|
10
|
-
4. 绑 fingerprint (C方案重算依赖)
|
|
11
|
-
|
|
12
|
-
embedding 成本高 (37515 实体), 用 llm_limit 限量 + 增量。
|
|
13
|
-
"""
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
import os
|
|
16
|
-
import sys
|
|
17
|
-
import time
|
|
18
|
-
import json
|
|
19
|
-
import urllib.request
|
|
20
|
-
from datetime import datetime, timedelta
|
|
21
|
-
|
|
22
|
-
_THIS = os.path.dirname(os.path.abspath(__file__))
|
|
23
|
-
for _i in range(8):
|
|
24
|
-
_p = os.path.dirname(_THIS)
|
|
25
|
-
if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
|
|
26
|
-
sys.path.insert(0, _p); break
|
|
27
|
-
_THIS = _p
|
|
28
|
-
|
|
29
|
-
from domain.kg.extract.inference import naming_matcher
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def embed_one(text: str) -> list:
|
|
33
|
-
"""调 DashScope text-embedding-v3 算单个向量 (1024维)。失败返回 []。"""
|
|
34
|
-
key = os.environ.get('DASHSCOPE_API_KEY', '')
|
|
35
|
-
if not key or not text:
|
|
36
|
-
return []
|
|
37
|
-
payload = json.dumps({
|
|
38
|
-
'model': 'text-embedding-v3',
|
|
39
|
-
'input': [text[:500]],
|
|
40
|
-
'dimensions': 1024,
|
|
41
|
-
}).encode('utf-8')
|
|
42
|
-
req = urllib.request.Request(
|
|
43
|
-
'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings',
|
|
44
|
-
data=payload,
|
|
45
|
-
headers={'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json'},
|
|
46
|
-
)
|
|
47
|
-
try:
|
|
48
|
-
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
49
|
-
r = json.loads(resp.read())
|
|
50
|
-
return r['data'][0]['embedding']
|
|
51
|
-
except Exception:
|
|
52
|
-
return []
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def cosine_sim(a: list, b: list) -> float:
|
|
56
|
-
"""余弦相似度 (纯 Python, 不依赖 numpy)。"""
|
|
57
|
-
if not a or not b or len(a) != len(b):
|
|
58
|
-
return 0.0
|
|
59
|
-
dot = sum(x * y for x, y in zip(a, b))
|
|
60
|
-
na = sum(x * x for x in a) ** 0.5
|
|
61
|
-
nb = sum(y * y for y in b) ** 0.5
|
|
62
|
-
if na == 0 or nb == 0:
|
|
63
|
-
return 0.0
|
|
64
|
-
return dot / (na * nb)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def infer_one(entity: dict, anchors: list, anchor_vecs: dict = None,
|
|
68
|
-
vec_matrix=None, vec_ids=None, preset_vec=None):
|
|
69
|
-
"""对单个实体推理弱关联 → edges 列表。
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
entity: {id, canonical, cn, type, props}
|
|
73
|
-
anchors: [{id, title, kind}] 全部锚点
|
|
74
|
-
anchor_vecs: {anchor_id: vec} 锚点向量缓存 (可选, 没有则用命名匹配)
|
|
75
|
-
vec_matrix: numpy 矩阵 (N, 1024) 持久化向量 (GOAL A 矩阵召回)
|
|
76
|
-
vec_ids: 矩阵对应的 entity_id 列表 (长度 N)
|
|
77
|
-
preset_vec: 预计算的实体向量 (GOAL A 批量 embed, 避免逐个调 API)
|
|
78
|
-
Returns: [{from_id, to_id, edge_type, confidence, source, expires_at, fingerprint, props}]
|
|
79
|
-
"""
|
|
80
|
-
eid = entity['id']
|
|
81
|
-
desc = (entity.get('canonical', '') + ' ' + entity.get('cn', '')).strip() or entity['id']
|
|
82
|
-
|
|
83
|
-
# 路1: numpy 矩阵召回 (GOAL A, 最快) — 一次矩阵乘算 top-K
|
|
84
|
-
if vec_matrix is not None and vec_ids is not None:
|
|
85
|
-
# ★ 优先用预计算向量 (GOAL A 批量 embed), 没有才逐个 embed
|
|
86
|
-
ent_vec = preset_vec or embed_one(desc)
|
|
87
|
-
if ent_vec:
|
|
88
|
-
scored = _numpy_recall(ent_vec, vec_matrix, vec_ids, top_k=5, min_score=0.6)
|
|
89
|
-
if scored:
|
|
90
|
-
return _build_edges(eid, scored, entity, anchors, via='embedding')
|
|
91
|
-
|
|
92
|
-
# 路2: 逐个 cosine_sim 召回 (有 anchor_vecs 时, 旧逻辑)
|
|
93
|
-
if anchor_vecs:
|
|
94
|
-
ent_vec = preset_vec or embed_one(desc)
|
|
95
|
-
if ent_vec:
|
|
96
|
-
scored = []
|
|
97
|
-
for aid, avec in anchor_vecs.items():
|
|
98
|
-
s = cosine_sim(ent_vec, avec)
|
|
99
|
-
if s >= 0.6:
|
|
100
|
-
scored.append((aid, s))
|
|
101
|
-
scored.sort(key=lambda x: x[1], reverse=True)
|
|
102
|
-
if scored:
|
|
103
|
-
return _build_edges(eid, scored, entity, anchors, via='embedding')
|
|
104
|
-
# 路3: 命名匹配降级 (embedding 不可用或没 anchor_vecs 或 embedding 0命中)
|
|
105
|
-
matches = naming_matcher.match_to_anchors(entity.get('canonical', ''), anchors)
|
|
106
|
-
return _build_edges(eid, matches, entity, anchors, via='naming')
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _numpy_recall(query_vec: list, matrix, ids: list, top_k: int = 5,
|
|
110
|
-
min_score: float = 0.6) -> list:
|
|
111
|
-
"""numpy 矩阵召回 top-K (GOAL A 核心, 替代逐个 cosine_sim)。
|
|
112
|
-
|
|
113
|
-
query_vec × matrix 一次矩阵乘 → 全量相似度 → top-K。
|
|
114
|
-
比逐个 Python cosine_sim 快 100x。
|
|
115
|
-
Returns: [(id, score)] 按 score 降序
|
|
116
|
-
"""
|
|
117
|
-
try:
|
|
118
|
-
import numpy as np
|
|
119
|
-
q = np.array(query_vec, dtype=np.float32)
|
|
120
|
-
# 归一化 (余弦相似度 = 点积/模长)
|
|
121
|
-
q_norm = q / (np.linalg.norm(q) + 1e-8)
|
|
122
|
-
m = matrix # 已归一化的矩阵 (N, dim)
|
|
123
|
-
# 一次矩阵乘: (1, dim) × (dim, N) → (1, N)
|
|
124
|
-
scores = m @ q_norm # m 已归一化, q 已归一化 → 点积=余弦
|
|
125
|
-
# top-K
|
|
126
|
-
top_idx = np.argsort(scores)[::-1][:top_k]
|
|
127
|
-
out = []
|
|
128
|
-
for idx in top_idx:
|
|
129
|
-
s = float(scores[idx])
|
|
130
|
-
if s >= min_score:
|
|
131
|
-
out.append((ids[idx], round(s, 4)))
|
|
132
|
-
return out
|
|
133
|
-
except Exception:
|
|
134
|
-
return []
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _load_vec_matrix(engine, kinds=None) -> tuple:
|
|
138
|
-
"""从持久化向量加载到 numpy 矩阵 (GOAL A)。
|
|
139
|
-
|
|
140
|
-
优先读 embeddings_ali (rag.py 的 19093 条, 已有), 兜底读 embeddings (GOAL A 新建)。
|
|
141
|
-
Returns: (matrix_normalized, id_list) matrix 是 (N, 1024) 已归一化矩阵
|
|
142
|
-
"""
|
|
143
|
-
import numpy as np
|
|
144
|
-
from sqlalchemy import text
|
|
145
|
-
import json
|
|
146
|
-
# 路1: embeddings_ali (rag.py 的向量源, 数据最多)
|
|
147
|
-
try:
|
|
148
|
-
with engine.connect() as conn:
|
|
149
|
-
rows = conn.execute(text("""
|
|
150
|
-
SELECT entity_id, vec FROM embeddings_ali
|
|
151
|
-
WHERE vec IS NOT NULL AND array_length(vec, 1) > 0
|
|
152
|
-
LIMIT 5000
|
|
153
|
-
""")).all()
|
|
154
|
-
if rows:
|
|
155
|
-
ids, vecs = [], []
|
|
156
|
-
for eid, v in rows:
|
|
157
|
-
if isinstance(v, list) and len(v) > 0:
|
|
158
|
-
ids.append(eid)
|
|
159
|
-
vecs.append(v)
|
|
160
|
-
if vecs:
|
|
161
|
-
matrix = np.array(vecs, dtype=np.float32)
|
|
162
|
-
norms = np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-8
|
|
163
|
-
return matrix / norms, ids
|
|
164
|
-
except Exception:
|
|
165
|
-
pass
|
|
166
|
-
# 路2: embeddings (GOAL A 新建, vec_json JSONB)
|
|
167
|
-
try:
|
|
168
|
-
with engine.connect() as conn:
|
|
169
|
-
rows = conn.execute(text("""
|
|
170
|
-
SELECT entity_id, vec_json FROM embeddings
|
|
171
|
-
WHERE vec_json IS NOT NULL
|
|
172
|
-
""")).all()
|
|
173
|
-
except Exception:
|
|
174
|
-
return None, None
|
|
175
|
-
if not rows:
|
|
176
|
-
return None, None
|
|
177
|
-
ids, vecs = [], []
|
|
178
|
-
for eid, vj in rows:
|
|
179
|
-
try:
|
|
180
|
-
v = vj if isinstance(vj, list) else json.loads(vj)
|
|
181
|
-
if isinstance(v, list) and len(v) > 0:
|
|
182
|
-
ids.append(eid)
|
|
183
|
-
vecs.append(v)
|
|
184
|
-
except Exception:
|
|
185
|
-
continue
|
|
186
|
-
if not vecs:
|
|
187
|
-
return None, None
|
|
188
|
-
matrix = np.array(vecs, dtype=np.float32)
|
|
189
|
-
norms = np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-8
|
|
190
|
-
matrix_norm = matrix / norms
|
|
191
|
-
return matrix_norm, ids
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def _build_edges(entity_id: str, scored: list, entity: dict, anchors: list, via: str = 'naming'):
|
|
195
|
-
"""按相似度分档建弱边。
|
|
196
|
-
|
|
197
|
-
via: 'embedding' 或 'naming' (影响 props 标注)
|
|
198
|
-
fingerprint: 从 file_signatures 表读 content_hash (C方案重算依赖真实文件指纹)
|
|
199
|
-
"""
|
|
200
|
-
edges = []
|
|
201
|
-
repo_id = entity.get('repo_id', '')
|
|
202
|
-
# fingerprint: 优先从 file_signatures 表读真实 content_hash
|
|
203
|
-
fp = _get_file_fingerprint(entity)
|
|
204
|
-
now = datetime.utcnow()
|
|
205
|
-
|
|
206
|
-
for anchor_id, score in scored[:3]: # 最多建3条弱边
|
|
207
|
-
if score > 0.85:
|
|
208
|
-
conf, days = 0.9, 30
|
|
209
|
-
elif score >= 0.6:
|
|
210
|
-
conf, days = 0.7, 14
|
|
211
|
-
else:
|
|
212
|
-
continue
|
|
213
|
-
edges.append({
|
|
214
|
-
'from_id': entity_id, 'to_id': anchor_id,
|
|
215
|
-
'edge_type': 'relates_to', 'from_repo': repo_id, 'to_repo': repo_id,
|
|
216
|
-
'confidence': conf, 'source': 'inferred',
|
|
217
|
-
'expires_at': now + timedelta(days=days),
|
|
218
|
-
'fingerprint': fp,
|
|
219
|
-
'props': {'score': round(score, 3), 'via': via},
|
|
220
|
-
})
|
|
221
|
-
return edges
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def _get_file_fingerprint(entity: dict) -> str:
|
|
225
|
-
"""从 file_signatures 表读实体文件的 content_hash。
|
|
226
|
-
|
|
227
|
-
entity.props.file 是相对路径 (data/code/fywl-ics\...),
|
|
228
|
-
file_signatures.file_path 是绝对路径, 用路径段末尾匹配。
|
|
229
|
-
读不到时降级用 canonical hash (best-effort)。
|
|
230
|
-
"""
|
|
231
|
-
import hashlib
|
|
232
|
-
props = entity.get('props') or {}
|
|
233
|
-
if isinstance(props, str):
|
|
234
|
-
try:
|
|
235
|
-
import json
|
|
236
|
-
props = json.loads(props)
|
|
237
|
-
except Exception:
|
|
238
|
-
props = {}
|
|
239
|
-
file_path = props.get('file', '') if isinstance(props, dict) else ''
|
|
240
|
-
if not file_path:
|
|
241
|
-
# 无文件信息 → 用 canonical hash 兜底
|
|
242
|
-
return hashlib.md5((entity.get('canonical', '') + entity.get('cn', '')).encode()).hexdigest()[:16]
|
|
243
|
-
# 从 file_signatures 查 content_hash (路径末尾段匹配)
|
|
244
|
-
try:
|
|
245
|
-
from domain.kg.extract.java import pg_upsert
|
|
246
|
-
engine, _ = pg_upsert._get_pg_engine()
|
|
247
|
-
if engine is None:
|
|
248
|
-
return hashlib.md5(file_path.encode()).hexdigest()[:16]
|
|
249
|
-
from sqlalchemy import text
|
|
250
|
-
# 标准化路径: 取最后两段做 LIKE 匹配 (跨绝对/相对路径)
|
|
251
|
-
norm = file_path.replace('\\', '/').split('/')
|
|
252
|
-
tail = '/'.join(norm[-3:]) if len(norm) >= 3 else file_path.replace('\\', '/')
|
|
253
|
-
with engine.connect() as conn:
|
|
254
|
-
row = conn.execute(text("""
|
|
255
|
-
SELECT content_hash FROM file_signatures
|
|
256
|
-
WHERE file_path ILIKE :tail LIMIT 1
|
|
257
|
-
"""), {'tail': '%' + tail}).first()
|
|
258
|
-
if row and row[0]:
|
|
259
|
-
return row[0][:16]
|
|
260
|
-
except Exception:
|
|
261
|
-
pass
|
|
262
|
-
return hashlib.md5(file_path.encode()).hexdigest()[:16]
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def build_anchor_vectors(anchors: list, logger=print) -> dict:
|
|
266
|
-
"""批量 embed anchors 算向量缓存 → {anchor_id: vec}。
|
|
267
|
-
|
|
268
|
-
让 weak_link 的向量召回主路径真正跑通 (不依赖 pgvector, 用 Python cosine_sim)。
|
|
269
|
-
anchors 多时只 embed 前 N 个 (限量, 避免全量 154 个 anchors 调 154 次 API)。
|
|
270
|
-
"""
|
|
271
|
-
out = {}
|
|
272
|
-
if not anchors:
|
|
273
|
-
return out
|
|
274
|
-
# 只 embed 前 30 个 anchors (够召回, 控成本)
|
|
275
|
-
for a in anchors[:30]:
|
|
276
|
-
desc = (a.get('title') or a.get('id', '')).strip()
|
|
277
|
-
if not desc:
|
|
278
|
-
continue
|
|
279
|
-
vec = embed_one(desc)
|
|
280
|
-
if vec:
|
|
281
|
-
out[a['id']] = vec
|
|
282
|
-
logger(' [infer] anchor 向量缓存: %d/%d' % (len(out), len(anchors)))
|
|
283
|
-
return out
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def infer_batch(entities: list, anchors: list, use_embedding: bool = True, logger=print):
|
|
287
|
-
"""批量推理弱关联 (GOAL A 升级: numpy 矩阵召回)。
|
|
288
|
-
|
|
289
|
-
Args:
|
|
290
|
-
entities: 待推理实体 (无强关联的)
|
|
291
|
-
anchors: 锚点列表
|
|
292
|
-
use_embedding: True 走向量主路径; False 只命名匹配
|
|
293
|
-
Returns: (edges, stats)
|
|
294
|
-
|
|
295
|
-
GOAL A 升级: 优先从 embeddings 表加载持久化向量到 numpy 矩阵,
|
|
296
|
-
每个实体只 embed 一次自己, 用矩阵乘召回 top-K (不再每个 anchor 都 embed)。
|
|
297
|
-
"""
|
|
298
|
-
t0 = time.time()
|
|
299
|
-
all_edges = []
|
|
300
|
-
matched = 0
|
|
301
|
-
|
|
302
|
-
# ★ GOAL A: 优先用持久化向量矩阵 (最快)
|
|
303
|
-
vec_matrix, vec_ids = None, None
|
|
304
|
-
if use_embedding:
|
|
305
|
-
try:
|
|
306
|
-
from domain.kg.extract.java import pg_upsert
|
|
307
|
-
engine, _ = pg_upsert._get_pg_engine()
|
|
308
|
-
if engine:
|
|
309
|
-
vec_matrix, vec_ids = _load_vec_matrix(engine)
|
|
310
|
-
if vec_matrix is not None:
|
|
311
|
-
logger(' [infer] 加载持久化向量矩阵: %d × %d' % vec_matrix.shape)
|
|
312
|
-
except Exception as e:
|
|
313
|
-
logger(' [infer] 矩阵加载失败, 降级: %s' % str(e)[:50])
|
|
314
|
-
|
|
315
|
-
# 矩阵不可用 → 降级到 build_anchor_vectors (旧路径)
|
|
316
|
-
anchor_vecs = None
|
|
317
|
-
if vec_matrix is None and use_embedding:
|
|
318
|
-
anchor_vecs = build_anchor_vectors(anchors, logger=logger)
|
|
319
|
-
|
|
320
|
-
# ★ GOAL A: 优先复用持久化向量, 缺的才批量 embed
|
|
321
|
-
entity_vecs = {}
|
|
322
|
-
if vec_matrix is not None and use_embedding:
|
|
323
|
-
from domain.kg.extract.inference.embed_builder import embed_batch as _embed_batch
|
|
324
|
-
from domain.kg.extract.java import pg_upsert as _pg
|
|
325
|
-
_engine, _ = _pg._get_pg_engine()
|
|
326
|
-
# ① 先从 embeddings_ali 批量读已有向量 (免 API 调用, 分chunk避免参数过多)
|
|
327
|
-
all_ids = [e['id'] for e in entities]
|
|
328
|
-
if _engine and all_ids:
|
|
329
|
-
try:
|
|
330
|
-
# 分 chunk 查 (每 chunk 100 个 id, 避免 PG 参数上限)
|
|
331
|
-
import json as _json
|
|
332
|
-
for ci in range(0, len(all_ids), 100):
|
|
333
|
-
chunk_ids = all_ids[ci:ci+100]
|
|
334
|
-
# 用 ANY(ARRAY[...]) 避免构造大量 :params
|
|
335
|
-
arr = "ARRAY[" + ','.join("'" + eid.replace("'", "''") + "'" for eid in chunk_ids) + "]"
|
|
336
|
-
from sqlalchemy import text as _text_fn
|
|
337
|
-
with _engine.connect() as conn:
|
|
338
|
-
rows = conn.execute(_text_fn(
|
|
339
|
-
f"SELECT entity_id, vec FROM embeddings_ali WHERE entity_id = ANY({arr}) AND vec IS NOT NULL"
|
|
340
|
-
)).all()
|
|
341
|
-
for eid, v in rows:
|
|
342
|
-
if isinstance(v, list) and len(v) > 0:
|
|
343
|
-
entity_vecs[eid] = v
|
|
344
|
-
logger(' [infer] 复用 embeddings_ali 已有向量: %d/%d' % (len(entity_vecs), len(all_ids)))
|
|
345
|
-
except Exception as _e:
|
|
346
|
-
logger(' [infer] 复用查询异常: %s' % str(_e)[:50])
|
|
347
|
-
# ② 缺的才并发批量 embed (4线程并行, 破10秒关键)
|
|
348
|
-
missing = [e for e in entities if e['id'] not in entity_vecs]
|
|
349
|
-
if missing:
|
|
350
|
-
from domain.kg.extract.inference.embed_builder import embed_batch as _embed2
|
|
351
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
352
|
-
descs = [((e.get('canonical', '') + ' ' + e.get('cn', '')).strip() or e['id'])[:200] for e in missing]
|
|
353
|
-
logger(' [infer] 并发 embed %d 缺失实体 (4线程)...' % len(missing))
|
|
354
|
-
try:
|
|
355
|
-
# 分 4 块并发, 每块调 embed_batch (内含 10条/批)
|
|
356
|
-
chunk = max(50, len(descs) // 4)
|
|
357
|
-
chunks = [descs[i:i+chunk] for i in range(0, len(descs), chunk)]
|
|
358
|
-
with ThreadPoolExecutor(max_workers=4) as ex:
|
|
359
|
-
results = list(ex.map(_embed2, chunks))
|
|
360
|
-
vecs = [v for cv in results for v in cv]
|
|
361
|
-
for ent, v in zip(missing, vecs):
|
|
362
|
-
if v:
|
|
363
|
-
entity_vecs[ent['id']] = v
|
|
364
|
-
logger(' [infer] 实体向量就绪: %d/%d' % (len(entity_vecs), len(entities)))
|
|
365
|
-
except Exception as e:
|
|
366
|
-
logger(' [infer] 并发 embed 失败: %s' % str(e)[:50])
|
|
367
|
-
|
|
368
|
-
for i, ent in enumerate(entities):
|
|
369
|
-
try:
|
|
370
|
-
eds = infer_one(ent, anchors, anchor_vecs=anchor_vecs,
|
|
371
|
-
vec_matrix=vec_matrix, vec_ids=vec_ids,
|
|
372
|
-
preset_vec=entity_vecs.get(ent['id']))
|
|
373
|
-
all_edges += eds
|
|
374
|
-
if eds:
|
|
375
|
-
matched += 1
|
|
376
|
-
except Exception:
|
|
377
|
-
pass
|
|
378
|
-
if (i + 1) % 100 == 0:
|
|
379
|
-
logger(' [infer] %d/%d (匹配%d, 边%d)' % (i + 1, len(entities), matched, len(all_edges)))
|
|
380
|
-
dt = time.time() - t0
|
|
381
|
-
logger(' [infer] 完成: %d 实体 → %d 匹配 → %d 弱边 / %.1fs' % (
|
|
382
|
-
len(entities), matched, len(all_edges), dt))
|
|
383
|
-
|
|
384
|
-
# ★ GOAL A: 把新 embed 的实体向量持久化 (下次跑免 API 调用)
|
|
385
|
-
if entity_vecs and vec_matrix is not None:
|
|
386
|
-
try:
|
|
387
|
-
_persist_entity_vecs(entity_vecs, entities)
|
|
388
|
-
except Exception:
|
|
389
|
-
pass
|
|
390
|
-
|
|
391
|
-
return all_edges, {'entities': len(entities), 'matched': matched,
|
|
392
|
-
'edges': len(all_edges), 'seconds': round(dt, 1)}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
def _persist_entity_vecs(vecs: dict, entities: list, logger=print):
|
|
396
|
-
"""把推理时计算的实体向量存回 embeddings_ali (下次复用, 免 API)。"""
|
|
397
|
-
from domain.kg.extract.java import pg_upsert
|
|
398
|
-
from sqlalchemy import text
|
|
399
|
-
import json
|
|
400
|
-
engine, _ = pg_upsert._get_pg_engine()
|
|
401
|
-
if engine is None:
|
|
402
|
-
return
|
|
403
|
-
ent_map = {e['id']: e for e in entities}
|
|
404
|
-
ok = 0
|
|
405
|
-
with engine.begin() as conn:
|
|
406
|
-
for eid, vec in vecs.items():
|
|
407
|
-
ent = ent_map.get(eid, {})
|
|
408
|
-
try:
|
|
409
|
-
# embeddings_ali 表 vec 是 ARRAY 类型
|
|
410
|
-
arr_str = '{' + ','.join(str(float(v)) for v in vec[:1024]) + '}'
|
|
411
|
-
conn.execute(text("""
|
|
412
|
-
INSERT INTO embeddings_ali (entity_id, kind, text, vec, model, embedded_at)
|
|
413
|
-
VALUES (:id, :kind, :txt, CAST(:vec AS FLOAT[]), 'text-embedding-v3', now())
|
|
414
|
-
ON CONFLICT (entity_id) DO NOTHING
|
|
415
|
-
"""), {'id': eid, 'kind': ent.get('type', 'entity'),
|
|
416
|
-
'txt': (ent.get('canonical', '') or eid)[:200], 'vec': arr_str})
|
|
417
|
-
ok += 1
|
|
418
|
-
except Exception:
|
|
419
|
-
pass
|
|
420
|
-
if ok:
|
|
421
|
-
logger(' [infer] 持久化 %d 条实体向量 (下次复用)' % ok)
|