- 模型锁定完整 ID 等值:role_policy 废弃子串匹配,前置校验+事后 MODEL_POLICY_VIOLATION 熔断+回执 modelMatch,治理链角色放行 BUDGET_CHAIN - 工具白名单只读机械强制:任务包 allowlist ⊆ 只读注册表,TOOL_NOT_READONLY - 本地向量检索 truthful 化:aiContext 裁剪、指针行跳过、资格失败关闭 (bindingStatus/productionRetrievalEligible 不再伪造 active) - 角色提示词 name 回归英文系统 ID;planner 补 fine_outline 绑定; writer 数据契约对齐 writer-candidate-body-v1 - 测试账本隔离:sqlite_path 三层透传(bridge/two_phase),9 处派发测试 改用临时库;清除 muse.db 测试残留 5 runs+26 events+reviews 5/6(有备份) - test-inventory 补 3 条登记并机械重算 summary;评测场景补 output_contract/fail_closed 与 stability 两类;死代码清理 (project_paths.py、offline_only 死参数、harness/harness 残骸) - 新增 metaphysical-diff-review 技能(红线 4.2 载体)并登记,共 59 技能
307 lines
13 KiB
Python
307 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""search-knowledge Skill:知识向量检索(余弦召回 + 授权过滤 + aiContext 字段裁剪)。
|
||
|
||
合同见同目录 SKILL.md。查询嵌入与知识行同模型同维(复用 embed-knowledge 的实现)。
|
||
"""
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any, Callable
|
||
|
||
import click
|
||
import psycopg
|
||
|
||
from muse_embed import _session, embed_texts
|
||
|
||
from muse_db import DSN
|
||
TENANT = 1
|
||
|
||
|
||
def _project_root() -> Path:
|
||
return next(
|
||
parent
|
||
for parent in (Path(__file__).resolve().parent, *Path(__file__).resolve().parents)
|
||
if (parent / "AGENTS.md").is_file() and (parent / ".git").exists()
|
||
)
|
||
|
||
|
||
def load_ai_context(conn, *, tenant_id: int = TENANT):
|
||
"""读库内 23 型的字段级 aiContext 细则:{target_type: {field: true/false/[用途]}}。"""
|
||
rows = conn.execute(
|
||
"""SELECT s.target_type, v.policy_snapshot->'fieldAiContext'
|
||
FROM muse_meta_schema s
|
||
JOIN muse_meta_schema_version sv ON sv.id = s.active_version_id
|
||
JOIN muse_meta_visibility_policy v ON v.schema_version_id = sv.id
|
||
WHERE s.tenant_id=%s AND s.deleted=FALSE""", (tenant_id,)).fetchall()
|
||
return {t: (m or {}) for t, m in rows}
|
||
|
||
|
||
def visible(ai_rule, purpose):
|
||
"""aiContext 判定:true 全用途可见;false 不可见;[用途] 仅列出的可见;无规则默认可见。"""
|
||
if ai_rule is None:
|
||
return True
|
||
if isinstance(ai_rule, bool):
|
||
return ai_rule
|
||
return purpose in ai_rule
|
||
|
||
|
||
def _default_embedder(intent: str) -> list[float]:
|
||
"""复用 embed-knowledge 生成单条查询向量,并统一失败语义。"""
|
||
|
||
vectors, bad = embed_texts(_session(), [intent])
|
||
if bad or not vectors:
|
||
raise ValueError("查询嵌入失败")
|
||
return vectors[0]
|
||
|
||
|
||
def _structured_source_refs(payload: dict[str, Any], lineage: Any) -> list[dict[str, Any]]:
|
||
"""只接收结构化来源指针;人类可读的“出处”文本不能冒充可回读引用。"""
|
||
|
||
candidates = []
|
||
if isinstance(lineage, dict):
|
||
candidates.append(lineage.get("sourceRefs"))
|
||
fields = payload.get("字段")
|
||
if isinstance(fields, dict):
|
||
candidates.append(fields.get("sourceRefs"))
|
||
candidates.append(payload.get("sourceRefs"))
|
||
for value in candidates:
|
||
if isinstance(value, list) and all(isinstance(item, dict) for item in value):
|
||
return value
|
||
return []
|
||
|
||
|
||
def _milestones(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||
"""从卡字段中提取历史里程碑,终态摘要不会进入冻结投影。"""
|
||
|
||
fields = payload.get("字段")
|
||
if not isinstance(fields, dict):
|
||
return []
|
||
for key in ("演变历程", "演变轨迹", "milestones"):
|
||
value = fields.get(key)
|
||
if isinstance(value, list) and all(isinstance(item, dict) for item in value):
|
||
return value
|
||
return []
|
||
|
||
|
||
def search_cards(
|
||
intent: str,
|
||
*,
|
||
scope: str = "admin",
|
||
work_id: int | None = None,
|
||
ttype: str | None = None,
|
||
purpose: str = "generation",
|
||
top: int = 5,
|
||
dsn: str = DSN,
|
||
tenant_id: int = TENANT,
|
||
connection_factory: Callable[..., Any] = psycopg.connect,
|
||
embedder: Callable[[str], list[float]] = _default_embedder,
|
||
sqlite_path: str | Path | None = None,
|
||
) -> list[dict[str, Any]]:
|
||
"""执行唯一的卡检索语义,CLI 与正文读取器共同调用本函数。
|
||
|
||
生产作品面只读 active Canonical entity、允许状态和有效绑定;治理面
|
||
保留原有 draft 能力,但正文生产适配器不会调用治理面。
|
||
"""
|
||
|
||
if scope not in {"admin", "public_pattern", "work"}:
|
||
raise ValueError("scope 只能是 admin、public_pattern 或 work")
|
||
if scope == "work" and not work_id:
|
||
raise ValueError("scope=work 必须提供 work_id")
|
||
if purpose not in {"generation", "planning", "detection", "extraction"}:
|
||
raise ValueError("purpose 非法")
|
||
if isinstance(top, bool) or not isinstance(top, int) or top <= 0:
|
||
raise ValueError("top 必须是正整数")
|
||
|
||
if sqlite_path is not None:
|
||
root = _project_root()
|
||
if str(root) not in sys.path:
|
||
sys.path.insert(0, str(root))
|
||
from muse.store import search_card_vectors
|
||
|
||
return search_card_vectors(
|
||
embedder(intent),
|
||
kind=ttype,
|
||
work_id=work_id if scope == "work" else None,
|
||
top=top,
|
||
path=sqlite_path,
|
||
purpose=purpose,
|
||
)
|
||
|
||
qvec = json.dumps(embedder(intent))
|
||
with connection_factory(dsn) as conn:
|
||
ai_rules = load_ai_context(conn, tenant_id=tenant_id)
|
||
if scope == "admin":
|
||
sql = """SELECT 'draft' AS src, d.id, d.draft_payload AS payload, d.status,
|
||
1 - (e.embedding <=> %s::vector) AS score,
|
||
d.revision, d.current_canonical_snapshot AS lineage,
|
||
NULL::varchar AS binding_status, d.source_status
|
||
FROM example_knowledge_embedding e
|
||
JOIN muse_knowledge_draft d ON d.id = e.draft_id
|
||
WHERE e.tenant_id=%s AND e.deleted=FALSE AND d.deleted=FALSE"""
|
||
args = [qvec, tenant_id]
|
||
elif scope == "public_pattern":
|
||
# WHY: 公共范式仍处于 draft 双轨,不能走作品 entity/binding 面;专用查询必须在
|
||
# SQL 层同时锁住全局作品号、公共目标库、全局库存在性和来源资格,不能复用会
|
||
# 召回同租户全部治理草稿的 admin 面。当前 draft schema 没有 kb_id/scope 列,
|
||
# 所以库归属按已登记的数据合同由 work_id + 目标库绑定,scope 缺省按 global。
|
||
sql = """SELECT 'draft' AS src, d.id, d.draft_payload AS payload, d.status,
|
||
1 - (e.embedding <=> %s::vector) AS score,
|
||
d.revision, d.current_canonical_snapshot AS lineage,
|
||
NULL::varchar AS binding_status, d.source_status
|
||
FROM example_knowledge_embedding e
|
||
JOIN muse_knowledge_draft d ON d.id = e.draft_id
|
||
WHERE e.tenant_id=%s AND d.tenant_id=%s
|
||
AND e.deleted=FALSE AND d.deleted=FALSE
|
||
AND d.work_id=0
|
||
AND d.draft_payload->>'目标库'='公共范式库'
|
||
AND COALESCE(d.draft_payload->>'scope', 'global')='global'
|
||
AND d.status IN ('pending','confirmed')
|
||
AND COALESCE(d.source_status, 'active') IN ('active','authorized')
|
||
AND COALESCE(d.source_action_policy, 'allowed')='allowed'
|
||
AND EXISTS (
|
||
SELECT 1 FROM muse_knowledge_base kb
|
||
WHERE kb.tenant_id=%s AND kb.deleted=FALSE
|
||
AND kb.kb_type='global' AND kb.status='active'
|
||
)"""
|
||
args = [qvec, tenant_id, tenant_id, tenant_id]
|
||
else:
|
||
sql = """SELECT 'entity' AS src, en.id,
|
||
jsonb_build_object('型', en.entity_type, '名称', en.normalized_name,
|
||
'一句话摘要', en.description, '字段', en.attributes) AS payload,
|
||
en.status, 1 - (e.embedding <=> %s::vector) AS score,
|
||
en.revision, en.lineage_payload AS lineage,
|
||
b.binding_status, en.source_status
|
||
FROM example_knowledge_embedding e
|
||
JOIN muse_knowledge_entity en ON en.id = e.entity_id
|
||
JOIN muse_knowledge_binding b ON b.kb_id = en.kb_id AND b.work_id = %s
|
||
AND b.binding_status='active' AND b.deleted=FALSE AND b.tenant_id=%s
|
||
WHERE e.tenant_id=%s AND e.deleted=FALSE AND en.deleted=FALSE
|
||
AND en.status='active' AND en.source_status IN ('active','authorized')
|
||
AND en.source_action_policy='allowed'"""
|
||
args = [qvec, work_id, tenant_id, tenant_id]
|
||
if ttype:
|
||
sql += (
|
||
" AND d.draft_payload->>'型' = %s"
|
||
if scope in {"admin", "public_pattern"}
|
||
else " AND en.entity_type = %s"
|
||
)
|
||
args.append(ttype)
|
||
draft_scope = scope in {"admin", "public_pattern"}
|
||
id_column = "d.id" if draft_scope else "en.id"
|
||
revision_column = "d.revision" if draft_scope else "en.revision"
|
||
sql += f" ORDER BY score DESC, {revision_column}::text ASC, {id_column}::text ASC LIMIT %s"
|
||
args.append(top)
|
||
rows = conn.execute(sql, args).fetchall()
|
||
|
||
results = []
|
||
for src, row_id, raw_payload, status, score, revision, lineage, binding_status, source_status in rows:
|
||
payload = raw_payload or {}
|
||
card_type = payload.get("型") or payload.get("type") or "?"
|
||
rules = ai_rules.get(card_type, {})
|
||
fields = payload.get("字段") or {}
|
||
visible_fields = {key: item for key, item in fields.items() if visible(rules.get(key), purpose)}
|
||
source_version = f"{src}-revision:{revision or 0}"
|
||
source_id = f"canonical-entity:{row_id}" if src == "entity" else f"draft:{row_id}"
|
||
results.append(
|
||
{
|
||
"cardId": str(row_id),
|
||
"type": card_type,
|
||
"name": payload.get("名称"),
|
||
"score": float(score),
|
||
"summary": payload.get("一句话摘要"),
|
||
"visibleFields": visible_fields,
|
||
"omittedFields": sorted(set(fields) - set(visible_fields)),
|
||
"sourceId": source_id,
|
||
"sourceVersion": source_version,
|
||
"sourceOffset": 0,
|
||
"sourceRefs": _structured_source_refs(payload, lineage),
|
||
"milestones": _milestones(payload),
|
||
"sourceKind": "canonical_entity" if src == "entity" else "draft",
|
||
"sourceStatus": source_status or status,
|
||
"bindingStatus": binding_status,
|
||
"retrievalScope": scope,
|
||
"productionRetrievalEligible": (
|
||
scope == "public_pattern"
|
||
or (src == "entity" and status == "active" and binding_status == "active")
|
||
),
|
||
}
|
||
)
|
||
return results
|
||
|
||
|
||
@click.command()
|
||
@click.argument("intent")
|
||
@click.option("--scope", type=click.Choice(["admin", "public_pattern", "work"]), default="admin", show_default=True,
|
||
help="admin=治理面; public_pattern=公共范式草稿; work=作品面")
|
||
@click.option("--work-id", type=int, help="scope=work 时必填")
|
||
@click.option("--type", "ttype", help="限定型(如 craft/combat/emotion/scene_pattern/trope)")
|
||
@click.option("--purpose", default="generation", show_default=True,
|
||
type=click.Choice(["generation", "planning", "detection", "extraction"]))
|
||
@click.option("--top", default=5, show_default=True)
|
||
@click.option("--json", "as_json", is_flag=True)
|
||
def main(intent, scope, work_id, ttype, purpose, top, as_json):
|
||
try:
|
||
root = _project_root()
|
||
if str(root) not in sys.path:
|
||
sys.path.insert(0, str(root))
|
||
from muse.store import connect, default_db_path
|
||
|
||
sqlite_path = None
|
||
db_path = default_db_path()
|
||
if db_path.is_file():
|
||
with connect(db_path) as conn:
|
||
filled = conn.execute(
|
||
"SELECT COUNT(*) FROM cards WHERE embedding IS NOT NULL"
|
||
).fetchone()[0]
|
||
if filled:
|
||
sqlite_path = db_path
|
||
cards = search_cards(
|
||
intent,
|
||
scope=scope,
|
||
work_id=work_id,
|
||
ttype=ttype,
|
||
purpose=purpose,
|
||
top=top,
|
||
sqlite_path=sqlite_path,
|
||
)
|
||
except ValueError as error:
|
||
raise click.ClickException(str(error)) from error
|
||
|
||
results = [
|
||
{
|
||
"来源": item["sourceId"],
|
||
"型": item["type"],
|
||
"名称": item["name"],
|
||
"状态": item["sourceStatus"],
|
||
"相似度": round(item["score"], 4),
|
||
"一句话摘要": item["summary"],
|
||
"可见字段": item["visibleFields"],
|
||
"出处": item["sourceRefs"],
|
||
"裁剪回显": item["omittedFields"],
|
||
}
|
||
for item in cards
|
||
]
|
||
|
||
if as_json:
|
||
click.echo(json.dumps(results, ensure_ascii=False, indent=1))
|
||
return
|
||
for i, r in enumerate(results, 1):
|
||
click.echo(f"── {i}. [{r['相似度']}] {r['型']} · {r['名称']}({r['状态']},{r['来源']})")
|
||
click.echo(f" 摘要: {r['一句话摘要']}")
|
||
for k, v in (r["可见字段"] or {}).items():
|
||
click.echo(f" {k}: {str(v)[:120]}")
|
||
if r["出处"]:
|
||
click.echo(f" 出处: {r['出处']}")
|
||
if r["裁剪回显"]:
|
||
click.echo(f" [裁剪回显·{purpose} 不可见] {','.join(r['裁剪回显'])}")
|
||
if not results:
|
||
click.echo("(无召回)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main()
|
||
except psycopg.Error as e:
|
||
click.echo(f"[db错误] {type(e).__name__}: {e}", err=True)
|
||
sys.exit(1)
|