mirror of
https://github.com/perstarkse/minne.git
synced 2026-07-06 21:05:17 +02:00
fix: better default naming of relationships
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
## Unreleased
|
## Unreleased
|
||||||
- Added an opt-in FastEmbed-based reranking stage behind `reranking_enabled`. It improves retrieval accuracy by re-scoring hybrid results.
|
- Added an opt-in FastEmbed-based reranking stage behind `reranking_enabled`. It improves retrieval accuracy by re-scoring hybrid results.
|
||||||
|
- Fix: default name for relationships harmonized across application
|
||||||
|
|
||||||
## Version 0.2.5 (2025-10-24)
|
## Version 0.2.5 (2025-10-24)
|
||||||
- Added manual knowledge entity creation flows using a modal, with the option for suggested relationships
|
- Added manual knowledge entity creation flows using a modal, with the option for suggested relationships
|
||||||
|
|||||||
@@ -39,11 +39,87 @@ use crate::{
|
|||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
|
|
||||||
const KNOWLEDGE_ENTITIES_PER_PAGE: usize = 12;
|
const KNOWLEDGE_ENTITIES_PER_PAGE: usize = 12;
|
||||||
const DEFAULT_RELATIONSHIP_TYPE: &str = "relates_to";
|
const RELATIONSHIP_TYPE_OPTIONS: &[&str] = &["RelatedTo", "RelevantTo", "SimilarTo", "References"];
|
||||||
|
const DEFAULT_RELATIONSHIP_TYPE: &str = RELATIONSHIP_TYPE_OPTIONS[0];
|
||||||
const MAX_RELATIONSHIP_SUGGESTIONS: usize = 10;
|
const MAX_RELATIONSHIP_SUGGESTIONS: usize = 10;
|
||||||
const SUGGESTION_MIN_SCORE: f32 = 0.5;
|
const SUGGESTION_MIN_SCORE: f32 = 0.5;
|
||||||
|
|
||||||
const GRAPH_REFRESH_TRIGGER: &str = r#"{"knowledge-graph-refresh":true}"#;
|
const GRAPH_REFRESH_TRIGGER: &str = r#"{"knowledge-graph-refresh":true}"#;
|
||||||
|
const RELATIONSHIP_TYPE_ALIASES: &[(&str, &str)] = &[("relatesto", "RelatedTo")];
|
||||||
|
|
||||||
|
fn relationship_type_or_default(value: Option<&str>) -> String {
|
||||||
|
match value {
|
||||||
|
Some(raw) => canonicalize_relationship_type(raw),
|
||||||
|
None => DEFAULT_RELATIONSHIP_TYPE.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canonicalize_relationship_type(value: &str) -> String {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return DEFAULT_RELATIONSHIP_TYPE.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let key: String = trimmed
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_ascii_alphanumeric())
|
||||||
|
.flat_map(|c| c.to_lowercase())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for option in RELATIONSHIP_TYPE_OPTIONS {
|
||||||
|
let option_key: String = option
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_ascii_alphanumeric())
|
||||||
|
.flat_map(|c| c.to_lowercase())
|
||||||
|
.collect();
|
||||||
|
if option_key == key {
|
||||||
|
return (*option).to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (alias, target) in RELATIONSHIP_TYPE_ALIASES {
|
||||||
|
if *alias == key {
|
||||||
|
return (*target).to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = String::new();
|
||||||
|
for segment in trimmed
|
||||||
|
.split(|c: char| !c.is_ascii_alphanumeric())
|
||||||
|
.filter(|segment| !segment.is_empty())
|
||||||
|
{
|
||||||
|
let mut chars = segment.chars();
|
||||||
|
if let Some(first) = chars.next() {
|
||||||
|
result.extend(first.to_uppercase());
|
||||||
|
for ch in chars {
|
||||||
|
result.extend(ch.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.is_empty() {
|
||||||
|
trimmed.to_string()
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_relationship_type_options(relationships: &[KnowledgeRelationship]) -> Vec<String> {
|
||||||
|
let mut options: HashSet<String> = RELATIONSHIP_TYPE_OPTIONS
|
||||||
|
.iter()
|
||||||
|
.map(|value| (*value).to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for relationship in relationships {
|
||||||
|
options.insert(canonicalize_relationship_type(
|
||||||
|
&relationship.metadata.relationship_type,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut options: Vec<String> = options.into_iter().collect();
|
||||||
|
options.sort();
|
||||||
|
options
|
||||||
|
}
|
||||||
|
|
||||||
fn respond_with_graph_refresh(response: TemplateResponse) -> Response {
|
fn respond_with_graph_refresh(response: TemplateResponse) -> Response {
|
||||||
let mut response = response.into_response();
|
let mut response = response.into_response();
|
||||||
@@ -70,6 +146,8 @@ pub async fn show_new_knowledge_entity_form(
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let existing_entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
let existing_entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
||||||
|
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
||||||
|
let relationship_type_options = collect_relationship_type_options(&relationships);
|
||||||
let empty_selected: HashSet<String> = HashSet::new();
|
let empty_selected: HashSet<String> = HashSet::new();
|
||||||
let empty_scores: HashMap<String, f32> = HashMap::new();
|
let empty_scores: HashMap<String, f32> = HashMap::new();
|
||||||
let relationship_options =
|
let relationship_options =
|
||||||
@@ -81,9 +159,10 @@ pub async fn show_new_knowledge_entity_form(
|
|||||||
entity_types,
|
entity_types,
|
||||||
relationship_list: RelationshipListData {
|
relationship_list: RelationshipListData {
|
||||||
relationship_options,
|
relationship_options,
|
||||||
relationship_type: DEFAULT_RELATIONSHIP_TYPE.to_string(),
|
relationship_type: relationship_type_or_default(None),
|
||||||
suggestion_count: 0,
|
suggestion_count: 0,
|
||||||
},
|
},
|
||||||
|
relationship_type_options,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -118,13 +197,7 @@ pub async fn create_knowledge_entity(
|
|||||||
|
|
||||||
state.db.store_item(new_entity.clone()).await?;
|
state.db.store_item(new_entity.clone()).await?;
|
||||||
|
|
||||||
let relationship_type = form
|
let relationship_type = relationship_type_or_default(form.relationship_type.as_deref());
|
||||||
.relationship_type
|
|
||||||
.as_deref()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.unwrap_or(DEFAULT_RELATIONSHIP_TYPE)
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
debug!("form: {:?}", form);
|
debug!("form: {:?}", form);
|
||||||
if !form.relationship_ids.is_empty() {
|
if !form.relationship_ids.is_empty() {
|
||||||
@@ -236,13 +309,7 @@ pub async fn suggest_knowledge_relationships(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let relationship_type = form
|
let relationship_type = relationship_type_or_default(form.relationship_type.as_deref());
|
||||||
.relationship_type
|
|
||||||
.as_deref()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.unwrap_or(DEFAULT_RELATIONSHIP_TYPE)
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let entities: Vec<KnowledgeEntity> = entity_lookup.into_values().collect();
|
let entities: Vec<KnowledgeEntity> = entity_lookup.into_values().collect();
|
||||||
let relationship_options =
|
let relationship_options =
|
||||||
@@ -262,7 +329,7 @@ pub async fn suggest_knowledge_relationships(
|
|||||||
pub struct KnowledgeBaseData {
|
pub struct KnowledgeBaseData {
|
||||||
entities: Vec<KnowledgeEntity>,
|
entities: Vec<KnowledgeEntity>,
|
||||||
visible_entities: Vec<KnowledgeEntity>,
|
visible_entities: Vec<KnowledgeEntity>,
|
||||||
relationships: Vec<KnowledgeRelationship>,
|
relationships: Vec<RelationshipTableRow>,
|
||||||
user: User,
|
user: User,
|
||||||
entity_types: Vec<String>,
|
entity_types: Vec<String>,
|
||||||
content_categories: Vec<String>,
|
content_categories: Vec<String>,
|
||||||
@@ -271,6 +338,8 @@ pub struct KnowledgeBaseData {
|
|||||||
conversation_archive: Vec<Conversation>,
|
conversation_archive: Vec<Conversation>,
|
||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
page_query: String,
|
page_query: String,
|
||||||
|
relationship_type_options: Vec<String>,
|
||||||
|
default_relationship_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -281,6 +350,12 @@ pub struct RelationshipOption {
|
|||||||
score: Option<f32>,
|
score: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RelationshipTableRow {
|
||||||
|
relationship: KnowledgeRelationship,
|
||||||
|
relationship_type_label: String,
|
||||||
|
}
|
||||||
|
|
||||||
fn build_relationship_options(
|
fn build_relationship_options(
|
||||||
entities: Vec<KnowledgeEntity>,
|
entities: Vec<KnowledgeEntity>,
|
||||||
selected_ids: &HashSet<String>,
|
selected_ids: &HashSet<String>,
|
||||||
@@ -320,6 +395,40 @@ fn build_relationship_options(
|
|||||||
options
|
options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_relationship_table_data(
|
||||||
|
entities: Vec<KnowledgeEntity>,
|
||||||
|
relationships: Vec<KnowledgeRelationship>,
|
||||||
|
) -> RelationshipTableData {
|
||||||
|
let relationship_type_options = collect_relationship_type_options(&relationships);
|
||||||
|
let mut frequency: HashMap<String, usize> = HashMap::new();
|
||||||
|
let relationships = relationships
|
||||||
|
.into_iter()
|
||||||
|
.map(|relationship| {
|
||||||
|
let relationship_type_label =
|
||||||
|
canonicalize_relationship_type(&relationship.metadata.relationship_type);
|
||||||
|
*frequency
|
||||||
|
.entry(relationship_type_label.clone())
|
||||||
|
.or_insert(0) += 1;
|
||||||
|
RelationshipTableRow {
|
||||||
|
relationship,
|
||||||
|
relationship_type_label,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let default_relationship_type = frequency
|
||||||
|
.into_iter()
|
||||||
|
.max_by_key(|(_, count)| *count)
|
||||||
|
.map(|(label, _)| label)
|
||||||
|
.unwrap_or_else(|| DEFAULT_RELATIONSHIP_TYPE.to_string());
|
||||||
|
|
||||||
|
RelationshipTableData {
|
||||||
|
entities,
|
||||||
|
relationships,
|
||||||
|
relationship_type_options,
|
||||||
|
default_relationship_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn build_knowledge_base_data(
|
async fn build_knowledge_base_data(
|
||||||
state: &HtmlState,
|
state: &HtmlState,
|
||||||
user: &User,
|
user: &User,
|
||||||
@@ -359,10 +468,16 @@ async fn build_knowledge_base_data(
|
|||||||
|
|
||||||
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
||||||
let entity_id_set: HashSet<String> = entities.iter().map(|e| e.id.clone()).collect();
|
let entity_id_set: HashSet<String> = entities.iter().map(|e| e.id.clone()).collect();
|
||||||
let relationships: Vec<KnowledgeRelationship> = relationships
|
let filtered_relationships: Vec<KnowledgeRelationship> = relationships
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|rel| entity_id_set.contains(&rel.in_) && entity_id_set.contains(&rel.out))
|
.filter(|rel| entity_id_set.contains(&rel.in_) && entity_id_set.contains(&rel.out))
|
||||||
.collect();
|
.collect();
|
||||||
|
let RelationshipTableData {
|
||||||
|
entities: _,
|
||||||
|
relationships,
|
||||||
|
relationship_type_options,
|
||||||
|
default_relationship_type,
|
||||||
|
} = build_relationship_table_data(entities.clone(), filtered_relationships);
|
||||||
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
||||||
|
|
||||||
Ok(KnowledgeBaseData {
|
Ok(KnowledgeBaseData {
|
||||||
@@ -377,6 +492,8 @@ async fn build_knowledge_base_data(
|
|||||||
conversation_archive,
|
conversation_archive,
|
||||||
pagination,
|
pagination,
|
||||||
page_query,
|
page_query,
|
||||||
|
relationship_type_options,
|
||||||
|
default_relationship_type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +508,7 @@ pub struct RelationshipListData {
|
|||||||
pub struct NewEntityModalData {
|
pub struct NewEntityModalData {
|
||||||
entity_types: Vec<String>,
|
entity_types: Vec<String>,
|
||||||
relationship_list: RelationshipListData,
|
relationship_list: RelationshipListData,
|
||||||
|
relationship_type_options: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -690,7 +808,7 @@ pub async fn get_knowledge_graph_json(
|
|||||||
links.push(GraphLink {
|
links.push(GraphLink {
|
||||||
source: rel.out.clone(),
|
source: rel.out.clone(),
|
||||||
target: rel.in_.clone(),
|
target: rel.in_.clone(),
|
||||||
relationship_type: rel.metadata.relationship_type.clone(),
|
relationship_type: canonicalize_relationship_type(&rel.metadata.relationship_type),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -874,7 +992,9 @@ pub async fn delete_knowledge_entity(
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct RelationshipTableData {
|
pub struct RelationshipTableData {
|
||||||
entities: Vec<KnowledgeEntity>,
|
entities: Vec<KnowledgeEntity>,
|
||||||
relationships: Vec<KnowledgeRelationship>,
|
relationships: Vec<RelationshipTableRow>,
|
||||||
|
relationship_type_options: Vec<String>,
|
||||||
|
default_relationship_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_knowledge_relationship(
|
pub async fn delete_knowledge_relationship(
|
||||||
@@ -887,14 +1007,12 @@ pub async fn delete_knowledge_relationship(
|
|||||||
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
||||||
|
|
||||||
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
||||||
|
let table_data = build_relationship_table_data(entities, relationships);
|
||||||
|
|
||||||
// Render updated list
|
// Render updated list
|
||||||
Ok(respond_with_graph_refresh(TemplateResponse::new_template(
|
Ok(respond_with_graph_refresh(TemplateResponse::new_template(
|
||||||
"knowledge/relationship_table.html",
|
"knowledge/relationship_table.html",
|
||||||
RelationshipTableData {
|
table_data,
|
||||||
entities,
|
|
||||||
relationships,
|
|
||||||
},
|
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -911,12 +1029,13 @@ pub async fn save_knowledge_relationship(
|
|||||||
Form(form): Form<SaveKnowledgeRelationshipInput>,
|
Form(form): Form<SaveKnowledgeRelationshipInput>,
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
// Construct relationship
|
// Construct relationship
|
||||||
|
let relationship_type = canonicalize_relationship_type(&form.relationship_type);
|
||||||
let relationship = KnowledgeRelationship::new(
|
let relationship = KnowledgeRelationship::new(
|
||||||
form.in_,
|
form.in_,
|
||||||
form.out,
|
form.out,
|
||||||
user.id.clone(),
|
user.id.clone(),
|
||||||
"manual".into(),
|
"manual".into(),
|
||||||
form.relationship_type,
|
relationship_type,
|
||||||
);
|
);
|
||||||
|
|
||||||
relationship.store_relationship(&state.db).await?;
|
relationship.store_relationship(&state.db).await?;
|
||||||
@@ -924,13 +1043,11 @@ pub async fn save_knowledge_relationship(
|
|||||||
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
||||||
|
|
||||||
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
||||||
|
let table_data = build_relationship_table_data(entities, relationships);
|
||||||
|
|
||||||
// Render updated list
|
// Render updated list
|
||||||
Ok(respond_with_graph_refresh(TemplateResponse::new_template(
|
Ok(respond_with_graph_refresh(TemplateResponse::new_template(
|
||||||
"knowledge/relationship_table.html",
|
"knowledge/relationship_table.html",
|
||||||
RelationshipTableData {
|
table_data,
|
||||||
entities,
|
|
||||||
relationships,
|
|
||||||
},
|
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,13 @@ hx-swap="outerHTML"
|
|||||||
<label class="flex items-center gap-2">
|
<label class="flex items-center gap-2">
|
||||||
<span class="text-xs uppercase tracking-wide opacity-70">Type</span>
|
<span class="text-xs uppercase tracking-wide opacity-70">Type</span>
|
||||||
<input type="text" name="relationship_type" value="{{ relationship_list.relationship_type }}"
|
<input type="text" name="relationship_type" value="{{ relationship_list.relationship_type }}"
|
||||||
class="nb-input w-28" placeholder="relates_to">
|
class="nb-input w-32" placeholder="RelatedTo" list="relationship-type-options">
|
||||||
</label>
|
</label>
|
||||||
|
<datalist id="relationship-type-options">
|
||||||
|
{% for rel_type in relationship_type_options %}
|
||||||
|
<option value="{{ rel_type }}"></option>
|
||||||
|
{% endfor %}
|
||||||
|
</datalist>
|
||||||
<button type="button" class="nb-btn btn-sm nb-cta sm:ml-2" hx-post="/knowledge-entity/suggestions"
|
<button type="button" class="nb-btn btn-sm nb-cta sm:ml-2" hx-post="/knowledge-entity/suggestions"
|
||||||
hx-target="#relationship-list" hx-swap="outerHTML" hx-include="#modal_form">
|
hx-target="#relationship-list" hx-swap="outerHTML" hx-include="#modal_form">
|
||||||
Suggest Relationships
|
Suggest Relationships
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for relationship in relationships %}
|
{% for row in relationships %}
|
||||||
|
{% set relationship = row.relationship %}
|
||||||
<tr>
|
<tr>
|
||||||
<!-- Origin column -->
|
<!-- Origin column -->
|
||||||
<td>
|
<td>
|
||||||
@@ -30,7 +31,7 @@
|
|||||||
{{ relationship.out }}
|
{{ relationship.out }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="uppercase tracking-wide text-xs">{{ relationship.metadata.relationship_type }}</td>
|
<td class="uppercase tracking-wide text-xs">{{ row.relationship_type_label }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="nb-btn btn-xs" hx-delete="/knowledge-relationship/{{ relationship.id }}"
|
<button class="nb-btn btn-xs" hx-delete="/knowledge-relationship/{{ relationship.id }}"
|
||||||
hx-target="#relationship_table_section" hx-swap="outerHTML">
|
hx-target="#relationship_table_section" hx-swap="outerHTML">
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input id="relationship_type_input" name="relationship_type" type="text" placeholder="RelatedTo"
|
<input id="relationship_type_input" name="relationship_type" type="text" placeholder="RelatedTo"
|
||||||
class="nb-input w-full new_relationship_input" />
|
class="nb-input w-full new_relationship_input" value="{{ default_relationship_type }}" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button id="save_relationship_button" type="button" class="nb-btn btn-sm" hx-post="/knowledge-relationship"
|
<button id="save_relationship_button" type="button" class="nb-btn btn-sm" hx-post="/knowledge-relationship"
|
||||||
|
|||||||
Reference in New Issue
Block a user