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
101 changes: 101 additions & 0 deletions hindsight-api/tests/test_observations.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,107 @@ async def test_regenerate_entity_observations(memory, request_context):
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)


@pytest.mark.asyncio
async def test_manual_regenerate_with_few_facts(memory, request_context):
"""
Test that manual regeneration works even with fewer than 5 facts.

This is important because:
- Automatic generation during retain requires MIN_FACTS_THRESHOLD (5)
- But manual regeneration via API should work with any number of facts
- The UI triggers manual regeneration, so it should work regardless of fact count
"""
bank_id = f"test_manual_regen_{datetime.now(timezone.utc).timestamp()}"

try:
# Store only 2 facts - below the automatic threshold
await memory.retain_async(
bank_id=bank_id,
content="Alice works at Google as a senior software engineer.",
context="work info",
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc),
request_context=request_context,
)
await memory.retain_async(
bank_id=bank_id,
content="Alice loves hiking and outdoor photography.",
context="hobbies",
event_date=datetime(2024, 1, 16, tzinfo=timezone.utc),
request_context=request_context,
)

# Find the Alice entity
pool = await memory._get_pool()
async with pool.acquire() as conn:
entity_row = await conn.fetchrow(
"""
SELECT id, canonical_name
FROM entities
WHERE bank_id = $1 AND LOWER(canonical_name) LIKE '%alice%'
LIMIT 1
""",
bank_id
)

assert entity_row is not None, "Alice entity should have been extracted"

entity_id = str(entity_row['id'])
entity_name = entity_row['canonical_name']

# Check fact count - should be < 5
async with pool.acquire() as conn:
fact_count = await conn.fetchval(
"SELECT COUNT(*) FROM unit_entities WHERE entity_id = $1",
entity_row['id']
)

print(f"\n=== Manual Regeneration Test ===")
print(f"Entity: {entity_name} (id: {entity_id})")
print(f"Linked facts: {fact_count}")

# Verify we're testing with fewer than the automatic threshold
assert fact_count < 5, f"Test requires < 5 facts, but entity has {fact_count}"

# Before regeneration - should have no observations (auto threshold not met)
obs_before = await memory.get_entity_observations(bank_id, entity_id, limit=10, request_context=request_context)
print(f"Observations before manual regenerate: {len(obs_before)}")

# Manually regenerate observations - this should work regardless of fact count
created_ids = await memory.regenerate_entity_observations(
bank_id=bank_id,
entity_id=entity_id,
entity_name=entity_name,
request_context=request_context,
)

print(f"Observations created by manual regenerate: {len(created_ids)}")

# Get observations after regeneration
observations = await memory.get_entity_observations(bank_id, entity_id, limit=10, request_context=request_context)
print(f"Observations after manual regenerate: {len(observations)}")
for obs in observations:
print(f" - {obs.text}")

# Manual regeneration should create observations even with < 5 facts
assert len(observations) > 0, \
f"Manual regeneration should create observations even with only {fact_count} facts. " \
f"The LLM should synthesize at least 1 observation from the available facts."

# Verify observations contain relevant content
obs_texts = " ".join([o.text.lower() for o in observations])
assert any(keyword in obs_texts for keyword in ["google", "engineer", "hiking", "photography", "alice"]), \
"Observations should contain relevant information about Alice"

print(f"✓ Manual regeneration works with {fact_count} facts (below automatic threshold of 5)")

finally:
# Cleanup
pool = await memory._get_pool()
async with pool.acquire() as conn:
await conn.execute("DELETE FROM memory_units WHERE bank_id = $1", bank_id)
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)


