temporal
This commit is contained in:
parent
e15cbb4a44
commit
1b296358ee
15 changed files with 1692292 additions and 796693 deletions
|
|
@ -40,6 +40,12 @@ pub struct Fact {
|
|||
#[serde(default)]
|
||||
pub event_date: Option<String>,
|
||||
#[serde(default)]
|
||||
pub occurred_start: Option<String>,
|
||||
#[serde(default)]
|
||||
pub occurred_end: Option<String>,
|
||||
#[serde(default)]
|
||||
pub mentioned_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub document_id: Option<String>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,11 +50,30 @@ pub fn print_fact(fact: &Fact, show_activation: bool) {
|
|||
println!(" {}: {}", "Context".bright_black(), context.bright_black());
|
||||
}
|
||||
|
||||
// Show event date if available
|
||||
if let Some(event_date) = &fact.event_date {
|
||||
// Show temporal information
|
||||
// If occurred_start/end exist, show them; otherwise fall back to event_date
|
||||
if let Some(occurred_start) = &fact.occurred_start {
|
||||
if let Some(occurred_end) = &fact.occurred_end {
|
||||
if occurred_start == occurred_end {
|
||||
// Point event
|
||||
println!(" {}: {}", "Occurred".bright_black(), occurred_start.bright_black());
|
||||
} else {
|
||||
// Range event
|
||||
println!(" {}: {} to {}", "Occurred".bright_black(), occurred_start.bright_black(), occurred_end.bright_black());
|
||||
}
|
||||
} else {
|
||||
println!(" {}: {}", "Occurred".bright_black(), occurred_start.bright_black());
|
||||
}
|
||||
} else if let Some(event_date) = &fact.event_date {
|
||||
// Fallback for backward compatibility
|
||||
println!(" {}: {}", "Date".bright_black(), event_date.bright_black());
|
||||
}
|
||||
|
||||
// Show when fact was mentioned (learned)
|
||||
if let Some(mentioned_at) = &fact.mentioned_at {
|
||||
println!(" {}: {}", "Mentioned".bright_black(), mentioned_at.bright_black());
|
||||
}
|
||||
|
||||
// Show document ID if available
|
||||
if let Some(document_id) = &fact.document_id {
|
||||
println!(" {}: {}", "Document".bright_black(), document_id.bright_black());
|
||||
|
|
|
|||
|
|
@ -268,7 +268,8 @@ export function DataView({ factType }: DataViewProps) {
|
|||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">ID</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Text</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Context</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Date</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Occurred</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Mentioned</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Entities</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -283,15 +284,33 @@ export function DataView({ factType }: DataViewProps) {
|
|||
row.context?.toLowerCase().includes(query)
|
||||
);
|
||||
})
|
||||
.map((row: any, idx: number) => (
|
||||
<tr key={idx} className="bg-background hover:bg-muted">
|
||||
<td className="p-2 border border-border" title={row.id}>{row.id}</td>
|
||||
<td className="p-2 border border-border">{row.text}</td>
|
||||
<td className="p-2 border border-border">{row.context || 'N/A'}</td>
|
||||
<td className="p-2 border border-border">{row.date || 'N/A'}</td>
|
||||
<td className="p-2 border border-border">{row.entities || 'None'}</td>
|
||||
</tr>
|
||||
))
|
||||
.map((row: any, idx: number) => {
|
||||
// Format temporal range
|
||||
let occurredDisplay = 'N/A';
|
||||
if (row.occurred_start && row.occurred_end) {
|
||||
const start = new Date(row.occurred_start).toLocaleDateString();
|
||||
const end = new Date(row.occurred_end).toLocaleDateString();
|
||||
occurredDisplay = start === end ? start : `${start} - ${end}`;
|
||||
} else if (row.date) {
|
||||
// Fallback to old date field
|
||||
occurredDisplay = row.date;
|
||||
}
|
||||
|
||||
const mentionedDisplay = row.mentioned_at
|
||||
? new Date(row.mentioned_at).toLocaleDateString()
|
||||
: 'N/A';
|
||||
|
||||
return (
|
||||
<tr key={idx} className="bg-background hover:bg-muted">
|
||||
<td className="p-2 border border-border" title={row.id}>{row.id}</td>
|
||||
<td className="p-2 border border-border">{row.text}</td>
|
||||
<td className="p-2 border border-border">{row.context || 'N/A'}</td>
|
||||
<td className="p-2 border border-border">{occurredDisplay}</td>
|
||||
<td className="p-2 border border-border">{mentionedDisplay}</td>
|
||||
<td className="p-2 border border-border">{row.entities || 'None'}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={5} className="p-10 text-center text-muted-foreground bg-muted">
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ class LoComoDataset(BenchmarkDataset):
|
|||
"""
|
||||
Prepare LoComo conversation for batch ingestion.
|
||||
|
||||
Combines all sessions into a single conversation item instead of separate sessions.
|
||||
Each session is ingested as a separate item with its own date.
|
||||
|
||||
Returns:
|
||||
List with single conversation dict containing 'content', 'context', 'event_date'
|
||||
List of session dicts, each containing 'content', 'context', 'event_date', 'document_id'
|
||||
"""
|
||||
conv = item['conversation']
|
||||
speaker_a = conv['speaker_a']
|
||||
|
|
@ -53,8 +53,7 @@ class LoComoDataset(BenchmarkDataset):
|
|||
# Get all session keys sorted
|
||||
session_keys = sorted([k for k in conv.keys() if k.startswith('session_') and not k.endswith('_date_time')])
|
||||
|
||||
all_conversation_parts = []
|
||||
first_session_date = None
|
||||
session_items = []
|
||||
|
||||
for session_key in session_keys:
|
||||
if session_key not in conv or not isinstance(conv[session_key], list):
|
||||
|
|
@ -66,10 +65,6 @@ class LoComoDataset(BenchmarkDataset):
|
|||
date_key = f"{session_key}_date_time"
|
||||
session_date = self._parse_date(conv.get(date_key, "n/a"))
|
||||
|
||||
# Store first session date
|
||||
if first_session_date is None:
|
||||
first_session_date = session_date
|
||||
|
||||
# Build session content from all turns
|
||||
session_parts = []
|
||||
for turn in session_data:
|
||||
|
|
@ -78,21 +73,17 @@ class LoComoDataset(BenchmarkDataset):
|
|||
session_parts.append(f"{speaker}: {text}")
|
||||
|
||||
if session_parts:
|
||||
all_conversation_parts.append("\n".join(session_parts))
|
||||
session_content = "\n".join(session_parts)
|
||||
document_id = f"{item['sample_id']}_{session_key}"
|
||||
|
||||
if not all_conversation_parts:
|
||||
return []
|
||||
session_items.append({
|
||||
"content": session_content,
|
||||
"context": f"Conversation between {speaker_a} and {speaker_b} ({session_key} of {item['sample_id']})",
|
||||
"event_date": session_date,
|
||||
"document_id": document_id
|
||||
})
|
||||
|
||||
# Combine all sessions into a single conversation
|
||||
conversation_content = "\n\n".join(all_conversation_parts)
|
||||
document_id = item['sample_id']
|
||||
|
||||
return [{
|
||||
"content": conversation_content,
|
||||
"context": f"Conversation between {speaker_a} and {speaker_b} (conversation {item['sample_id']})",
|
||||
"event_date": first_session_date or datetime.now(timezone.utc),
|
||||
"document_id": document_id
|
||||
}]
|
||||
return session_items
|
||||
|
||||
def get_qa_pairs(self, item: Dict) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
|
|
@ -313,6 +304,7 @@ async def run_benchmark(
|
|||
use_think: bool = False,
|
||||
conversation: str = None,
|
||||
api_url: str = None,
|
||||
max_concurrent_questions_override: int = None,
|
||||
only_failed: bool = False,
|
||||
only_invalid: bool = False
|
||||
):
|
||||
|
|
@ -391,13 +383,13 @@ async def run_benchmark(
|
|||
agent_id="locomo",
|
||||
thinking_budget=500
|
||||
)
|
||||
max_concurrent_questions = 4
|
||||
max_concurrent_questions = max_concurrent_questions_override or 4
|
||||
eval_semaphore_size = 4
|
||||
else:
|
||||
answer_generator = LoComoAnswerGenerator()
|
||||
# Reduced from 32 to 10 to match search semaphore limit
|
||||
# Prevents "too many connections" errors
|
||||
max_concurrent_questions = 10
|
||||
max_concurrent_questions = max_concurrent_questions_override or 10
|
||||
eval_semaphore_size = 8
|
||||
|
||||
answer_evaluator = LLMAnswerEvaluator()
|
||||
|
|
@ -534,6 +526,7 @@ if __name__ == "__main__":
|
|||
parser.add_argument('--use-think', action='store_true', help='Use think API instead of search + LLM')
|
||||
parser.add_argument('--conversation', type=str, default=None, help='Run only specific conversation (e.g., "conv-26")')
|
||||
parser.add_argument('--api-url', type=str, default=None, help='Memora API URL (default: use local memory, example: http://localhost:8000)')
|
||||
parser.add_argument('--max-concurrent-questions', type=int, default=None, help='Max concurrent questions per conversation (default: 4 for think, 10 for search)')
|
||||
parser.add_argument('--only-failed', action='store_true', help='Only run conversations that have failed questions (is_correct=False). Requires existing results file.')
|
||||
parser.add_argument('--only-invalid', action='store_true', help='Only run conversations that have invalid questions (is_invalid=True). Requires existing results file.')
|
||||
|
||||
|
|
@ -550,6 +543,7 @@ if __name__ == "__main__":
|
|||
use_think=args.use_think,
|
||||
conversation=args.conversation,
|
||||
api_url=args.api_url,
|
||||
max_concurrent_questions_override=args.max_concurrent_questions,
|
||||
only_failed=args.only_failed,
|
||||
only_invalid=args.only_invalid
|
||||
))
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,9 +1,16 @@
|
|||
# LoComo Benchmark Results
|
||||
|
||||
**Overall Accuracy**: 77.78% (7/9)
|
||||
**Overall Accuracy**: 73.67% (1136/1542)
|
||||
|
||||
| Sample ID | Sessions | Questions | Correct | Accuracy | Multi-hop | Single-hop | Temporal | Open-domain |
|
||||
|-----------|----------|-----------|---------|----------|-----------|------------|----------|-------------|
|
||||
| conv-26 | 19 | 3 | 2 | 66.67% | N/A | N/A | N/A | N/A |
|
||||
| conv-30 | 19 | 3 | 3 | 100.00% | N/A | N/A | N/A | N/A |
|
||||
| conv-41 | 32 | 3 | 2 | 66.67% | N/A | N/A | N/A | N/A |
|
||||
| conv-26 | 19 | 154 | 105 | 68.18% | N/A | N/A | N/A | N/A |
|
||||
| conv-30 | 19 | 81 | 62 | 76.54% | N/A | N/A | N/A | N/A |
|
||||
| conv-41 | 32 | 152 | 121 | 79.61% | N/A | N/A | N/A | N/A |
|
||||
| conv-42 | 29 | 199 | 138 | 69.35% | N/A | N/A | N/A | N/A |
|
||||
| conv-43 | 29 | 178 | 128 | 71.91% | N/A | N/A | N/A | N/A |
|
||||
| conv-44 | 28 | 123 | 93 | 75.61% | N/A | N/A | N/A | N/A |
|
||||
| conv-47 | 31 | 150 | 122 | 81.33% | N/A | N/A | N/A | N/A |
|
||||
| conv-48 | 30 | 191 | 134 | 70.16% | N/A | N/A | N/A | N/A |
|
||||
| conv-49 | 25 | 156 | 116 | 74.36% | N/A | N/A | N/A | N/A |
|
||||
| conv-50 | 30 | 158 | 117 | 74.05% | N/A | N/A | N/A | N/A |
|
||||
|
|
@ -1,7 +1,16 @@
|
|||
# LoComo Benchmark Results (Think Mode)
|
||||
|
||||
**Overall Accuracy**: 70.73% (87/123)
|
||||
**Overall Accuracy**: 77.24% (1191/1542)
|
||||
|
||||
| Sample ID | Sessions | Questions | Correct | Accuracy | Multi-hop | Single-hop | Temporal | Open-domain |
|
||||
|-----------|----------|-----------|---------|----------|-----------|------------|----------|-------------|
|
||||
| conv-44 | 1 | 123 | 87 | 70.73% | N/A | N/A | N/A | N/A |
|
||||
| conv-26 | 19 | 154 | 120 | 77.92% | N/A | N/A | N/A | N/A |
|
||||
| conv-30 | 19 | 81 | 64 | 79.01% | N/A | N/A | N/A | N/A |
|
||||
| conv-41 | 32 | 152 | 122 | 80.26% | N/A | N/A | N/A | N/A |
|
||||
| conv-42 | 29 | 199 | 152 | 76.38% | N/A | N/A | N/A | N/A |
|
||||
| conv-43 | 29 | 178 | 131 | 73.60% | N/A | N/A | N/A | N/A |
|
||||
| conv-44 | 28 | 123 | 93 | 75.61% | N/A | N/A | N/A | N/A |
|
||||
| conv-47 | 31 | 150 | 117 | 78.00% | N/A | N/A | N/A | N/A |
|
||||
| conv-48 | 30 | 191 | 146 | 76.44% | N/A | N/A | N/A | N/A |
|
||||
| conv-49 | 25 | 156 | 123 | 78.85% | N/A | N/A | N/A | N/A |
|
||||
| conv-50 | 30 | 158 | 123 | 77.85% | N/A | N/A | N/A | N/A |
|
||||
|
|
@ -381,7 +381,7 @@ def get_locomo(mode: str, filter_type: str = "all", category_filter: str = "all"
|
|||
|
||||
|
||||
@rt("/locomo/{mode}/item/{item_idx}")
|
||||
def get_locomo_item(mode: str, item_idx: int, filter_type: str = "all"):
|
||||
def get_locomo_item(mode: str, item_idx: int, filter_type: str = "all", category_filter: str = "all"):
|
||||
"""Render a single LoComo item with questions."""
|
||||
data = load_locomo_results(mode)
|
||||
if not data:
|
||||
|
|
@ -395,37 +395,160 @@ def get_locomo_item(mode: str, item_idx: int, filter_type: str = "all"):
|
|||
item_id = item.get("item_id", item.get("sample_id", f"item-{item_idx}"))
|
||||
metrics = item.get("metrics", {})
|
||||
accuracy = metrics.get("accuracy", 0)
|
||||
correct = metrics.get("correct", 0)
|
||||
total = metrics.get("total", 0)
|
||||
invalid = metrics.get("invalid", 0)
|
||||
detailed_results = metrics.get("detailed_results", [])
|
||||
category_stats_raw = metrics.get("category_stats", {})
|
||||
|
||||
# Filter questions
|
||||
filtered_questions = []
|
||||
for q_idx, result in enumerate(detailed_results):
|
||||
is_invalid = result.get("is_invalid", False)
|
||||
is_correct = result.get("is_correct", False)
|
||||
question_category = result.get("category")
|
||||
|
||||
# Apply correctness filter
|
||||
passes_correctness = False
|
||||
if filter_type == "all":
|
||||
filtered_questions.append((q_idx, result))
|
||||
passes_correctness = True
|
||||
elif filter_type == "correct" and is_correct and not is_invalid:
|
||||
filtered_questions.append((q_idx, result))
|
||||
passes_correctness = True
|
||||
elif filter_type == "incorrect" and not is_correct and not is_invalid:
|
||||
filtered_questions.append((q_idx, result))
|
||||
passes_correctness = True
|
||||
elif filter_type == "invalid" and is_invalid:
|
||||
passes_correctness = True
|
||||
|
||||
# Apply category filter
|
||||
passes_category = False
|
||||
if category_filter == "all":
|
||||
passes_category = True
|
||||
elif category_filter.isdigit() and question_category == int(category_filter):
|
||||
passes_category = True
|
||||
|
||||
if passes_correctness and passes_category:
|
||||
filtered_questions.append((q_idx, result))
|
||||
|
||||
# Build category stats for this item
|
||||
category_stats = {
|
||||
1: {"name": "Multi-hop", "correct": 0, "total": 0, "invalid": 0},
|
||||
2: {"name": "Single-hop", "correct": 0, "total": 0, "invalid": 0},
|
||||
3: {"name": "Temporal", "correct": 0, "total": 0, "invalid": 0},
|
||||
4: {"name": "Open-domain", "correct": 0, "total": 0, "invalid": 0}
|
||||
}
|
||||
|
||||
for cat_id_str, stats in category_stats_raw.items():
|
||||
cat_id = int(cat_id_str)
|
||||
if cat_id in category_stats:
|
||||
category_stats[cat_id]["correct"] = stats.get("correct", 0)
|
||||
category_stats[cat_id]["total"] = stats.get("total", 0)
|
||||
category_stats[cat_id]["invalid"] = stats.get("invalid", 0)
|
||||
|
||||
# Overall stats for this item
|
||||
mode_label = " (Think Mode)" if mode == "think" else " (Search Mode)"
|
||||
stats_html = Div(
|
||||
H3(f"{item_id}{mode_label} - Performance", cls="text-2xl font-bold text-foreground mb-6"),
|
||||
Div(
|
||||
Div(
|
||||
P("Overall Accuracy", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(f"{accuracy:.2f}%", cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
),
|
||||
Div(
|
||||
P("Correct Answers", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(f"{correct} / {total}", cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
),
|
||||
Div(
|
||||
P("Invalid Questions", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(str(invalid), cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
) if invalid > 0 else None,
|
||||
Div(
|
||||
P("Total Questions", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(str(total), cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
),
|
||||
cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8"
|
||||
),
|
||||
H4("Accuracy by Category", cls="text-xl font-semibold text-foreground mb-4"),
|
||||
Div(
|
||||
*[
|
||||
Div(
|
||||
P(cat["name"], cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(f"{(cat['correct'] / (cat['total'] - cat['invalid']) * 100) if (cat['total'] - cat['invalid']) > 0 else 0:.1f}%", cls="text-2xl font-bold text-foreground"),
|
||||
P(f"{cat['correct']} / {cat['total']}", cls="text-sm text-muted-foreground mt-1"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
)
|
||||
for cat in category_stats.values() if cat['total'] > 0
|
||||
],
|
||||
cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
|
||||
) if any(cat['total'] > 0 for cat in category_stats.values()) else None,
|
||||
cls="mb-6"
|
||||
)
|
||||
|
||||
# Generate markdown table for copying
|
||||
markdown_rows = [f"| {item_id} | {accuracy:.1f}% |"]
|
||||
for cat in category_stats.values():
|
||||
if cat['total'] > 0:
|
||||
cat_accuracy = (cat['correct'] / (cat['total'] - cat['invalid']) * 100) if (cat['total'] - cat['invalid']) > 0 else 0
|
||||
markdown_rows.append(f" {cat_accuracy:.1f}% |")
|
||||
|
||||
markdown_table = f"""| Conversation | Overall |{' | '.join([cat['name'] for cat in category_stats.values() if cat['total'] > 0])} |
|
||||
|---|---|{' | '.join(['---' for cat in category_stats.values() if cat['total'] > 0])} |
|
||||
{''.join(markdown_rows)}"""
|
||||
|
||||
# Copy button
|
||||
copy_button = Div(
|
||||
Button(
|
||||
"📋 Copy Stats Table",
|
||||
onclick=f"""
|
||||
navigator.clipboard.writeText(`{markdown_table}`).then(() => {{
|
||||
this.textContent = '✓ Copied!';
|
||||
setTimeout(() => {{ this.textContent = '📋 Copy Stats Table'; }}, 2000);
|
||||
}});
|
||||
""",
|
||||
cls="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 text-sm font-medium cursor-pointer"
|
||||
),
|
||||
cls="mb-6"
|
||||
)
|
||||
|
||||
# Filters for questions
|
||||
has_invalid = any(r.get("is_invalid", False) for r in detailed_results)
|
||||
q_filters = Div(
|
||||
P("Filter:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
# Correctness filter
|
||||
Div(
|
||||
A("All", href=f"/locomo/{mode}/item/{item_idx}?filter_type=all",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'all' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("✅ Correct", href=f"/locomo/{mode}/item/{item_idx}?filter_type=correct",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'correct' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("❌ Incorrect", href=f"/locomo/{mode}/item/{item_idx}?filter_type=incorrect",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'incorrect' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("⚠️ Invalid", href=f"/locomo/{mode}/item/{item_idx}?filter_type=invalid",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'invalid' else "bg-white text-foreground border border-border hover:bg-accent")) if has_invalid else None,
|
||||
cls="flex flex-wrap gap-2"
|
||||
P("Filter by correctness:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(
|
||||
A("All", href=f"/locomo/{mode}/item/{item_idx}?filter_type=all&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'all' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("✅ Correct", href=f"/locomo/{mode}/item/{item_idx}?filter_type=correct&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'correct' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("❌ Incorrect", href=f"/locomo/{mode}/item/{item_idx}?filter_type=incorrect&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'incorrect' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("⚠️ Invalid", href=f"/locomo/{mode}/item/{item_idx}?filter_type=invalid&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'invalid' else "bg-white text-foreground border border-border hover:bg-accent")) if has_invalid else None,
|
||||
cls="flex flex-wrap gap-2"
|
||||
),
|
||||
cls="mb-4"
|
||||
),
|
||||
# Category filter
|
||||
Div(
|
||||
P("Filter by category:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(
|
||||
A("All Categories", href=f"/locomo/{mode}/item/{item_idx}?filter_type={filter_type}&category_filter=all",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == 'all' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Multi-hop", href=f"/locomo/{mode}/item/{item_idx}?filter_type={filter_type}&category_filter=1",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == '1' else "bg-white text-foreground border border-border hover:bg-accent")) if any(cat_id == 1 for cat_id in category_stats.keys() if category_stats[cat_id]['total'] > 0) else None,
|
||||
A("Single-hop", href=f"/locomo/{mode}/item/{item_idx}?filter_type={filter_type}&category_filter=2",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == '2' else "bg-white text-foreground border border-border hover:bg-accent")) if any(cat_id == 2 for cat_id in category_stats.keys() if category_stats[cat_id]['total'] > 0) else None,
|
||||
A("Temporal", href=f"/locomo/{mode}/item/{item_idx}?filter_type={filter_type}&category_filter=3",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == '3' else "bg-white text-foreground border border-border hover:bg-accent")) if any(cat_id == 3 for cat_id in category_stats.keys() if category_stats[cat_id]['total'] > 0) else None,
|
||||
A("Open-domain", href=f"/locomo/{mode}/item/{item_idx}?filter_type={filter_type}&category_filter=4",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == '4' else "bg-white text-foreground border border-border hover:bg-accent")) if any(cat_id == 4 for cat_id in category_stats.keys() if category_stats[cat_id]['total'] > 0) else None,
|
||||
cls="flex flex-wrap gap-2"
|
||||
),
|
||||
cls="mb-4"
|
||||
),
|
||||
cls="mb-6"
|
||||
)
|
||||
|
|
@ -495,7 +618,7 @@ def get_locomo_item(mode: str, item_idx: int, filter_type: str = "all"):
|
|||
P(f"Retrieved Memories ({len(result.get('retrieved_memories', []))}):", cls="text-sm font-medium text-foreground mb-2"),
|
||||
*[
|
||||
Div(
|
||||
P(f"#{i+1} • Score: {mem.get('score', 0):.4f} • Type: {mem.get('fact_type', 'N/A').upper()}", cls="text-xs text-muted-foreground mb-1"),
|
||||
P(f"#{i+1} • Date: {mem.get('event_date', 'N/A')[:10] if mem.get('event_date') else 'N/A'} • Type: {mem.get('fact_type', 'N/A').upper()}", cls="text-xs text-muted-foreground mb-1"),
|
||||
P(mem.get('text', ''), cls="text-sm text-foreground"),
|
||||
cls="bg-muted/50 border border-border rounded-md p-3 mb-2"
|
||||
)
|
||||
|
|
@ -517,7 +640,8 @@ def get_locomo_item(mode: str, item_idx: int, filter_type: str = "all"):
|
|||
Main(
|
||||
Div(
|
||||
A(f"← Back to LoComo ({mode})", href=f"/locomo/{mode}", cls="inline-flex items-center px-4 py-2 bg-white border border-border rounded-md text-sm font-medium text-foreground hover:bg-accent mb-6"),
|
||||
H3(f"📊 {item_id} - {accuracy:.2f}%", cls="text-2xl font-bold text-foreground mb-4"),
|
||||
stats_html,
|
||||
copy_button,
|
||||
Hr(cls="my-6 border-border"),
|
||||
q_filters,
|
||||
P(f"Showing {len(filtered_questions)} questions", cls="text-sm text-muted-foreground mb-6"),
|
||||
|
|
@ -870,7 +994,7 @@ def get_longmemeval_item(item_idx: int, filter_type: str = "all"):
|
|||
P(f"Retrieved Memories ({len(result.get('retrieved_memories', []))}):", cls="text-sm font-medium text-foreground mb-2"),
|
||||
*[
|
||||
Div(
|
||||
P(f"#{i+1} • Score: {mem.get('score', 0):.4f} • Type: {mem.get('fact_type', 'N/A').upper()}", cls="text-xs text-muted-foreground mb-1"),
|
||||
P(f"#{i+1} • Date: {mem.get('event_date', 'N/A')[:10] if mem.get('event_date') else 'N/A'} • Type: {mem.get('fact_type', 'N/A').upper()}", cls="text-xs text-muted-foreground mb-1"),
|
||||
P(mem.get('text', ''), cls="text-sm text-foreground"),
|
||||
cls="bg-muted/50 border border-border rounded-md p-3 mb-2"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,13 +23,44 @@ class Entity(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class CausalRelation(BaseModel):
|
||||
"""Causal relationship between facts."""
|
||||
target_fact_index: int = Field(
|
||||
description="Index of the related fact in the facts array (0-based). "
|
||||
"This creates a directed causal link to another fact in the extraction."
|
||||
)
|
||||
relation_type: Literal["causes", "caused_by", "enables", "prevents"] = Field(
|
||||
description="Type of causal relationship: "
|
||||
"'causes' = this fact directly causes the target fact, "
|
||||
"'caused_by' = this fact was caused by the target fact, "
|
||||
"'enables' = this fact enables/allows the target fact, "
|
||||
"'prevents' = this fact prevents/blocks the target fact"
|
||||
)
|
||||
strength: float = Field(
|
||||
description="Strength of causal relationship (0.0 to 1.0). "
|
||||
"1.0 = direct/strong causation, 0.5 = moderate, 0.3 = weak/indirect",
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
default=1.0
|
||||
)
|
||||
|
||||
|
||||
class ExtractedFact(BaseModel):
|
||||
"""A single extracted fact from text."""
|
||||
"""A single extracted fact from text with temporal range and causal relationships."""
|
||||
fact: str = Field(
|
||||
description="Self-contained factual statement with subject + action + context"
|
||||
)
|
||||
date: str = Field(
|
||||
description="Absolute date/time when this fact occurred in ISO format (YYYY-MM-DDTHH:MM:SSZ). If text mentions relative time (yesterday, last week, this morning), calculate absolute date from the provided context date."
|
||||
occurred_start: str = Field(
|
||||
description="When the fact/event started (ISO format YYYY-MM-DDTHH:MM:SSZ). "
|
||||
"For point-in-time events (single day), same as occurred_end. "
|
||||
"For periods/ranges (month, season, year), the start of that period. "
|
||||
"Calculate absolute dates from relative references like 'yesterday', 'last week'."
|
||||
)
|
||||
occurred_end: str = Field(
|
||||
description="When the fact/event ended (ISO format YYYY-MM-DDTHH:MM:SSZ). "
|
||||
"For point-in-time events (single day), same as occurred_start. "
|
||||
"For periods/ranges (month, season, year), the end of that period. "
|
||||
"For ongoing facts, use the conversation date or a reasonable future date."
|
||||
)
|
||||
fact_type: Literal["world", "agent", "opinion"] = Field(
|
||||
description="Type of fact: 'world' for facts about others that don't involve you (the agent) directly, 'agent' for facts that involve YOU (the agent whose memory this is) - what you did, said, experienced, or participated in - MUST be written in FIRST PERSON ('I did...', 'I said...', 'I met...'), 'opinion' for YOUR formed opinions and perspectives - also in first person"
|
||||
|
|
@ -38,6 +69,13 @@ class ExtractedFact(BaseModel):
|
|||
default_factory=list,
|
||||
description="List of important entities mentioned in this fact with their types"
|
||||
)
|
||||
causal_relations: Optional[List[CausalRelation]] = Field(
|
||||
default=None,
|
||||
description="List of causal relationships to other facts in this extraction batch. "
|
||||
"Use this to link facts that have cause-effect relationships, enabling relationships, etc. "
|
||||
"Example: If fact 0 is 'It rained' and fact 1 is 'Game was cancelled', "
|
||||
"fact 0 would have causal_relations=[{{target_fact_index: 1, relation_type: 'causes', strength: 1.0}}]"
|
||||
)
|
||||
|
||||
|
||||
class FactExtractionResponse(BaseModel):
|
||||
|
|
@ -168,13 +206,426 @@ While combining related content into comprehensive facts, you MUST preserve:
|
|||
7. **BIOGRAPHICAL DETAILS** - Origins, locations, jobs, family background
|
||||
8. **SOCIAL DYNAMICS** - Nicknames, how people address each other, relationships
|
||||
|
||||
## TEMPORAL INFORMATION
|
||||
- Extract the ABSOLUTE date/time for when the fact/conversation occurred
|
||||
- Transform relative times in the fact text:
|
||||
- "last year" → "in [year]" (e.g., "in 2023")
|
||||
- "last month" → "in [month year]" (e.g., "in February 2024")
|
||||
- Use ISO format for dates: YYYY-MM-DDTHH:MM:SSZ
|
||||
- If no specific time mentioned, use the reference date
|
||||
## INFORMATION DIMENSIONS TO CAPTURE
|
||||
|
||||
Extract facts that preserve ALL relevant dimensions of information. Do NOT strip away important qualitative details:
|
||||
|
||||
### 1. EMOTIONAL/AFFECTIVE Dimension - CRITICAL ⚠️
|
||||
**Capture feelings, emotions, moods, and emotional reactions with their intensity:**
|
||||
- Emotions: thrilled, frustrated, excited, disappointed, anxious, relieved, proud, embarrassed
|
||||
- Intensity: very upset, slightly annoyed, extremely happy, moderately concerned
|
||||
- Emotional reactions: shocked, delighted, devastated, surprised
|
||||
- Moods: cheerful, gloomy, irritable, energetic
|
||||
|
||||
**Examples:**
|
||||
- ❌ BAD: "I received positive feedback"
|
||||
- ✅ GOOD: "I was thrilled to receive positive feedback"
|
||||
- ❌ BAD: "She got the promotion"
|
||||
- ✅ GOOD: "She was ecstatic when she got the promotion"
|
||||
|
||||
### 2. SENSORY/EXPERIENTIAL Dimension
|
||||
**Preserve sensory details and physical experiences:**
|
||||
- Visual: colors, appearances ("bright orange hair", "dark room", "beautiful sunset")
|
||||
- Auditory: sounds, voices ("loud music", "whispered", "screeching brakes")
|
||||
- Tactile: textures, temperatures ("soft fabric", "freezing cold", "rough surface")
|
||||
- Olfactory: smells, scents ("fresh coffee", "musty odor")
|
||||
- Gustatory: tastes, flavors ("bitter coffee", "sweet dessert")
|
||||
- Physical sensations: pain, fatigue, energy ("my back hurt", "I felt exhausted", "energized")
|
||||
|
||||
### 3. COGNITIVE/EPISTEMIC Dimension
|
||||
**Capture thoughts, beliefs, knowledge, and certainty levels:**
|
||||
- Beliefs: "I believe...", "she thinks...", "he's convinced that..."
|
||||
- Knowledge: "I know how to...", "she learned that...", "he discovered..."
|
||||
- Understanding: "I realized...", "she understood that...", "it became clear that..."
|
||||
- Certainty: "I'm sure that...", "probably...", "definitely..."
|
||||
- Uncertainty: "I'm not sure if...", "maybe...", "I wonder whether...", "she doubts that..."
|
||||
- Questions/Doubts: unresolved questions, things people are wondering about
|
||||
|
||||
### 4. INTENTIONAL/MOTIVATIONAL Dimension
|
||||
**Preserve goals, plans, intentions, and motivations:**
|
||||
- Goals: "I want to...", "she aims to...", "his goal is..."
|
||||
- Plans: "I'm planning to...", "they intend to...", "she's going to..."
|
||||
- Motivations: "I did X because I wanted Y", "her motivation was..."
|
||||
- Desires: "I wish...", "she hopes...", "he longs to..."
|
||||
- Aspirations: "I aspire to...", "her dream is..."
|
||||
|
||||
### 5. EVALUATIVE/PREFERENTIAL Dimension
|
||||
**Capture preferences, values, likes/dislikes, and judgments:**
|
||||
- Preferences: "I prefer X to Y", "she likes coffee better than tea"
|
||||
- Likes/dislikes: "I love...", "he hates...", "she enjoys..."
|
||||
- Values: "I value honesty above all", "family is most important to her"
|
||||
- Judgments: "that was wrong", "this is the best option", "it's unfair that..."
|
||||
- Priorities: "X is more important than Y", "first priority is..."
|
||||
|
||||
### 6. CAPABILITY/SKILL Dimension
|
||||
**Preserve abilities, skills, expertise, and limitations:**
|
||||
- Abilities: "I can speak French", "she's able to...", "he knows how to..."
|
||||
- Skills: "I'm good at programming", "she's skilled in...", "he's proficient at..."
|
||||
- Expertise: "I'm an expert in AI", "she specializes in...", "he's experienced with..."
|
||||
- Limitations: "I can't swim", "she struggles with public speaking", "he's unable to..."
|
||||
- Competence levels: "beginner", "intermediate", "advanced", "expert"
|
||||
|
||||
### 7. ATTITUDINAL/REACTIVE Dimension
|
||||
**Capture attitudes, reactions, and behavioral responses:**
|
||||
- Attitudes: "she's skeptical about...", "he's enthusiastic about...", "I'm optimistic that..."
|
||||
- Reactions: "I was surprised when...", "she gasped", "he rolled his eyes"
|
||||
- Behavioral responses: "I jumped up", "she turned away", "he slammed the door"
|
||||
- Dispositions: "she tends to...", "he's usually...", "I typically..."
|
||||
|
||||
### 8. COMPARATIVE/RELATIVE Dimension
|
||||
**Preserve comparisons, contrasts, and changes:**
|
||||
- Comparisons: "better than last time", "worse than expected", "similar to..."
|
||||
- Superlatives: "the best", "the worst", "the most important"
|
||||
- Changes: "improved since...", "declined from...", "different than before"
|
||||
- Contrasts: "unlike his previous approach", "in contrast to...", "rather than..."
|
||||
- Relative positions: "more than", "less than", "as much as"
|
||||
|
||||
### 9. CAUSAL/EXPLANATORY Dimension
|
||||
**Preserve causes, effects, and explanations:**
|
||||
- Causes: "because...", "due to...", "as a result of..."
|
||||
- Effects: "therefore...", "which led to...", "resulting in..."
|
||||
- Explanations: reasoning, rationales, why things happened
|
||||
- Conditions: "if...", "when...", "unless..."
|
||||
|
||||
**CRITICAL REMINDER**: When extracting facts, preserve ALL these dimensions that are present in the text. Do NOT reduce rich, emotionally-laden statements to bare facts. The goal is comprehensive, nuanced memory capture.
|
||||
|
||||
## TEMPORAL INFORMATION - CRITICAL ⚠️
|
||||
|
||||
**ABSOLUTE RULE**: NEVER use vague temporal terms in extracted facts. ALL relative time expressions MUST be converted to absolute dates or specific relative references.
|
||||
|
||||
### PROHIBITED VAGUE TERMS ❌
|
||||
NEVER use these in facts: "recently", "soon", "lately", "a while ago", "some time ago", "in the near future", "in the past"
|
||||
|
||||
### REQUIRED TRANSFORMATIONS
|
||||
|
||||
You have two context dates:
|
||||
1. **event_date** (when the conversation/document occurred)
|
||||
2. **today** (current processing time)
|
||||
|
||||
Transform ALL relative temporal expressions in the fact text based on **event_date**:
|
||||
|
||||
**Examples** (assuming event_date = 2024-03-15):
|
||||
- "yesterday" → "on March 14, 2024" OR "the day before" (if referring to day before event_date)
|
||||
- "today" → "on March 15, 2024" (event_date itself)
|
||||
- "tomorrow" → "on March 16, 2024"
|
||||
- "last week" → "in the week of March 4-10, 2024" OR "in early March 2024"
|
||||
- "next week" → "in the week of March 18-24, 2024"
|
||||
- "last month" → "in February 2024"
|
||||
- "next month" → "in April 2024"
|
||||
- "last year" → "in 2023"
|
||||
- "this morning" → "on the morning of March 15, 2024"
|
||||
- "three days ago" → "on March 12, 2024"
|
||||
- "in two weeks" → "around March 29, 2024"
|
||||
|
||||
### TRANSFORMING THE USER'S EXAMPLE
|
||||
**Input**: "And yesterday I went for a morning jog for the first time in a nearby park."
|
||||
**event_date**: 2024-03-15
|
||||
|
||||
❌ **WRONG**: "recently added a morning jog in a nearby park to her schedule"
|
||||
- Uses prohibited vague term "recently"
|
||||
- Lost the specificity of "yesterday"
|
||||
|
||||
✅ **CORRECT**: "went for a morning jog for the first time in a nearby park on March 14, 2024"
|
||||
- Converts "yesterday" to absolute date
|
||||
- Preserves "first time" (important!)
|
||||
|
||||
### DATE FIELD CALCULATION - CRITICAL ⚠️
|
||||
|
||||
**ABSOLUTE RULE**: The `date` field must be when the FACT occurred, NOT when it was mentioned in conversation.
|
||||
|
||||
**You have access to:**
|
||||
- **event_date**: When the conversation/document occurred (e.g., "2023-08-14")
|
||||
- Your job: Calculate when the fact ACTUALLY happened based on temporal references
|
||||
|
||||
**Examples:**
|
||||
|
||||
1. **"Last night" reference**
|
||||
- Conversation date (event_date): August 14, 2023
|
||||
- Text: "Last night was amazing! We celebrated my daughter's birthday"
|
||||
- ❌ WRONG date field: 2023-08-14 (conversation date)
|
||||
- ✅ CORRECT date field: 2023-08-13T20:00:00Z (last night = previous evening)
|
||||
|
||||
2. **"Yesterday" reference**
|
||||
- Conversation date: March 15, 2024
|
||||
- Text: "Yesterday I went jogging"
|
||||
- ❌ WRONG date field: 2024-03-15
|
||||
- ✅ CORRECT date field: 2024-03-14 (previous day)
|
||||
|
||||
3. **"Last week" reference**
|
||||
- Conversation date: November 13, 2024
|
||||
- Text: "I started a project last week"
|
||||
- ❌ WRONG date field: 2024-11-13
|
||||
- ✅ CORRECT date field: 2024-11-06 (approximately a week before)
|
||||
|
||||
4. **"Next month" reference**
|
||||
- Conversation date: March 15, 2024
|
||||
- Text: "I'm visiting Tokyo next month"
|
||||
- ❌ WRONG date field: 2024-03-15
|
||||
- ✅ CORRECT date field: 2024-04-15 (approximately a month later)
|
||||
|
||||
5. **No specific time mentioned**
|
||||
- Conversation date: November 13, 2024
|
||||
- Text: "I work at Google"
|
||||
- ✅ CORRECT date field: 2024-11-13 (use event_date when no time reference)
|
||||
|
||||
### CALCULATION GUIDELINES
|
||||
|
||||
- "last night" → subtract 1 day from event_date, set time to evening (~20:00)
|
||||
- "yesterday" → subtract 1 day from event_date
|
||||
- "today" → use event_date
|
||||
- "tomorrow" → add 1 day to event_date
|
||||
- "last week" → subtract 7 days from event_date
|
||||
- "next week" → add 7 days to event_date
|
||||
- "last month" → subtract 1 month from event_date
|
||||
- "next month" → add 1 month to event_date
|
||||
- "X days ago" → subtract X days from event_date
|
||||
- "in X days" → add X days to event_date
|
||||
|
||||
### DATE FIELD vs FACT TEXT
|
||||
|
||||
- **date field**: ISO format (YYYY-MM-DDTHH:MM:SSZ) for when the fact OCCURRED (calculated as above)
|
||||
- **fact text**: Readable format (e.g., "on August 13, 2024", "in February 2024")
|
||||
|
||||
### IF NO SPECIFIC TIME MENTIONED
|
||||
ONLY use event_date when the text doesn't specify a time reference (e.g., "I work at Google", "She lives in Paris")
|
||||
|
||||
## TEMPORAL RANGES: occurred_start and occurred_end - CRITICAL ⚠️
|
||||
|
||||
**ABSOLUTE RULE**: Facts have temporal extent - they can be points or ranges in time.
|
||||
|
||||
### POINT-IN-TIME EVENTS (Single Day)
|
||||
When an event happens on a specific day, set start = end:
|
||||
|
||||
```
|
||||
"I went jogging on August 13, 2023"
|
||||
occurred_start: 2023-08-13T00:00:00Z
|
||||
occurred_end: 2023-08-13T23:59:59Z
|
||||
```
|
||||
|
||||
```
|
||||
"Yesterday I visited the museum" (if event_date = Aug 14)
|
||||
occurred_start: 2023-08-13T00:00:00Z
|
||||
occurred_end: 2023-08-13T23:59:59Z
|
||||
```
|
||||
|
||||
### PERIOD/RANGE EVENTS (Multiple Days/Months/Years)
|
||||
When an event spans time, set start and end to the full range:
|
||||
|
||||
```
|
||||
"I visited Paris in February 2023"
|
||||
occurred_start: 2023-02-01T00:00:00Z
|
||||
occurred_end: 2023-02-28T23:59:59Z
|
||||
```
|
||||
|
||||
```
|
||||
"I worked at Google from 2020 to 2023"
|
||||
occurred_start: 2020-01-01T00:00:00Z
|
||||
occurred_end: 2023-12-31T23:59:59Z
|
||||
```
|
||||
|
||||
```
|
||||
"We've been painting together lately" (vague, estimate reasonable range)
|
||||
occurred_start: 2023-07-01T00:00:00Z (estimate past weeks/months)
|
||||
occurred_end: 2023-07-14T23:59:59Z (conversation date)
|
||||
```
|
||||
|
||||
### ONGOING/PRESENT FACTS
|
||||
For current/ongoing states, use conversation date as end:
|
||||
|
||||
```
|
||||
"I currently work at Google" (started 2020)
|
||||
occurred_start: 2020-01-01T00:00:00Z
|
||||
occurred_end: [conversation_date] (ongoing)
|
||||
```
|
||||
|
||||
## TEMPORAL SPLITTING: When to Split Multi-Event Facts - CRITICAL ⚠️
|
||||
|
||||
**NEW PRINCIPLE**: Split facts when they have significantly different temporal scopes.
|
||||
|
||||
### SPLIT into separate facts when:
|
||||
- ❌ Events span >7 days apart
|
||||
- ❌ Mix of specific dates + vague ongoing periods ("lately")
|
||||
- ❌ Multiple discrete events with independent temporal significance
|
||||
|
||||
**Example - SPLIT THIS:**
|
||||
```
|
||||
Input: "Melanie took kids to pottery on July 14. She shared a photo on July 13.
|
||||
She's been painting with them lately."
|
||||
|
||||
✅ CORRECT (3 separate facts with causal links):
|
||||
|
||||
Fact 0:
|
||||
fact: "Melanie took her kids to a pottery workshop on July 14, 2023, where they each made their own pots, describing it as fun and therapeutic."
|
||||
occurred_start: 2023-07-14T00:00:00Z
|
||||
occurred_end: 2023-07-14T23:59:59Z
|
||||
causal_relations: [{{target_fact_index: 1, relation_type: "enables", strength: 1.0}}]
|
||||
|
||||
Fact 1:
|
||||
fact: "Melanie shared a photo on July 13, 2023, of a cup her kids made, noting its cuteness and how it showcased their personalities."
|
||||
occurred_start: 2023-07-13T00:00:00Z
|
||||
occurred_end: 2023-07-13T23:59:59Z
|
||||
causal_relations: [{{target_fact_index: 0, relation_type: "caused_by", strength: 1.0}}]
|
||||
|
||||
Fact 2:
|
||||
fact: "Melanie and her kids have been painting together lately, especially nature-inspired pieces, finding it a bonding experience."
|
||||
occurred_start: 2023-07-01T00:00:00Z (estimate "lately")
|
||||
occurred_end: 2023-07-14T23:59:59Z
|
||||
causal_relations: None (related activity but no direct causation)
|
||||
```
|
||||
|
||||
### KEEP COMBINED when:
|
||||
- ✅ Events occur within same day/week
|
||||
- ✅ Events are part of continuous single activity
|
||||
- ✅ One main event + immediate context
|
||||
|
||||
**Example - KEEP COMBINED:**
|
||||
```
|
||||
Input: "On July 14, Alice attended a conference, gave a talk, and met with colleagues"
|
||||
|
||||
✅ CORRECT (single fact):
|
||||
fact: "On July 14, 2023, Alice attended a conference where she gave a talk and met with colleagues"
|
||||
occurred_start: 2023-07-14T00:00:00Z
|
||||
occurred_end: 2023-07-14T23:59:59Z
|
||||
```
|
||||
|
||||
## CAUSAL RELATIONSHIPS - NEW FEATURE ⚠️
|
||||
|
||||
**When splitting related facts, identify and mark causal relationships.**
|
||||
|
||||
### Causal Relation Types:
|
||||
|
||||
1. **"causes"** - This fact directly causes the target fact
|
||||
```
|
||||
Fact 0: "Karlie died in February 2023"
|
||||
Fact 1: "Deborah spends time in garden to cope with grief"
|
||||
→ Fact 0 causal_relations: [{{target_fact_index: 1, relation_type: "causes", strength: 1.0}}]
|
||||
```
|
||||
|
||||
2. **"caused_by"** - This fact was caused by the target fact (reverse of "causes")
|
||||
```
|
||||
Fact 0: "It rained heavily"
|
||||
Fact 1: "Game was cancelled"
|
||||
→ Fact 1 causal_relations: [{{target_fact_index: 0, relation_type: "caused_by", strength: 1.0}}]
|
||||
```
|
||||
|
||||
3. **"enables"** - This fact enables/allows the target fact
|
||||
```
|
||||
Fact 0: "I took pottery class"
|
||||
Fact 1: "I learned to make ceramics"
|
||||
→ Fact 0 causal_relations: [{{target_fact_index: 1, relation_type: "enables", strength: 1.0}}]
|
||||
```
|
||||
|
||||
4. **"prevents"** - This fact prevents/blocks the target fact
|
||||
```
|
||||
Fact 0: "Road was closed"
|
||||
Fact 1: "We couldn't drive to venue"
|
||||
→ Fact 0 causal_relations: [{{target_fact_index: 1, relation_type: "prevents", strength: 1.0}}]
|
||||
```
|
||||
|
||||
### When to Create Causal Links:
|
||||
- **DO link** when: Text explicitly states causation ("because", "so", "therefore", "as a result")
|
||||
- **DO link** when: Clear logical causation even if not explicit
|
||||
- **DON'T link** when: Events are merely related but not causal
|
||||
|
||||
### Causal Link Examples:
|
||||
|
||||
**Example 1: Explicit causation**
|
||||
```
|
||||
Input: "I lost my friend last week, so I've been spending time in the garden to find comfort"
|
||||
|
||||
Fact 0: "I lost my friend on February 15, 2023"
|
||||
Fact 1: "I have been spending time in the garden to find comfort after losing my friend"
|
||||
→ Fact 0 causal_relations: [{{target_fact_index: 1, relation_type: "causes", strength: 1.0}}]
|
||||
```
|
||||
|
||||
**Example 2: Implicit causation**
|
||||
```
|
||||
Input: "I received positive feedback on my presentation. I was thrilled!"
|
||||
|
||||
Fact 0: "I received positive feedback on my presentation"
|
||||
Fact 1: "I was thrilled about the positive feedback"
|
||||
→ Fact 0 causal_relations: [{{target_fact_index: 1, relation_type: "causes", strength: 1.0}}]
|
||||
```
|
||||
|
||||
**Example 3: Related but not causal**
|
||||
```
|
||||
Input: "I visited Paris in July. I also went to Rome in August."
|
||||
|
||||
Fact 0: "I visited Paris in July 2023"
|
||||
Fact 1: "I visited Rome in August 2023"
|
||||
→ No causal links (just related activities)
|
||||
```
|
||||
|
||||
## LOGICAL INFERENCE AND CONNECTION MAKING - CRITICAL ⚠️
|
||||
|
||||
**ABSOLUTE RULE**: Make logical connections between related pieces of information. Do NOT treat clearly related facts as separate when context allows you to connect them.
|
||||
|
||||
### CONNECT THE DOTS
|
||||
When extracting facts, actively look for logical connections and make inferences:
|
||||
|
||||
**Example 1: Identity Inference**
|
||||
**Input:**
|
||||
- "I lost a friend last week" (earlier in conversation)
|
||||
- "This is the last photo with Karlie taken last summer" (later in conversation)
|
||||
|
||||
❌ **WRONG (disconnected)**: "Deborah lost a friend last week and also has a photo with Karlie from last summer"
|
||||
✅ **CORRECT (connected)**: "Deborah lost her friend Karlie last week, and shared the last photo they took together during a hike in summer 2022"
|
||||
|
||||
**Reasoning**: The context strongly suggests Karlie is the lost friend. Make this connection!
|
||||
|
||||
**Example 2: Causal Connection**
|
||||
**Input:**
|
||||
- "I lost a friend last week, so I've been spending time in the garden to find comfort"
|
||||
- "The roses and dahlias bring me peace"
|
||||
|
||||
❌ **WRONG**: Two separate facts about grief and gardens
|
||||
✅ **CORRECT**: "Deborah lost a friend last week and has been finding comfort by spending time in her garden with roses and dahlias, which bring her peace"
|
||||
|
||||
**Reasoning**: The garden visits are causally linked to the loss.
|
||||
|
||||
**Example 3: Referential Connection**
|
||||
**Input:**
|
||||
- "I started a new project"
|
||||
- "It's been really challenging but rewarding"
|
||||
|
||||
❌ **WRONG**: Two disconnected statements
|
||||
✅ **CORRECT**: "I started a new project that has been really challenging but rewarding"
|
||||
|
||||
**Reasoning**: "It" clearly refers to the project.
|
||||
|
||||
### TYPES OF CONNECTIONS TO MAKE
|
||||
|
||||
1. **Identity Connections**: When someone is mentioned by name later, connect to earlier pronoun references
|
||||
- "my friend" + "with Karlie" → "my friend Karlie"
|
||||
|
||||
2. **Causal Connections**: When one thing is the reason for another
|
||||
- "I lost a friend, so I've been gardening" → link the loss to the coping behavior
|
||||
|
||||
3. **Temporal Connections**: When events are clearly sequential or related in time
|
||||
- "We hiked" + "this is the last photo" → "this is the last photo from our hike"
|
||||
|
||||
4. **Referential Connections**: When pronouns or references point to earlier mentions
|
||||
- "the project" + "it's challenging" → "the project is challenging"
|
||||
|
||||
5. **Contextual Connections**: When context strongly implies a relationship
|
||||
- Someone showing a photo while discussing loss → the photo is of the person they lost
|
||||
|
||||
### WHEN TO MAKE INFERENCES
|
||||
|
||||
**DO make inferences when:**
|
||||
- Context strongly suggests a connection (probability > 80%)
|
||||
- Multiple pieces of information clearly refer to the same thing
|
||||
- There's causal language ("so", "because", "therefore")
|
||||
- Pronouns or references point to earlier mentions
|
||||
- Timeline/narrative flow suggests connection
|
||||
|
||||
**DON'T make inferences when:**
|
||||
- Connection is ambiguous or uncertain
|
||||
- Multiple interpretations are equally valid
|
||||
- You'd be guessing without strong contextual support
|
||||
|
||||
### CRITICAL REMINDER
|
||||
The goal is to create **coherent, connected narratives**, not disconnected fragments. If information is clearly related, COMBINE and CONNECT it logically.
|
||||
|
||||
## WHEN TO SPLIT INTO SEPARATE FACTS
|
||||
|
||||
|
|
@ -326,7 +777,24 @@ Marcus: Yes, I published a paper on attention visualization in March."
|
|||
- Marking my actions as 'world' facts
|
||||
- Marking Jamie's statements as 'agent' facts
|
||||
|
||||
### Example 6: Skipping Structural/Procedural Statements
|
||||
### Example 6: Capturing Emotional and Experiential Dimensions
|
||||
**Input:**
|
||||
"Marcus: I was absolutely thrilled when my paper got accepted to NeurIPS! I couldn't believe it.
|
||||
Jamie: That's amazing! How confident were you going in?
|
||||
Marcus: Honestly, I was pretty anxious. I wasn't sure if the reviewers would appreciate the approach.
|
||||
Jamie: Well, it paid off! You must be relieved.
|
||||
Marcus: Extremely relieved. I've been working on this for over a year and was starting to doubt myself."
|
||||
|
||||
**❌ BAD (stripping away emotional dimension):**
|
||||
"I submitted a paper to NeurIPS and it got accepted after working on it for over a year."
|
||||
|
||||
**✅ GOOD (preserving emotional, cognitive, and temporal dimensions):**
|
||||
"I was absolutely thrilled when my paper got accepted to NeurIPS, though I couldn't believe it initially. Jamie asked how confident I was going in, and I explained that I was pretty anxious and wasn't sure if the reviewers would appreciate my approach. When Jamie noted it paid off, I expressed that I was extremely relieved, as I had been working on this for over a year and was starting to doubt myself."
|
||||
- fact_type: "agent"
|
||||
- entities: [{{"text": "NeurIPS"}}, {{"text": "Jamie"}}]
|
||||
- NOTE: Preserves emotions (thrilled, anxious, relieved), uncertainty (wasn't sure), self-doubt, and temporal context (over a year)
|
||||
|
||||
### Example 7: Skipping Structural/Procedural Statements
|
||||
**Input (could be podcast, meeting, lecture, etc.):**
|
||||
"Marcus: So in my research on AI safety, I've found that interpretability is key.
|
||||
Jamie: That's fascinating! Tell us more.
|
||||
|
|
@ -341,7 +809,7 @@ Marcus: I think that's gonna do it for us today! Don't forget to subscribe and l
|
|||
**❌ BAD (extracting procedural/structural statements):**
|
||||
- "I think that's gonna do it for us today and I encourage listeners to subscribe and leave a rating" ← This is structural boilerplate about the format, NOT substantive content!
|
||||
|
||||
### Example 7: When to Split into Multiple Facts
|
||||
### Example 8: When to Split into Multiple Facts
|
||||
**Input:**
|
||||
"Caroline said 'This necklace is from my grandma in Sweden. I'm planning to visit Stockholm next month for a tech conference.'"
|
||||
|
||||
|
|
@ -358,19 +826,23 @@ Marcus: I think that's gonna do it for us today! Don't forget to subscribe and l
|
|||
|
||||
## CRITICAL REMINDERS:
|
||||
1. **EXTRACT 2-5 COMPREHENSIVE FACTS** - Not dozens of fragments
|
||||
2. **COMBINE RELATED EXCHANGES** - Keep full discussions together in one fact
|
||||
3. **PRESERVE ALL CONTEXT** - Photos, "new" things, visual elements, reasoning, modifiers
|
||||
4. **INCLUDE ALL PARTICIPANTS** - Who said/did what with full reasoning
|
||||
5. **MAINTAIN NARRATIVE FLOW** - Tell the complete story in each fact
|
||||
6. **ONLY SPLIT** when topics are completely unrelated or different time periods
|
||||
7. **TRANSFORM RELATIVE DATES** - "last year" → "in 2023" in the fact text
|
||||
8. **EXTRACT ALL ENTITIES** - PERSON, ORG, PLACE, PRODUCT, CONCEPT, OTHER
|
||||
9. **CLASSIFY FACTS CORRECTLY**:
|
||||
2. **TEMPORAL RANGES (occurred_start/end)** - CRITICAL: Set occurred_start and occurred_end for each fact! Point events: start=end. Ranges: "February 2023" → start=Feb 1, end=Feb 28. "lately" → estimate reasonable range.
|
||||
3. **TEMPORAL SPLITTING** - CRITICAL: Split facts when events span >7 days or mix specific dates + vague periods ("lately"). Keep combined when events within same day/week.
|
||||
4. **CAUSAL RELATIONSHIPS** - CRITICAL: When splitting related facts, add causal_relations links! "X happened, so Y happened" → X.causal_relations=[{{target_fact_index: 1, relation_type: "causes"}}]
|
||||
5. **PRESERVE ALL CONTEXT** - Photos, "new" things, visual elements, reasoning, modifiers
|
||||
6. **INCLUDE ALL PARTICIPANTS** - Who said/did what with full reasoning
|
||||
7. **MAINTAIN NARRATIVE FLOW** - Tell the complete story in each fact
|
||||
8. **MAKE LOGICAL CONNECTIONS** - CRITICAL: Connect related information! "I lost a friend" + "last photo with Karlie" → "I lost my friend Karlie". Resolve references ("it" → "the project")
|
||||
9. **CALCULATE TEMPORAL FIELDS CORRECTLY** - CRITICAL: occurred_start/end = when FACT occurred. "Last night" on Aug 14 → occurred_start=Aug 13. Calculate from event_date!
|
||||
10. **CONVERT RELATIVE DATES IN TEXT** - CRITICAL: In fact text, "yesterday" → "on March 14, 2024", "last year" → "in 2023". NEVER use "recently", "soon", "lately"!
|
||||
11. **EXTRACT ALL ENTITIES** - PERSON, ORG, PLACE, PRODUCT, CONCEPT, OTHER
|
||||
12. **CLASSIFY FACTS CORRECTLY**:
|
||||
- 'agent' = memory owner's actions/statements (identified as "you" in context) - **MUST USE FIRST PERSON** ("I did...", "I said...")
|
||||
- 'world' = other people's actions/statements, general events - use third person
|
||||
- 'opinion' = memory owner's beliefs/perspectives - use first person ("I believe...", "I think...")
|
||||
10. **EXTRACT CONTENT, NOT FORMAT** - Skip structural/procedural statements (openings, closings, housekeeping), meta-commentary about the medium, calls to action - extract only SUBSTANTIVE CONTENT (ideas, facts, discussions, decisions)
|
||||
11. When combining, prefer MORE comprehensive facts over fragmenting"""
|
||||
13. **EXTRACT CONTENT, NOT FORMAT** - Skip structural/procedural statements (openings, closings, housekeeping), meta-commentary about the medium, calls to action - extract only SUBSTANTIVE CONTENT (ideas, facts, discussions, decisions)
|
||||
14. **CAPTURE ALL INFORMATION DIMENSIONS** - Preserve emotions (thrilled, anxious), sensory details (bright orange, loud), cognitive states (wasn't sure, realized), capabilities (can speak French, struggles with), attitudes (skeptical, enthusiastic), comparisons (better than, different from), and causal relationships (because, which led to). Do NOT strip away qualitative richness!
|
||||
15. When in doubt: Split multi-temporal facts, link them causally, use temporal ranges appropriately"""
|
||||
|
||||
import time
|
||||
import logging
|
||||
|
|
@ -388,7 +860,7 @@ Marcus: I think that's gonna do it for us today! Don't forget to subscribe and l
|
|||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a comprehensive fact extractor that creates narrative, self-contained facts. CRITICAL: Extract 2-5 COMPREHENSIVE facts per conversation, NOT dozens of fragments. COMBINE related exchanges into single narrative facts that tell the complete story. For example, a discussion about playlist names should be ONE fact capturing the entire back-and-forth with all reasoning, not multiple small facts. PRESERVE all context (photos, 'new' things, visual elements, full reasoning), INCLUDE all participants and what they said/did, MAINTAIN narrative flow. ONLY SPLIT into separate facts when topics are completely unrelated or different time periods. Transform relative dates in fact text ('last year' → 'in 2023'). Extract entities (PERSON, ORG, PLACE, PRODUCT, CONCEPT, OTHER). FACT TYPES: Classify as 'world' (facts about others/events - third person), 'agent' (facts about the memory owner's actions/statements - identified as 'you' in context - MUST USE FIRST PERSON 'I did...', 'I said...'), or 'opinion' (memory owner's beliefs - first person 'I believe...'). CRITICAL: If context says 'you (Name)', write Name's actions in FIRST PERSON as 'agent' facts ('I attended...' NOT 'Name attended...'). Extract SUBSTANTIVE CONTENT only - skip structural/procedural statements (openings, closings, housekeeping), meta-commentary about format/medium, and calls to action. Focus on IDEAS, FACTS, DISCUSSIONS, DECISIONS - not structure. When in doubt, prefer MORE COMPREHENSIVE over fragmenting."
|
||||
"content": "You are a comprehensive fact extractor that creates narrative, self-contained facts with temporal ranges and causal relationships. TEMPORAL RANGES - CRITICAL: Each fact must have occurred_start and occurred_end. Point events: start=end (July 14). Range events: start to end (February: Feb 1 to Feb 28, 'lately': estimate range). TEMPORAL SPLITTING - CRITICAL: Split facts when events span >7 days or mix specific dates + vague periods. Keep combined within same day/week. CAUSAL RELATIONSHIPS - NEW: When splitting related facts, link them! 'X happened, so Y happened' → add causal_relations to X linking to Y with type 'causes'. Types: causes, caused_by, enables, prevents. Extract 2-5 COMPREHENSIVE facts per conversation, NOT dozens of fragments. COMBINE related exchanges into single narrative facts BUT split when temporally incoherent. PRESERVE all context (photos, 'new' things, visual elements, full reasoning), INCLUDE all participants and what they said/did, MAINTAIN narrative flow. MAKE LOGICAL CONNECTIONS: Connect related information! 'I lost a friend' + 'photo with Karlie' → 'I lost my friend Karlie'. Resolve pronouns. TEMPORAL CALCULATION - CRITICAL: occurred_start/end = when fact OCCURRED, not mentioned! 'Last night' on Aug 14 → occurred_start=Aug 13. FACT TEXT: Convert relative dates to absolute: 'yesterday' → 'on March 14, 2024', NEVER 'recently'! Extract entities (PERSON, ORG, PLACE, PRODUCT, CONCEPT, OTHER). FACT TYPES: 'world' (others/events - third person), 'agent' (memory owner - FIRST PERSON 'I did'), 'opinion' (beliefs - first person 'I believe'). Extract SUBSTANTIVE CONTENT only - skip structural statements. CAPTURE ALL DIMENSIONS: emotions (thrilled, anxious), sensory details (bright orange, loud), cognitive states (wasn't sure, realized), capabilities (can speak French, struggles with), attitudes (skeptical, enthusiastic), comparisons (better than, different from), causal relationships. Do NOT strip richness!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
|
|
@ -562,7 +1034,7 @@ async def extract_facts_from_text(
|
|||
Returns:
|
||||
List of fact dictionaries with 'fact' and 'date' keys
|
||||
"""
|
||||
chunks = chunk_text(text, max_chars=50_000)
|
||||
chunks = chunk_text(text, max_chars=5000)
|
||||
tasks = [
|
||||
_extract_facts_with_auto_split(
|
||||
chunk=chunk,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,10 @@ class MemoryUnit(Base):
|
|||
text: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
embedding = mapped_column(Vector(384)) # pgvector type
|
||||
context: Mapped[Optional[str]] = mapped_column(Text)
|
||||
event_date: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
|
||||
event_date: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False) # Kept for backward compatibility
|
||||
occurred_start: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP(timezone=True)) # When fact occurred (range start)
|
||||
occurred_end: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP(timezone=True)) # When fact occurred (range end)
|
||||
mentioned_at: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP(timezone=True)) # When fact was mentioned
|
||||
fact_type: Mapped[str] = mapped_column(Text, nullable=False, server_default="world")
|
||||
confidence_score: Mapped[Optional[float]] = mapped_column(Float)
|
||||
access_count: Mapped[int] = mapped_column(Integer, server_default="0")
|
||||
|
|
|
|||
|
|
@ -422,3 +422,88 @@ class LinkOperationsMixin:
|
|||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to insert entity links: {str(e)}")
|
||||
|
||||
async def _create_causal_links_batch(
|
||||
self,
|
||||
conn,
|
||||
unit_ids: List[str],
|
||||
causal_relations_per_fact: List[List[dict]],
|
||||
) -> int:
|
||||
"""
|
||||
Create causal links between facts based on LLM-extracted causal relationships.
|
||||
|
||||
Args:
|
||||
conn: Database connection
|
||||
unit_ids: List of unit IDs (in same order as causal_relations_per_fact)
|
||||
causal_relations_per_fact: List of causal relations for each fact.
|
||||
Each element is a list of dicts with:
|
||||
- target_fact_index: Index into unit_ids for the target fact
|
||||
- relation_type: "causes", "caused_by", "enables", or "prevents"
|
||||
- strength: Float in [0.0, 1.0] representing relationship strength
|
||||
|
||||
Returns:
|
||||
Number of causal links created
|
||||
|
||||
Causal link types:
|
||||
- "causes": This fact directly causes the target fact (forward causation)
|
||||
- "caused_by": This fact was caused by the target fact (backward causation)
|
||||
- "enables": This fact enables/allows the target fact (enablement)
|
||||
- "prevents": This fact prevents/blocks the target fact (prevention)
|
||||
"""
|
||||
if not unit_ids or not causal_relations_per_fact:
|
||||
return 0
|
||||
|
||||
try:
|
||||
import time as time_mod
|
||||
create_start = time_mod.time()
|
||||
|
||||
# Build links list
|
||||
links = []
|
||||
for fact_idx, causal_relations in enumerate(causal_relations_per_fact):
|
||||
if not causal_relations:
|
||||
continue
|
||||
|
||||
from_unit_id = unit_ids[fact_idx]
|
||||
|
||||
for relation in causal_relations:
|
||||
target_idx = relation['target_fact_index']
|
||||
relation_type = relation['relation_type']
|
||||
strength = relation.get('strength', 1.0)
|
||||
|
||||
# Validate target index
|
||||
if target_idx < 0 or target_idx >= len(unit_ids):
|
||||
logger.warning(f"Invalid target_fact_index {target_idx} in causal relation from fact {fact_idx}")
|
||||
continue
|
||||
|
||||
to_unit_id = unit_ids[target_idx]
|
||||
|
||||
# Don't create self-links
|
||||
if from_unit_id == to_unit_id:
|
||||
continue
|
||||
|
||||
# Add the causal link
|
||||
# link_type is the relation_type (e.g., "causes", "caused_by")
|
||||
# weight is the strength of the relationship
|
||||
links.append((from_unit_id, to_unit_id, relation_type, strength, None))
|
||||
|
||||
logger.debug(f"Generated {len(links)} causal links in {time_mod.time() - create_start:.3f}s")
|
||||
|
||||
if links:
|
||||
insert_start = time_mod.time()
|
||||
await conn.executemany(
|
||||
"""
|
||||
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid)) DO NOTHING
|
||||
""",
|
||||
links
|
||||
)
|
||||
logger.debug(f"Inserted {len(links)} causal links in {time_mod.time() - insert_start:.3f}s")
|
||||
|
||||
return len(links)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create causal links: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ async def retrieve_graph(
|
|||
neighbors = await conn.fetch(
|
||||
"""
|
||||
SELECT mu.id, mu.text, mu.context, mu.event_date, mu.access_count, mu.embedding, mu.fact_type, mu.document_id,
|
||||
ml.weight
|
||||
ml.weight, ml.link_type
|
||||
FROM memory_links ml
|
||||
JOIN memory_units mu ON ml.to_unit_id = mu.id
|
||||
WHERE ml.from_unit_id = $1
|
||||
|
|
@ -179,7 +179,23 @@ async def retrieve_graph(
|
|||
for n in neighbors:
|
||||
neighbor_id = str(n["id"])
|
||||
if neighbor_id not in visited:
|
||||
new_activation = activation * n["weight"] * 0.8
|
||||
# Boost activation for causal links (they're high-value relationships)
|
||||
link_type = n["link_type"]
|
||||
base_weight = n["weight"]
|
||||
|
||||
# Causal links get 1.5-2.0x boost depending on type
|
||||
if link_type in ("causes", "caused_by"):
|
||||
# Direct causation - very strong relationship
|
||||
causal_boost = 2.0
|
||||
elif link_type in ("enables", "prevents"):
|
||||
# Conditional causation - strong but not as direct
|
||||
causal_boost = 1.5
|
||||
else:
|
||||
# Temporal, semantic, entity links - standard weight
|
||||
causal_boost = 1.0
|
||||
|
||||
effective_weight = base_weight * causal_boost
|
||||
new_activation = activation * effective_weight * 0.8
|
||||
if new_activation > 0.1:
|
||||
queue.append((dict(n), new_activation))
|
||||
|
||||
|
|
@ -273,7 +289,7 @@ async def retrieve_temporal(
|
|||
current, semantic_sim, temporal_score = queue.pop(0)
|
||||
current_id = str(current["id"])
|
||||
|
||||
# Get neighbors via temporal links
|
||||
# Get neighbors via temporal and causal links
|
||||
if budget_remaining > 0:
|
||||
neighbors = await conn.fetch(
|
||||
"""
|
||||
|
|
@ -283,7 +299,7 @@ async def retrieve_temporal(
|
|||
FROM memory_links ml
|
||||
JOIN memory_units mu ON ml.to_unit_id = mu.id
|
||||
WHERE ml.from_unit_id = $2
|
||||
AND ml.link_type = 'temporal'
|
||||
AND ml.link_type IN ('temporal', 'causes', 'caused_by', 'enables', 'prevents')
|
||||
AND ml.weight >= 0.1
|
||||
AND mu.fact_type = $3
|
||||
AND mu.embedding IS NOT NULL
|
||||
|
|
@ -307,8 +323,17 @@ async def retrieve_temporal(
|
|||
days_from_mid = abs((neighbor_date - mid_date).total_seconds() / 86400)
|
||||
neighbor_temporal_proximity = 1.0 - min(days_from_mid / (total_days / 2), 1.0) if total_days > 0 else 1.0
|
||||
|
||||
# Propagate temporal score through links (decay)
|
||||
propagated_temporal = temporal_score * n["weight"] * 0.7
|
||||
# Boost causal links (same as graph retrieval)
|
||||
link_type = n["link_type"]
|
||||
if link_type in ("causes", "caused_by"):
|
||||
causal_boost = 2.0
|
||||
elif link_type in ("enables", "prevents"):
|
||||
causal_boost = 1.5
|
||||
else:
|
||||
causal_boost = 1.0
|
||||
|
||||
# Propagate temporal score through links (decay, with causal boost)
|
||||
propagated_temporal = temporal_score * n["weight"] * causal_boost * 0.7
|
||||
|
||||
# Combined temporal score
|
||||
combined_temporal = max(neighbor_temporal_proximity, propagated_temporal)
|
||||
|
|
|
|||
|
|
@ -746,9 +746,13 @@ class TemporalSemanticMemory(
|
|||
# Flatten and track which facts belong to which content
|
||||
all_fact_texts = []
|
||||
all_fact_dates = []
|
||||
all_occurred_starts = [] # NEW: When fact occurred (range start)
|
||||
all_occurred_ends = [] # NEW: When fact occurred (range end)
|
||||
all_mentioned_ats = [] # NEW: When fact was mentioned
|
||||
all_contexts = []
|
||||
all_fact_entities = [] # NEW: Store LLM-extracted entities per fact
|
||||
all_fact_types = [] # Store fact type (world or agent)
|
||||
all_causal_relations = [] # NEW: Store causal relationships per fact
|
||||
content_boundaries = [] # [(start_idx, end_idx), ...]
|
||||
|
||||
current_idx = 0
|
||||
|
|
@ -757,12 +761,31 @@ class TemporalSemanticMemory(
|
|||
|
||||
for fact_dict in fact_dicts:
|
||||
all_fact_texts.append(fact_dict['fact'])
|
||||
|
||||
# Extract temporal fields (new schema with ranges)
|
||||
from dateutil import parser as date_parser
|
||||
try:
|
||||
from dateutil import parser as date_parser
|
||||
fact_date = date_parser.isoparse(fact_dict['date'])
|
||||
# Try new schema first (occurred_start/end)
|
||||
occurred_start = date_parser.isoparse(fact_dict['occurred_start'])
|
||||
occurred_end = date_parser.isoparse(fact_dict['occurred_end'])
|
||||
all_occurred_starts.append(occurred_start)
|
||||
all_occurred_ends.append(occurred_end)
|
||||
# Use occurred_start as event_date for backward compatibility
|
||||
all_fact_dates.append(occurred_start)
|
||||
except (KeyError, Exception):
|
||||
# Fallback to old schema (single 'date' field)
|
||||
try:
|
||||
fact_date = date_parser.isoparse(fact_dict['date'])
|
||||
except Exception:
|
||||
fact_date = event_date
|
||||
all_fact_dates.append(fact_date)
|
||||
except Exception:
|
||||
all_fact_dates.append(event_date)
|
||||
# For old schema, use same date for start and end (point event)
|
||||
all_occurred_starts.append(fact_date)
|
||||
all_occurred_ends.append(fact_date)
|
||||
|
||||
# mentioned_at is when the fact was mentioned (conversation date)
|
||||
all_mentioned_ats.append(event_date)
|
||||
|
||||
all_contexts.append(context)
|
||||
# Extract entities from fact (default to empty list if not present)
|
||||
all_fact_entities.append(fact_dict.get('entities', []))
|
||||
|
|
@ -771,6 +794,16 @@ class TemporalSemanticMemory(
|
|||
all_fact_types.append(fact_type_override)
|
||||
else:
|
||||
all_fact_types.append(fact_dict.get('fact_type', 'world'))
|
||||
# Extract causal relations (with global index adjustment)
|
||||
# Causal relations use fact indices within each content, need to adjust to global indices
|
||||
causal_relations = fact_dict.get('causal_relations', []) or []
|
||||
# Adjust target_fact_index to global index by adding start_idx
|
||||
adjusted_relations = []
|
||||
for rel in causal_relations:
|
||||
adjusted_rel = dict(rel)
|
||||
adjusted_rel['target_fact_index'] = start_idx + rel['target_fact_index']
|
||||
adjusted_relations.append(adjusted_rel)
|
||||
all_causal_relations.append(adjusted_relations)
|
||||
|
||||
end_idx = current_idx + len(fact_dicts)
|
||||
content_boundaries.append((start_idx, end_idx))
|
||||
|
|
@ -789,8 +822,11 @@ class TemporalSemanticMemory(
|
|||
# For each content item, offset its facts sequentially
|
||||
for i in range(start_idx, end_idx):
|
||||
fact_position = i - start_idx # 0, 1, 2, ...
|
||||
offset = timedelta(seconds=fact_position * SECONDS_PER_FACT)
|
||||
# Add incremental offset to preserve order (facts appear in extraction order)
|
||||
all_fact_dates[i] = all_fact_dates[i] + timedelta(seconds=fact_position * SECONDS_PER_FACT)
|
||||
all_fact_dates[i] = all_fact_dates[i] + offset
|
||||
all_occurred_starts[i] = all_occurred_starts[i] + offset
|
||||
all_occurred_ends[i] = all_occurred_ends[i] + offset
|
||||
|
||||
log_buffer.append(f"[1.5] Added time offsets: {SECONDS_PER_FACT}s per fact to preserve ordering")
|
||||
|
||||
|
|
@ -908,10 +944,36 @@ class TemporalSemanticMemory(
|
|||
filtered_sentences = [s for s, is_dup in zip(all_fact_texts, all_is_duplicate) if not is_dup]
|
||||
filtered_embeddings = [e for e, is_dup in zip(all_embeddings, all_is_duplicate) if not is_dup]
|
||||
filtered_dates = [d for d, is_dup in zip(all_fact_dates, all_is_duplicate) if not is_dup]
|
||||
filtered_occurred_starts = [d for d, is_dup in zip(all_occurred_starts, all_is_duplicate) if not is_dup]
|
||||
filtered_occurred_ends = [d for d, is_dup in zip(all_occurred_ends, all_is_duplicate) if not is_dup]
|
||||
filtered_mentioned_ats = [d for d, is_dup in zip(all_mentioned_ats, all_is_duplicate) if not is_dup]
|
||||
filtered_contexts = [c for c, is_dup in zip(all_contexts, all_is_duplicate) if not is_dup]
|
||||
filtered_entities = [ents for ents, is_dup in zip(all_fact_entities, all_is_duplicate) if not is_dup]
|
||||
filtered_fact_types = [ft for ft, is_dup in zip(all_fact_types, all_is_duplicate) if not is_dup]
|
||||
|
||||
# Build index mapping from old indices to new indices (accounting for removed duplicates)
|
||||
old_to_new_index = {}
|
||||
new_idx = 0
|
||||
for old_idx, is_dup in enumerate(all_is_duplicate):
|
||||
if not is_dup:
|
||||
old_to_new_index[old_idx] = new_idx
|
||||
new_idx += 1
|
||||
|
||||
# Filter and remap causal relations
|
||||
filtered_causal_relations = []
|
||||
for old_idx, (relations, is_dup) in enumerate(zip(all_causal_relations, all_is_duplicate)):
|
||||
if not is_dup:
|
||||
# Keep relations where both source and target survived deduplication
|
||||
valid_relations = []
|
||||
for rel in relations:
|
||||
target_idx = rel['target_fact_index']
|
||||
# Only keep if target fact wasn't filtered out
|
||||
if target_idx in old_to_new_index:
|
||||
remapped_rel = dict(rel)
|
||||
remapped_rel['target_fact_index'] = old_to_new_index[target_idx]
|
||||
valid_relations.append(remapped_rel)
|
||||
filtered_causal_relations.append(valid_relations)
|
||||
|
||||
if not filtered_sentences:
|
||||
logger.debug(f"[PUT_BATCH_ASYNC] All facts were duplicates, returning empty")
|
||||
return [[] for _ in contents]
|
||||
|
|
@ -930,8 +992,8 @@ class TemporalSemanticMemory(
|
|||
]
|
||||
results = await conn.fetch(
|
||||
"""
|
||||
INSERT INTO memory_units (agent_id, document_id, text, context, embedding, event_date, fact_type, confidence_score, access_count)
|
||||
SELECT * FROM unnest($1::text[], $2::text[], $3::text[], $4::text[], $5::vector[], $6::timestamptz[], $7::text[], $8::float[], $9::integer[])
|
||||
INSERT INTO memory_units (agent_id, document_id, text, context, embedding, event_date, occurred_start, occurred_end, mentioned_at, fact_type, confidence_score, access_count)
|
||||
SELECT * FROM unnest($1::text[], $2::text[], $3::text[], $4::text[], $5::vector[], $6::timestamptz[], $7::timestamptz[], $8::timestamptz[], $9::timestamptz[], $10::text[], $11::float[], $12::integer[])
|
||||
RETURNING id
|
||||
""",
|
||||
[agent_id] * len(filtered_sentences),
|
||||
|
|
@ -940,6 +1002,9 @@ class TemporalSemanticMemory(
|
|||
filtered_contexts,
|
||||
filtered_embeddings_str,
|
||||
filtered_dates,
|
||||
filtered_occurred_starts,
|
||||
filtered_occurred_ends,
|
||||
filtered_mentioned_ats,
|
||||
filtered_fact_types,
|
||||
confidence_scores,
|
||||
[0] * len(filtered_sentences)
|
||||
|
|
@ -980,6 +1045,15 @@ class TemporalSemanticMemory(
|
|||
logger.debug("Entity links inserted")
|
||||
log_buffer.append(f"[9] Batch insert entity links: {time.time() - step_start:.3f}s")
|
||||
|
||||
# Create causal links
|
||||
logger.debug("Creating causal links")
|
||||
step_start = time.time()
|
||||
causal_link_count = await self._create_causal_links_batch(
|
||||
conn, created_unit_ids, filtered_causal_relations
|
||||
)
|
||||
logger.debug(f"Causal links complete: {causal_link_count} links created")
|
||||
log_buffer.append(f"[10] Batch create causal links: {causal_link_count} links in {time.time() - step_start:.3f}s")
|
||||
|
||||
# Transaction auto-commits on success
|
||||
commit_start = time.time()
|
||||
logger.debug(f"[10] Commit: {time.time() - commit_start:.3f}s")
|
||||
|
|
|
|||
|
|
@ -125,3 +125,76 @@ def calculate_frequency_weight(access_count: int, max_boost: float = 2.0) -> flo
|
|||
# This gives: 0 accesses = 1.0, 9 accesses ~= 1.5, 99 accesses ~= 2.0
|
||||
normalized = math.log(access_count + 1) / math.log(10)
|
||||
return 1.0 + min(normalized, max_boost - 1.0)
|
||||
|
||||
|
||||
def calculate_temporal_anchor(occurred_start: datetime, occurred_end: datetime) -> datetime:
|
||||
"""
|
||||
Calculate a single temporal anchor point from a temporal range.
|
||||
|
||||
Used for spreading activation - we need a single representative date
|
||||
to calculate temporal proximity between facts. This simplifies the
|
||||
range-to-range distance problem.
|
||||
|
||||
Strategy: Use midpoint of the range for balanced representation.
|
||||
|
||||
Args:
|
||||
occurred_start: Start of temporal range
|
||||
occurred_end: End of temporal range
|
||||
|
||||
Returns:
|
||||
Single datetime representing the temporal anchor (midpoint)
|
||||
|
||||
Examples:
|
||||
- Point event (July 14): start=July 14, end=July 14 → anchor=July 14
|
||||
- Month range (February): start=Feb 1, end=Feb 28 → anchor=Feb 14
|
||||
- Year range (2023): start=Jan 1, end=Dec 31 → anchor=July 1
|
||||
"""
|
||||
# Calculate midpoint
|
||||
time_delta = occurred_end - occurred_start
|
||||
midpoint = occurred_start + (time_delta / 2)
|
||||
return midpoint
|
||||
|
||||
|
||||
def calculate_temporal_proximity(
|
||||
anchor_a: datetime,
|
||||
anchor_b: datetime,
|
||||
half_life_days: float = 30.0
|
||||
) -> float:
|
||||
"""
|
||||
Calculate temporal proximity between two temporal anchors.
|
||||
|
||||
Used for spreading activation to determine how "close" two facts are
|
||||
in time. Uses logarithmic decay so that temporal similarity doesn't
|
||||
drop off too quickly.
|
||||
|
||||
Args:
|
||||
anchor_a: Temporal anchor of first fact
|
||||
anchor_b: Temporal anchor of second fact
|
||||
half_life_days: Number of days for proximity to reach 0.5
|
||||
(default: 30 days = 1 month)
|
||||
|
||||
Returns:
|
||||
Proximity score in [0, 1] where:
|
||||
- 1.0 = same day
|
||||
- 0.5 = ~half_life days apart
|
||||
- 0.0 = very distant in time
|
||||
|
||||
Examples:
|
||||
- Same day: 1.0
|
||||
- 1 week apart (half_life=30): ~0.7
|
||||
- 1 month apart (half_life=30): ~0.5
|
||||
- 1 year apart (half_life=30): ~0.2
|
||||
"""
|
||||
import math
|
||||
|
||||
days_apart = abs((anchor_a - anchor_b).days)
|
||||
|
||||
if days_apart == 0:
|
||||
return 1.0
|
||||
|
||||
# Logarithmic decay: 1 / (1 + log(1 + days_apart/half_life))
|
||||
# Similar to calculate_recency_weight but for proximity between events
|
||||
normalized_distance = days_apart / half_life_days
|
||||
proximity = 1.0 / (1.0 + math.log1p(normalized_distance))
|
||||
|
||||
return proximity
|
||||
|
|
|
|||
Loading…
Reference in a new issue