temporal
This commit is contained in:
parent
1b296358ee
commit
b095a382d2
6 changed files with 3963 additions and 2 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
3181
memora-dev/benchmarks/uv.lock
Normal file
3181
memora-dev/benchmarks/uv.lock
Normal file
File diff suppressed because it is too large
Load diff
1
memora-dev/benchmarks/visualizer/.sesskey
Normal file
1
memora-dev/benchmarks/visualizer/.sesskey
Normal file
|
|
@ -0,0 +1 @@
|
|||
24b08b0a-f859-46ba-a5a1-756f140601ce
|
||||
|
|
@ -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')
|
||||
708
memora/tests/test_dimension_extraction.py
Normal file
708
memora/tests/test_dimension_extraction.py
Normal 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"
|
||||
Loading…
Reference in a new issue