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
79 changes: 48 additions & 31 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,6 @@ concurrency:
cancel-in-progress: true

jobs:
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
- name: hindsight-embed
path: hindsight-embed

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-api-python-versions:
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -790,6 +759,54 @@ jobs:
working-directory: ./hindsight-embed
run: ./test.sh

test-hindsight-all:
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: groq
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
# For test_server_integration.py compatibility
HINDSIGHT_LLM_PROVIDER: groq
HINDSIGHT_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
HINDSIGHT_LLM_MODEL: openai/gpt-oss-20b
# Prefer CPU-only PyTorch in CI
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
prune-cache: false

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Build hindsight-all
working-directory: ./hindsight
run: uv build

- name: Install dependencies
working-directory: ./hindsight
run: uv sync --frozen --extra test --index-strategy unsafe-best-match

- name: Cache HuggingFace models
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: ${{ runner.os }}-huggingface-all-${{ hashFiles('hindsight/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-huggingface-all-
${{ runner.os }}-huggingface-

- name: Run unit tests
working-directory: ./hindsight
run: uv run pytest tests/ -v

test-doc-examples:
runs-on: ubuntu-latest
needs: test-rust-cli
Expand Down
15 changes: 0 additions & 15 deletions hindsight-docs/docs/sdks/embed.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,21 +230,6 @@ rm ~/.hindsight/embed
hindsight-embed configure
```

## Advanced Configuration

While `hindsight-embed` aims to be zero-config, you can customize the underlying API behavior by setting `HINDSIGHT_API_*` variables in `~/.hindsight/embed`:

```bash
# Example: Custom embedding model
HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-large

# Example: Verbose extraction
HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbose
```

See [Configuration](/developer/configuration) for all available `HINDSIGHT_API_*` options.

## When to Use

**Perfect for:**
Expand Down
106 changes: 104 additions & 2 deletions hindsight-docs/docs/sdks/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,119 @@ print(answer.text)
</TabItem>
</Tabs>

## Embedded Client (Easiest Option)

`HindsightEmbedded` provides the simplest way to use Hindsight in Python. It automatically manages a background server for you - no manual setup required:

```python
from hindsight import HindsightEmbedded
import os

# Server starts automatically on first use
client = HindsightEmbedded(
profile="myapp", # Profile for data isolation
llm_provider="openai",
llm_model="gpt-4o-mini",
llm_api_key=os.environ["OPENAI_API_KEY"],
)

# Use immediately - no manual server management needed
client.retain(bank_id="my-bank", content="Alice works at Google")
results = client.recall(bank_id="my-bank", query="What does Alice do?")

# Server continues running (auto-stops after idle timeout)
# Or explicitly stop it:
client.close(stop_daemon=True)
```

**What's a Profile?**

A profile is an isolated Hindsight environment. Each profile gets its own PostgreSQL database (stored in `~/.pg0/instances/hindsight-embed-{profile}/`) and its own API server. Use different profiles to separate environments (dev/prod), applications, or users.

**When to Use HindsightEmbedded**

Use `HindsightEmbedded` when you want the server to start automatically and manage itself. Use `HindsightServer` when you need explicit control over server lifecycle (e.g., testing where you want immediate startup/shutdown).

**Advanced Operations**

`HindsightEmbedded` provides organized API namespaces for advanced operations. Each method call automatically ensures the daemon is running:

```python
from hindsight import HindsightEmbedded
import os

embedded = HindsightEmbedded(
profile="myapp",
llm_provider="openai",
llm_api_key=os.environ["OPENAI_API_KEY"],
)

# Core operations (automatically proxied)
embedded.retain(bank_id="test", content="Hello")
results = embedded.recall(bank_id="test", query="Hello")

# Bank management
embedded.banks.create(bank_id="test", name="Test Bank", mission="Help users")
embedded.banks.set_mission(bank_id="test", mission="Updated mission")
embedded.banks.delete(bank_id="test")

# Mental models
embedded.mental_models.create(
bank_id="test",
name="User Preferences",
content="User prefers dark mode"
)
models = embedded.mental_models.list(bank_id="test")
embedded.mental_models.update(bank_id="test", mental_model_id="...", content="New content")

# Directives
embedded.directives.create(
bank_id="test",
name="Response Style",
content="Be concise and friendly"
)
directives = embedded.directives.list(bank_id="test")

# List memories
memories = embedded.memories.list(bank_id="test", type="world", limit=50)
```

**Why Use API Namespaces?**

API namespaces (`banks`, `mental_models`, `directives`, `memories`) ensure the daemon is running before each call. This handles daemon crashes gracefully:

```python
# ✅ GOOD - Uses API namespace (daemon restarts handled)
embedded.banks.create(bank_id="test", name="Test")

# ❌ BAD - Direct client access (daemon crashes NOT handled)
client = embedded.client
client.create_bank(bank_id="test", name="Test") # Fails if daemon crashed
```

## Client Initialization

```python
from hindsight_client import Hindsight
from hindsight import HindsightClient

client = Hindsight(
client = HindsightClient(
base_url="http://localhost:8888", # Hindsight API URL
timeout=30.0, # Request timeout in seconds
)

# Core operations
client.retain(bank_id="test", content="Hello world")
results = client.recall(bank_id="test", query="Hello")

# Organized API access (same as HindsightEmbedded)
client.banks.create(bank_id="test", name="Test Bank")
models = client.mental_models.list(bank_id="test")
directives = client.directives.list(bank_id="test")
memories = client.memories.list(bank_id="test")
```

Both `HindsightClient` and `HindsightEmbedded` provide the same organized API namespaces (`banks`, `mental_models`, `directives`, `memories`) for consistent developer experience.

## Core Operations

### Retain (Store Memory)
Expand Down
13 changes: 13 additions & 0 deletions hindsight-embed/hindsight_embed/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
"""Hindsight embedded CLI - local memory operations without a server."""

from .daemon_embed_manager import DaemonEmbedManager
from .embed_manager import EmbedManager

__version__ = "0.4.8"

__all__ = [
"EmbedManager",
"DaemonEmbedManager",
]


def get_embed_manager() -> EmbedManager:
"""Get the default embed manager instance."""
return DaemonEmbedManager()
16 changes: 8 additions & 8 deletions hindsight-embed/hindsight_embed/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import sys
from pathlib import Path

from . import get_embed_manager

CONFIG_DIR = Path.home() / ".hindsight"
CONFIG_FILE = CONFIG_DIR / "embed"
CONFIG_FILE_ALT = CONFIG_DIR / "config.env" # Alternative config file location
Expand Down Expand Up @@ -424,7 +426,7 @@ def _do_configure_interactive(profile_name: str | None = None, port: int | None
from . import daemon_client

daemon_profile = profile_name if profile_name else None
if daemon_client._is_daemon_running(daemon_profile):
if daemon_client.is_daemon_running(daemon_profile):
print("\n \033[2mRestarting daemon with new configuration...\033[0m")
daemon_client.stop_daemon(daemon_profile)

Expand Down Expand Up @@ -479,7 +481,7 @@ def do_daemon(args, config: dict, logger):

console = Console()

if daemon_client._is_daemon_running(profile):
if daemon_client.is_daemon_running(profile):
# Build title with profile and port
if profile:
already_running_title = (
Expand Down Expand Up @@ -516,7 +518,7 @@ def do_daemon(args, config: dict, logger):

console = Console()

if not daemon_client._is_daemon_running(profile):
if not daemon_client.is_daemon_running(profile):
# Build title for not running status
if profile:
not_running_title = f"[bold]Daemon Status[/bold] [dim]({profile})[/dim]"
Expand Down Expand Up @@ -559,7 +561,6 @@ def do_daemon(args, config: dict, logger):

elif args.daemon_command == "status":
import os
import re
from pathlib import Path

from rich.console import Console
Expand All @@ -568,7 +569,7 @@ def do_daemon(args, config: dict, logger):

console = Console()

if daemon_client._is_daemon_running(profile):
if daemon_client.is_daemon_running(profile):
status_text = Text()
status_text.append("Daemon is running\n\n", style="green bold")
status_text.append(" URL: ", style="dim")
Expand All @@ -579,9 +580,8 @@ def do_daemon(args, config: dict, logger):
# Check if using pg0 and show database location
database_url = os.getenv("HINDSIGHT_EMBED_API_DATABASE_URL")
if not database_url:
# Default: use profile-specific pg0
safe_profile = re.sub(r"[^a-zA-Z0-9_-]", "-", profile or "default")
database_url = f"pg0://hindsight-embed-{safe_profile}"
# Default: use profile-specific pg0 (shared utility ensures consistency)
database_url = get_embed_manager().get_database_url(profile)

if database_url.startswith("pg0://"):
pg0_name = database_url.replace("pg0://", "")
Expand Down
Loading
Loading