Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions crates/utopia-server/src/api/kbs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,14 +359,30 @@ async fn install_packs(
if pack_ids.is_empty() {
return Ok(());
}
let mut packs = Vec::with_capacity(pack_ids.len());
for id in pack_ids {
let pack = crate::ontology_packs::get(id)
.ok_or_else(|| AppError::invalid("unknown_pack", format!("未知的本体包:{id}")))?;
let bytes = crate::ontology_packs::bytes(pack)?;
crate::owl_import::apply(state, kb_id, actor, pack.filename, &bytes)
packs.push((pack, crate::ontology_packs::bytes(pack)?));
}
for (pack, bytes) in &packs {
crate::owl_import::apply(state, kb_id, actor, pack.filename, bytes)
.await
.map_err(|e| AppError::Other(anyhow::anyhow!("装本体包 {} 失败:{e}", pack.id)))?;
}
// 第二遍:跨包的 domain / range。包是挨个装的,先装的看不见后装的类——
// W3C Org 的 headOf 要等 FOAF 的 Agent(#222)。只装一个包时没有"别的包"
if packs.len() > 1 {
for (pack, bytes) in &packs {
let (d, r) =
crate::owl_import::relink_domains_ranges(state, kb_id, pack.filename, bytes)
.await
.map_err(|e| {
AppError::Other(anyhow::anyhow!("补本体包 {} 的签名失败:{e}", pack.id))
})?;
tracing::debug!(%kb_id, pack = pack.id, domains = d, ranges = r, "跨包签名补链");
}
}
Ok(())
}

Expand Down
35 changes: 35 additions & 0 deletions crates/utopia-server/src/extraction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ async fn run(state: &AppState, document_id: Uuid, proposed_by: Option<Uuid>) ->
utopia_store::documents::set_graph_status(&state.pool, document_id, "extracting").await?;
state.emit_document(doc.kb_id, document_id);
let etypes = utopia_store::graph::entity_types(&state.pool, doc.kb_id).await?;
// 这一轮落过的事实(新建或重复观察):结尾对它们跑一遍签名检查
let mut touched_facts: Vec<Uuid> = Vec::new();
let rtypes = utopia_store::graph::relation_types(&state.pool, doc.kb_id).await?;
// 关系与属性分道:属性走字面值通道,不进关系清单。
//
Expand Down Expand Up @@ -762,6 +764,7 @@ async fn run(state: &AppState, document_id: Uuid, proposed_by: Option<Uuid>) ->
confidence,
)
.await?;
touched_facts.push(fact_id);
// 属性谓词也留原词:模型偶尔照抄 "person.salary" 全限定名,
// 命中的是剥掉前缀后的 key,原样是什么值得留着
utopia_store::graph::add_evidence(
Expand Down Expand Up @@ -882,6 +885,7 @@ async fn run(state: &AppState, document_id: Uuid, proposed_by: Option<Uuid>) ->
confidence,
)
.await?;
touched_facts.push(fact_id);
utopia_store::graph::add_evidence(
&state.pool,
fact_id,
Expand Down Expand Up @@ -1126,6 +1130,7 @@ async fn run(state: &AppState, document_id: Uuid, proposed_by: Option<Uuid>) ->
confidence,
)
.await?;
touched_facts.push(fact_id);
// 重复观察也要挂证据:多来源相互印证,任一来源删除后事实不孤儿化。
// 表层谓词随每次观察落笔——甲块说 "runs on"、乙块说 "optimized for"
// 会并进同一条事实,放事实上就是先写者胜,放证据上两个都留着
Expand Down Expand Up @@ -1221,6 +1226,36 @@ async fn run(state: &AppState, document_id: Uuid, proposed_by: Option<Uuid>) ->
if let Some(msg) = incomplete_reason(&unextracted, chunks.len()) {
return Err(anyhow::anyhow!(msg));
}
// 刚落的事实立刻过一遍签名。写入时只掰方向(judge_direction);掰不动的
// ——两个方向都对不上、或宾语没类型判不了——从前要等人按 Review 里的
// Run check 才露面,Axioms 一直是 0,图里却躺着反向事实(#222)。
// 检查失败不影响抽取本身:事实已经在库里,下一次 Run check 仍然查得到
if !touched_facts.is_empty() {
match utopia_store::reasoning::signature_breaks(
&state.pool,
doc.kb_id,
Some(&touched_facts),
)
.await
{
Ok(broken) if !broken.is_empty() => {
match utopia_store::reasoning::record_signature_breaks(
&state.pool,
doc.kb_id,
&broken,
)
.await
{
Ok(_) => state.emit_review(doc.kb_id),
Err(e) => {
tracing::warn!(%document_id, error = %e, "抽取后的签名违规没记进队列")
}
}
}
Ok(_) => {}
Err(e) => tracing::warn!(%document_id, error = %e, "抽取后的签名检查失败"),
}
}
utopia_store::documents::set_graph_status(&state.pool, document_id, "done").await?;
state.emit_document(doc.kb_id, document_id);

