knowledge: entity-graph endpoint for force-directed view
New GET /knowledge/graph?type=&limit= returns the data the
curation UI's graph tab needs:
- nodes = entities with at least one in-scope fact (rejected /
superseded excluded). Carries fact_count for visual sizing.
Top-N by count desc; default cap 200 (clamped 1..1000).
- edges = relational facts (object_entity_id set) grouped by
(subject, object, predicate) so 3 "is_friend_of" facts
between the same pair collapse into one edge with count=3.
Two raw SQL queries: an INNER JOIN onto a persona-scoped fact-
count subquery for nodes (skips 0-fact entities entirely so the
sim doesn't waste time on disconnected islands), then a follow-
up GROUP BY over the persona-scoped fact set restricted to the
node id set via IN clauses (ids are i32 so inlining is safe).
Pairs with the Apollo-side GraphPanel that runs d3-force over
the returned payload and renders SVG with click-to-open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,8 @@ use std::sync::Mutex;
|
||||
use crate::data::Claims;
|
||||
use crate::database::models::{Entity, EntityFact, EntityPhotoLink, InsertEntityFact};
|
||||
use crate::database::{
|
||||
ConsolidationGroup, EntityFilter, EntityPatch, EntitySort, FactFilter, FactPatch, KnowledgeDao,
|
||||
PersonaFilter, RecentActivity,
|
||||
ConsolidationGroup, EntityFilter, EntityGraph, EntityPatch, EntitySort, FactFilter, FactPatch,
|
||||
KnowledgeDao, PersonaFilter, RecentActivity,
|
||||
};
|
||||
use crate::personas::PersonaDaoData;
|
||||
use crate::state::AppState;
|
||||
@@ -330,6 +330,35 @@ pub struct RecentQuery {
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GraphQuery {
|
||||
#[serde(rename = "type")]
|
||||
pub entity_type: Option<String>,
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GraphNodeView {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub entity_type: String,
|
||||
pub fact_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GraphEdgeView {
|
||||
pub source: i32,
|
||||
pub target: i32,
|
||||
pub predicate: String,
|
||||
pub count: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GraphResponse {
|
||||
pub nodes: Vec<GraphNodeView>,
|
||||
pub edges: Vec<GraphEdgeView>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ConsolidationQuery {
|
||||
/// Cosine threshold for clustering. Default 0.85 — looser than
|
||||
@@ -391,7 +420,8 @@ where
|
||||
.service(
|
||||
web::resource("/consolidation-proposals")
|
||||
.route(web::get().to(get_consolidation_proposals::<D>)),
|
||||
),
|
||||
)
|
||||
.service(web::resource("/graph").route(web::get().to(get_graph::<D>))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1162,6 +1192,45 @@ async fn get_recent<D: KnowledgeDao + 'static>(
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_graph<D: KnowledgeDao + 'static>(
|
||||
req: HttpRequest,
|
||||
claims: Claims,
|
||||
query: web::Query<GraphQuery>,
|
||||
dao: web::Data<Mutex<D>>,
|
||||
persona_dao: PersonaDaoData,
|
||||
) -> impl Responder {
|
||||
let limit = query.limit.unwrap_or(200).clamp(1, 1000) as usize;
|
||||
let persona = resolve_persona_filter(&req, &claims, &persona_dao);
|
||||
let cx = opentelemetry::Context::current();
|
||||
let mut dao = dao.lock().expect("Unable to lock KnowledgeDao");
|
||||
match dao.build_entity_graph(&cx, query.entity_type.as_deref(), limit, &persona) {
|
||||
Ok(EntityGraph { nodes, edges }) => HttpResponse::Ok().json(GraphResponse {
|
||||
nodes: nodes
|
||||
.into_iter()
|
||||
.map(|n| GraphNodeView {
|
||||
id: n.id,
|
||||
name: n.name,
|
||||
entity_type: n.entity_type,
|
||||
fact_count: n.fact_count,
|
||||
})
|
||||
.collect(),
|
||||
edges: edges
|
||||
.into_iter()
|
||||
.map(|e| GraphEdgeView {
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
predicate: e.predicate,
|
||||
count: e.count,
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
Err(e) => {
|
||||
log::error!("build_entity_graph error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_consolidation_proposals<D: KnowledgeDao + 'static>(
|
||||
req: HttpRequest,
|
||||
claims: Claims,
|
||||
|
||||
Reference in New Issue
Block a user