@pytest.mark.asyncio
async def test_search_with_include_entities(memory, request_context):
"""
Expand Down
157 changes: 157 additions & 0 deletions hindsight-clients/python/tests/test_main_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,160 @@ async def do_delete():
assert response.success is True
assert response.document_id == doc_id
assert response.memory_units_deleted >= 0

def test_get_document(self, client, bank_id):
"""Test getting a document."""
import asyncio
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import DocumentsApi

# First create a document
doc_id = f"test-doc-{uuid.uuid4().hex[:8]}"
client.retain(
bank_id=bank_id,
content="Test document content for retrieval",
document_id=doc_id,
)

async def do_get():
config = Configuration(host=HINDSIGHT_API_URL)
api_client = ApiClient(config)
api = DocumentsApi(api_client)
return await api.get_document(bank_id=bank_id, document_id=doc_id)

document = asyncio.get_event_loop().run_until_complete(do_get())

assert document is not None
assert document.id == doc_id
assert "Test document content" in document.original_text


class TestEntities:
"""Tests for entity endpoints."""

@pytest.fixture(autouse=True)
def setup_memories(self, client, bank_id):
"""Setup: Store memories that will generate entities."""
client.retain_batch(
bank_id=bank_id,
items=[
{"content": "Alice works at Google as a software engineer"},
{"content": "Bob is friends with Alice and works at Microsoft"},
],
retain_async=False,
)

def test_list_entities(self, client, bank_id):
"""Test listing entities."""
import asyncio
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import EntitiesApi

async def do_list():
config = Configuration(host=HINDSIGHT_API_URL)
api_client = ApiClient(config)
api = EntitiesApi(api_client)
return await api.list_entities(bank_id=bank_id)

response = asyncio.get_event_loop().run_until_complete(do_list())

assert response is not None
assert response.items is not None
assert isinstance(response.items, list)

def test_get_entity(self, client, bank_id):
"""Test getting a specific entity."""
import asyncio
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import EntitiesApi

async def do_test():
config = Configuration(host=HINDSIGHT_API_URL)
api_client = ApiClient(config)
api = EntitiesApi(api_client)

# First list entities to get an ID
list_response = await api.list_entities(bank_id=bank_id)

if list_response.items and len(list_response.items) > 0:
entity_id = list_response.items[0].id

# Get the entity
entity = await api.get_entity(bank_id=bank_id, entity_id=entity_id)
return entity_id, entity
return None, None

entity_id, entity = asyncio.get_event_loop().run_until_complete(do_test())

if entity_id:
assert entity is not None
assert entity.id == entity_id

def test_regenerate_entity_observations(self, client, bank_id):
"""Test regenerating observations for an entity."""
import asyncio
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import EntitiesApi

async def do_test():
config = Configuration(host=HINDSIGHT_API_URL)
api_client = ApiClient(config)
api = EntitiesApi(api_client)

# First list entities to get an ID
list_response = await api.list_entities(bank_id=bank_id)

if list_response.items and len(list_response.items) > 0:
entity_id = list_response.items[0].id

# Regenerate observations
result = await api.regenerate_entity_observations(
bank_id=bank_id,
entity_id=entity_id,
)
return entity_id, result
return None, None

entity_id, result = asyncio.get_event_loop().run_until_complete(do_test())

if entity_id:
assert result is not None
assert result.id == entity_id


class TestDeleteBank:
"""Tests for bank deletion."""

def test_delete_bank(self, client):
"""Test deleting a bank."""
import asyncio
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import BanksApi

# Create a unique bank for this test
bank_id = f"test_bank_delete_{uuid.uuid4().hex[:12]}"

# Create bank with some data
client.create_bank(
bank_id=bank_id,
background="This bank will be deleted",
)
client.retain(
bank_id=bank_id,
content="Some memory to store",
)

async def do_delete():
config = Configuration(host=HINDSIGHT_API_URL)
api_client = ApiClient(config)
api = BanksApi(api_client)
return await api.delete_bank(bank_id=bank_id)

response = asyncio.get_event_loop().run_until_complete(do_delete())

assert response is not None
assert response.success is True

# Verify bank data is deleted - memories should be gone
memories = client.list_memories(bank_id=bank_id)
assert memories.total == 0
10 changes: 8 additions & 2 deletions hindsight-control-plane/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@
"lint": "next lint",
"prepublishOnly": "npm run build"
},
"keywords": ["hindsight", "memory", "semantic", "ai"],
"keywords": [
"hindsight",
"memory",
"semantic",
"ai"
],
"author": "Hindsight Team",
"license": "ISC",
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
Expand Down Expand Up @@ -58,9 +64,9 @@
"typescript": "^5.9.3"
},
"devDependencies": {
"@vectorize-io/hindsight-client": "file:../hindsight-clients/typescript",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@vectorize-io/hindsight-client": "file:../hindsight-clients/typescript",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"prettier": "^3.7.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,28 @@ export async function GET(
return NextResponse.json({ error: "Failed to fetch document" }, { status: 500 });
}
}

export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ documentId: string }> }
) {
try {
const { documentId } = await params;
const searchParams = request.nextUrl.searchParams;
const bankId = searchParams.get("bank_id");

if (!bankId) {
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
}

const response = await sdk.deleteDocument({
client: lowLevelClient,
path: { bank_id: bankId, document_id: documentId },
});

return NextResponse.json(response.data, { status: 200 });
} catch (error) {
console.error("Error deleting document:", error);
return NextResponse.json({ error: "Failed to delete document" }, { status: 500 });
}
}
Loading