Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,6 @@ jobs:
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

package-helm-chart:
runs-on: ubuntu-latest
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,6 @@ jobs:
file: docker/standalone/Dockerfile
target: ${{ matrix.target }}
push: false
cache-from: type=gha
cache-to: type=gha,mode=max

test-api:
runs-on: ubuntu-latest
Expand Down
16 changes: 12 additions & 4 deletions hindsight-api/hindsight_api/engine/memory_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,8 @@ async def _search_with_retries(
temporal_results = []
aggregated_timings = {"semantic": 0.0, "bm25": 0.0, "graph": 0.0, "temporal": 0.0}

for idx, (ft_semantic, ft_bm25, ft_graph, ft_temporal, ft_timings) in enumerate(all_retrievals):
detected_temporal_constraint = None
for idx, (ft_semantic, ft_bm25, ft_graph, ft_temporal, ft_timings, ft_temporal_constraint) in enumerate(all_retrievals):
# Log fact types in this retrieval batch
ft_name = fact_type[idx] if idx < len(fact_type) else "unknown"
logger.debug(f"[SEARCH {search_id}] Fact type '{ft_name}': semantic={len(ft_semantic)}, bm25={len(ft_bm25)}, graph={len(ft_graph)}, temporal={len(ft_temporal) if ft_temporal else 0}")
Expand All @@ -1097,6 +1098,9 @@ async def _search_with_retries(
# Track max timing for each method (since they run in parallel across fact types)
for method, duration in ft_timings.items():
aggregated_timings[method] = max(aggregated_timings[method], duration)
# Capture temporal constraint (same across all fact types)
if ft_temporal_constraint:
detected_temporal_constraint = ft_temporal_constraint

# If no temporal results from any fact type, set to None
if not temporal_results:
Expand All @@ -1120,9 +1124,13 @@ async def _search_with_retries(
f"bm25={len(bm25_results)}({aggregated_timings['bm25']:.3f}s)",
f"graph={len(graph_results)}({aggregated_timings['graph']:.3f}s)"
]
if temporal_results:
timing_parts.append(f"temporal={len(temporal_results)}({aggregated_timings['temporal']:.3f}s)")
log_buffer.append(f" [2] {total_retrievals}-way retrieval ({len(fact_type)} fact_types): {', '.join(timing_parts)} in {step_duration:.3f}s")
temporal_info = ""
if detected_temporal_constraint:
start_dt, end_dt = detected_temporal_constraint
temporal_count = len(temporal_results) if temporal_results else 0
timing_parts.append(f"temporal={temporal_count}({aggregated_timings['temporal']:.3f}s)")
temporal_info = f" | temporal_range={start_dt.strftime('%Y-%m-%d')} to {end_dt.strftime('%Y-%m-%d')}"
log_buffer.append(f" [2] {total_retrievals}-way retrieval ({len(fact_type)} fact_types): {', '.join(timing_parts)} in {step_duration:.3f}s{temporal_info}")

# Record retrieval results for tracer (convert typed results to old format)
if tracer:
Expand Down
28 changes: 28 additions & 0 deletions hindsight-api/hindsight_api/engine/query_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,34 @@ def constraint(start: datetime, end: datetime) -> TemporalConstraint:
if re.search(r'\b(today|hoy|oggi|aujourd\'?hui|heute)\b', query, re.IGNORECASE):
return constraint(reference_date, reference_date)

# "a couple of days ago" / "a few days ago" patterns
# These are imprecise so we create a range
if re.search(r'\b(a\s+)?couple\s+(of\s+)?days?\s+ago\b', query, re.IGNORECASE):
# "a couple of days" = approximately 2 days, give range of 1-3 days
return constraint(reference_date - timedelta(days=3), reference_date - timedelta(days=1))

if re.search(r'\b(a\s+)?few\s+days?\s+ago\b', query, re.IGNORECASE):
# "a few days" = approximately 3-4 days, give range of 2-5 days
return constraint(reference_date - timedelta(days=5), reference_date - timedelta(days=2))

# "a couple of weeks ago" / "a few weeks ago" patterns
if re.search(r'\b(a\s+)?couple\s+(of\s+)?weeks?\s+ago\b', query, re.IGNORECASE):
# "a couple of weeks" = approximately 2 weeks, give range of 1-3 weeks
return constraint(reference_date - timedelta(weeks=3), reference_date - timedelta(weeks=1))

if re.search(r'\b(a\s+)?few\s+weeks?\s+ago\b', query, re.IGNORECASE):
# "a few weeks" = approximately 3-4 weeks, give range of 2-5 weeks
return constraint(reference_date - timedelta(weeks=5), reference_date - timedelta(weeks=2))

# "a couple of months ago" / "a few months ago" patterns
if re.search(r'\b(a\s+)?couple\s+(of\s+)?months?\s+ago\b', query, re.IGNORECASE):
# "a couple of months" = approximately 2 months, give range of 1-3 months
return constraint(reference_date - timedelta(days=90), reference_date - timedelta(days=30))

if re.search(r'\b(a\s+)?few\s+months?\s+ago\b', query, re.IGNORECASE):
# "a few months" = approximately 3-4 months, give range of 2-5 months
return constraint(reference_date - timedelta(days=150), reference_date - timedelta(days=60))

# Last week patterns (English, Spanish, Italian, French, German)
if re.search(r'\b(last\s+week|la\s+semana\s+pasada|la\s+settimana\s+scorsa|la\s+semaine\s+derni[eè]re|letzte\s+woche)\b', query, re.IGNORECASE):
start = reference_date - timedelta(days=reference_date.weekday() + 7)
Expand Down
13 changes: 9 additions & 4 deletions hindsight-api/hindsight_api/engine/retain/fact_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,9 @@ async def _extract_facts_from_chunk(
For EACH fact, CAPTURE ALL DETAILS - NEVER SUMMARIZE OR OMIT:

1. **what**: WHAT happened - COMPLETE description with ALL specifics (objects, actions, quantities, details)
2. **when**: WHEN it happened - ALWAYS include temporal info (dates, times, durations, relative times)
2. **when**: WHEN it happened - ALWAYS include temporal info with DAY OF WEEK (e.g., "Monday, June 10, 2024")
- Always include the day name: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
- Format: "day_name, month day, year" (e.g., "Saturday, June 9, 2024")
3. **where**: WHERE it happened or is about - SPECIFIC locations, places, areas, regions (if applicable)
4. **who**: WHO is involved - ALL people/entities with FULL relationships and background
5. **why**: WHY it matters - ALL emotions, preferences, motivations, significance, nuance
Expand All @@ -350,7 +352,7 @@ async def _extract_facts_from_chunk(

CORRECT output:
- what: "Emily got married to Sarah at a rooftop garden ceremony"
- when: "in June 2024, after dating for 5 years"
- when: "Saturday, June 8, 2024, after dating for 5 years"
- where: "downtown San Francisco, at a rooftop garden venue"
- who: "Emily (user's college roommate), Sarah (Emily's partner of 5 years)"
- why: "User found it romantic and beautiful, dreams of similar outdoor ceremony"
Expand All @@ -366,7 +368,8 @@ async def _extract_facts_from_chunk(
══════════════════════════════════════════════════════════════════════════

For EVENTS (fact_kind="event"):
- Convert relative dates → absolute: "yesterday" on March 15 → "March 14, 2024"
- Convert relative dates → absolute WITH DAY OF WEEK: "yesterday" on Saturday March 15 → "Friday, March 14, 2024"
- Always include the day name (Monday, Tuesday, etc.) in the 'when' field
- Set occurred_start/occurred_end to WHEN IT HAPPENED (not when mentioned)

For CONVERSATIONS (fact_kind="conversation"):
Expand Down Expand Up @@ -468,10 +471,12 @@ async def _extract_facts_from_chunk(
last_error = None

# Build user message with metadata and chunk content in a clear format
# Format event_date with day of week for better temporal reasoning
event_date_formatted = event_date.strftime('%A, %B %d, %Y') # e.g., "Monday, June 10, 2024"
user_message = f"""Extract facts from the following text chunk.

Chunk: {chunk_index + 1}/{total_chunks}
Event Date: {event_date.isoformat()}
Event Date: {event_date_formatted} ({event_date.isoformat()})
Context: {context if context else 'none'}

Text:
Expand Down
40 changes: 31 additions & 9 deletions hindsight-api/hindsight_api/engine/search/retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ async def retrieve_temporal(
start_date: datetime,
end_date: datetime,
budget: int,
semantic_threshold: float = 0.4
semantic_threshold: float = 0.1
) -> List[RetrievalResult]:
"""
Temporal retrieval with spreading activation.
Expand Down Expand Up @@ -287,6 +287,9 @@ async def retrieve_temporal(
query_emb_str, bank_id, fact_type, start_date, end_date, semantic_threshold
)

import logging
logger = logging.getLogger(__name__)

if not entry_points:
# Check if there are ANY memories with temporal metadata for this bank
total_with_dates = await conn.fetchval(
Expand All @@ -295,9 +298,28 @@ async def retrieve_temporal(
AND (occurred_start IS NOT NULL OR occurred_end IS NOT NULL OR mentioned_at IS NOT NULL)""",
bank_id, fact_type
)
import logging
logger = logging.getLogger(__name__)
logger.info(f"[TEMPORAL] No entry points found for {bank_id}/{fact_type} in range {start_date} to {end_date}. Total facts with dates: {total_with_dates}")
# Check how many have mentioned_at in the range
in_range = await conn.fetchval(
"""SELECT COUNT(*) FROM memory_units
WHERE bank_id = $1 AND fact_type = $2
AND mentioned_at IS NOT NULL AND mentioned_at BETWEEN $3 AND $4""",
bank_id, fact_type, start_date, end_date
)
# Check semantic similarity of those in range
sample = await conn.fetch(
"""SELECT id, text, mentioned_at, 1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE bank_id = $2 AND fact_type = $3
AND mentioned_at IS NOT NULL AND mentioned_at BETWEEN $4 AND $5
AND embedding IS NOT NULL
ORDER BY mentioned_at DESC
LIMIT 5""",
query_emb_str, bank_id, fact_type, start_date, end_date
)
logger.info(f"[TEMPORAL] No entry points for {bank_id}/{fact_type} in {start_date} to {end_date}.")
logger.info(f"[TEMPORAL] Total with dates: {total_with_dates}, In date range: {in_range}")
for row in sample:
logger.info(f"[TEMPORAL] Sample: {row['text'][:60]}... mentioned_at={row['mentioned_at']} sim={row['similarity']:.3f}")
return []

# Calculate temporal scores for entry points
Expand Down Expand Up @@ -430,7 +452,7 @@ async def retrieve_parallel(
thinking_budget: int,
question_date: Optional[datetime] = None,
query_analyzer: Optional["QueryAnalyzer"] = None
) -> Tuple[List[RetrievalResult], List[RetrievalResult], List[RetrievalResult], Optional[List[RetrievalResult]], Dict[str, float]]:
) -> Tuple[List[RetrievalResult], List[RetrievalResult], List[RetrievalResult], Optional[List[RetrievalResult]], Dict[str, float], Optional[Tuple[datetime, datetime]]]:
"""
Run 3-way or 4-way parallel retrieval (adds temporal if detected).

Expand All @@ -445,10 +467,11 @@ async def retrieve_parallel(
query_analyzer: Query analyzer to use (defaults to TransformerQueryAnalyzer)

Returns:
Tuple of (semantic_results, bm25_results, graph_results, temporal_results, timings)
Tuple of (semantic_results, bm25_results, graph_results, temporal_results, timings, temporal_constraint)
Each results list contains RetrievalResult objects
temporal_results is None if no temporal constraint detected
timings is a dict with per-method latencies in seconds
temporal_constraint is the (start_date, end_date) tuple if detected, else None
"""
# Detect temporal constraint
from .temporal_extraction import extract_temporal_constraint
Expand All @@ -459,7 +482,6 @@ async def retrieve_parallel(
temporal_constraint = extract_temporal_constraint(
query_text, reference_date=question_date, analyzer=query_analyzer
)
logger.info(f"[TEMPORAL] Query: {query_text[:50]}... -> constraint={temporal_constraint}")

# Wrapper to track timing for each retrieval method
async def timed_retrieval(name: str, coro):
Expand All @@ -484,7 +506,7 @@ async def run_temporal(start_date, end_date):
async with acquire_with_retry(pool) as conn:
return await retrieve_temporal(
conn, query_embedding_str, bank_id, fact_type,
start_date, end_date, budget=thinking_budget, semantic_threshold=0.4
start_date, end_date, budget=thinking_budget, semantic_threshold=0.1
)

# Run retrievals in parallel with timing
Expand Down Expand Up @@ -512,4 +534,4 @@ async def run_temporal(start_date, end_date):
graph_results, _, timings["graph"] = results[2]
temporal_results = None

return semantic_results, bm25_results, graph_results, temporal_results, timings
return semantic_results, bm25_results, graph_results, temporal_results, timings, temporal_constraint
51 changes: 51 additions & 0 deletions hindsight-api/tests/test_query_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,54 @@ def test_query_analyzer_last_weekend(query_analyzer):
assert analysis.temporal_constraint.end_date.day == 12 # Sunday


def test_query_analyzer_couple_days_ago(query_analyzer):
"""Test extraction of 'a couple of days ago' colloquial expression."""
reference_date = datetime(2025, 1, 15, 12, 0, 0)

query = "I mentioned cooking something for my friend a couple of days ago. What was it?"
analysis = query_analyzer.analyze(query, reference_date)

print(f"\nQuery: '{query}'")
print(f"Reference date: {reference_date.strftime('%A, %Y-%m-%d')}")
print(f"Analysis: {analysis}")

assert analysis.temporal_constraint is not None, "Should extract temporal constraint for 'a couple of days ago'"
# Range should be 1-3 days ago: Jan 12-14
assert analysis.temporal_constraint.start_date.day == 12
assert analysis.temporal_constraint.end_date.day == 14


def test_query_analyzer_few_days_ago(query_analyzer):
"""Test extraction of 'a few days ago' colloquial expression."""
reference_date = datetime(2025, 1, 15, 12, 0, 0)

query = "What did I do a few days ago?"
analysis = query_analyzer.analyze(query, reference_date)

print(f"\nQuery: '{query}'")
print(f"Reference date: {reference_date.strftime('%A, %Y-%m-%d')}")
print(f"Analysis: {analysis}")

assert analysis.temporal_constraint is not None, "Should extract temporal constraint for 'a few days ago'"
# Range should be 2-5 days ago: Jan 10-13
assert analysis.temporal_constraint.start_date.day == 10
assert analysis.temporal_constraint.end_date.day == 13


def test_query_analyzer_couple_weeks_ago(query_analyzer):
"""Test extraction of 'a couple of weeks ago' colloquial expression."""
reference_date = datetime(2025, 1, 15, 12, 0, 0)

query = "a couple of weeks ago we discussed this"
analysis = query_analyzer.analyze(query, reference_date)

print(f"\nQuery: '{query}'")
print(f"Reference date: {reference_date.strftime('%A, %Y-%m-%d')}")
print(f"Analysis: {analysis}")

assert analysis.temporal_constraint is not None, "Should extract temporal constraint for 'a couple of weeks ago'"
# Range should be 1-3 weeks ago
assert analysis.temporal_constraint.start_date.month == 12 # Dec 25 (3 weeks before Jan 15)
assert analysis.temporal_constraint.end_date.month == 1 # Jan 8 (1 week before Jan 15)


5 changes: 3 additions & 2 deletions hindsight-control-plane/src/app/api/recall/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ export async function POST(request: NextRequest) {
try {
const body = await request.json();
const bankId = body.bank_id || body.agent_id || 'default';
const { query, types, fact_type, max_tokens, trace, budget, include } = body;
const { query, types, fact_type, max_tokens, trace, budget, include, query_timestamp } = body;

console.log('[Recall API] Request:', { bankId, query, types: types || fact_type, max_tokens, trace, budget });
console.log('[Recall API] Request:', { bankId, query, types: types || fact_type, max_tokens, trace, budget, query_timestamp });
console.log('[Recall API] Include options:', JSON.stringify(include, null, 2));

const response = await sdk.recallMemories({
Expand All @@ -20,6 +20,7 @@ export async function POST(request: NextRequest) {
trace,
budget: budget || 'mid',
include,
query_timestamp,
},
});

Expand Down
22 changes: 17 additions & 5 deletions hindsight-control-plane/src/components/data-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,8 @@ export function DataView({ factType }: DataViewProps) {
)}

{viewMode === 'table' && (
<div className="flex gap-4">
<div className={`transition-all ${selectedTableMemory ? 'w-2/3' : 'w-full'}`}>
<div>
<div className="w-full">
<div className="px-5 mb-4">
<Input
type="text"
Expand Down Expand Up @@ -358,7 +358,8 @@ export function DataView({ factType }: DataViewProps) {
<TableHead className="w-[80px]">ID</TableHead>
<TableHead>Text</TableHead>
<TableHead className="w-[150px]">Context</TableHead>
<TableHead className="w-[120px]">Occurred</TableHead>
<TableHead className="w-[100px]">Occurred</TableHead>
<TableHead className="w-[100px]">Mentioned</TableHead>
<TableHead className="w-[60px]">Actions</TableHead>
</TableRow>
</TableHeader>
Expand All @@ -367,6 +368,9 @@ export function DataView({ factType }: DataViewProps) {
const occurredDisplay = row.occurred_start
? new Date(row.occurred_start).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
: null;
const mentionedDisplay = row.mentioned_at
? new Date(row.mentioned_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
: null;

return (
<TableRow
Expand Down Expand Up @@ -407,6 +411,14 @@ export function DataView({ factType }: DataViewProps) {
</span>
) : '-'}
</TableCell>
<TableCell className="text-xs">
{mentionedDisplay ? (
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{mentionedDisplay}
</span>
) : '-'}
</TableCell>
<TableCell>
<Button
onClick={(e) => {
Expand Down Expand Up @@ -492,9 +504,9 @@ export function DataView({ factType }: DataViewProps) {
</div>
</div>

{/* Memory Detail Panel for Table View */}
{/* Memory Detail Panel for Table View - Fixed on Right */}
{selectedTableMemory && (
<div className="w-1/3 pr-5 pb-5">
<div className="fixed right-0 top-0 h-screen w-96 bg-background border-l border-border shadow-lg z-50 overflow-y-auto p-4">
<MemoryDetailPanel
memory={selectedTableMemory}
onClose={() => setSelectedTableMemory(null)}
Expand Down
Loading
Loading