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
55 changes: 55 additions & 0 deletions crates/utopia-server/src/mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,39 @@ use uuid::Uuid;

const MAX_SCHEMA_CHARS: usize = 12_000;

/// 探索把 schema 里的量与维度落成 Metric / Dimension 实体,而这两个类不在任何
/// 内置本体包里——0009 之后建库不再自带类。没有它们,下面的 `type_id` 查不到,
/// 每条提议都被 `continue` 吞掉,页面只说"已排队"就再无下文(#223)。
/// 所以探索前把两个类补上:builtin,描述给抽取提示词,本体页可以改
async fn ensure_concept_types(pool: &sqlx::PgPool, kb_id: Uuid) -> anyhow::Result<()> {
for (key, label, description) in [
(
"metric",
"Metric",
"An aggregatable business quantity (revenue, order count, average ticket) that maps to a definition in a mounted database.",
),
(
"dimension",
"Dimension",
"A group-by attribute (region, month, product line) that maps to a column in a mounted database.",
),
] {
sqlx::query(
"INSERT INTO entity_types (id, kb_id, key, label, builtin, description)
SELECT $1, $2, $3, $4, TRUE, $5
WHERE NOT EXISTS (SELECT 1 FROM entity_types WHERE kb_id = $2 AND key = $3)",
)
.bind(Uuid::now_v7())
.bind(kb_id)
.bind(key)
.bind(label)
.bind(description)
.execute(pool)
.await?;
}
Ok(())
}

pub async fn explore_mappings(state: &AppState, kb_id: Uuid) -> anyhow::Result<()> {
let kb = utopia_store::kbs::get(&state.pool, kb_id).await?;
let settings = utopia_store::settings::get(&state.pool, kb.workspace_id)
Expand All @@ -27,6 +60,7 @@ pub async fn explore_mappings(state: &AppState, kb_id: Uuid) -> anyhow::Result<(
if sources.is_empty() {
anyhow::bail!("No data sources mounted");
}
ensure_concept_types(&state.pool, kb_id).await?;

// 各源 schema(引擎直读,保证新鲜;限量防 prompt 爆炸)
let mut schema_txt = String::new();
Expand Down Expand Up @@ -168,6 +202,27 @@ pub async fn explore_mappings(state: &AppState, kb_id: Uuid) -> anyhow::Result<(
}

tracing::info!(%kb_id, proposals = accepted, "映射探索完成,提议已入审核队列");
// 一条都没提出来时页面上什么都不会变——Pending 还是 0,而"已排队"那句
// 早就翻篇了。走告警中心说一声,人才知道该去刷新结构或给列加注释
if accepted == 0 {
if let Err(e) = utopia_store::alerts::raise(
&state.pool,
utopia_store::alerts::NewAlert {
kb_id: Some(kb_id),
severity: "info",
kind: utopia_store::alerts::kind::MAPPING_EXPLORATION_EMPTY,
min_role: utopia_core::models::Role::Editor,
subject_type: None,
subject_id: None,
detail: serde_json::json!({ "proposals": 0, "sources": source_names }),
},
)
.await
{
tracing::warn!(%kb_id, error = %e, "映射探索空结果的告警没写进去");
}
state.emit_alert();
}
state.emit_review(kb_id);
Ok(())
}
3 changes: 3 additions & 0 deletions crates/utopia-store/src/alerts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ pub mod kind {
/// 哪些表——`query_data` 照样入列,模型却只能瞎猜列名。挂载那一刻的报错
/// 只有点按钮的人看得见,此后这个库就一直这样静默地缺着。
pub const SCHEMA_SYNC_FAILED: &str = "data_source.schema_sync_failed";
/// 库级:映射探索跑完了,一条口径都没提出来。`severity = info`,`min_role = editor`——
/// 不是故障,是"你在等的那件事没有结果",而页面上没有别的地方能说这句话(#223)
pub const MAPPING_EXPLORATION_EMPTY: &str = "mapping.exploration_empty";
}

/// 一次故障。打包成结构体不只是为了参数个数——调用点写 `severity: "error"`
Expand Down
7 changes: 6 additions & 1 deletion web/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ export const en = {
title: "A data source is mounted, but its schema is not",
hint: "Ask cannot see which tables exist, so it will guess column names. Check the connection, then use Refresh schema.",
},
"mapping.exploration_empty": {
title: "Mapping exploration proposed nothing",
hint: "The model read the mounted schema but found no metric or dimension to propose. Refresh the schema under Data mapping > Data sources, add column comments if you can, then run Explore again.",
},
"llm.unreachable": {
title: "The model endpoint gave no usable answer",
hint: "Extraction and embedding are stopped. Check the endpoint URL in system settings.",
Expand Down Expand Up @@ -1214,7 +1218,8 @@ export const en = {
explore: "Explore mappings",
exploreHint:
"An agent reads these schemas and proposes metric and dimension definitions. Proposals land in Pending; Ask uses them only once confirmed.",
exploreQueued: "Exploration queued — proposals will appear under Pending.",
exploreQueued:
"Exploration queued — proposals will appear under Pending. If nothing can be proposed, the alert bell will say so.",
sourcesEmpty: "No data sources mounted.",
sourcesNoneAvailable:
"No data sources registered yet — ask a deployment admin to register one.",
Expand Down
6 changes: 5 additions & 1 deletion web/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ export const zh: Strings = {
title: "数据源挂上了,库表结构没进来",
hint: "问数看不见有哪些表,只能猜列名。检查连接串,然后点「刷新结构」。",
},
"mapping.exploration_empty": {
title: "映射探索没有提出任何口径",
hint: "模型读了挂载的库表结构,但没找到可提的指标或维度。到「数据映射 > 数据源」刷新结构、给列加注释,再探索一次。",
},
"llm.unreachable": {
title: "模型端点没有给出可用的回答",
hint: "抽取与向量化已停摆。去系统设置里检查端点地址。",
Expand Down Expand Up @@ -1095,7 +1099,7 @@ export const zh: Strings = {
explore: "探查映射",
exploreHint:
"一个智能体读这些库表结构,提出指标/维度的口径。提出来的落在「待审批」,确认之后问数才会用。",
exploreQueued: "探查已排队——提案稍后出现在「待审批」。",
exploreQueued: "探索已排队,提议会出现在「待确认」里;一条都提不出来时,铃铛会告诉你。",
sourcesEmpty: "没有挂载任何数据源。",
sourcesNoneAvailable: "还没有登记数据源——请部署管理员登记一个。",
newConn: "登记新连接",
Expand Down
2 changes: 2 additions & 0 deletions web/src/useKbEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export function useKbEvents(kbId: string | undefined) {
});
es.addEventListener("review", () => {
queryClient.invalidateQueries({ queryKey: ["review", kbId] });
// 映射探索跑完发的也是 review:Pending 那一栏得跟着刷新
queryClient.invalidateQueries({ queryKey: ["mappings", kbId] });
});
// 一句记忆抽出了等人点头的事实(0015):对话里那张确认卡跟着长出来
es.addEventListener("pending", () => {
Expand Down
Loading