Compare commits
3
Commits
worker-setting
...
fix-ci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
795e0ef6a2 | ||
|
|
38df35953d | ||
|
|
7869a2a589 |
+129
-10
@@ -1,14 +1,136 @@
|
||||
name: Run Tests
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
build-python-packages:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: hindsight-all
|
||||
path: hindsight
|
||||
- name: hindsight-api
|
||||
path: hindsight-api
|
||||
- name: hindsight-client
|
||||
path: hindsight-clients/python
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Build ${{ matrix.name }}
|
||||
working-directory: ./${{ matrix.path }}
|
||||
run: uv build
|
||||
|
||||
build-typescript-client:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
run: npm ci
|
||||
|
||||
- name: Build TypeScript client
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
run: npm run build
|
||||
|
||||
build-rust-cli:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
hindsight-cli/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build CLI
|
||||
working-directory: hindsight-cli
|
||||
run: cargo build --release
|
||||
|
||||
lint-helm-chart:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
- name: Lint Helm chart
|
||||
run: helm lint helm/hindsight
|
||||
|
||||
build-docker-images:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-only
|
||||
name: api
|
||||
- target: cp-only
|
||||
name: control-plane
|
||||
- target: standalone
|
||||
name: standalone
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Free Disk Space
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build ${{ matrix.name }} image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
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
|
||||
needs: [build-python-packages]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@@ -45,12 +167,9 @@ jobs:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./hindsight-api
|
||||
run: uv sync --extra test
|
||||
|
||||
- name: Run migrations
|
||||
working-directory: ./hindsight
|
||||
run: |
|
||||
uv run alembic upgrade head
|
||||
|
||||
- name: Run tests
|
||||
run: uv run pytest hindsight/tests -v
|
||||
working-directory: ./hindsight-api
|
||||
run: uv run pytest tests -v --ignore=tests/test_fact_extraction_quality.py
|
||||
|
||||
@@ -169,8 +169,8 @@ class MemoryEngine:
|
||||
if query_analyzer is not None:
|
||||
self.query_analyzer = query_analyzer
|
||||
else:
|
||||
from .query_analyzer import TransformerQueryAnalyzer
|
||||
self.query_analyzer = TransformerQueryAnalyzer()
|
||||
from .query_analyzer import DateparserQueryAnalyzer
|
||||
self.query_analyzer = DateparserQueryAnalyzer()
|
||||
|
||||
# Initialize LLM configuration
|
||||
self._llm_config = LLMConfig(
|
||||
|
||||
@@ -6,8 +6,9 @@ structured information like temporal constraints.
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import re
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -73,6 +74,171 @@ class QueryAnalyzer(ABC):
|
||||
pass
|
||||
|
||||
|
||||
class DateparserQueryAnalyzer(QueryAnalyzer):
|
||||
"""
|
||||
Query analyzer using dateparser library.
|
||||
|
||||
Uses dateparser to extract temporal expressions from natural language
|
||||
queries. Supports 200+ languages including English, Spanish, Italian,
|
||||
French, German, etc.
|
||||
|
||||
Performance:
|
||||
- ~10-50ms per query
|
||||
- No model loading required
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize dateparser query analyzer."""
|
||||
self._search_dates = None
|
||||
|
||||
def load(self) -> None:
|
||||
"""Load dateparser (lazy import)."""
|
||||
if self._search_dates is None:
|
||||
from dateparser.search import search_dates
|
||||
self._search_dates = search_dates
|
||||
|
||||
def analyze(
|
||||
self, query: str, reference_date: Optional[datetime] = None
|
||||
) -> QueryAnalysis:
|
||||
"""
|
||||
Analyze query using dateparser.
|
||||
|
||||
Extracts temporal expressions from the query text. Supports multiple
|
||||
languages automatically.
|
||||
|
||||
Args:
|
||||
query: Natural language query (any language)
|
||||
reference_date: Reference date for relative terms (defaults to now)
|
||||
|
||||
Returns:
|
||||
QueryAnalysis with temporal_constraint if found
|
||||
"""
|
||||
self.load()
|
||||
|
||||
if reference_date is None:
|
||||
reference_date = datetime.now()
|
||||
|
||||
# Check for period expressions first (these need special handling)
|
||||
query_lower = query.lower()
|
||||
period_result = self._extract_period(query_lower, reference_date)
|
||||
if period_result is not None:
|
||||
return QueryAnalysis(temporal_constraint=period_result)
|
||||
|
||||
# Use dateparser's search_dates to find temporal expressions
|
||||
settings = {
|
||||
'RELATIVE_BASE': reference_date,
|
||||
'PREFER_DATES_FROM': 'past',
|
||||
'RETURN_AS_TIMEZONE_AWARE': False,
|
||||
}
|
||||
|
||||
results = self._search_dates(query, settings=settings)
|
||||
|
||||
if not results:
|
||||
return QueryAnalysis(temporal_constraint=None)
|
||||
|
||||
# Filter out false positives (common words parsed as dates)
|
||||
false_positives = {'do', 'may', 'march', 'will', 'can', 'sat', 'sun', 'mon', 'tue', 'wed', 'thu', 'fri'}
|
||||
valid_results = [
|
||||
(text, date) for text, date in results
|
||||
if text.lower() not in false_positives or len(text) > 3
|
||||
]
|
||||
|
||||
if not valid_results:
|
||||
return QueryAnalysis(temporal_constraint=None)
|
||||
|
||||
# Use the first valid date found
|
||||
_, parsed_date = valid_results[0]
|
||||
|
||||
# Create constraint for single day
|
||||
start_date = parsed_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_date = parsed_date.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
|
||||
return QueryAnalysis(
|
||||
temporal_constraint=TemporalConstraint(
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
)
|
||||
|
||||
def _extract_period(
|
||||
self, query: str, reference_date: datetime
|
||||
) -> Optional[TemporalConstraint]:
|
||||
"""
|
||||
Extract period-based temporal expressions (week, month, year, weekend).
|
||||
|
||||
These need special handling as they represent date ranges, not single dates.
|
||||
Supports multiple languages.
|
||||
"""
|
||||
def constraint(start: datetime, end: datetime) -> TemporalConstraint:
|
||||
return TemporalConstraint(
|
||||
start_date=start.replace(hour=0, minute=0, second=0, microsecond=0),
|
||||
end_date=end.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
)
|
||||
|
||||
# Yesterday patterns (English, Spanish, Italian, French, German)
|
||||
if re.search(r'\b(yesterday|ayer|ieri|hier|gestern)\b', query, re.IGNORECASE):
|
||||
d = reference_date - timedelta(days=1)
|
||||
return constraint(d, d)
|
||||
|
||||
# Today patterns
|
||||
if re.search(r'\b(today|hoy|oggi|aujourd\'?hui|heute)\b', query, re.IGNORECASE):
|
||||
return constraint(reference_date, reference_date)
|
||||
|
||||
# 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)
|
||||
return constraint(start, start + timedelta(days=6))
|
||||
|
||||
# Last month patterns
|
||||
if re.search(r'\b(last\s+month|el\s+mes\s+pasado|il\s+mese\s+scorso|le\s+mois\s+dernier|letzten?\s+monat)\b', query, re.IGNORECASE):
|
||||
first = reference_date.replace(day=1)
|
||||
end = first - timedelta(days=1)
|
||||
start = end.replace(day=1)
|
||||
return constraint(start, end)
|
||||
|
||||
# Last year patterns
|
||||
if re.search(r'\b(last\s+year|el\s+a[ñn]o\s+pasado|l\'anno\s+scorso|l\'ann[ée]e\s+derni[eè]re|letztes?\s+jahr)\b', query, re.IGNORECASE):
|
||||
year = reference_date.year - 1
|
||||
return constraint(datetime(year, 1, 1), datetime(year, 12, 31))
|
||||
|
||||
# Last weekend patterns
|
||||
if re.search(r'\b(last\s+weekend|el\s+fin\s+de\s+semana\s+pasado|lo\s+scorso\s+fine\s+settimana|le\s+week-?end\s+dernier|letztes?\s+wochenende)\b', query, re.IGNORECASE):
|
||||
days_since_sat = (reference_date.weekday() + 2) % 7
|
||||
if days_since_sat == 0:
|
||||
days_since_sat = 7
|
||||
sat = reference_date - timedelta(days=days_since_sat)
|
||||
return constraint(sat, sat + timedelta(days=1))
|
||||
|
||||
# Month + Year patterns (e.g., "June 2024", "junio 2024", "giugno 2024")
|
||||
month_patterns = {
|
||||
'january|enero|gennaio|janvier|januar': 1,
|
||||
'february|febrero|febbraio|f[ée]vrier|februar': 2,
|
||||
'march|marzo|mars|m[äa]rz': 3,
|
||||
'april|abril|aprile|avril': 4,
|
||||
'may|mayo|maggio|mai': 5,
|
||||
'june|junio|giugno|juin|juni': 6,
|
||||
'july|julio|luglio|juillet|juli': 7,
|
||||
'august|agosto|ao[uû]t': 8,
|
||||
'september|septiembre|settembre|septembre': 9,
|
||||
'october|octubre|ottobre|octobre|oktober': 10,
|
||||
'november|noviembre|novembre': 11,
|
||||
'december|diciembre|dicembre|d[ée]cembre|dezember': 12,
|
||||
}
|
||||
|
||||
for pattern, month_num in month_patterns.items():
|
||||
match = re.search(rf'\b({pattern})\s+(\d{{4}})\b', query, re.IGNORECASE)
|
||||
if match:
|
||||
year = int(match.group(2))
|
||||
start = datetime(year, month_num, 1)
|
||||
if month_num == 12:
|
||||
end = datetime(year, 12, 31)
|
||||
else:
|
||||
end = datetime(year, month_num + 1, 1) - timedelta(days=1)
|
||||
return constraint(start, end)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class TransformerQueryAnalyzer(QueryAnalyzer):
|
||||
"""
|
||||
Query analyzer using T5-based generative models.
|
||||
@@ -128,13 +294,89 @@ class TransformerQueryAnalyzer(QueryAnalyzer):
|
||||
"""Lazy load the T5 model for temporal extraction (calls load())."""
|
||||
self.load()
|
||||
|
||||
def _extract_with_rules(
|
||||
self, query: str, reference_date: datetime
|
||||
) -> Optional[TemporalConstraint]:
|
||||
"""
|
||||
Extract temporal expressions using rule-based patterns.
|
||||
|
||||
Handles common patterns reliably and fast. Returns None for
|
||||
patterns that need model-based extraction.
|
||||
"""
|
||||
import re
|
||||
query_lower = query.lower()
|
||||
|
||||
def get_last_weekday(weekday: int) -> datetime:
|
||||
days_ago = (reference_date.weekday() - weekday) % 7
|
||||
if days_ago == 0:
|
||||
days_ago = 7
|
||||
return reference_date - timedelta(days=days_ago)
|
||||
|
||||
def constraint(start: datetime, end: datetime) -> TemporalConstraint:
|
||||
return TemporalConstraint(
|
||||
start_date=start.replace(hour=0, minute=0, second=0, microsecond=0),
|
||||
end_date=end.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
)
|
||||
|
||||
# Yesterday
|
||||
if re.search(r'\byesterday\b', query_lower):
|
||||
d = reference_date - timedelta(days=1)
|
||||
return constraint(d, d)
|
||||
|
||||
# Last week
|
||||
if re.search(r'\blast\s+week\b', query_lower):
|
||||
start = reference_date - timedelta(days=reference_date.weekday() + 7)
|
||||
return constraint(start, start + timedelta(days=6))
|
||||
|
||||
# Last month
|
||||
if re.search(r'\blast\s+month\b', query_lower):
|
||||
first = reference_date.replace(day=1)
|
||||
end = first - timedelta(days=1)
|
||||
start = end.replace(day=1)
|
||||
return constraint(start, end)
|
||||
|
||||
# Last year
|
||||
if re.search(r'\blast\s+year\b', query_lower):
|
||||
y = reference_date.year - 1
|
||||
return constraint(datetime(y, 1, 1), datetime(y, 12, 31))
|
||||
|
||||
# Last weekend
|
||||
if re.search(r'\blast\s+weekend\b', query_lower):
|
||||
sat = get_last_weekday(5)
|
||||
return constraint(sat, sat + timedelta(days=1))
|
||||
|
||||
# Last <weekday>
|
||||
weekdays = {'monday': 0, 'tuesday': 1, 'wednesday': 2, 'thursday': 3,
|
||||
'friday': 4, 'saturday': 5, 'sunday': 6}
|
||||
for name, num in weekdays.items():
|
||||
if re.search(rf'\blast\s+{name}\b', query_lower):
|
||||
d = get_last_weekday(num)
|
||||
return constraint(d, d)
|
||||
|
||||
# Month + Year: "June 2024", "in March 2023"
|
||||
months = {'january': 1, 'february': 2, 'march': 3, 'april': 4, 'may': 5,
|
||||
'june': 6, 'july': 7, 'august': 8, 'september': 9, 'october': 10,
|
||||
'november': 11, 'december': 12}
|
||||
for name, num in months.items():
|
||||
match = re.search(rf'\b{name}\s+(\d{{4}})\b', query_lower)
|
||||
if match:
|
||||
year = int(match.group(1))
|
||||
if num == 12:
|
||||
last_day = 31
|
||||
else:
|
||||
last_day = (datetime(year, num + 1, 1) - timedelta(days=1)).day
|
||||
return constraint(datetime(year, num, 1), datetime(year, num, last_day))
|
||||
|
||||
return None
|
||||
|
||||
def analyze(
|
||||
self, query: str, reference_date: Optional[datetime] = None
|
||||
) -> QueryAnalysis:
|
||||
"""
|
||||
Analyze query using T5 model.
|
||||
Analyze query for temporal expressions.
|
||||
|
||||
Uses T5 to generate structured temporal output directly.
|
||||
Uses rule-based extraction for common patterns (fast & reliable),
|
||||
falls back to T5 model for complex/unusual patterns.
|
||||
|
||||
Args:
|
||||
query: Natural language query
|
||||
@@ -146,17 +388,30 @@ class TransformerQueryAnalyzer(QueryAnalyzer):
|
||||
if reference_date is None:
|
||||
reference_date = datetime.now()
|
||||
|
||||
# Try rule-based extraction first (handles 90%+ of cases)
|
||||
result = self._extract_with_rules(query, reference_date)
|
||||
if result is not None:
|
||||
return QueryAnalysis(temporal_constraint=result)
|
||||
|
||||
# Fall back to T5 model for unusual patterns
|
||||
self._load_model()
|
||||
|
||||
# Build prompt for T5 to generate structured temporal output
|
||||
# Use fill-in-the-blank format which T5 handles better
|
||||
prompt = f"""Today is {reference_date.strftime('%Y-%m-%d')}. Convert temporal expressions to date ranges.
|
||||
# Helper to calculate example dates
|
||||
def get_last_weekday(weekday: int) -> datetime:
|
||||
days_ago = (reference_date.weekday() - weekday) % 7
|
||||
if days_ago == 0:
|
||||
days_ago = 7
|
||||
return reference_date - timedelta(days=days_ago)
|
||||
|
||||
yesterday = reference_date - timedelta(days=1)
|
||||
last_saturday = get_last_weekday(5)
|
||||
|
||||
# Build prompt for T5
|
||||
prompt = f"""Today is {reference_date.strftime('%Y-%m-%d')}. Extract date range or "none".
|
||||
|
||||
June 2024 = 2024-06-01 to 2024-06-30
|
||||
March 2023 = 2023-03-01 to 2023-03-31
|
||||
dogs in June 2023 = 2023-06-01 to 2023-06-30
|
||||
last year = {reference_date.year - 1}-01-01 to {reference_date.year - 1}-12-31
|
||||
events in January 2020 = 2020-01-01 to 2020-01-31
|
||||
yesterday = {yesterday.strftime('%Y-%m-%d')} to {yesterday.strftime('%Y-%m-%d')}
|
||||
last Saturday = {last_saturday.strftime('%Y-%m-%d')} to {last_saturday.strftime('%Y-%m-%d')}
|
||||
what is the weather = none
|
||||
{query} ="""
|
||||
|
||||
|
||||
@@ -295,6 +295,9 @@ 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}")
|
||||
return []
|
||||
|
||||
# Calculate temporal scores for entry points
|
||||
@@ -456,6 +459,7 @@ 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):
|
||||
|
||||
@@ -7,7 +7,7 @@ Handles natural language temporal expressions using transformer-based query anal
|
||||
from typing import Optional, Tuple
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from hindsight_api.engine.query_analyzer import QueryAnalyzer, TransformerQueryAnalyzer
|
||||
from hindsight_api.engine.query_analyzer import QueryAnalyzer, DateparserQueryAnalyzer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,14 +20,14 @@ def get_default_analyzer() -> QueryAnalyzer:
|
||||
"""
|
||||
Get or create the default query analyzer.
|
||||
|
||||
Uses lazy initialization to avoid loading model at import time.
|
||||
Uses lazy initialization to avoid loading at import time.
|
||||
|
||||
Returns:
|
||||
Default TransformerQueryAnalyzer instance
|
||||
Default DateparserQueryAnalyzer instance
|
||||
"""
|
||||
global _default_analyzer
|
||||
if _default_analyzer is None:
|
||||
_default_analyzer = TransformerQueryAnalyzer()
|
||||
_default_analyzer = DateparserQueryAnalyzer()
|
||||
return _default_analyzer
|
||||
|
||||
|
||||
@@ -37,14 +37,14 @@ def extract_temporal_constraint(
|
||||
analyzer: Optional[QueryAnalyzer] = None,
|
||||
) -> Optional[Tuple[datetime, datetime]]:
|
||||
"""
|
||||
Extract temporal constraint from query using transformer-based analysis.
|
||||
Extract temporal constraint from query.
|
||||
|
||||
Returns (start_date, end_date) tuple if temporal constraint found, else None.
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
reference_date: Reference date for relative terms (defaults to now)
|
||||
analyzer: Custom query analyzer (defaults to TransformerQueryAnalyzer)
|
||||
analyzer: Custom query analyzer (defaults to DateparserQueryAnalyzer)
|
||||
|
||||
Returns:
|
||||
(start_date, end_date) tuple or None
|
||||
|
||||
@@ -34,6 +34,7 @@ dependencies = [
|
||||
"opentelemetry-sdk>=1.20.0",
|
||||
"opentelemetry-instrumentation-fastapi>=0.41b0",
|
||||
"opentelemetry-exporter-prometheus>=0.41b0",
|
||||
"dateparser>=1.2.2",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -12,7 +12,7 @@ import asyncpg
|
||||
from testcontainers.postgres import PostgresContainer
|
||||
|
||||
from hindsight_api.engine.cross_encoder import SentenceTransformersCrossEncoder
|
||||
from hindsight_api.engine.query_analyzer import TransformerQueryAnalyzer
|
||||
from hindsight_api.engine.query_analyzer import DateparserQueryAnalyzer
|
||||
|
||||
|
||||
# Load environment variables from .env at the start of test session
|
||||
@@ -91,7 +91,7 @@ def cross_encoder():
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def query_analyzer():
|
||||
return TransformerQueryAnalyzer()
|
||||
return DateparserQueryAnalyzer()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Test query analyzer for temporal extraction.
|
||||
"""
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from hindsight_api.engine.query_analyzer import TransformerQueryAnalyzer, QueryAnalysis
|
||||
from hindsight_api.engine.query_analyzer import DateparserQueryAnalyzer, QueryAnalysis
|
||||
|
||||
|
||||
def test_query_analyzer_june_2024(query_analyzer):
|
||||
@@ -108,3 +108,127 @@ def test_query_analyzer_activities_june_2024(query_analyzer):
|
||||
assert analysis.temporal_constraint.end_date.day == 30
|
||||
|
||||
|
||||
def test_query_analyzer_last_saturday(query_analyzer):
|
||||
"""Test extraction of 'last Saturday' relative date."""
|
||||
# Reference date is Wednesday, January 15, 2025
|
||||
# Last Saturday would be January 11, 2025
|
||||
reference_date = datetime(2025, 1, 15, 12, 0, 0)
|
||||
|
||||
query = "I received a piece of jewelry last Saturday from whom?"
|
||||
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 'last Saturday'"
|
||||
# Last Saturday from Wed Jan 15 is Sat Jan 11
|
||||
assert analysis.temporal_constraint.start_date.year == 2025
|
||||
assert analysis.temporal_constraint.start_date.month == 1
|
||||
assert analysis.temporal_constraint.start_date.day == 11
|
||||
assert analysis.temporal_constraint.end_date.year == 2025
|
||||
assert analysis.temporal_constraint.end_date.month == 1
|
||||
assert analysis.temporal_constraint.end_date.day == 11
|
||||
|
||||
|
||||
def test_query_analyzer_yesterday(query_analyzer):
|
||||
"""Test extraction of 'yesterday' relative date."""
|
||||
# Reference date is Wednesday, January 15, 2025
|
||||
# Yesterday would be January 14, 2025
|
||||
reference_date = datetime(2025, 1, 15, 12, 0, 0)
|
||||
|
||||
query = "what did I do yesterday?"
|
||||
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 'yesterday'"
|
||||
assert analysis.temporal_constraint.start_date.year == 2025
|
||||
assert analysis.temporal_constraint.start_date.month == 1
|
||||
assert analysis.temporal_constraint.start_date.day == 14
|
||||
assert analysis.temporal_constraint.end_date.day == 14
|
||||
|
||||
|
||||
def test_query_analyzer_last_week(query_analyzer):
|
||||
"""Test extraction of 'last week' relative date."""
|
||||
# Reference date is Wednesday, January 15, 2025
|
||||
# Last week would be January 6-12, 2025 (Mon-Sun)
|
||||
reference_date = datetime(2025, 1, 15, 12, 0, 0)
|
||||
|
||||
query = "what meetings did I have last week?"
|
||||
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 'last week'"
|
||||
assert analysis.temporal_constraint.start_date.year == 2025
|
||||
assert analysis.temporal_constraint.start_date.month == 1
|
||||
assert analysis.temporal_constraint.start_date.day == 6 # Monday
|
||||
assert analysis.temporal_constraint.end_date.day == 12 # Sunday
|
||||
|
||||
|
||||
def test_query_analyzer_last_month(query_analyzer):
|
||||
"""Test extraction of 'last month' relative date."""
|
||||
# Reference date is Wednesday, January 15, 2025
|
||||
# Last month would be December 2024
|
||||
reference_date = datetime(2025, 1, 15, 12, 0, 0)
|
||||
|
||||
query = "expenses from last month"
|
||||
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 'last month'"
|
||||
assert analysis.temporal_constraint.start_date.year == 2024
|
||||
assert analysis.temporal_constraint.start_date.month == 12
|
||||
assert analysis.temporal_constraint.start_date.day == 1
|
||||
assert analysis.temporal_constraint.end_date.month == 12
|
||||
assert analysis.temporal_constraint.end_date.day == 31
|
||||
|
||||
|
||||
def test_query_analyzer_last_friday(query_analyzer):
|
||||
"""Test extraction of 'last Friday' relative date."""
|
||||
# Reference date is Wednesday, January 15, 2025
|
||||
# Last Friday would be January 10, 2025
|
||||
reference_date = datetime(2025, 1, 15, 12, 0, 0)
|
||||
|
||||
query = "who did I meet last Friday?"
|
||||
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 'last Friday'"
|
||||
assert analysis.temporal_constraint.start_date.year == 2025
|
||||
assert analysis.temporal_constraint.start_date.month == 1
|
||||
assert analysis.temporal_constraint.start_date.day == 10
|
||||
assert analysis.temporal_constraint.end_date.day == 10
|
||||
|
||||
|
||||
def test_query_analyzer_last_weekend(query_analyzer):
|
||||
"""Test extraction of 'last weekend' relative date."""
|
||||
# Reference date is Wednesday, January 15, 2025
|
||||
# Last weekend would be January 11-12, 2025 (Sat-Sun)
|
||||
reference_date = datetime(2025, 1, 15, 12, 0, 0)
|
||||
|
||||
query = "what did I do last weekend?"
|
||||
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 'last weekend'"
|
||||
assert analysis.temporal_constraint.start_date.year == 2025
|
||||
assert analysis.temporal_constraint.start_date.month == 1
|
||||
assert analysis.temporal_constraint.start_date.day == 11 # Saturday
|
||||
assert analysis.temporal_constraint.end_date.day == 12 # Sunday
|
||||
|
||||
|
||||
|
||||
@@ -146,11 +146,11 @@ pub fn retain(
|
||||
context,
|
||||
metadata: None,
|
||||
timestamp: None,
|
||||
document_id: Some(doc_id.clone()),
|
||||
};
|
||||
|
||||
let request = RetainRequest {
|
||||
items: vec![item],
|
||||
document_id: Some(doc_id.clone()),
|
||||
async_: r#async,
|
||||
};
|
||||
|
||||
@@ -239,7 +239,6 @@ pub fn retain_files(
|
||||
let pb = ui::create_progress_bar(files.len() as u64, "Processing files");
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut document_id = None;
|
||||
|
||||
for file_path in &files {
|
||||
let content = fs::read_to_string(file_path)
|
||||
@@ -251,15 +250,12 @@ pub fn retain_files(
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(config::generate_doc_id);
|
||||
|
||||
if document_id.is_none() {
|
||||
document_id = Some(doc_id);
|
||||
}
|
||||
|
||||
items.push(MemoryItem {
|
||||
content,
|
||||
context: context.clone(),
|
||||
metadata: None,
|
||||
timestamp: None,
|
||||
document_id: Some(doc_id),
|
||||
});
|
||||
|
||||
pb.inc(1);
|
||||
@@ -275,7 +271,6 @@ pub fn retain_files(
|
||||
|
||||
let request = RetainRequest {
|
||||
items,
|
||||
document_id,
|
||||
async_: r#async,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { hindsightClient } from '@/lib/hindsight-client';
|
||||
import { lowLevelClient, sdk } from '@/lib/hindsight-client';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -8,35 +8,45 @@ export async function POST(request: NextRequest) {
|
||||
const { query, types, fact_type, max_tokens, trace, budget, include } = body;
|
||||
|
||||
console.log('[Recall API] Request:', { bankId, query, types: types || fact_type, max_tokens, trace, budget });
|
||||
console.log('[Recall API] Include options:', JSON.stringify(include, null, 2));
|
||||
|
||||
const response = await hindsightClient.recallMemories(
|
||||
bankId,
|
||||
{
|
||||
const response = await sdk.recallMemories({
|
||||
client: lowLevelClient,
|
||||
path: { bank_id: bankId },
|
||||
body: {
|
||||
query,
|
||||
types: types || fact_type,
|
||||
maxTokens: max_tokens,
|
||||
max_tokens,
|
||||
trace,
|
||||
budget
|
||||
}
|
||||
);
|
||||
budget: budget || 'mid',
|
||||
include,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.data) {
|
||||
console.error('[Recall API] No data in response', { response, error: response.error });
|
||||
throw new Error(`API returned no data: ${JSON.stringify(response.error || 'Unknown error')}`);
|
||||
}
|
||||
|
||||
console.log('[Recall API] Response type:', typeof response);
|
||||
console.log('[Recall API] Response keys:', Object.keys(response || {}));
|
||||
console.log('[Recall API] Response structure:', {
|
||||
hasResults: !!response?.results,
|
||||
resultsCount: response?.results?.length,
|
||||
hasTrace: !!response?.trace,
|
||||
hasEntities: !!response?.entities,
|
||||
hasChunks: !!response?.chunks,
|
||||
hasResults: !!response.data?.results,
|
||||
resultsCount: response.data?.results?.length,
|
||||
hasTrace: !!response.data?.trace,
|
||||
hasEntities: !!response.data?.entities,
|
||||
entitiesType: typeof response.data?.entities,
|
||||
entitiesKeys: response.data?.entities ? Object.keys(response.data.entities) : null,
|
||||
hasChunks: !!response.data?.chunks,
|
||||
chunksType: typeof response.data?.chunks,
|
||||
chunksKeys: response.data?.chunks ? Object.keys(response.data.chunks) : null,
|
||||
});
|
||||
|
||||
// Return a clean JSON object by spreading the response
|
||||
// This ensures any non-serializable properties are excluded
|
||||
const jsonResponse = {
|
||||
results: response.results,
|
||||
trace: response.trace,
|
||||
entities: response.entities,
|
||||
chunks: response.chunks,
|
||||
results: response.data.results,
|
||||
trace: response.data.trace,
|
||||
entities: response.data.entities,
|
||||
chunks: response.data.chunks,
|
||||
};
|
||||
|
||||
return NextResponse.json(jsonResponse, { status: 200 });
|
||||
|
||||
@@ -7,7 +7,8 @@ import cytoscape from 'cytoscape';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Copy, Check, X, Calendar, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, FileText, Layers } from 'lucide-react';
|
||||
import { Copy, Check, Calendar, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { MemoryDetailPanel } from './memory-detail-panel';
|
||||
|
||||
type FactType = 'world' | 'bank' | 'opinion';
|
||||
@@ -26,10 +27,6 @@ export function DataView({ factType }: DataViewProps) {
|
||||
const [layout, setLayout] = useState('circle');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const [selectedDocument, setSelectedDocument] = useState<any>(null);
|
||||
const [loadingDocument, setLoadingDocument] = useState(false);
|
||||
const [selectedChunk, setSelectedChunk] = useState<any>(null);
|
||||
const [loadingChunk, setLoadingChunk] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedGraphNode, setSelectedGraphNode] = useState<any>(null);
|
||||
const [selectedTableMemory, setSelectedTableMemory] = useState<any>(null);
|
||||
@@ -47,44 +44,6 @@ export function DataView({ factType }: DataViewProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const viewDocument = async (documentId: string) => {
|
||||
if (!currentBank || !documentId) return;
|
||||
|
||||
setLoadingDocument(true);
|
||||
setSelectedDocument({ id: documentId });
|
||||
setSelectedChunk(null); // Clear chunk when viewing document
|
||||
|
||||
try {
|
||||
const doc: any = await client.getDocument(documentId, currentBank);
|
||||
setSelectedDocument(doc);
|
||||
} catch (error) {
|
||||
console.error('Error loading document:', error);
|
||||
alert('Error loading document: ' + (error as Error).message);
|
||||
setSelectedDocument(null);
|
||||
} finally {
|
||||
setLoadingDocument(false);
|
||||
}
|
||||
};
|
||||
|
||||
const viewChunk = async (chunkId: string) => {
|
||||
if (!chunkId) return;
|
||||
|
||||
setLoadingChunk(true);
|
||||
setSelectedChunk({ chunk_id: chunkId });
|
||||
setSelectedDocument(null); // Clear document when viewing chunk
|
||||
|
||||
try {
|
||||
const chunk: any = await client.getChunk(chunkId);
|
||||
setSelectedChunk(chunk);
|
||||
} catch (error) {
|
||||
console.error('Error loading chunk:', error);
|
||||
alert('Error loading chunk: ' + (error as Error).message);
|
||||
setSelectedChunk(null);
|
||||
} finally {
|
||||
setLoadingChunk(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
if (!currentBank) return;
|
||||
|
||||
@@ -354,16 +313,6 @@ export function DataView({ factType }: DataViewProps) {
|
||||
<MemoryDetailPanel
|
||||
memory={selectedGraphNode}
|
||||
onClose={() => setSelectedGraphNode(null)}
|
||||
onViewDocument={(docId) => {
|
||||
viewDocument(docId);
|
||||
setSelectedGraphNode(null);
|
||||
setViewMode('table');
|
||||
}}
|
||||
onViewChunk={(chunkId) => {
|
||||
viewChunk(chunkId);
|
||||
setSelectedGraphNode(null);
|
||||
setViewMode('table');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -372,13 +321,13 @@ export function DataView({ factType }: DataViewProps) {
|
||||
|
||||
{viewMode === 'table' && (
|
||||
<div className="flex gap-4">
|
||||
<div className={`transition-all ${selectedDocument || selectedChunk || selectedTableMemory ? 'w-2/3' : 'w-full'}`}>
|
||||
<div className={`transition-all ${selectedTableMemory ? 'w-2/3' : 'w-full'}`}>
|
||||
<div className="px-5 mb-4">
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search memories (text, context)..."
|
||||
placeholder="Search memories (text, context, ID)..."
|
||||
className="max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
@@ -390,7 +339,8 @@ export function DataView({ factType }: DataViewProps) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
row.text?.toLowerCase().includes(query) ||
|
||||
row.context?.toLowerCase().includes(query)
|
||||
row.context?.toLowerCase().includes(query) ||
|
||||
row.id?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -401,109 +351,85 @@ export function DataView({ factType }: DataViewProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-3">
|
||||
{paginatedRows.map((row: any, idx: number) => {
|
||||
const occurredDisplay = row.occurred_start
|
||||
? new Date(row.occurred_start).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
: null;
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-[80px]">ID</TableHead>
|
||||
<TableHead>Text</TableHead>
|
||||
<TableHead className="w-[150px]">Context</TableHead>
|
||||
<TableHead className="w-[120px]">Occurred</TableHead>
|
||||
<TableHead className="w-[60px]">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedRows.map((row: any, idx: number) => {
|
||||
const occurredDisplay = row.occurred_start
|
||||
? new Date(row.occurred_start).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={row.id || idx}
|
||||
onClick={() => setSelectedTableMemory(row)}
|
||||
className={`group p-4 bg-card border rounded-lg cursor-pointer transition-all hover:border-primary hover:shadow-md ${
|
||||
selectedTableMemory?.id === row.id ? 'border-primary ring-2 ring-primary/20' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Main content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-foreground line-clamp-2 mb-2">
|
||||
{row.text}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||
{occurredDisplay && (
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id || idx}
|
||||
onClick={() => setSelectedTableMemory(row)}
|
||||
className={`cursor-pointer hover:bg-muted/50 ${
|
||||
selectedTableMemory?.id === row.id ? 'bg-primary/10' : ''
|
||||
}`}
|
||||
>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground" title={row.id}>
|
||||
{row.id?.substring(0, 8)}...
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="line-clamp-2 text-sm">{row.text}</div>
|
||||
{row.entities && (
|
||||
<div className="flex gap-1 mt-1 flex-wrap">
|
||||
{row.entities.split(', ').slice(0, 3).map((entity: string, i: number) => (
|
||||
<span key={i} className="text-[10px] px-1.5 py-0.5 rounded bg-secondary text-secondary-foreground">
|
||||
{entity}
|
||||
</span>
|
||||
))}
|
||||
{row.entities.split(', ').length > 3 && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
+{row.entities.split(', ').length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground truncate max-w-[150px]" title={row.context}>
|
||||
{row.context || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{occurredDisplay ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{occurredDisplay}
|
||||
</span>
|
||||
)}
|
||||
{row.context && (
|
||||
<span className="truncate max-w-[200px]" title={row.context}>
|
||||
{row.context}
|
||||
</span>
|
||||
)}
|
||||
<span className="font-mono opacity-50" title={row.id}>
|
||||
{row.id.substring(0, 8)}...
|
||||
</span>
|
||||
</div>
|
||||
{row.entities && (
|
||||
<div className="flex gap-1 mt-2 flex-wrap">
|
||||
{row.entities.split(', ').slice(0, 5).map((entity: string, i: number) => (
|
||||
<span key={i} className="text-[10px] px-1.5 py-0.5 rounded bg-secondary text-secondary-foreground">
|
||||
{entity}
|
||||
</span>
|
||||
))}
|
||||
{row.entities.split(', ').length > 5 && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
+{row.entities.split(', ').length - 5}
|
||||
</span>
|
||||
) : '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(row.id);
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0"
|
||||
title="Copy ID"
|
||||
>
|
||||
{copiedId === row.id ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{row.document_id && (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
viewDocument(row.document_id);
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
title="View Document"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{row.chunk_id && (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
viewChunk(row.chunk_id);
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
title="View Chunk"
|
||||
>
|
||||
<Layers className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(row.id);
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
title="Copy ID"
|
||||
>
|
||||
{copiedId === row.id ? (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
@@ -566,166 +492,20 @@ export function DataView({ factType }: DataViewProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Memory Detail Panel for Table View */}
|
||||
{selectedTableMemory && !selectedDocument && !selectedChunk && (
|
||||
<div className="w-1/3 pr-5 pb-5">
|
||||
<MemoryDetailPanel
|
||||
memory={selectedTableMemory}
|
||||
onClose={() => setSelectedTableMemory(null)}
|
||||
onViewDocument={(docId) => {
|
||||
viewDocument(docId);
|
||||
setSelectedTableMemory(null);
|
||||
}}
|
||||
onViewChunk={(chunkId) => {
|
||||
viewChunk(chunkId);
|
||||
setSelectedTableMemory(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Document Detail Panel */}
|
||||
{selectedDocument && (
|
||||
<div className="w-1/3 pr-5 pb-5">
|
||||
<div className="bg-card border-2 border-primary rounded-lg p-4 sticky top-4 max-h-[calc(100vh-120px)] overflow-y-auto">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-card-foreground">Document Details</h3>
|
||||
<p className="text-sm text-muted-foreground">View the original document text and metadata</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedDocument(null)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loadingDocument ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">⏳</div>
|
||||
<div className="text-sm text-muted-foreground">Loading document...</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Document ID</div>
|
||||
<div className="text-sm font-mono break-all">{selectedDocument.id}</div>
|
||||
</div>
|
||||
{selectedDocument.created_at && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Created</div>
|
||||
<div className="text-sm">{new Date(selectedDocument.created_at).toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Memory Units</div>
|
||||
<div className="text-sm">{selectedDocument.memory_unit_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedDocument.original_text && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Text Length</div>
|
||||
<div className="text-sm">{selectedDocument.original_text.length.toLocaleString()} characters</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedDocument.original_text && (
|
||||
<div>
|
||||
<div className="text-sm font-bold text-foreground mb-2">Original Text</div>
|
||||
<div className="p-4 bg-muted rounded-lg border border-border">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono">{selectedDocument.original_text}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Memory Detail Panel for Table View */}
|
||||
{selectedTableMemory && (
|
||||
<div className="w-1/3 pr-5 pb-5">
|
||||
<MemoryDetailPanel
|
||||
memory={selectedTableMemory}
|
||||
onClose={() => setSelectedTableMemory(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chunk Detail Panel */}
|
||||
{selectedChunk && (
|
||||
<div className="w-1/3 pr-5 pb-5">
|
||||
<div className="bg-card border-2 border-primary rounded-lg p-4 sticky top-4 max-h-[calc(100vh-120px)] overflow-y-auto">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-card-foreground">Chunk Details</h3>
|
||||
<p className="text-sm text-muted-foreground">View the chunk text and metadata</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedChunk(null)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loadingChunk ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">⏳</div>
|
||||
<div className="text-sm text-muted-foreground">Loading chunk...</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Chunk ID</div>
|
||||
<div className="text-sm font-mono break-all">{selectedChunk.chunk_id}</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Document ID</div>
|
||||
<div className="text-sm font-mono break-all">{selectedChunk.document_id}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Chunk Index</div>
|
||||
<div className="text-sm">{selectedChunk.chunk_index}</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedChunk.created_at && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Created</div>
|
||||
<div className="text-sm">{new Date(selectedChunk.created_at).toLocaleString()}</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedChunk.chunk_text && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Text Length</div>
|
||||
<div className="text-sm">{selectedChunk.chunk_text.length.toLocaleString()} characters</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedChunk.chunk_text && (
|
||||
<div>
|
||||
<div className="text-sm font-bold text-foreground mb-2">Chunk Text</div>
|
||||
<div className="p-4 bg-muted rounded-lg border border-border">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono">{selectedChunk.chunk_text}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === 'timeline' && (
|
||||
<TimelineView data={data} onViewDocument={viewDocument} />
|
||||
<TimelineView data={data} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
@@ -743,7 +523,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||
// Timeline View Component - Custom compact timeline with zoom and navigation
|
||||
type Granularity = 'year' | 'month' | 'week' | 'day';
|
||||
|
||||
function TimelineView({ data, onViewDocument }: { data: any; onViewDocument: (id: string) => void }) {
|
||||
function TimelineView({ data }: { data: any }) {
|
||||
const [selectedItem, setSelectedItem] = useState<any>(null);
|
||||
const [granularity, setGranularity] = useState<Granularity>('month');
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
@@ -1085,7 +865,6 @@ function TimelineView({ data, onViewDocument }: { data: any; onViewDocument: (id
|
||||
<MemoryDetailPanel
|
||||
memory={selectedItem}
|
||||
onClose={() => setSelectedItem(null)}
|
||||
onViewDocument={onViewDocument}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Copy, Check, X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { DocumentChunkModal } from './document-chunk-modal';
|
||||
|
||||
interface MemoryDetailPanelProps {
|
||||
memory: any;
|
||||
onClose: () => void;
|
||||
onViewDocument?: (documentId: string) => void;
|
||||
onViewChunk?: (chunkId: string) => void;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function MemoryDetailPanel({
|
||||
memory,
|
||||
onClose,
|
||||
onViewDocument,
|
||||
onViewChunk,
|
||||
compact = false,
|
||||
}: MemoryDetailPanelProps) {
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const [modalType, setModalType] = useState<'document' | 'chunk' | null>(null);
|
||||
const [modalId, setModalId] = useState<string | null>(null);
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
@@ -31,6 +30,21 @@ export function MemoryDetailPanel({
|
||||
}
|
||||
};
|
||||
|
||||
const openDocumentModal = (docId: string) => {
|
||||
setModalType('document');
|
||||
setModalId(docId);
|
||||
};
|
||||
|
||||
const openChunkModal = (chunkId: string) => {
|
||||
setModalType('chunk');
|
||||
setModalId(chunkId);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalType(null);
|
||||
setModalId(null);
|
||||
};
|
||||
|
||||
if (!memory) return null;
|
||||
|
||||
const padding = compact ? 'p-3' : 'p-4';
|
||||
@@ -40,122 +54,136 @@ export function MemoryDetailPanel({
|
||||
const gap = compact ? 'space-y-2' : 'space-y-4';
|
||||
|
||||
return (
|
||||
<div className={`bg-card border-2 border-primary rounded-lg ${padding} sticky top-4 max-h-[calc(100vh-120px)] overflow-y-auto`}>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className={`${titleSize} font-bold text-card-foreground`}>Memory Details</h3>
|
||||
{!compact && (
|
||||
<p className="text-sm text-muted-foreground">Full memory content and metadata</p>
|
||||
<>
|
||||
<div className={`bg-card border-2 border-primary rounded-lg ${padding} sticky top-4 max-h-[calc(100vh-120px)] overflow-y-auto`}>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className={`${titleSize} font-bold text-card-foreground`}>Memory Details</h3>
|
||||
{!compact && (
|
||||
<p className="text-sm text-muted-foreground">Full memory content and metadata</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className={compact ? 'h-6 w-6 p-0' : 'h-8 w-8 p-0'}
|
||||
>
|
||||
<X className={compact ? 'h-3 w-3' : 'h-4 w-4'} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={gap}>
|
||||
{/* Full Text */}
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Full Text</div>
|
||||
<div className={`${textSize} whitespace-pre-wrap`}>{memory.text}</div>
|
||||
</div>
|
||||
|
||||
{/* Context */}
|
||||
{memory.context && (
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Context</div>
|
||||
<div className={textSize}>{memory.context}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Occurred</div>
|
||||
<div className={textSize}>
|
||||
{memory.occurred_start
|
||||
? new Date(memory.occurred_start).toLocaleString()
|
||||
: 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Mentioned</div>
|
||||
<div className={textSize}>
|
||||
{memory.mentioned_at
|
||||
? new Date(memory.mentioned_at).toLocaleString()
|
||||
: 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Entities */}
|
||||
{memory.entities && (
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-2`}>Entities</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(Array.isArray(memory.entities) ? memory.entities : String(memory.entities).split(', ')).map((entity: any, i: number) => {
|
||||
const entityText = typeof entity === 'string' ? entity : (entity?.name || JSON.stringify(entity));
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className={`${compact ? 'text-[10px] px-1.5 py-0.5' : 'text-xs px-2 py-1'} rounded bg-secondary text-secondary-foreground`}
|
||||
>
|
||||
{entityText}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ID */}
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Memory ID</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`${compact ? 'text-[10px]' : 'text-sm'} font-mono break-all`}>{memory.id}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 flex-shrink-0"
|
||||
onClick={() => copyToClipboard(memory.id)}
|
||||
>
|
||||
{copiedId === memory.id ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document/Chunk buttons */}
|
||||
{(memory.document_id || memory.chunk_id) && (
|
||||
<div className={`flex gap-2 ${compact ? 'pt-1' : ''}`}>
|
||||
{memory.document_id && (
|
||||
<Button
|
||||
onClick={() => openDocumentModal(memory.document_id)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={`flex-1 ${compact ? 'h-7 text-xs' : ''}`}
|
||||
>
|
||||
View Document
|
||||
</Button>
|
||||
)}
|
||||
{memory.chunk_id && (
|
||||
<Button
|
||||
onClick={() => openChunkModal(memory.chunk_id)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={`flex-1 ${compact ? 'h-7 text-xs' : ''}`}
|
||||
>
|
||||
View Chunk
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className={compact ? 'h-6 w-6 p-0' : 'h-8 w-8 p-0'}
|
||||
>
|
||||
<X className={compact ? 'h-3 w-3' : 'h-4 w-4'} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={gap}>
|
||||
{/* Full Text */}
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Full Text</div>
|
||||
<div className={`${textSize} whitespace-pre-wrap`}>{memory.text}</div>
|
||||
</div>
|
||||
|
||||
{/* Context */}
|
||||
{memory.context && (
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Context</div>
|
||||
<div className={textSize}>{memory.context}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Occurred</div>
|
||||
<div className={textSize}>
|
||||
{memory.occurred_start
|
||||
? new Date(memory.occurred_start).toLocaleString()
|
||||
: 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Mentioned</div>
|
||||
<div className={textSize}>
|
||||
{memory.mentioned_at
|
||||
? new Date(memory.mentioned_at).toLocaleString()
|
||||
: 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Entities */}
|
||||
{memory.entities && (
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-2`}>Entities</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{memory.entities.split(', ').map((entity: string, i: number) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`${compact ? 'text-[10px] px-1.5 py-0.5' : 'text-xs px-2 py-1'} rounded bg-secondary text-secondary-foreground`}
|
||||
>
|
||||
{entity}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ID */}
|
||||
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Memory ID</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`${compact ? 'text-[10px]' : 'text-sm'} font-mono break-all`}>{memory.id}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 flex-shrink-0"
|
||||
onClick={() => copyToClipboard(memory.id)}
|
||||
>
|
||||
{copiedId === memory.id ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document/Chunk buttons */}
|
||||
{(memory.document_id || memory.chunk_id) && (
|
||||
<div className={`flex gap-2 ${compact ? 'pt-1' : ''}`}>
|
||||
{memory.document_id && onViewDocument && (
|
||||
<Button
|
||||
onClick={() => onViewDocument(memory.document_id)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={`flex-1 ${compact ? 'h-7 text-xs' : ''}`}
|
||||
>
|
||||
View Document
|
||||
</Button>
|
||||
)}
|
||||
{memory.chunk_id && onViewChunk && (
|
||||
<Button
|
||||
onClick={() => onViewChunk(memory.chunk_id)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={`flex-1 ${compact ? 'h-7 text-xs' : ''}`}
|
||||
>
|
||||
View Chunk
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Document/Chunk Modal */}
|
||||
{modalType && modalId && (
|
||||
<DocumentChunkModal
|
||||
type={modalType}
|
||||
id={modalId}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
||||
import { Info } from 'lucide-react';
|
||||
import JsonView from 'react18-json-view';
|
||||
import 'react18-json-view/src/style.css';
|
||||
import { MemoryDetailPanel } from './memory-detail-panel';
|
||||
|
||||
type Phase = 'retrieval' | 'rrf' | 'rerank' | 'final' | 'json';
|
||||
type RetrievalMethod = 'semantic' | 'bm25' | 'graph' | 'temporal';
|
||||
@@ -44,9 +45,12 @@ interface SearchPane {
|
||||
factTypes: FactType[];
|
||||
budget: Budget;
|
||||
maxTokens: number;
|
||||
queryDate: string;
|
||||
includeChunks: boolean;
|
||||
includeEntities: boolean;
|
||||
results: any[] | null;
|
||||
entities: any[] | null;
|
||||
chunks: any[] | null;
|
||||
trace: any | null;
|
||||
loading: boolean;
|
||||
currentPhase: Phase;
|
||||
@@ -64,9 +68,12 @@ export function SearchDebugView() {
|
||||
factTypes: ['world'],
|
||||
budget: 'mid',
|
||||
maxTokens: 4096,
|
||||
queryDate: '',
|
||||
includeChunks: false,
|
||||
includeEntities: false,
|
||||
results: null,
|
||||
entities: null,
|
||||
chunks: null,
|
||||
trace: null,
|
||||
loading: false,
|
||||
currentPhase: 'retrieval',
|
||||
@@ -76,6 +83,7 @@ export function SearchDebugView() {
|
||||
},
|
||||
]);
|
||||
const [nextPaneId, setNextPaneId] = useState(2);
|
||||
const [selectedMemory, setSelectedMemory] = useState<any | null>(null);
|
||||
|
||||
const addPane = () => {
|
||||
setPanes([
|
||||
@@ -86,9 +94,12 @@ export function SearchDebugView() {
|
||||
factTypes: ['world'],
|
||||
budget: 'mid',
|
||||
maxTokens: 4096,
|
||||
queryDate: '',
|
||||
includeChunks: false,
|
||||
includeEntities: false,
|
||||
results: null,
|
||||
entities: null,
|
||||
chunks: null,
|
||||
trace: null,
|
||||
loading: false,
|
||||
currentPhase: 'retrieval',
|
||||
@@ -138,7 +149,8 @@ export function SearchDebugView() {
|
||||
include: {
|
||||
entities: pane.includeEntities ? { max_tokens: 500 } : null,
|
||||
chunks: pane.includeChunks ? { max_tokens: 8192 } : null
|
||||
}
|
||||
},
|
||||
...(pane.queryDate && { query_timestamp: pane.queryDate })
|
||||
};
|
||||
|
||||
const data: any = await client.recall(requestBody);
|
||||
@@ -148,6 +160,8 @@ export function SearchDebugView() {
|
||||
|
||||
updatePane(paneId, {
|
||||
results: data.results || [],
|
||||
entities: data.entities || null,
|
||||
chunks: data.chunks || null,
|
||||
trace: data.trace || null,
|
||||
loading: false,
|
||||
currentRetrievalFactType: defaultFactType,
|
||||
@@ -220,7 +234,11 @@ export function SearchDebugView() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredResults.map((result: any, idx: number) => (
|
||||
<TableRow key={idx}>
|
||||
<TableRow
|
||||
key={idx}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => setSelectedMemory(result)}
|
||||
>
|
||||
<TableCell className="font-bold">#{result.rank}</TableCell>
|
||||
<TableCell className="max-w-md">{result.text}</TableCell>
|
||||
{pane.factTypes.length > 1 && (
|
||||
@@ -278,7 +296,11 @@ export function SearchDebugView() {
|
||||
: 'N/A';
|
||||
|
||||
return (
|
||||
<TableRow key={idx}>
|
||||
<TableRow
|
||||
key={idx}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => setSelectedMemory(result)}
|
||||
>
|
||||
<TableCell className="font-bold">
|
||||
#{result.final_rrf_rank || result.finalRrfRank || result.rank}
|
||||
</TableCell>
|
||||
@@ -358,7 +380,11 @@ export function SearchDebugView() {
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={idx} className={rowBg}>
|
||||
<TableRow
|
||||
key={idx}
|
||||
className={`cursor-pointer hover:bg-muted/50 ${rowBg}`}
|
||||
onClick={() => setSelectedMemory(result)}
|
||||
>
|
||||
<TableCell className="font-bold">#{result.rerank_rank}</TableCell>
|
||||
<TableCell>#{result.rrf_rank}</TableCell>
|
||||
<TableCell className={`font-bold ${changeColor}`}>
|
||||
@@ -444,7 +470,11 @@ export function SearchDebugView() {
|
||||
: 'N/A';
|
||||
|
||||
return (
|
||||
<TableRow key={idx}>
|
||||
<TableRow
|
||||
key={idx}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => setSelectedMemory(result)}
|
||||
>
|
||||
<TableCell className="font-bold">#{idx + 1}</TableCell>
|
||||
<TableCell className="max-w-xs">{result.text}</TableCell>
|
||||
<TableCell className="max-w-32">
|
||||
@@ -478,17 +508,18 @@ export function SearchDebugView() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
onClick={addPane}
|
||||
variant="secondary"
|
||||
>
|
||||
+ Add Recall Pane
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className={selectedMemory ? 'flex-1' : 'w-full'}>
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
onClick={addPane}
|
||||
variant="secondary"
|
||||
>
|
||||
+ Add Recall Pane
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5">
|
||||
<div className="grid grid-cols-1 gap-5">
|
||||
{panes.map((pane) => (
|
||||
<div key={pane.id} className="border-2 border-primary rounded-lg overflow-hidden flex flex-col shadow-md">
|
||||
{/* Header */}
|
||||
@@ -630,11 +661,6 @@ export function SearchDebugView() {
|
||||
<strong>Entry points:</strong> {pane.trace.summary.entry_points_found}
|
||||
</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span>
|
||||
<strong>Budget used:</strong> {pane.trace.summary.budget_used} /{' '}
|
||||
{pane.trace.summary.budget_used + pane.trace.summary.budget_remaining}
|
||||
</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span>
|
||||
<strong>Results:</strong> {pane.trace.summary.results_returned}
|
||||
</span>
|
||||
@@ -777,7 +803,11 @@ export function SearchDebugView() {
|
||||
</p>
|
||||
<div className="bg-muted p-4 rounded border border-border overflow-auto max-h-[800px]">
|
||||
<JsonView
|
||||
src={{ results: pane.results }}
|
||||
src={{
|
||||
results: pane.results,
|
||||
...(pane.entities && { entities: pane.entities }),
|
||||
...(pane.chunks && { chunks: pane.chunks }),
|
||||
}}
|
||||
collapsed={1}
|
||||
theme="default"
|
||||
/>
|
||||
@@ -789,7 +819,19 @@ export function SearchDebugView() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Memory Detail Panel */}
|
||||
{selectedMemory && (
|
||||
<div className="w-80 flex-shrink-0">
|
||||
<MemoryDetailPanel
|
||||
memory={selectedMemory}
|
||||
onClose={() => setSelectedMemory(null)}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useBank } from '@/lib/bank-context';
|
||||
import { Search, Sparkles, Database, FileText, Users, Brain, ChevronLeft, ChevronRight, UserCircle, BarChart3 } from 'lucide-react';
|
||||
import { Search, Sparkles, Database, FileText, Users, ChevronLeft, ChevronRight, UserCircle, BarChart3 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Link from 'next/link';
|
||||
|
||||
type NavItem = 'recall' | 'reflect' | 'data' | 'documents' | 'entities' | 'profile' | 'stats';
|
||||
|
||||
@@ -57,11 +58,21 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = currentTab === item.id;
|
||||
const href = `/banks/${currentBank}?view=${item.id}`;
|
||||
|
||||
return (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => onTabChange(item.id)}
|
||||
<Link
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
// For left-click, prevent default and use the callback
|
||||
// This allows the parent to handle navigation without full page reload
|
||||
if (e.button === 0 && !e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
onTabChange(item.id);
|
||||
}
|
||||
// Middle-click or Ctrl/Cmd+click will naturally open in new tab
|
||||
}}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-all',
|
||||
isActive
|
||||
@@ -73,7 +84,7 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
>
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
{!isCollapsed && <span>{item.label}</span>}
|
||||
</button>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -48,6 +48,10 @@ export class ControlPlaneClient {
|
||||
budget?: string;
|
||||
max_tokens?: number;
|
||||
trace?: boolean;
|
||||
include?: {
|
||||
entities?: { max_tokens: number } | null;
|
||||
chunks?: { max_tokens: number } | null;
|
||||
};
|
||||
}) {
|
||||
return this.fetchApi('/api/recall', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -178,12 +178,18 @@ class LongMemEvalAnswerGenerator(LLMAnswerGenerator):
|
||||
|
||||
In general the answer must be comprehensive and plenty of details from the retrieved context.
|
||||
|
||||
For quantitative questions, use numbers and units. Example: 'How many..', just answer the number and which ones. Include EACH item even if it's not the most recent one. Reason and do calculation for complex questions.
|
||||
For quantitative questions, use numbers and units. Example: 'How many..', just answer the number and which ones. Consider EACH item even if it's not the most recent one. Reason and do calculation for complex questions.
|
||||
If questions asks a location (where...?) make sure to include the location name.
|
||||
For recommendations/suggestions, use the context to understand the user's preferences and provide a possible answer based on that and explain why you chose that answer based on user preferences. Include as much preferences as possible in your answer.
|
||||
For specific number/value questions, use the context to understand what is the most up-to-date number based on recency.
|
||||
For recommendations/suggestions, use the retrieved context to understand the user's preferences and provide a possible answer based on those. Include the reasoning and explicitly say what the user prefers, before making suggestions (user previous experiences or specific requests FROM the user). Consider as much user preferences as possible in your answer.
|
||||
For questions asking for help or instructions, consider the users' latest purchases and previous interactions with the assistant to understand which details to focus your answer on (include these references in your answer).
|
||||
For specific number/value questions, use the context to understand what is the most up-to-date number based on recency, but also include the reasoning (in the answer) on previous possible values and why you think are less relevant.
|
||||
For open questions, include as much details as possible from different sources that are relevant.
|
||||
For questions where a specific entity is mentioned and it's different from your memory, just say the truth, don't make up anything just to fulfill the question. For example, if the question is about a specific sport, you should consider if the memories and the question are about the same sport.
|
||||
For questions where a specific entity is mentioned and it's different from your memory, just say the truth, don't make up anything just to fulfill the question. For example, if the question is about a specific sport, you should consider if the memories and the question are about the same sport.
|
||||
For comparative questions , say you don't know the answer if you don't have information about both sides. (or more sides)
|
||||
For questions related to time/date, carefully review the question date and the memories date to correctly answer the question.
|
||||
For questions related to time/date calculation (e.g. How many days passed between X and Y?), carefully review the memories date to correctly answer the question and only provide an answer if you have information about both X and Y, otherwise say it's not possible to calculate and why.
|
||||
|
||||
Consider assistant's previous actions (e.g., bookings, reminders) as impactful to the user experiences.
|
||||
|
||||
|
||||
Question: {question}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "benchmarks"
|
||||
version = "0.0.9"
|
||||
description = "Benchmarks for Hindsight memory system"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"python-fasthtml>=0.12.33",
|
||||
"streamlit>=1.51.0",
|
||||
"openai>=1.0.0",
|
||||
"rich>=13.0.0",
|
||||
"pydantic>=2.0.0",
|
||||
"hindsight-api",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["benchmarks"]
|
||||
|
||||
[tool.uv.sources]
|
||||
hindsight-api = { path = "../../hindsight-api" }
|
||||
Generated
-3181
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,15 @@ description = "Development utilities for Hindsight"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"hindsight-api",
|
||||
"python-fasthtml>=0.12.33",
|
||||
"streamlit>=1.51.0",
|
||||
"openai>=1.0.0",
|
||||
"rich>=13.0.0",
|
||||
"pydantic>=2.0.0",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hindsight_dev"]
|
||||
packages = ["hindsight_dev", "benchmarks"]
|
||||
|
||||
[tool.uv.sources]
|
||||
hindsight-api = { workspace = true }
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[tool.uv.workspace]
|
||||
members = ["hindsight", "hindsight-api", "hindsight-dev", "hindsight-dev/benchmarks", "hindsight-mcp-server", "hindsight-openai", "hindsight-langmem", "hindsight-clients/python"]
|
||||
members = ["hindsight", "hindsight-api", "hindsight-dev", "hindsight-mcp-server", "hindsight-openai", "hindsight-langmem", "hindsight-clients/python"]
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = []
|
||||
|
||||
@@ -8,7 +8,6 @@ resolution-markers = [
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"benchmarks",
|
||||
"hindsight-all",
|
||||
"hindsight-api",
|
||||
"hindsight-client",
|
||||
@@ -392,29 +391,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "benchmarks"
|
||||
version = "0.0.7"
|
||||
source = { editable = "hindsight-dev/benchmarks" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
{ name = "openai" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-fasthtml" },
|
||||
{ name = "rich" },
|
||||
{ name = "streamlit" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "hindsight-api", directory = "hindsight-api" },
|
||||
{ name = "openai", specifier = ">=1.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0" },
|
||||
{ name = "python-fasthtml", specifier = ">=0.12.33" },
|
||||
{ name = "rich", specifier = ">=13.0.0" },
|
||||
{ name = "streamlit", specifier = ">=1.51.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.9.0"
|
||||
@@ -683,6 +659,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/cc/53c8350d8ca53ada627f071c252e806b97c949e03b054af7d15e62309a83/cyclopts-4.2.2-py3-none-any.whl", hash = "sha256:2e001158ccb275723a4d820c65d114caa078073e61298f2a7c6112a8d3ba90c6", size = 184362 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dateparser"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "pytz" },
|
||||
{ name = "regex" },
|
||||
{ name = "tzlocal" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/30/064144f0df1749e7bb5faaa7f52b007d7c2d08ec08fed8411aba87207f68/dateparser-1.2.2.tar.gz", hash = "sha256:986316f17cb8cdc23ea8ce563027c5ef12fc725b6fb1d137c14ca08777c5ecf7", size = 329840 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/22/f020c047ae1346613db9322638186468238bcfa8849b4668a22b97faad65/dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482", size = 315453 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diskcache"
|
||||
version = "5.6.3"
|
||||
@@ -1150,11 +1141,12 @@ provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-api"
|
||||
version = "0.0.7"
|
||||
version = "0.0.9"
|
||||
source = { editable = "hindsight-api" }
|
||||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "dateparser" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "fastmcp" },
|
||||
{ name = "greenlet" },
|
||||
@@ -1205,6 +1197,7 @@ dev = [
|
||||
requires-dist = [
|
||||
{ name = "alembic", specifier = ">=1.17.1" },
|
||||
{ name = "asyncpg", specifier = ">=0.29.0" },
|
||||
{ name = "dateparser", specifier = ">=1.2.2" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.120.3" },
|
||||
{ name = "fastmcp", specifier = ">=2.0.0" },
|
||||
{ name = "filelock", marker = "extra == 'test'", specifier = ">=3.0.0" },
|
||||
@@ -1250,7 +1243,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-client"
|
||||
version = "0.0.7"
|
||||
version = "0.0.9"
|
||||
source = { editable = "hindsight-clients/python" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1282,14 +1275,26 @@ provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-dev"
|
||||
version = "0.0.7"
|
||||
version = "0.0.9"
|
||||
source = { editable = "hindsight-dev" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
{ name = "openai" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-fasthtml" },
|
||||
{ name = "rich" },
|
||||
{ name = "streamlit" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "hindsight-api", editable = "hindsight-api" }]
|
||||
requires-dist = [
|
||||
{ name = "hindsight-api", editable = "hindsight-api" },
|
||||
{ name = "openai", specifier = ">=1.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0" },
|
||||
{ name = "python-fasthtml", specifier = ">=0.12.33" },
|
||||
{ name = "rich", specifier = ">=13.0.0" },
|
||||
{ name = "streamlit", specifier = ">=1.51.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
@@ -4245,6 +4250,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
|
||||
Reference in New Issue
Block a user