This commit is contained in:
Nicolò Boschi 2025-11-17 14:55:37 +01:00
parent 1b296358ee
commit b095a382d2
6 changed files with 3963 additions and 2 deletions

View file

@ -313,7 +313,7 @@ export function DataView({ factType }: DataViewProps) {
})
) : (
<tr>
<td colSpan={5} className="p-10 text-center text-muted-foreground bg-muted">
<td colSpan={6} className="p-10 text-center text-muted-foreground bg-muted">
{data.table_rows ? 'No facts match your search' : 'No facts found for this agent and fact type'}
</td>
</tr>

View file

@ -353,7 +353,8 @@ export function SearchDebugView() {
<th className="p-2 text-left border border-border text-card-foreground">Rank</th>
<th className="p-2 text-left border border-border text-card-foreground">Text</th>
<th className="p-2 text-left border border-border text-card-foreground">Context</th>
<th className="p-2 text-left border border-border text-card-foreground">Date</th>
<th className="p-2 text-left border border-border text-card-foreground">Occurred</th>
<th className="p-2 text-left border border-border text-card-foreground">Mentioned</th>
<th className="p-2 text-left border border-border text-card-foreground" title="Final weighted score">
Final Score
</th>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
24b08b0a-f859-46ba-a5a1-756f140601ce

View file

@ -0,0 +1,70 @@
"""add_temporal_ranges_to_memory_units
Revision ID: 9d42e6f91234
Revises: 8c55f5602451
Create Date: 2025-11-17 00:00:00.000000
This migration adds temporal range support to memory_units table:
- occurred_start: When the fact/event started
- occurred_end: When the fact/event ended
- mentioned_at: When the fact was mentioned/learned
For existing rows, these are initialized from event_date (point events).
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import TIMESTAMP
# revision identifiers, used by Alembic.
revision: str = '9d42e6f91234'
down_revision: Union[str, Sequence[str], None] = '8c55f5602451'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema: add temporal range columns to memory_units."""
# Add new temporal range columns (nullable initially)
op.add_column(
'memory_units',
sa.Column('occurred_start', TIMESTAMP(timezone=True), nullable=True)
)
op.add_column(
'memory_units',
sa.Column('occurred_end', TIMESTAMP(timezone=True), nullable=True)
)
op.add_column(
'memory_units',
sa.Column('mentioned_at', TIMESTAMP(timezone=True), nullable=True)
)
# Populate new columns from existing event_date for backward compatibility
# For existing facts, treat them as point events (start = end = event_date)
# and assume they were mentioned at the same time
op.execute("""
UPDATE memory_units
SET
occurred_start = event_date,
occurred_end = event_date,
mentioned_at = event_date
WHERE occurred_start IS NULL
""")
# Optional: Make columns non-nullable after populating
# Uncomment if you want to enforce NOT NULL constraint
# op.alter_column('memory_units', 'occurred_start', nullable=False)
# op.alter_column('memory_units', 'occurred_end', nullable=False)
# op.alter_column('memory_units', 'mentioned_at', nullable=False)
def downgrade() -> None:
"""Downgrade schema: remove temporal range columns from memory_units."""
# Remove the temporal range columns
op.drop_column('memory_units', 'mentioned_at')
op.drop_column('memory_units', 'occurred_end')
op.drop_column('memory_units', 'occurred_start')

View file

@ -0,0 +1,708 @@
"""
Test that fact extraction correctly captures all information dimensions.
This test suite verifies that the fact extraction system properly preserves:
1. Emotional/Affective dimension
2. Sensory/Experiential dimension
3. Cognitive/Epistemic dimension
4. Intentional/Motivational dimension
5. Evaluative/Preferential dimension
6. Capability/Skill dimension
7. Attitudinal/Reactive dimension
8. Comparative/Relative dimension
9. Temporal dimension (with absolute date conversion)
"""
import pytest
from datetime import datetime
from memora.fact_extraction import extract_facts_from_text
from memora.llm_wrapper import LLMConfig
@pytest.mark.asyncio
async def test_emotional_dimension_preservation():
"""
Test that emotional states and feelings are preserved, not stripped away.
Example: "I was thrilled to receive positive feedback"
Should NOT become: "I received positive feedback"
"""
text = """
I was absolutely thrilled when I received such positive feedback on my presentation!
Sarah seemed disappointed when she heard the news about the delay.
Marcus felt anxious about the upcoming interview.
"""
context = "Personal journal entry"
llm_config = LLMConfig.for_memory()
facts = await extract_facts_from_text(
text=text,
event_date=datetime(2024, 11, 13),
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
# Check that emotional words are preserved
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should preserve emotional intensity and specific emotions
emotional_indicators = ["thrilled", "disappointed", "anxious", "positive feedback"]
found_emotions = [word for word in emotional_indicators if word in all_facts_text]
assert len(found_emotions) >= 2, (
f"Should preserve emotional dimension. "
f"Found: {found_emotions}, Expected at least 2 from: {emotional_indicators}"
)
@pytest.mark.asyncio
async def test_temporal_absolute_conversion():
"""
Test that relative temporal expressions are converted to absolute dates.
Critical: "yesterday" should become "on November 12, 2024", NOT "recently"
"""
text = """
Yesterday I went for a morning jog for the first time in a nearby park.
Last week I started a new project.
I'm planning to visit Tokyo next month.
"""
context = "Personal conversation"
llm_config = LLMConfig.for_memory()
# Event date is November 13, 2024
event_date = datetime(2024, 11, 13)
facts = await extract_facts_from_text(
text=text,
event_date=event_date,
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should NOT contain vague temporal terms
prohibited_terms = ["recently", "soon", "lately", "a while ago", "some time ago"]
found_prohibited = [term for term in prohibited_terms if term in all_facts_text]
assert len(found_prohibited) == 0, (
f"Should NOT use vague temporal terms. Found: {found_prohibited}"
)
# Should contain specific date references (either month names or dates)
# For "yesterday" (Nov 12), "last week" (early Nov), "next month" (December)
temporal_indicators = ["november", "12", "early november", "week of", "december"]
found_temporal = [term for term in temporal_indicators if term in all_facts_text]
assert len(found_temporal) >= 1, (
f"Should convert relative dates to absolute. "
f"Found: {found_temporal}, Expected month/date references"
)
@pytest.mark.asyncio
async def test_sensory_dimension_preservation():
"""
Test that sensory details (visual, auditory, etc.) are preserved.
"""
text = """
The coffee tasted bitter and burnt.
She showed me her bright orange hair, which looked stunning under the lights.
The music was so loud I could barely hear myself think.
"""
context = "Personal experience"
llm_config = LLMConfig.for_memory()
facts = await extract_facts_from_text(
text=text,
event_date=datetime(2024, 11, 13),
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should preserve sensory descriptors
sensory_indicators = ["bitter", "burnt", "bright orange", "loud", "stunning"]
found_sensory = [word for word in sensory_indicators if word in all_facts_text]
assert len(found_sensory) >= 2, (
f"Should preserve sensory details. "
f"Found: {found_sensory}, Expected at least 2 from: {sensory_indicators}"
)
@pytest.mark.asyncio
async def test_cognitive_epistemic_dimension():
"""
Test that cognitive states and certainty levels are preserved.
"""
text = """
I realized that the approach wasn't working.
She wasn't sure if the meeting would happen.
He's convinced that AI will transform healthcare.
Maybe we should reconsider the timeline.
"""
context = "Team discussion"
llm_config = LLMConfig.for_memory()
facts = await extract_facts_from_text(
text=text,
event_date=datetime(2024, 11, 13),
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should preserve cognitive/epistemic indicators
cognitive_indicators = ["realized", "wasn't sure", "convinced", "maybe", "reconsider"]
found_cognitive = [word for word in cognitive_indicators if word in all_facts_text]
assert len(found_cognitive) >= 2, (
f"Should preserve cognitive/epistemic dimension. "
f"Found: {found_cognitive}"
)
@pytest.mark.asyncio
async def test_capability_skill_dimension():
"""
Test that capabilities, skills, and limitations are preserved.
"""
text = """
I can speak French fluently.
Sarah struggles with public speaking.
He's an expert in machine learning.
I'm unable to attend the conference due to scheduling conflicts.
"""
context = "Personal profile discussion"
llm_config = LLMConfig.for_memory()
facts = await extract_facts_from_text(
text=text,
event_date=datetime(2024, 11, 13),
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should preserve capability indicators
capability_indicators = ["can speak", "fluently", "struggles with", "expert in", "unable to"]
found_capability = [word for word in capability_indicators if word in all_facts_text]
assert len(found_capability) >= 2, (
f"Should preserve capability/skill dimension. "
f"Found: {found_capability}"
)
@pytest.mark.asyncio
async def test_comparative_dimension():
"""
Test that comparisons and contrasts are preserved.
"""
text = """
This approach is much better than the previous one.
The new design is worse than expected.
Unlike last year, we're ahead of schedule.
"""
context = "Project review"
llm_config = LLMConfig.for_memory()
facts = await extract_facts_from_text(
text=text,
event_date=datetime(2024, 11, 13),
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should preserve comparative indicators
comparative_indicators = ["better than", "worse than", "unlike", "ahead of"]
found_comparative = [word for word in comparative_indicators if word in all_facts_text]
assert len(found_comparative) >= 1, (
f"Should preserve comparative dimension. "
f"Found: {found_comparative}"
)
@pytest.mark.asyncio
async def test_attitudinal_reactive_dimension():
"""
Test that attitudes and reactions are preserved.
"""
text = """
She's very skeptical about the new technology.
I was surprised when he announced his resignation.
Marcus rolled his eyes when the topic came up.
She's enthusiastic about the opportunity.
"""
context = "Team meeting"
llm_config = LLMConfig.for_memory()
facts = await extract_facts_from_text(
text=text,
event_date=datetime(2024, 11, 13),
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should preserve attitudinal/reactive indicators
attitudinal_indicators = ["skeptical", "surprised", "rolled his eyes", "enthusiastic"]
found_attitudinal = [word for word in attitudinal_indicators if word in all_facts_text]
assert len(found_attitudinal) >= 2, (
f"Should preserve attitudinal/reactive dimension. "
f"Found: {found_attitudinal}"
)
@pytest.mark.asyncio
async def test_intentional_motivational_dimension():
"""
Test that goals, plans, and motivations are preserved.
"""
text = """
I want to learn Mandarin before my trip to China.
She aims to complete her PhD within three years.
His goal is to build a sustainable business.
I'm planning to switch careers because I'm not fulfilled in my current role.
"""
context = "Personal goals discussion"
llm_config = LLMConfig.for_memory()
facts = await extract_facts_from_text(
text=text,
event_date=datetime(2024, 11, 13),
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should preserve intentional/motivational indicators
intentional_indicators = ["want to", "aims to", "goal is", "planning to", "because"]
found_intentional = [word for word in intentional_indicators if word in all_facts_text]
assert len(found_intentional) >= 2, (
f"Should preserve intentional/motivational dimension. "
f"Found: {found_intentional}"
)
@pytest.mark.asyncio
async def test_evaluative_preferential_dimension():
"""
Test that preferences and values are preserved.
"""
text = """
I prefer working remotely to being in an office.
She values honesty above all else.
He hates being late to meetings.
Family is the most important thing to her.
"""
context = "Personal values discussion"
llm_config = LLMConfig.for_memory()
facts = await extract_facts_from_text(
text=text,
event_date=datetime(2024, 11, 13),
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should preserve evaluative/preferential indicators
evaluative_indicators = ["prefer", "values", "hates", "important", "above all"]
found_evaluative = [word for word in evaluative_indicators if word in all_facts_text]
assert len(found_evaluative) >= 2, (
f"Should preserve evaluative/preferential dimension. "
f"Found: {found_evaluative}"
)
@pytest.mark.asyncio
async def test_comprehensive_multi_dimension():
"""
Test a realistic scenario with multiple dimensions in one fact.
"""
text = """
I was thrilled to receive such positive feedback on my presentation yesterday!
I wasn't sure if my approach would resonate, but the audience seemed enthusiastic.
I prefer presenting in person rather than virtually because I can read the room better.
"""
context = "Personal reflection"
llm_config = LLMConfig.for_memory()
event_date = datetime(2024, 11, 13)
facts = await extract_facts_from_text(
text=text,
event_date=event_date,
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should preserve multiple dimensions:
# 1. Emotional: "thrilled"
# 2. Temporal: "yesterday" → absolute date (not "recently")
# 3. Cognitive: "wasn't sure"
# 4. Attitudinal: "enthusiastic"
# 5. Preferential: "prefer"
# 6. Comparative: "better"
# Check emotional
assert "thrilled" in all_facts_text or "positive feedback" in all_facts_text, \
"Should preserve emotional dimension (thrilled)"
# Check no vague temporal terms
prohibited_terms = ["recently", "soon", "lately"]
found_prohibited = [term for term in prohibited_terms if term in all_facts_text]
assert len(found_prohibited) == 0, \
f"Should NOT use vague temporal terms. Found: {found_prohibited}"
# Check cognitive uncertainty
assert "wasn't sure" in all_facts_text or "unsure" in all_facts_text or "uncertain" in all_facts_text, \
"Should preserve cognitive uncertainty"
# Check preference
assert "prefer" in all_facts_text or "rather than" in all_facts_text, \
"Should preserve preferential dimension"
@pytest.mark.asyncio
async def test_logical_inference_identity_connection():
"""
Test that the system makes logical inferences to connect related information.
Example: "I lost a friend" + "this photo with Karlie" "I lost my friend Karlie"
"""
text = """
Deborah: The roses and dahlias bring me peace. I lost a friend last week,
so I've been spending time in the garden to find some comfort.
Jolene: Sorry to hear about your friend, Deb. Losing someone can be really tough.
How are you holding up?
Deborah: Thanks for the kind words. It's been tough, but I'm comforted by
remembering our time together. It reminds me of how special life is.
Jolene: Memories can give us so much comfort and joy.
Deborah: Memories keep our loved ones close. This is the last photo with Karlie
which was taken last summer when we hiked. It was our last one. We had such a
great time! Every time I see it, I can't help but smile.
"""
context = "Conversation between Deborah and Jolene"
llm_config = LLMConfig.for_memory()
# Event date is February 23, 2023
event_date = datetime(2023, 2, 23)
facts = await extract_facts_from_text(
text=text,
event_date=event_date,
context=context,
llm_config=llm_config,
agent_name="Deborah"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# CRITICAL: Should make the logical connection that Karlie is the lost friend
# The fact should mention both "lost" (or similar) AND "Karlie" together
has_karlie = "karlie" in all_facts_text
has_loss = any(word in all_facts_text for word in ["lost", "death", "passed", "died", "losing"])
assert has_karlie, "Should mention Karlie in the extracted facts"
assert has_loss, "Should mention the loss/death in the extracted facts"
# Ideally, they should be in the same fact (connected)
# Check if any single fact contains both Karlie and loss-related terms
connected_fact_found = False
for fact in facts:
fact_text = fact['fact'].lower()
if "karlie" in fact_text and any(word in fact_text for word in ["lost", "death", "passed", "died", "losing"]):
connected_fact_found = True
print(f"\n✓ Found connected fact: {fact['fact']}")
break
assert connected_fact_found, (
"Should connect 'lost a friend' with 'Karlie' in the same fact. "
f"The inference should be: Karlie is the lost friend. "
f"Facts: {[f['fact'] for f in facts]}"
)
@pytest.mark.asyncio
async def test_logical_inference_pronoun_resolution():
"""
Test that pronouns are resolved to their referents.
Example: "I started a project" + "It's challenging" "The project is challenging"
"""
text = """
I started a new machine learning project last month.
It's been really challenging but very rewarding.
I've learned so much from it.
"""
context = "Personal update"
llm_config = LLMConfig.for_memory()
facts = await extract_facts_from_text(
text=text,
event_date=datetime(2024, 11, 13),
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. {f['fact']}")
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should connect the project with its characteristics
# The fact should mention "project" AND the qualities (challenging, rewarding)
has_project = "project" in all_facts_text
has_qualities = any(word in all_facts_text for word in ["challenging", "rewarding", "learned"])
assert has_project, "Should mention the project"
assert has_qualities, "Should mention the qualities/learning"
# Check if a single fact connects the project with its characteristics
connected_fact_found = False
for fact in facts:
fact_text = fact['fact'].lower()
if "project" in fact_text and any(word in fact_text for word in ["challenging", "rewarding"]):
connected_fact_found = True
print(f"\n✓ Found connected fact: {fact['fact']}")
break
assert connected_fact_found, (
"Should resolve 'it' to 'the project' and connect characteristics in the same fact. "
f"Facts: {[f['fact'] for f in facts]}"
)
@pytest.mark.asyncio
async def test_date_field_calculation_last_night():
"""
Test that the date field is calculated correctly for "last night" events.
CRITICAL: If conversation is on August 14, 2023 and text says "last night",
the date field should be August 13, NOT August 14.
"""
text = """
Melanie: Hey Caroline! Last night was amazing! We celebrated my daughter's birthday
with a concert surrounded by music, joy and the warm summer breeze.
"""
context = "Conversation between Melanie and Caroline"
llm_config = LLMConfig.for_memory()
# Conversation happened on August 14, 2023
event_date = datetime(2023, 8, 14, 14, 24) # 2:24 PM on Aug 14
facts = await extract_facts_from_text(
text=text,
event_date=event_date,
context=context,
llm_config=llm_config,
agent_name="Melanie"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. Date: {f['date']} - {f['fact']}")
# Find the fact about the birthday celebration
birthday_fact = None
for fact in facts:
if "birthday" in fact['fact'].lower() or "concert" in fact['fact'].lower():
birthday_fact = fact
break
assert birthday_fact is not None, "Should extract fact about birthday celebration"
# The date field should be August 13, 2023 (last night), NOT August 14
fact_date_str = birthday_fact['date']
# Parse the date
if 'T' in fact_date_str:
fact_date = datetime.fromisoformat(fact_date_str.replace('Z', '+00:00'))
else:
fact_date = datetime.fromisoformat(fact_date_str)
assert fact_date.year == 2023, "Year should be 2023"
assert fact_date.month == 8, "Month should be August"
assert fact_date.day == 13, (
f"Day should be 13 (last night relative to Aug 14), but got {fact_date.day}. "
f"Date field should be when FACT occurred, not when mentioned!"
)
@pytest.mark.asyncio
async def test_date_field_calculation_yesterday():
"""
Test that the date field is calculated correctly for "yesterday" events.
"""
text = """
Yesterday I went for a morning jog for the first time in a nearby park.
"""
context = "Personal diary"
llm_config = LLMConfig.for_memory()
# Conversation on November 13, 2024
event_date = datetime(2024, 11, 13)
facts = await extract_facts_from_text(
text=text,
event_date=event_date,
context=context,
llm_config=llm_config,
agent_name="TestUser"
)
assert len(facts) > 0, "Should extract at least one fact"
print(f"\nExtracted {len(facts)} facts:")
for i, f in enumerate(facts):
print(f"{i+1}. Date: {f['date']} - {f['fact']}")
# Get the jogging fact
jogging_fact = facts[0]
# Parse the date
fact_date_str = jogging_fact['date']
if 'T' in fact_date_str:
fact_date = datetime.fromisoformat(fact_date_str.replace('Z', '+00:00'))
else:
fact_date = datetime.fromisoformat(fact_date_str)
# Should be November 12, 2024 (yesterday)
assert fact_date.year == 2024, "Year should be 2024"
assert fact_date.month == 11, "Month should be November"
assert fact_date.day == 12, (
f"Day should be 12 (yesterday relative to Nov 13), but got {fact_date.day}. "
f"Date field: {fact_date_str}"
)
# Check fact text
all_facts_text = " ".join([f['fact'].lower() for f in facts])
# Should preserve "first time"
assert "first time" in all_facts_text or "first" in all_facts_text, \
"Should preserve 'first time' qualifier"
# Should NOT use "recently"
assert "recently" not in all_facts_text, \
"Should NOT convert 'yesterday' to 'recently'"
# Should have absolute date in text (November 12 or specific date)
assert any(term in all_facts_text for term in ["november", "12", "nov"]), \
"Should convert 'yesterday' to absolute date in fact text"