Expand Down
73 changes: 71 additions & 2 deletions crates/utopia-server/src/owl_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,64 @@ pub async fn plan(
Ok((plan, proj, format))
}

/// 装完一组包之后再过一遍:把 domain / range 指向**别的包里的类**的那些关系接上。
///
/// 单次导入认本文件里的类和库里已有的类(见 [`apply`] 里的 resolve),但包是挨个
/// 装的:装 W3C Org 时 FOAF 还没来,`headOf` 的 `rdfs:domain foaf:Agent` 就落了空;
/// 等 FOAF 装好,没人回头补。没有 domain 的谓词 `judge_direction` 不判方向,
/// 反向的 `Project Aurora head_of Li Ting` 就原样进图(#222)。
///
/// 只补不删,关联表 ON CONFLICT DO NOTHING,重复跑无害。属性不在这里:属性的
/// 去向在计划阶段就定了(没有 domain 的根本建不出来),事后补 domain 改不了它
/// 已经是不是一列的事实
pub async fn relink_domains_ranges(
state: &AppState,
kb_id: Uuid,
filename: &str,
bytes: &[u8],
) -> AppResult<(usize, usize)> {
let format = RdfFormat::detect(filename, bytes);
let proj = ontology_rdf::project(bytes, format).map_err(|e| {
utopia_core::AppError::invalid_detail(
"bad_ontology_file",
"Could not parse this ontology file",
e.to_string(),
)
})?;
let classes: HashMap<String, Uuid> = utopia_store::graph::entity_types(&state.pool, kb_id)
.await?
.into_iter()
.filter_map(|t| t.iri.clone().map(|i| (i, t.id)))
.collect();
let relations: HashMap<String, Uuid> = utopia_store::graph::relation_types(&state.pool, kb_id)
.await?
.into_iter()
.filter_map(|r| r.iri.clone().map(|i| (i, r.id)))
.collect();
let mut link_d: Vec<(Uuid, Uuid)> = Vec::new();
let mut link_r: Vec<(Uuid, Uuid)> = Vec::new();
for p in &proj.properties {
if p.is_datatype {
continue;
}
let Some(&rid) = relations.get(&p.iri) else {
continue;
};
link_d.extend(
p.domains
.iter()
.filter_map(|d| classes.get(d).map(|&t| (rid, t))),
);
link_r.extend(
p.ranges
.iter()
.filter_map(|r| classes.get(r).map(|&t| (rid, t))),
);
}
utopia_store::ontology::link_domains_ranges_bulk(&state.pool, &link_d, &link_r).await?;
Ok((link_d.len(), link_r.len()))
}

