knowledge: list sort + persona-scoped fact_count per entity
Two related additions to /knowledge/entities: - New EntitySort enum (UpdatedDesc default, NameAsc, FactCountDesc) surfaced via `?sort=updated|name|count`. NameAsc clusters near- duplicate names so dupes stand out at a glance; FactCountDesc surfaces heavily-used entities and demotes 0-fact noise to the bottom. - New `list_entities_with_fact_counts` DAO method that returns each entity alongside a persona-scoped count of its non-rejected facts (subject side). Persona scope follows X-Persona-Id via the existing resolve_persona_filter chain — Single filters on (user_id, persona_id), All unions across the user's personas. Implemented as one raw SQL query with a LEFT JOIN to a fact-count subquery and ORDER BY tied to the chosen sort, so count-sort needs no second round trip. The agent's existing list_entities call site is unchanged — it doesn't need persona-scoped counts and the trait method stays cheap. EntitySummary grows an Option<i64> fact_count (skip_serializing_if none) so PATCH responses stay shaped as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,8 @@ use std::sync::Mutex;
|
||||
use crate::data::Claims;
|
||||
use crate::database::models::{Entity, EntityFact, EntityPhotoLink, InsertEntityFact};
|
||||
use crate::database::{
|
||||
EntityFilter, EntityPatch, FactFilter, FactPatch, KnowledgeDao, PersonaFilter, RecentActivity,
|
||||
EntityFilter, EntityPatch, EntitySort, FactFilter, FactPatch, KnowledgeDao, PersonaFilter,
|
||||
RecentActivity,
|
||||
};
|
||||
use crate::personas::PersonaDaoData;
|
||||
|
||||
@@ -57,6 +58,11 @@ pub struct EntitySummary {
|
||||
pub status: String,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
/// Persona-scoped count of non-rejected facts about this entity
|
||||
/// (subject side). 0 when not provided by the call site, e.g.
|
||||
/// PATCH responses return the bare entity without scoping context.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fact_count: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<Entity> for EntitySummary {
|
||||
@@ -70,10 +76,19 @@ impl From<Entity> for EntitySummary {
|
||||
status: e.status,
|
||||
created_at: e.created_at,
|
||||
updated_at: e.updated_at,
|
||||
fact_count: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EntitySummary {
|
||||
fn from_entity_with_count(e: Entity, fact_count: i64) -> Self {
|
||||
let mut s = EntitySummary::from(e);
|
||||
s.fact_count = Some(fact_count);
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EntityListResponse {
|
||||
pub entities: Vec<EntitySummary>,
|
||||
@@ -197,6 +212,9 @@ pub struct EntityListQuery {
|
||||
pub entity_type: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub search: Option<String>,
|
||||
/// "updated" (default) | "name" | "count". `count` is persona-scoped
|
||||
/// via the X-Persona-Id header.
|
||||
pub sort: Option<String>,
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
@@ -253,9 +271,11 @@ where
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn list_entities<D: KnowledgeDao + 'static>(
|
||||
_claims: Claims,
|
||||
req: HttpRequest,
|
||||
claims: Claims,
|
||||
query: web::Query<EntityListQuery>,
|
||||
dao: web::Data<Mutex<D>>,
|
||||
persona_dao: PersonaDaoData,
|
||||
) -> impl Responder {
|
||||
let limit = query.limit.unwrap_or(50).min(200);
|
||||
let offset = query.offset.unwrap_or(0);
|
||||
@@ -266,6 +286,15 @@ async fn list_entities<D: KnowledgeDao + 'static>(
|
||||
Some(s) => Some(s.to_string()),
|
||||
};
|
||||
|
||||
let sort = match query.sort.as_deref() {
|
||||
Some("name") => EntitySort::NameAsc,
|
||||
Some("count") => EntitySort::FactCountDesc,
|
||||
// "updated" or anything else falls through to the default.
|
||||
_ => EntitySort::UpdatedDesc,
|
||||
};
|
||||
|
||||
let persona = resolve_persona_filter(&req, &claims, &persona_dao);
|
||||
|
||||
let filter = EntityFilter {
|
||||
entity_type: query.entity_type.clone(),
|
||||
status: status_filter,
|
||||
@@ -276,10 +305,12 @@ async fn list_entities<D: KnowledgeDao + 'static>(
|
||||
|
||||
let cx = opentelemetry::Context::current();
|
||||
let mut dao = dao.lock().expect("Unable to lock KnowledgeDao");
|
||||
match dao.list_entities(&cx, filter) {
|
||||
Ok((entities, total)) => {
|
||||
let summaries: Vec<EntitySummary> =
|
||||
entities.into_iter().map(EntitySummary::from).collect();
|
||||
match dao.list_entities_with_fact_counts(&cx, filter, sort, &persona) {
|
||||
Ok((pairs, total)) => {
|
||||
let summaries: Vec<EntitySummary> = pairs
|
||||
.into_iter()
|
||||
.map(|(e, c)| EntitySummary::from_entity_with_count(e, c))
|
||||
.collect();
|
||||
HttpResponse::Ok().json(EntityListResponse {
|
||||
entities: summaries,
|
||||
total,
|
||||
|
||||
Reference in New Issue
Block a user