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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ jobs:
failed=$(grep -o '[0-9]* failed' store-tests.log | awk '{s+=$1} END {print s+0}')
echo "utopia-store against Postgres: **${passed} passed**, ${failed} failed — a missing database fails this job instead of skipping" >> "$GITHUB_STEP_SUMMARY"

- name: MCP structured reads against Postgres
run: cargo test -p utopia-server api::mcp::tests
env:
UTOPIA_DATABASE_URL: postgres://utopia:utopia@localhost:5432/utopia
UTOPIA_TEST_REQUIRE_DB: "1"

web:
runs-on: ubuntu-latest
defaults:
Expand Down
11 changes: 11 additions & 0 deletions crates/utopia-core/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,10 @@ pub struct GraphEdge {
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
pub struct EntityFact {
pub id: Uuid,
pub recorded_at: DateTime<Utc>,
pub invalidated_at: Option<DateTime<Utc>>,
pub supersedes: Option<Uuid>,
pub document_ids: Vec<Uuid>,
/// out = 该实体为主语;in = 为宾语
pub direction: String,
/// 本体没认下这条关系时回落到原文说法;两者都拿不出时为 None(更早的历史数据长这样)
Expand Down Expand Up @@ -1217,6 +1221,13 @@ pub struct OntologyDefect {
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
pub struct DerivedFactView {
pub id: Uuid,
pub predicate_id: Uuid,
pub object_value: Option<serde_json::Value>,
pub rule_id: Option<Uuid>,
pub attribute_rule_id: Option<Uuid>,
pub invalidated_at: Option<DateTime<Utc>>,
pub valid_from_precision: Option<String>,
pub valid_to_precision: Option<String>,
pub subject_id: Uuid,
pub subject: String,
/// 字面值结论(业务规则的归类与属性)没有实体宾语(0021)
Expand Down
5 changes: 3 additions & 2 deletions crates/utopia-server/src/api/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ pub(super) fn tools_schema(can_write: bool, data_source_names: &[String]) -> ser
///
/// 判据直接取自工具表里的 `required`:加一个必填参数,这里自动跟上,
/// 不必记得来改第二处。
fn check_call(
pub(super) fn check_call(
tools: &serde_json::Value,
name: &str,
raw_args: &str,
Expand Down Expand Up @@ -980,7 +980,8 @@ pub async fn chat(
via_token: None,
question: Some(&query),
};
let (result, step) = tools::dispatch(&ctx, &mut sink, &call.name, &args).await;
let tools::ToolResult { text: result, step, .. } =
tools::dispatch(&ctx, &mut sink, &call.name, &args).await;
// **这一步发生在正文的哪个位置。**
//
// 模型是边说边调的:说一句、查一下、再说一句。SSE 上 `delta` 与
Expand Down
36 changes: 28 additions & 8 deletions crates/utopia-server/src/api/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ fn ok(id: Option<Value>, result: Value) -> Json<Value> {
Json(json!({ "jsonrpc": "2.0", "id": id, "result": result }))
}

fn tool_result(result: tools::ToolResult) -> Value {
let mut response = json!({
"content": [{ "type": "text", "text": result.text }],
"isError": result.is_error,
});
if let Some(content) = result.structured_content {
response["structuredContent"] = content;
}
response
}

/// JSON-RPC 的错误不是 HTTP 的错误:**传输成功了,方法失败了**。
/// 回 200 带 error 体,客户端才解析得动。
fn rpc_err(id: Option<Value>, code: i64, message: &str) -> Json<Value> {
Expand Down Expand Up @@ -207,6 +218,17 @@ pub async fn handle(
};
return Ok(rpc_err(id, -32601, &message));
}
// 与聊天共用参数守卫:缺少 query 不能变成一次成功的空搜索。
if let Err((text, step)) = super::chat::check_call(
&super::chat::tools_schema(can_write, &[]),
name,
&args.to_string(),
) {
return Ok(ok(
id,
tool_result(tools::ToolResult::new(text, step).error()),
));
}
// `mounted_sources` 仍旧空着:`query_data` 没放出来,给了也没人用。
// `can_write` 不再写死 false——它现在是令牌与角色一起算出来的
let ctx = ToolCtx {
Expand All @@ -222,7 +244,7 @@ pub async fn handle(
question: None,
};
let mut sink = ToolSink::default();
let (text, _step) = tools::dispatch(&ctx, &mut sink, name, &args).await;
let result = tools::dispatch(&ctx, &mut sink, name, &args).await;
let _ = utopia_store::audit::record(
&state.pool,
Some(kb_id),
Expand All @@ -233,14 +255,12 @@ pub async fn handle(
json!({ "tool": name }),
)
.await;
ok(
id,
json!({
"content": [{ "type": "text", "text": text }],
"isError": false,
}),
)
ok(id, tool_result(result))
}
other => rpc_err(id, -32601, &format!("Unknown method: {other}")),
})
}

#[cfg(test)]
#[path = "mcp_tests.rs"]
mod tests;
Loading
Loading