/// 执行计划。属性在类之后落库——它们要挂在 domain 上,而 domain 要等
/// 类先建好并解析 IRI → id(就是下面那个 `id_of`)。
pub async fn apply(
Expand Down Expand Up @@ -500,9 +558,20 @@ pub async fn apply(
continue;
};
// domain/range 指向没被建出来的类时只丢那一个,不丢整条关系:
// 关系不像属性那样必须挂在类上,没有 domain 就是"不限主语类型"
// 关系不像属性那样必须挂在类上,没有 domain 就是"不限主语类型"。
//
// **库里已有的类也算数。** 从前只认本文件里的类,于是 W3C Org 的
// `headOf rdfs:domain foaf:Agent` 在 FOAF 已经装好的库里照样丢 domain,
// 而没有 domain 的谓词 `judge_direction` 根本不判方向(#222)
let resolve = |iris: &[String]| -> Vec<Uuid> {
iris.iter().filter_map(|i| id_of.get(i).copied()).collect()
iris.iter()
.filter_map(|i| {
id_of
.get(i)
.copied()
.or_else(|| existing_by_iri.get(i).copied())
})
.collect()
};
let domains = resolve(&p.domains);
let ranges = resolve(&p.ranges);
Expand Down
60 changes: 50 additions & 10 deletions crates/utopia-store/src/ontology.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1784,23 +1784,63 @@ pub enum Fit {
/// **三条写谓词的路共用的那一道判断**(#190 / #196):抽取落新事实、采纳把谓词
/// 挂回旧事实、合并换掉主语——从前只有抽取查,另外两条各自绕了过去。
///
/// 判据刻意窄(0012):只看 domain,只在**主语违反且宾语符合**时对调,两边都对不上
/// 就留空谓词。参数顺序不是关于世界的断言,是这个 key 的编码约定,所以本体在这一处
/// 是执法的;哪些类型能参与仍是引导,不在这里裁。
/// 判据刻意窄(0012):只看签名,只在**正向违反而反向成立**时对调,两个方向都
/// 对不上就留空谓词。参数顺序不是关于世界的断言,是这个 key 的编码约定,所以本体
/// 在这一处是执法的;哪些类型能参与仍是引导,不在这里裁。
///
/// **range 也算进来**(#222)。从前只看 domain:`headOf` 的 domain 是 Agent,
/// schema.org 里 Project 也是 Organization 也是 Agent,于是 `Project Aurora head_of
/// Li Ting` 主语过关就 Keep,宾语是个人、range 要 Organization 这件事没人看。
/// 现在两端各看各的:正向两端都不违反才 Keep;否则反过来两端都不违反才 Swap。
/// 没判出类型的实体在 range 这一端不算违反("不知道"不是"不符合",与
/// `signature_breaks` 同一条纪律);domain 那一端沿用旧规矩,那是 0012 定下的
pub async fn judge_direction(
pool: &PgPool,
relation_type_id: Uuid,
subject_id: Uuid,
object_id: Uuid,
) -> AppResult<Fit> {
match entity_fits_domain(pool, relation_type_id, subject_id).await? {
None => Ok(Fit::Unchecked),
Some(true) => Ok(Fit::Keep),
Some(false) => match entity_fits_domain(pool, relation_type_id, object_id).await? {
Some(true) => Ok(Fit::Swap),
_ => Ok(Fit::Neither),
},
let subject_in_domain = entity_fits_domain(pool, relation_type_id, subject_id).await?;
let object_in_range = entity_fits_range(pool, relation_type_id, object_id).await?;
if subject_in_domain.is_none() && object_in_range.is_none() {
return Ok(Fit::Unchecked);
}
if subject_in_domain != Some(false) && object_in_range != Some(false) {
return Ok(Fit::Keep);
}
let object_in_domain = entity_fits_domain(pool, relation_type_id, object_id).await?;
let subject_in_range = entity_fits_range(pool, relation_type_id, subject_id).await?;
if object_in_domain != Some(false) && subject_in_range != Some(false) {
return Ok(Fit::Swap);
}
Ok(Fit::Neither)
}

/// [`entity_fits_domain`] 的 range 版。多一条规矩:实体还没判出类型 → None,
/// 不当违反——range 这一端是新加的判据(#222),不该让未分类实体的事实因此
/// 丢掉谓词
pub async fn entity_fits_range(
pool: &PgPool,
relation_type_id: Uuid,
entity_id: Uuid,
) -> AppResult<Option<bool>> {
let (declared, typed, ok): (i64, bool, i64) = sqlx::query_as(
"WITH RECURSIVE up(id) AS (
SELECT type_id FROM entities WHERE id = $2
UNION
SELECT p.parent_id FROM entity_type_parents p JOIN up ON p.child_id = up.id
)
SELECT (SELECT count(*) FROM relation_type_ranges WHERE relation_type_id = $1),
(SELECT type_id IS NOT NULL FROM entities WHERE id = $2),
(SELECT count(*) FROM relation_type_ranges g
JOIN up ON up.id = g.entity_type_id
WHERE g.relation_type_id = $1)",
)
.bind(relation_type_id)
.bind(entity_id)
.fetch_one(pool)
.await?;
Ok((declared > 0 && typed).then_some(ok > 0))
}

/// 把 `owl:inverseOf` / `rdfs:subPropertyOf` 从 IRI 解析成 id。
Expand Down
Loading
Loading