mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-03 11:42:23 +01:00
fix(memory): rank identity records with other relevance matches (#6925)
This commit is contained in:
+15
-26
@@ -364,32 +364,32 @@ class MemoryManager:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# Define keyword categories for semantic matching
|
# Define keyword categories for semantic matching
|
||||||
identity_words = ["name", "who", "i", "am", "called", "identity", "myself", "me", "my"]
|
identity_words = ["name", "called", "identity", "myself"]
|
||||||
contact_words = ["phone", "email", "address", "contact", "number", "where", "located", "reach"]
|
contact_words = ["phone", "email", "address", "contact", "number", "where", "located", "reach"]
|
||||||
preference_words = ["like", "prefer", "favorite", "want", "love", "hate", "dislike", "enjoy", "interested"]
|
preference_words = ["like", "prefer", "favorite", "want", "love", "hate", "dislike", "enjoy", "interested"]
|
||||||
task_words = ["todo", "task", "remind", "meeting", "appointment", "schedule", "deadline"]
|
task_words = ["todo", "task", "remind", "meeting", "appointment", "schedule", "deadline"]
|
||||||
fact_words = ["what", "when", "where", "how", "why", "explain", "describe", "information", "know"]
|
fact_words = ["what", "when", "where", "how", "why", "explain", "describe", "information", "know"]
|
||||||
|
|
||||||
query_lower = query.lower()
|
query_lower = query.lower()
|
||||||
|
query_words = set(re.findall(r'\w+', query_lower))
|
||||||
|
query_tokens = set(tokenize(query_lower))
|
||||||
|
|
||||||
# Determine query type based on keywords
|
# Determine query type based on keywords
|
||||||
query_type = None
|
query_type = None
|
||||||
if any(word in query_lower for word in identity_words):
|
if query_words.intersection(identity_words) or re.search(r'\bwho\s+am\s+i\b', query_lower):
|
||||||
query_type = "identity"
|
query_type = "identity"
|
||||||
elif any(word in query_lower for word in contact_words):
|
elif query_words.intersection(contact_words):
|
||||||
query_type = "contact"
|
query_type = "contact"
|
||||||
elif any(word in query_lower for word in preference_words):
|
elif query_words.intersection(preference_words):
|
||||||
query_type = "preference"
|
query_type = "preference"
|
||||||
elif any(word in query_lower for word in task_words):
|
elif query_words.intersection(task_words):
|
||||||
query_type = "task"
|
query_type = "task"
|
||||||
elif any(word in query_lower for word in fact_words):
|
elif query_words.intersection(fact_words):
|
||||||
query_type = "fact"
|
query_type = "fact"
|
||||||
|
|
||||||
relevant = []
|
relevant = []
|
||||||
identity_memories = []
|
# Every memory participates in the same ranking and threshold check.
|
||||||
other_memories = []
|
# Query type is a scoring hint, not an unconditional insertion path.
|
||||||
|
|
||||||
# Separate identity memories from others
|
|
||||||
for memory in memories:
|
for memory in memories:
|
||||||
memory_text = memory["text"].lower()
|
memory_text = memory["text"].lower()
|
||||||
# Check if this is an identity memory (contains name patterns or identity indicators)
|
# Check if this is an identity memory (contains name patterns or identity indicators)
|
||||||
@@ -397,22 +397,7 @@ class MemoryManager:
|
|||||||
re.search(r'\b[A-Z][a-z]+ [A-Z][a-z]+\b', memory["text"]),
|
re.search(r'\b[A-Z][a-z]+ [A-Z][a-z]+\b', memory["text"]),
|
||||||
any(word in memory_text for word in ["name is", "i'm", "i am", "called", "my name", "named", "call me"])
|
any(word in memory_text for word in ["name is", "i'm", "i am", "called", "my name", "named", "call me"])
|
||||||
])
|
])
|
||||||
if is_identity:
|
|
||||||
identity_memories.append(memory)
|
|
||||||
else:
|
|
||||||
other_memories.append(memory)
|
|
||||||
|
|
||||||
# For identity queries, include all identity memories regardless of similarity
|
|
||||||
if query_type == "identity" and identity_memories:
|
|
||||||
# Give them high scores to ensure they're included first
|
|
||||||
for memory in identity_memories:
|
|
||||||
relevant.append((0.9, memory)) # High score for identity memories in identity queries
|
|
||||||
|
|
||||||
# Process other memories with similarity scoring
|
|
||||||
for memory in other_memories:
|
|
||||||
memory_text = memory["text"].lower()
|
|
||||||
memory_tokens = set(tokenize(memory_text))
|
memory_tokens = set(tokenize(memory_text))
|
||||||
query_tokens = set(tokenize(query_lower))
|
|
||||||
|
|
||||||
# Calculate base Jaccard similarity
|
# Calculate base Jaccard similarity
|
||||||
if not query_tokens or not memory_tokens:
|
if not query_tokens or not memory_tokens:
|
||||||
@@ -422,7 +407,11 @@ class MemoryManager:
|
|||||||
final_score = base_similarity
|
final_score = base_similarity
|
||||||
|
|
||||||
# Apply boosts based on semantic matching
|
# Apply boosts based on semantic matching
|
||||||
if query_type == "contact":
|
if query_type == "identity" and is_identity:
|
||||||
|
# Keep explicit name/identity recall useful without outranking
|
||||||
|
# a closer textual match or bypassing the caller's threshold.
|
||||||
|
final_score = max(final_score * 1.3, 0.6)
|
||||||
|
elif query_type == "contact":
|
||||||
# Boost memories with contact information
|
# Boost memories with contact information
|
||||||
has_contact_info = any(word in memory_text for word in ["@gmail.com", "@", ".com",
|
has_contact_info = any(word in memory_text for word in ["@gmail.com", "@", ".com",
|
||||||
"phone", "number", "address",
|
"phone", "number", "address",
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""Native memory ranking must not promote names on substring/first-person guesses."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.memory import MemoryManager
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def manager(tmp_path):
|
||||||
|
return MemoryManager(str(tmp_path))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('query', ['bitcoin price', 'list tasks', 'time forecast', 'summary'])
|
||||||
|
def test_unrelated_query_does_not_force_name_into_results(manager, query):
|
||||||
|
name = {'id':'name', 'text':'My name is Rowan Example'}
|
||||||
|
match = {'id':'match', 'text':query}
|
||||||
|
assert manager.get_relevant_memories(query, [name, match]) == [match]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('query, text', [
|
||||||
|
('what is my email', 'email is contact@example.test'),
|
||||||
|
('I like jazz', 'I like jazz music'),
|
||||||
|
('remind me about dentist', 'dentist appointment reminder'),
|
||||||
|
])
|
||||||
|
def test_first_person_words_do_not_override_the_actual_topic(manager, query, text):
|
||||||
|
name = {'id':'name', 'text':'My name is Rowan Example'}
|
||||||
|
topic = {'id':'topic', 'text':text}
|
||||||
|
assert manager.get_relevant_memories(query, [name, topic], max_items=1) == [topic]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('query', ['Who am I?', 'what am I called', 'my name', 'NAME:'])
|
||||||
|
def test_explicit_identity_query_still_finds_name(manager, query):
|
||||||
|
name = {'id':'name', 'text':'My name is Rowan Example'}
|
||||||
|
assert manager.get_relevant_memories(query, [name]) == [name]
|
||||||
|
|
||||||
|
|
||||||
|
def test_closer_match_outranks_identity_signal_and_threshold_still_applies(manager):
|
||||||
|
name = {'id':'name', 'text':'My name is Rowan Example'}
|
||||||
|
project = {'id':'project', 'text':'the project name is Polaris'}
|
||||||
|
assert manager.get_relevant_memories('what is the project name', [name, project], max_items=1) == [project]
|
||||||
|
assert manager.get_relevant_memories('who am I', [name], threshold=0.7) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_name_is_still_searchable_by_its_actual_text_without_a_type_keyword(manager):
|
||||||
|
name = {'id':'name', 'text':'My name is Rowan Example'}
|
||||||
|
assert manager.get_relevant_memories('Rowan', [name]) == [name]
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_queries_and_limits_keep_their_contract(manager):
|
||||||
|
memories = [{'text':'tea preference'}, {'text':'tea recipe'}]
|
||||||
|
assert manager.get_relevant_memories(' ', memories) == []
|
||||||
|
assert manager.get_relevant_memories('tea', []) == []
|
||||||
|
assert len(manager.get_relevant_memories('tea', memories, max_items=1)) == 1
|
||||||
Reference in New Issue
Block a user